Skip to contentSkip to conversation

Dynamic Kernel Stacks

PATCH Applied David Stevens 13 participants 72 messages Apr 24 – Jul 14

Subsystems (11)
lore
Share & tools
lore
#0 lore raw

v2 cover Dynamic Kernel Stacks

This RFC is a continuation of Pasha Tatashin's original RFC [1], and is based on Linus Walleij's rebased version of the patches [2]. My focus was x86_64 devices, so I didn't include his arm64 WIP patches.

The impetus for reviving this RFC is kernel stack usage on Android. On regular Android (i.e. non-wear/automotive), system processes typically have 2000-3000 threads. When adding threads from app processes, this means that systems with 4GB of memory are using 1-2% of total memory for kernel thread stacks. Dynamic kernel stacks reduce this by 65%-70%.

The main change compared to Pasha's v1 RFC is how x86_64 handles kernel stack faults. On systems where FRED is available, it handles kernel page faults on stack level 1. When FRED isn't available, it uses a dedicated IST stack for page faults. In both cases, page faults which aren't dynamic stack faults are moved back onto the regular kernel stack. This does introduce some overhead for page faults on user memory that originate in the kernel (note that non-FRED systems already needed to bounce userspace page faults through the entry stack), but such faults aren't as hot a path as regular user page faults. There are certainly systems where the memory savings are worth the overhead. That said, the config could be made optional to give systems the option to pay the memory cost to avoid the CPU overhead.

The biggest open issue is how to deal with reliability. This series uses GFP_ATOMIC when refilling the per-CPU magazines during context switch, which is necessary to avoid deadlock. This of course raises concerns about allocation failure. If a magazine got depleted, then refilling the magazine failed due to atomic reserve depletion, and then another thread triggered a dynamic stack fault, that would trigger a fatal page fault. There is also a secondary concern about additional pressure on the memory reserves causing allocation failures at other atomic call sites.

The question is then: is this approach something that is fundamentally untenable in the kernel, or are there compromises that would allow it to be merged? One obvious compromise is to make the feature optional. Both kernel stack faults and running out of memory reserves are rare events. I've never seen this failure in my testing, although I don't have field data to back that up at this point. Some sysadmins may view it as low enough risk to be worth the memory savings. There are also additional measures that could be taken to reduce the likelihood of failure (e.g. magazine management on kernel entry/exit, tunable magazine sizes, adding best-effort trylock reclaim or oom kill).

This series was developed and tested on devices running 6.18 kernels. It has been rebased onto 7.0, with minimal smoke testing after rebasing.

[1] https://lore.kernel.org/all/[email protected]/
[2] https://git.kernel.org/pub/scm/linux/kernel/git/linusw/linux-integrator.git/log/?h=b4/aarch64-dynamic-kernel-stacks-v6.18-rc1

David Stevens (7):
fork: Don't assume fully populated stack during reuse
fork: Move vm_stack to the beginning of the stack
fork: Move vmap stack freeing to work queue
fork: Store task pointer in unpopulated stack ptes x86/entry/fred: encode frame pointer on entry
x86: Add support for dynamic kernel stacks via FRED
x86: Add support for dynamic kernel stacks via IST

Pasha Tatashin (6):
fork: Remove assumption that vm_area->nr_pages equals to THREAD_SIZE
fork: separate vmap stack allocation and free calls mm/vmalloc: Add a get_vm_area_node() and vmap_pages_range() public
functions
fork: Dynamic Kernel Stacks
task_stack.h: Add stack_not_used() support for dynamic stack
fork: Dynamic Kernel Stack accounting

 arch/Kconfig                          |  38 ++
 arch/x86/Kconfig                      |   1 +
 arch/x86/entry/entry_64.S             |  49 ++-
 arch/x86/entry/entry_64_fred.S        |  57 +++
 arch/x86/include/asm/cpu_entry_area.h |  18 +
 arch/x86/include/asm/idtentry.h       |  38 +-
 arch/x86/include/asm/page_64_types.h  |  10 +-
 arch/x86/include/asm/pgtable_64.h     |  36 ++
 arch/x86/include/asm/processor.h      |   6 +
 arch/x86/include/asm/traps.h          |   5 +
 arch/x86/kernel/cpu/common.c          |  11 +
 arch/x86/kernel/dumpstack_64.c        |  10 +-
 arch/x86/kernel/fred.c                |  20 +-
 arch/x86/kernel/idt.c                 |  57 +--
 arch/x86/kernel/nmi.c                 |   9 +
 arch/x86/lib/usercopy.c               |   9 +
 arch/x86/mm/cpu_entry_area.c          |  17 +
 arch/x86/mm/dump_pagetables.c         |  14 +-
 arch/x86/mm/fault.c                   | 101 +++++-
 include/linux/mmzone.h                |   3 +
 include/linux/sched.h                 |  11 +-
 include/linux/sched/task_stack.h      |  48 ++-
 include/linux/vmalloc.h               |  14 +
 init/init_task.c                      |   4 +
 kernel/exit.c                         |  22 ++
 kernel/fork.c                         | 481 ++++++++++++++++++++++++--
 kernel/sched/core.c                   |   1 +
 mm/memcontrol.c                       |  10 +
 mm/vmalloc.c                          |  27 +-
 mm/vmstat.c                           |   3 +
 30 files changed, 1049 insertions(+), 81 deletions(-)

base-commit: 028ef9c96e96197026887c0f092424679298aae8

Signature · 1 line
2.54.0.rc2.544.gc7ae2d5bb8-goog
#1 lore raw

PATCH v2 1/13 fork: Remove assumption that vm_area->nr_pages equals to THREAD_SIZE

From: Pasha Tatashin <[email protected]>

In many places number of pages in the stack is detremined via (THREAD_SIZE / PAGE_SIZE). There is also a BUG_ON() that ensures that (THREAD_SIZE / PAGE_SIZE) is indeed equals to vm_area->nr_pages.

However, with dynamic stacks, the number of pages in vm_area will grow with stack, therefore, use vm_area->nr_pages to determine the actual number of pages allocated in stack.

[Rebased, also skipped intermediary helper variable nr_pages]
---
 kernel/fork.c | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)
kernel/fork.c +3 -5
index bc2bf58b93b6..8961b895bf05 100644
@@ -312,9 +312,7 @@ static int memcg_charge_kernel_stack(struct vm_struct *vm_area)
 	int ret;
 	int nr_charged = 0;
 
-	BUG_ON(vm_area->nr_pages != THREAD_SIZE / PAGE_SIZE);
-
-	for (i = 0; i < THREAD_SIZE / PAGE_SIZE; i++) {
+	for (i = 0; i < vm_area->nr_pages; i++) {
 		ret = memcg_kmem_charge_page(vm_area->pages[i], GFP_KERNEL, 0);
 		if (ret)
 			goto err;
@@ -484,7 +482,7 @@ static void account_kernel_stack(struct task_struct *tsk, int account)
 		struct vm_struct *vm_area = task_stack_vm_area(tsk);
 		int i;
 
-		for (i = 0; i < THREAD_SIZE / PAGE_SIZE; i++)
+		for (i = 0; i < vm_area->nr_pages; i++)
 			mod_lruvec_page_state(vm_area->pages[i], NR_KERNEL_STACK_KB,
 					      account * (PAGE_SIZE / 1024));
 	} else {
@@ -505,7 +503,7 @@ void exit_task_stack_account(struct task_struct *tsk)
 		int i;
 
 		vm_area = task_stack_vm_area(tsk);
-		for (i = 0; i < THREAD_SIZE / PAGE_SIZE; i++)
+		for (i = 0; i < vm_area->nr_pages; i++)
 			memcg_kmem_uncharge_page(vm_area->pages[i], 0);
 	}
 }
Signature · 1 line
2.54.0.rc2.544.gc7ae2d5bb8-goog
#2 lore raw

PATCH v2 2/13 fork: Don't assume fully populated stack during reuse

In reply to David Stevens

In preparation for dynamic kernel stacks, don't assume that vm_area->nr_pages matches THREAD_SIZE when clearing a stack for reuse.

---
 kernel/fork.c | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)
kernel/fork.c +5 -1
index 8961b895bf05..50772c0cc5da 100644
@@ -332,6 +332,8 @@ static int alloc_thread_stack_node(struct task_struct *tsk, int node)
 
 	vm_area = alloc_thread_stack_node_from_cache(tsk, node);
 	if (vm_area) {
+		unsigned long memset_offset = 0;
+
 		if (memcg_charge_kernel_stack(vm_area)) {
 			vfree(vm_area->addr);
 			return -ENOMEM;
@@ -343,7 +345,9 @@ static int alloc_thread_stack_node(struct task_struct *tsk, int node)
 		stack = kasan_reset_tag(vm_area->addr);
 
 		/* Clear stale pointers from reused stack. */
-		memset(stack, 0, THREAD_SIZE);
+		if (!IS_ENABLED(CONFIG_STACK_GROWSUP))
+			memset_offset = THREAD_SIZE - vm_area->nr_pages * PAGE_SIZE;
+		memset(stack + memset_offset, 0, vm_area->nr_pages * PAGE_SIZE);
 
 		tsk->stack_vm_area = vm_area;
 		tsk->stack = stack;
Signature · 1 line
2.54.0.rc2.544.gc7ae2d5bb8-goog
#3 lore raw

PATCH v2 3/13 fork: Move vm_stack to the beginning of the stack

In reply to David Stevens

The vm_stack struct used to free stacks via an RCU callback is stored directly in the stack being freed. Make sure it's stored at the beginning of the stack regardless of stack growth direction, to avoid faults on partially allocated dynamic stacks.

---
 kernel/fork.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)
kernel/fork.c +6 -1
index 50772c0cc5da..72c081db492c 100644
@@ -282,7 +282,12 @@ static void thread_stack_free_rcu(struct rcu_head *rh)
 
 static void thread_stack_delayed_free(struct task_struct *tsk)
 {
-	struct vm_stack *vm_stack = tsk->stack;
+	struct vm_stack *vm_stack;
+
+	if (IS_ENABLED(CONFIG_STACK_GROWSUP))
+		vm_stack = tsk->stack;
+	else
+		vm_stack = tsk->stack + THREAD_SIZE - sizeof(*vm_stack);
 
 	vm_stack->stack_vm_area = tsk->stack_vm_area;
 	call_rcu(&vm_stack->rcu, thread_stack_free_rcu);
Signature · 1 line
2.54.0.rc2.544.gc7ae2d5bb8-goog
#4 lore raw

PATCH v2 4/13 fork: separate vmap stack allocation and free calls

In reply to David Stevens

From: Pasha Tatashin <[email protected]>

In preparation for the dynamic stacks, separate out the __vmalloc_node_range and vfree calls from the vmap based stack allocations. The dynamic stacks will use their own variants of these functions.

[Fix a bug in original patch: free_vmap_stack(vm_area->addr)]
[Add missing free_vmap_stack conversion, fix typos, rebase]
---
 kernel/fork.c | 40 ++++++++++++++++++++++++----------------
 1 file changed, 24 insertions(+), 16 deletions(-)
kernel/fork.c +24 -16
index 72c081db492c..8bf32815f422 100644
@@ -269,6 +269,21 @@ static bool try_release_thread_stack_to_cache(struct vm_struct *vm_area)
 	return false;
 }
 
+static inline struct vm_struct *alloc_vmap_stack(int node)
+{
+	void *stack;
+
+	stack = __vmalloc_node(THREAD_SIZE, THREAD_ALIGN, GFP_VMAP_STACK,
+			       node, __builtin_return_address(0));
+
+	return stack ? find_vm_area(stack) : NULL;
+}
+
+static inline void free_vmap_stack(struct vm_struct *vm_area)
+{
+	vfree(vm_area->addr);
+}
+
 static void thread_stack_free_rcu(struct rcu_head *rh)
 {
 	struct vm_stack *vm_stack = container_of(rh, struct vm_stack, rcu);
@@ -277,7 +292,7 @@ static void thread_stack_free_rcu(struct rcu_head *rh)
 	if (try_release_thread_stack_to_cache(vm_stack->stack_vm_area))
 		return;
 
-	vfree(vm_area->addr);
+	free_vmap_stack(vm_area);
 }
 
 static void thread_stack_delayed_free(struct task_struct *tsk)
@@ -304,7 +319,7 @@ static int free_vm_stack_cache(unsigned int cpu)
 		if (!vm_area)
 			continue;
 
-		vfree(vm_area->addr);
+		free_vmap_stack(vm_area);
 		cached_vm_stack_areas[i] = NULL;
 	}
 
@@ -333,41 +348,35 @@ static int memcg_charge_kernel_stack(struct vm_struct *vm_area)
 static int alloc_thread_stack_node(struct task_struct *tsk, int node)
 {
 	struct vm_struct *vm_area;
-	void *stack;
 
 	vm_area = alloc_thread_stack_node_from_cache(tsk, node);
 	if (vm_area) {
 		unsigned long memset_offset = 0;
 
 		if (memcg_charge_kernel_stack(vm_area)) {
-			vfree(vm_area->addr);
+			free_vmap_stack(vm_area);
 			return -ENOMEM;
 		}
 
 		/* Reset stack metadata. */
 		kasan_unpoison_range(vm_area->addr, THREAD_SIZE);
-
-		stack = kasan_reset_tag(vm_area->addr);
+		tsk->stack = kasan_reset_tag(vm_area->addr);
 
 		/* Clear stale pointers from reused stack. */
 		if (!IS_ENABLED(CONFIG_STACK_GROWSUP))
 			memset_offset = THREAD_SIZE - vm_area->nr_pages * PAGE_SIZE;
-		memset(stack + memset_offset, 0, vm_area->nr_pages * PAGE_SIZE);
+		memset(tsk->stack + memset_offset, 0, vm_area->nr_pages * PAGE_SIZE);
 
 		tsk->stack_vm_area = vm_area;
-		tsk->stack = stack;
 		return 0;
 	}
 
-	stack = __vmalloc_node(THREAD_SIZE, THREAD_ALIGN,
-				     GFP_VMAP_STACK,
-				     node, __builtin_return_address(0));
-	if (!stack)
+	vm_area = alloc_vmap_stack(node);
+	if (!vm_area)
 		return -ENOMEM;
 
-	vm_area = find_vm_area(stack);
 	if (memcg_charge_kernel_stack(vm_area)) {
-		vfree(stack);
+		free_vmap_stack(vm_area);
 		return -ENOMEM;
 	}
 	/*
@@ -376,8 +385,7 @@ static int alloc_thread_stack_node(struct task_struct *tsk, int node)
 	 * so cache the vm_struct.
 	 */
 	tsk->stack_vm_area = vm_area;
-	stack = kasan_reset_tag(stack);
-	tsk->stack = stack;
+	tsk->stack = kasan_reset_tag(vm_area->addr);
 	return 0;
 }
 
Signature · 1 line
2.54.0.rc2.544.gc7ae2d5bb8-goog
#5 lore raw

PATCH v2 5/13 mm/vmalloc: Add a get_vm_area_node() and vmap_pages_range() public functions

In reply to David Stevens

From: Pasha Tatashin <[email protected]>

get_vm_area_node()
Unlike the other public get_vm_area_* variants, this one accepts node from which to allocate data structure, and also the align, which allows to create vm area with a specific alignment.

This call is going to be used by dynamic stacks in order to ensure that the stack VM area of a specific alignment, and that even if there is only one page mapped, no page table allocations are going to be needed to map the other stack pages.

vmap_pages_range()
We will need it from kernel/fork.c in order to map the initial stack pages, so export the function and add a forward declaration of this function to the linux/vmalloc.h header.

[Switched to vmap_pages_range instead of noflush variant, fix typos]
---
 include/linux/vmalloc.h | 14 ++++++++++++++
 mm/vmalloc.c            | 25 +++++++++++++++++++++++++
 2 files changed, 39 insertions(+)
include/linux/vmalloc.h +14 -0
index e8e94f90d686..7b56a0b998ab 100644
@@ -250,6 +250,9 @@ extern struct vm_struct *__get_vm_area_caller(unsigned long size,
 					unsigned long flags,
 					unsigned long start, unsigned long end,
 					const void *caller);
+struct vm_struct *get_vm_area_node(unsigned long size, unsigned long align,
+				   unsigned long flags, int node, gfp_t gfp,
+				   const void *caller);
 void free_vm_area(struct vm_struct *area);
 extern struct vm_struct *remove_vm_area(const void *addr);
 extern struct vm_struct *find_vm_area(const void *addr);
@@ -301,11 +304,22 @@ static inline void set_vm_flush_reset_perms(void *addr)
 	if (vm)
 		vm->flags |= VM_FLUSH_RESET_PERMS;
 }
+
+int __must_check vmap_pages_range(unsigned long addr, unsigned long end,
+				  pgprot_t prot, struct page **pages, unsigned int page_shift);
+
 #else  /* !CONFIG_MMU */
 #define VMALLOC_TOTAL 0UL
 
 static inline unsigned long vmalloc_nr_pages(void) { return 0; }
 static inline void set_vm_flush_reset_perms(void *addr) {}
+static inline
+int __must_check vmap_pages_range(unsigned long addr, unsigned long end,
+				  pgprot_t prot, struct page **pages, unsigned int page_shift)
+{
+	return -EINVAL;
+}
+
 #endif /* CONFIG_MMU */
 
 #if defined(CONFIG_MMU) && defined(CONFIG_SMP)
mm/vmalloc.c +25 -0
index 61caa55a4402..39b7e118cbce 100644
@@ -722,6 +722,7 @@ int vmap_pages_range(unsigned long addr, unsigned long end,
 {
 	return __vmap_pages_range(addr, end, prot, pages, page_shift, GFP_KERNEL);
 }
+EXPORT_SYMBOL_GPL(vmap_pages_range);
 
 static int check_sparse_vm_area(struct vm_struct *area, unsigned long start,
 				unsigned long end)
@@ -3285,6 +3286,30 @@ struct vm_struct *get_vm_area_caller(unsigned long size, unsigned long flags,
 				  NUMA_NO_NODE, GFP_KERNEL, caller);
 }
 
+/**
+ * get_vm_area_node - reserve a contiguous and aligned kernel virtual area
+ * @size:	 size of the area
+ * @align:	 alignment of the start address of the area
+ * @flags:	 %VM_IOREMAP for I/O mappings
+ * @node:	 NUMA node from which to allocate the area data structure
+ * @gfp:	 Flags to pass to the allocator
+ * @caller:	 Caller to be stored in the vm area data structure
+ *
+ * Search for an area of @size/align in the kernel virtual mapping area and
+ * reserve it for our purposes. Returns the area descriptor on success or %NULL
+ * on failure.
+ *
+ * Return: the area descriptor on success or %NULL on failure.
+ */
+struct vm_struct *get_vm_area_node(unsigned long size, unsigned long align,
+				   unsigned long flags, int node, gfp_t gfp,
+				   const void *caller)
+{
+	return __get_vm_area_node(size, align, PAGE_SHIFT, flags,
+				  VMALLOC_START, VMALLOC_END,
+				  node, gfp, caller);
+}
+
 /**
  * find_vm_area - find a continuous kernel virtual area
  * @addr:	  base address
Signature · 1 line
2.54.0.rc2.544.gc7ae2d5bb8-goog
#6 lore raw

PATCH v2 6/13 fork: Move vmap stack freeing to work queue

In reply to David Stevens

For vmap stacks not immediately released into the stack cache, free them in a workqueue instead of via call_rcu(). In an RCU context, vfree already schedules the actual freeing on the per-cpu system workqueue, so this change only affects when exactly the second attempt to put the stack into the stack cache occurs.

Moving freeing to a workqueue will allow for freeing dynamic stacks in a sleepable context (for remove_vm_area), rather than relying on vfree dispatching to a workqueue via vfree_atomic.

---
 kernel/fork.c | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)
kernel/fork.c +5 -4
index 8bf32815f422..01e0bf4f4b02 100644
@@ -205,7 +205,7 @@ static DEFINE_PER_CPU(struct vm_struct *, cached_stacks[NR_CACHED_STACKS]);
 #define GFP_VMAP_STACK (GFP_KERNEL | __GFP_ZERO)
 
 struct vm_stack {
-	struct rcu_head rcu;
+	struct rcu_work work;
 	struct vm_struct *stack_vm_area;
 };
 
@@ -284,9 +284,9 @@ static inline void free_vmap_stack(struct vm_struct *vm_area)
 	vfree(vm_area->addr);
 }
 
-static void thread_stack_free_rcu(struct rcu_head *rh)
+static void thread_stack_free_work(struct work_struct *work)
 {
-	struct vm_stack *vm_stack = container_of(rh, struct vm_stack, rcu);
+	struct vm_stack *vm_stack = container_of(to_rcu_work(work), struct vm_stack, work);
 	struct vm_struct *vm_area = vm_stack->stack_vm_area;
 
 	if (try_release_thread_stack_to_cache(vm_stack->stack_vm_area))
@@ -305,7 +305,8 @@ static void thread_stack_delayed_free(struct task_struct *tsk)
 		vm_stack = tsk->stack + THREAD_SIZE - sizeof(*vm_stack);
 
 	vm_stack->stack_vm_area = tsk->stack_vm_area;
-	call_rcu(&vm_stack->rcu, thread_stack_free_rcu);
+	INIT_RCU_WORK(&vm_stack->work, thread_stack_free_work);
+	queue_rcu_work(system_wq, &vm_stack->work);
 }
 
 static int free_vm_stack_cache(unsigned int cpu)
Signature · 1 line
2.54.0.rc2.544.gc7ae2d5bb8-goog
#7 lore raw

PATCH v2 7/13 fork: Dynamic Kernel Stacks

In reply to David Stevens

From: Pasha Tatashin <[email protected]>

The core implementation of dynamic kernel stacks.

Unlike traditional kernel stacks, these stacks auto-grow as they are used. This allows to save a significant amount of memory in the fleet environments. Also, potentially the default size of kernel thread can be increased in order to prevent stack overflows without compromising on the overall memory overhead.

The dynamic kernel stacks interface provides two global functions:

1. dynamic_stack_fault().
Architectures that support dynamic kernel stacks, must call this function in order to handle the fault in the stack.

It allocates and maps new pages into the stack. The pages are maintained in a per-cpu data structure.

2. dynamic_stack()
Must be called as a thread leaving CPU to check if the thread has allocated dynamic stack pages (tsk->flags & PF_DYNAMIC_STACK) is set. If this is the case, there are two things need to be performed:
a. Charge the thread for the allocated stack pages.
b. refill the per-cpu array so the next thread can also fault.

Dynamic kernel threads do not support "STACK_END_MAGIC", as the last page does not have to be faulted in. However, since they are based off vmap stacks, the guard pages always protect the dynamic kernel stacks from overflow.

The average depth of a kernel thread depends on the workload, profiling, virtualization, compiler optimizations, and driver implementations.

Therefore, the numbers should be tested for a specific workload. From my tests I found the following values on a freshly booted idling machines:

CPU #Cores #Stacks Regular(kb) Dynamic(kb)

AMD Genoa        384    5786    92576       23388
Intel Skylake    112    3182    50912       12860
AMD Rome         128    3401    54416       14784
AMD Rome         256    4908    78528       20876
Intel Haswell     72    2644    42304       10624

On all machines dynamic kernel stacks take about 25% of the original stack memory. Only 5% of active tasks performed a stack page fault in their life cycles.

[Rebased, used vm_area->nr_pages directly in one instance]
[Depends on !PREEMPT_RT]
[Fix races around accounting]
[Use GFP_ATOMIC when executing in the scheduler]
[Depend on INIT_STACK_ALL_* config]
[Fix bugs in some error paths and edge cases]
[Don't cache partially faulted stacks]
[Added out-var to tell if address is on target stack]
---
 arch/Kconfig                     |  39 ++++
 include/linux/sched.h            |  11 +-
 include/linux/sched/task_stack.h |  47 +++-
 init/init_task.c                 |   4 +
 kernel/fork.c                    | 357 +++++++++++++++++++++++++++++--
 kernel/sched/core.c              |   1 +
 6 files changed, 439 insertions(+), 20 deletions(-)
arch/Kconfig +39 -0
index 102ddbd4298e..95ded79f0825 100644
@@ -1515,6 +1515,45 @@ config VMAP_STACK
 	  backing virtual mappings with real shadow memory, and KASAN_VMALLOC
 	  must be enabled.
 
+config HAVE_ARCH_DYNAMIC_STACK
+	def_bool n
+	help
+	  An arch should select this symbol if it can support kernel stacks
+	  that grow dynamically.
+
+	  - Arch must have support for HAVE_ARCH_VMAP_STACK, in order to handle
+	    stack related page faults.
+
+	  - Arch must be able to fault from interrupt context.
+
+	  - Arch must allow the kernel to handle stack faults gracefully, even
+	    during interrupt handling.
+
+	  - Exceptions such as no pages available should be handled the same
+	    in the consistent and predictable way. I.e. the exception should be
+	    handled the same as when stack overflow occurs when guard pages are
+	    touched with extra information about the allocation error.
+
+config DYNAMIC_STACK
+	default y
+	bool "Dynamically grow kernel stacks"
+	depends on THREAD_INFO_IN_TASK
+	depends on HAVE_ARCH_DYNAMIC_STACK
+	depends on VMAP_STACK
+	depends on INIT_STACK_ALL_ZERO || INIT_STACK_ALL_PATTERN
+	depends on !KASAN
+	depends on !DEBUG_STACK_USAGE
+	depends on !STACK_GROWSUP
+	depends on !PREEMPT_RT
+	help
+	  Dynamic kernel stacks allow to save memory on machines with a lot of
+	  threads by starting with small stacks, and grow them only when needed.
+	  On workloads where most of the stack depth do not reach over one page
+	  the memory saving can be substantial. The feature requires virtually
+	  mapped kernel stacks in order to handle page faults. It requires stack
+	  initialization to preclude one thread from faulting on another thread's
+	  stack.
+
 config HAVE_ARCH_RANDOMIZE_KSTACK_OFFSET
 	def_bool n
 	help
include/linux/sched.h +10 -1
index 5a5d3dbc9cdf..7aa06233afd5 100644
@@ -836,7 +836,11 @@ struct task_struct {
 	 */
 	randomized_struct_fields_start
 
+#ifdef CONFIG_DYNAMIC_STACK
+	unsigned long			packed_stack;
+#else
 	void				*stack;
+#endif
 	refcount_t			usage;
 	/* Per task flags (PF_*), defined further below: */
 	unsigned int			flags;
@@ -1563,6 +1567,11 @@ struct task_struct {
 	struct timer_list		oom_reaper_timer;
 #endif
 #ifdef CONFIG_VMAP_STACK
+	/*
+	 * We can't call find_vm_area() in interrupt context, and
+	 * free_thread_stack() can be called in interrupt context,
+	 * so cache the vm_struct.
+	 */
 	struct vm_struct		*stack_vm_area;
 #endif
 #ifdef CONFIG_THREAD_INFO_IN_TASK
@@ -1773,7 +1782,7 @@ extern struct pid *cad_pid;
 						 * I am cleaning dirty pages from some other bdi. */
 #define PF_KTHREAD		0x00200000	/* I am a kernel thread */
 #define PF_RANDOMIZE		0x00400000	/* Randomize virtual address space */
-#define PF__HOLE__00800000	0x00800000
+#define PF_DYNAMIC_STACK	0x00800000	/* This thread allocated dynamic stack pages */
 #define PF__HOLE__01000000	0x01000000
 #define PF__HOLE__02000000	0x02000000
 #define PF_NO_SETAFFINITY	0x04000000	/* Userland is not allowed to meddle with cpus_mask */
include/linux/sched/task_stack.h +45 -2

81 lines not on this page · raw message

init/init_task.c +4 -0
index 5c838757fc10..e3645ec4ab02 100644
@@ -99,7 +99,11 @@ struct task_struct init_task __aligned(L1_CACHE_BYTES) = {
 	.stack_refcount	= REFCOUNT_INIT(1),
 #endif
 	.__state	= 0,
+#ifdef CONFIG_DYNAMIC_STACK
+	.packed_stack	= (unsigned long)init_stack,
+#else
 	.stack		= init_stack,
+#endif
 	.usage		= REFCOUNT_INIT(2),
 	.flags		= PF_KTHREAD,
 	.prio		= MAX_PRIO - 20,
kernel/fork.c +340 -17

442 lines not on this page · raw message

kernel/sched/core.c +1 -0
index 496dff740dca..417269a86973 100644
@@ -6783,6 +6783,7 @@ static void __sched notrace __schedule(int sched_mode)
 	rq = cpu_rq(cpu);
 	prev = rq->curr;
 
+	dynamic_stack(prev);
 	schedule_debug(prev, preempt);
 
 	if (sched_feat(HRTICK) || sched_feat(HRTICK_DL))
Signature · 1 line
2.54.0.rc2.544.gc7ae2d5bb8-goog
#8 lore raw

PATCH v2 8/13 task_stack.h: Add stack_not_used() support for dynamic stack

In reply to David Stevens

From: Pasha Tatashin <[email protected]>

CONFIG_DEBUG_STACK_USAGE is enabled by default on most architectures.

Its purpose is to determine and print the maximum stack depth on thread exit.

The way it works, is it starts from the bottom of the stack and searches the first non-zero word in the stack. With dynamic stack it does not work very well, as it means it faults every pages in every stack.

Instead, add a specific version of stack_not_used() for dynamic stacks where instead of starting from the bottom of the stack, we start from the last page mapped in the stack.

In addition to not doing unnecessary page faulting, this search is optimized by skipping search through zero pages.

Also, because dynamic stack does not end with MAGIC_NUMBER, there is no need to skip the bottom most word in the stack.

[Rebased, Kasan oneliner needed preserving, rewrote a bit due to bugs]
[Handle init_task's use of init_stack, fix typos]
---
 arch/Kconfig  |  1 -
 kernel/exit.c | 22 ++++++++++++++++++++++
 2 files changed, 22 insertions(+), 1 deletion(-)
arch/Kconfig +0 -1
index 95ded79f0825..beffe7e01296 100644
@@ -1542,7 +1542,6 @@ config DYNAMIC_STACK
 	depends on VMAP_STACK
 	depends on INIT_STACK_ALL_ZERO || INIT_STACK_ALL_PATTERN
 	depends on !KASAN
-	depends on !DEBUG_STACK_USAGE
 	depends on !STACK_GROWSUP
 	depends on !PREEMPT_RT
 	help
kernel/exit.c +22 -0
index ede3117fa7d4..6caf4030e8f4 100644
@@ -71,6 +71,7 @@
 #include <linux/unwind_deferred.h>
 #include <linux/uaccess.h>
 #include <linux/pidfs.h>
+#include <linux/vmalloc.h>
 
 #include <uapi/linux/wait.h>
 
@@ -791,6 +792,26 @@ unsigned long stack_not_used(struct task_struct *p)
 	return (unsigned long)end_of_stack(p) - (unsigned long)n;
 }
 #else /* !CONFIG_STACK_GROWSUP */
+#ifdef CONFIG_DYNAMIC_STACK
+unsigned long stack_not_used(struct task_struct *p)
+{
+	struct vm_struct *vm_area = task_stack_vm_area(p);
+	unsigned long stack = (unsigned long)task_stack_page(p);
+	unsigned long alloc_size, *n;
+
+	/* This is NULL only for init_task, where init_stack is fully allocated. */
+	if (likely(vm_area))
+		alloc_size = vm_area->nr_pages << PAGE_SHIFT;
+	else
+		alloc_size = THREAD_SIZE;
+	n = (unsigned long *)(stack + THREAD_SIZE - alloc_size);
+
+	while (!*n)
+		n++;
+
+	return (unsigned long)n - stack;
+}
+#else
 unsigned long stack_not_used(struct task_struct *p)
 {
 	unsigned long *n = end_of_stack(p);
@@ -801,6 +822,7 @@ unsigned long stack_not_used(struct task_struct *p)
 
 	return (unsigned long)n - (unsigned long)end_of_stack(p);
 }
+#endif /* CONFIG_DYNAMIC_STACK */
 #endif /* CONFIG_STACK_GROWSUP */
 
 /* Count the maximum pages reached in kernel stacks */
Signature · 1 line
2.54.0.rc2.544.gc7ae2d5bb8-goog
#9 lore raw

PATCH v2 9/13 fork: Dynamic Kernel Stack accounting

In reply to David Stevens

From: Pasha Tatashin <[email protected]>

Add an accounting of the amount of stack pages that have been faulted in and are currently in use.

Example use case:

  $ cat /proc/vmstat | grep stack
  nr_kernel_stack 18684
  nr_dynamic_stacks_faults 156

The above shows that the kernel stacks use total 18684KiB, out of which 156KiB were faulted in.

Given that the pre-allocated stacks are 4KiB, we can determine the total number of tasks:

tasks = (nr_kernel_stack - nr_dynamic_stacks_faults) / 4 = 4632.

The amount of kernel stack memory without dynamic stack on this machine would be:

4632 * 16 KiB = 74,112 KiB

Therefore, in this example dynamic stacks save: 55,428 KiB

[Rebased]
[add to memcg stats, fix typos]
---
 include/linux/mmzone.h |  3 +++
 kernel/fork.c          | 12 +++++++++++-
 mm/memcontrol.c        | 10 ++++++++++
 mm/vmstat.c            |  3 +++
 4 files changed, 27 insertions(+), 1 deletion(-)
include/linux/mmzone.h +3 -0
index 3e51190a55e4..4458fa7016a1 100644
@@ -221,6 +221,9 @@ enum node_stat_item {
 	NR_FOLL_PIN_ACQUIRED,	/* via: pin_user_page(), gup flag: FOLL_PIN */
 	NR_FOLL_PIN_RELEASED,	/* pages returned via unpin_user_page() */
 	NR_KERNEL_STACK_KB,	/* measured in KiB */
+#ifdef CONFIG_DYNAMIC_STACK
+	NR_DYNAMIC_STACKS_FAULTS_KB, /* KiB of faulted kernel stack memory */
+#endif
 #if IS_ENABLED(CONFIG_SHADOW_CALL_STACK)
 	NR_KERNEL_SCS_KB,	/* measured in KiB */
 #endif
kernel/fork.c +11 -1
index e615ef736dc0..9ac9d23f5f4b 100644
@@ -463,6 +463,8 @@ unsigned long dynamic_stack_accounting(struct task_struct *tsk, bool finalize)
 
 		mod_lruvec_page_state(page, NR_KERNEL_STACK_KB,
 				      PAGE_SIZE / 1024);
+		mod_lruvec_page_state(page, NR_DYNAMIC_STACKS_FAULTS_KB,
+				      PAGE_SIZE / 1024);
 	}
 
 	if (finalize) {
@@ -811,9 +813,17 @@ static void account_kernel_stack(struct task_struct *tsk, int account)
 		nr_accounted = vm_area->nr_pages;
 #endif
 
-		for (i = 0; i < nr_accounted; i++)
+		for (i = 0; i < nr_accounted; i++) {
 			mod_lruvec_page_state(vm_area->pages[i], NR_KERNEL_STACK_KB,
 					      account * (PAGE_SIZE / 1024));
+#ifdef CONFIG_DYNAMIC_STACK
+			if (i >= THREAD_PREALLOC_PAGES) {
+				mod_lruvec_page_state(vm_area->pages[i],
+						      NR_DYNAMIC_STACKS_FAULTS_KB,
+						      account * (PAGE_SIZE / 1024));
+			}
+#endif
+		}
 	} else {
 		void *stack = task_stack_page(tsk);
 
mm/memcontrol.c +10 -0
index 772bac21d155..cd2195a735ab 100644
@@ -318,6 +318,9 @@ static const unsigned int memcg_node_stat_items[] = {
 	NR_FILE_THPS,
 	NR_ANON_THPS,
 	NR_KERNEL_STACK_KB,
+#ifdef CONFIG_DYNAMIC_STACK
+	NR_DYNAMIC_STACKS_FAULTS_KB,
+#endif
 	NR_PAGETABLE,
 	NR_SECONDARY_PAGETABLE,
 #ifdef CONFIG_SWAP
@@ -1403,6 +1406,10 @@ static const struct memory_stat memory_stats[] = {
 #ifdef CONFIG_NUMA_BALANCING
 	{ "pgpromote_success",		PGPROMOTE_SUCCESS	},
 #endif
+
+#ifdef CONFIG_DYNAMIC_STACK
+	{ "dynamic_stack_faults",	NR_DYNAMIC_STACKS_FAULTS_KB     },
+#endif
 };
 
 /* The actual unit of the state item, not the same as the output unit */
@@ -1415,6 +1422,9 @@ static int memcg_page_state_unit(int item)
 	case NR_SLAB_UNRECLAIMABLE_B:
 		return 1;
 	case NR_KERNEL_STACK_KB:
+#ifdef CONFIG_DYNAMIC_STACK
+	case NR_DYNAMIC_STACKS_FAULTS_KB:
+#endif
 		return SZ_1K;
 	default:
 		return PAGE_SIZE;
mm/vmstat.c +3 -0
index 86b14b0f77b5..8fa1c7bcbaea 100644
@@ -1256,6 +1256,9 @@ const char * const vmstat_text[] = {
 	[I(NR_FOLL_PIN_ACQUIRED)]		= "nr_foll_pin_acquired",
 	[I(NR_FOLL_PIN_RELEASED)]		= "nr_foll_pin_released",
 	[I(NR_KERNEL_STACK_KB)]			= "nr_kernel_stack",
+#ifdef CONFIG_DYNAMIC_STACK
+	[I(NR_DYNAMIC_STACKS_FAULTS_KB)]	= "nr_dynamic_stacks_faults",
+#endif
 #if IS_ENABLED(CONFIG_SHADOW_CALL_STACK)
 	[I(NR_KERNEL_SCS_KB)]			= "nr_shadow_call_stack",
 #endif
Signature · 1 line
2.54.0.rc2.544.gc7ae2d5bb8-goog
#10 lore raw

PATCH v2 10/13 fork: Store task pointer in unpopulated stack ptes

In reply to David Stevens

Store the task pointer in the ptes of the unpopulated pages of dynamic stacks, to allow the vm_struct pointer to be retrieved without relying on any locks or current.

This relies on being able to pack the struct task_struct pointer into a pte. Since the struct is 64 byte aligned, that gives 5 bits of leeway, which should be viable on most architectures. Any architecture which enables dynamic thread stacks must provide make_data_kpte() and unpack_data_kpte(), which pack/unpack a right shifted pointer value into/from a pte.

---
 include/linux/sched/task_stack.h |  1 +
 kernel/fork.c                    | 74 +++++++++++++++++++++++++++++---
 mm/vmalloc.c                     |  2 +-
 3 files changed, 69 insertions(+), 8 deletions(-)
include/linux/sched/task_stack.h +1 -0
index 7dcff2836d7e..7cf00ce97f7c 100644
@@ -105,6 +105,7 @@ void exit_task_stack_account(struct task_struct *tsk);
 void dynamic_stack_refill_pages(void);
 unsigned long dynamic_stack_accounting(struct task_struct *tsk, bool finalize);
 bool dynamic_stack_fault(struct task_struct *tsk, unsigned long address, bool *on_stack);
+struct task_struct *task_from_stack_address(unsigned long address);
 
 /*
  * Refill and charge for the used pages.
kernel/fork.c +67 -7

131 lines not on this page · raw message

mm/vmalloc.c +1 -1
index 39b7e118cbce..76955c101180 100644
@@ -76,7 +76,7 @@ early_param("nohugevmalloc", set_nohugevmalloc);
 static const bool vmap_allow_huge = false;
 #endif	/* CONFIG_HAVE_ARCH_HUGE_VMALLOC */
 
-bool is_vmalloc_addr(const void *x)
+noinstr bool is_vmalloc_addr(const void *x)
 {
 	unsigned long addr = (unsigned long)kasan_reset_tag(x);
 
Signature · 1 line
2.54.0.rc2.544.gc7ae2d5bb8-goog
maintainer
#11 lore raw

Re: [PATCH v2 10/13] fork: Store task pointer in unpopulated stack ptes

Quoted 3 lines · David Stevens

Store the task pointer in the ptes of the unpopulated pages of dynamic stacks, to allow the vm_struct pointer to be retrieved without relying on any locks or current.

You fail to explain why you can't use current. Changelogs have to describe the WHY and not the WHAT.

mm/vmalloc.c early_param("nohugevmalloc", set_nohugevmalloc); · line 81

Quoted 5 lines
mm/vmalloc.c +1 -1
-bool is_vmalloc_addr(const void *x)
+noinstr bool is_vmalloc_addr(const void *x)
 {
 	unsigned long addr = (unsigned long)kasan_reset_tag(x);

noinstr is not enough here unless this depends on !KASAN because kasan_reset_tag() is a 'static inline' which CLANG happily puts out of line into the instrumentable section...

maintainer
#12 lore raw

Re: [PATCH v2 10/13] fork: Store task pointer in unpopulated stack ptes

Quoted 7 lines · Thomas Gleixner
Quoted 3 lines · David Stevens

Store the task pointer in the ptes of the unpopulated pages of dynamic stacks, to allow the vm_struct pointer to be retrieved without relying on any locks or current.

You fail to explain why you can't use current. Changelogs have to describe the WHY and not the WHAT.

I obviously know why you can't use it. But the absence of a proper explanation and my disgust for the implementation bothered me enough to look deeper into it.

Let's look at the only problematic case:

      schedule()
          ....
          switch_to(prev, next)
              switch_to_asm(prev, next)
1)                switch(RSP)
                  __switch_to(prev, next)

2) this_cpu_write(current_task, next);

There is obviously a hole between #1 and #2 where 'current_task' is not giving the right answer. You work around that with this PTE storage magic which is admittedly smart, but completely overengineered and not necessary at all.

Why?

If you look at the above condensed context switch logic related to this problem thoroughly, you'll notice that there are three sources of information:

- prev: the task being scheduled out
- next: the task being scheduled in
- RSP: the stack pointer

Between #1 and #2 it cannot be determined whether RSP belongs to 'prev' or 'next' because 'next' is not exposed to the fault handler. But if it would be exposed it would allow to answer the question where RSP belongs to, no?

So the obvious _and_ simple solution _is_ to expose 'next':

      schedule()
          ....
          switch_to(prev, next)
1)            raw_cpu_write(next_task, next);
              switch_to_asm(prev, next)
2)                switch(RSP)
                  __switch_to(prev, next)

3) raw_cpu_write(current_task, next);

With that the stack fault handler logic becomes:

   curr = raw_cpu_read(current_task);
   addr = fred_event_data(regs);

   if (within_task_stack(addr, curr))
   	return handle_stack_fault(regs, curr);

   next = raw_cpu_read(next_task);
   if (curr != next && within_task_stack(addr, next)
   	return handle_stack_fault(regs, next);

return 0;

Which is correct at any point in time and that pattern works on _all_ supported architectures because it's all CPU local. All you need is one extra store. When done right that's ending up in the same cache line which is anyway dirtied by the context switch (i.e. current_task), so you won't even be able to measure the overhead.

Thanks,

tglx

---
Everything should be made as simple as possible, but not simpler. - Einstein

#13 lore raw

PATCH v2 11/13 x86/entry/fred: encode frame pointer on entry

In reply to David Stevens

Add missing ENCODE_FRAME_POINTER macro invocation into FRED_ENTER macro, to prevent the unwinder from encountering a NULL stack frame pointer when CONFIG_UNWINDER_FRAME_POINTER is enabled

Fixes 14619d912b65 ("x86/fred: FRED entry/exit and dispatch code")
---
 arch/x86/entry/entry_64_fred.S | 2 ++
 1 file changed, 2 insertions(+)
arch/x86/entry/entry_64_fred.S +2 -0
index 894f7f16eb80..119b8214748e 100644
@@ -7,6 +7,7 @@
 #include <linux/kvm_types.h>
 
 #include <asm/asm.h>
+#include <asm/frame.h>
 #include <asm/fred.h>
 #include <asm/segment.h>
 
@@ -19,6 +20,7 @@
 	UNWIND_HINT_END_OF_STACK
 	ANNOTATE_NOENDBR
 	PUSH_AND_CLEAR_REGS
+	ENCODE_FRAME_POINTER
 	movq	%rsp, %rdi	/* %rdi -> pt_regs */
 .endm
 
Signature · 1 line
2.54.0.rc2.544.gc7ae2d5bb8-goog
#14 lore raw

Re: [PATCH v2 11/13] x86/entry/fred: encode frame pointer on entry

Setting aside the rest of the RFC, should this patch be merged? If someone tries to use CONFIG_UNWINDER_FRAME_POINTER on a system with FRED, they'll get a "kernel stack regs ... has bad 'bp' value" warning in the logs the first time the unwinder runs.

Thanks,
David

Quoted 34 lines · David Stevens

Add missing ENCODE_FRAME_POINTER macro invocation into FRED_ENTER macro, to prevent the unwinder from encountering a NULL stack frame pointer when CONFIG_UNWINDER_FRAME_POINTER is enabled

Fixes 14619d912b65 ("x86/fred: FRED entry/exit and dispatch code")
---
 arch/x86/entry/entry_64_fred.S | 2 ++
 1 file changed, 2 insertions(+)
arch/x86/entry/entry_64_fred.S +2 -1
index 894f7f16eb80..119b8214748e 100644
@@ -7,6 +7,7 @@
 #include <linux/kvm_types.h>
 
 #include <asm/asm.h>
+#include <asm/frame.h>
 #include <asm/fred.h>
 #include <asm/segment.h>
 
@@ -19,6 +20,7 @@
        UNWIND_HINT_END_OF_STACK
        ANNOTATE_NOENDBR
        PUSH_AND_CLEAR_REGS
+       ENCODE_FRAME_POINTER
        movq    %rsp, %rdi      /* %rdi -> pt_regs */
 .endm
 
--

2.54.0.rc2.544.gc7ae2d5bb8-goog

maintainer Acked-by
#15 lore raw

Re: [PATCH v2 11/13] x86/entry/fred: encode frame pointer on entry

Quoted 40 lines · David Stevens

Setting aside the rest of the RFC, should this patch be merged? If someone tries to use CONFIG_UNWINDER_FRAME_POINTER on a system with FRED, they'll get a "kernel stack regs ... has bad 'bp' value" warning in the logs the first time the unwinder runs.

Thanks,
David

Quoted 31 lines · David Stevens

Add missing ENCODE_FRAME_POINTER macro invocation into FRED_ENTER macro, to prevent the unwinder from encountering a NULL stack frame pointer when CONFIG_UNWINDER_FRAME_POINTER is enabled

Fixes 14619d912b65 ("x86/fred: FRED entry/exit and dispatch code")
---
 arch/x86/entry/entry_64_fred.S | 2 ++
 1 file changed, 2 insertions(+)
arch/x86/entry/entry_64_fred.S +2 -0
index 894f7f16eb80..119b8214748e 100644
@@ -7,6 +7,7 @@
 #include <linux/kvm_types.h>
 
 #include <asm/asm.h>
+#include <asm/frame.h>
 #include <asm/fred.h>
 #include <asm/segment.h>
 
@@ -19,6 +20,7 @@
        UNWIND_HINT_END_OF_STACK
        ANNOTATE_NOENDBR
        PUSH_AND_CLEAR_REGS
+       ENCODE_FRAME_POINTER
        movq    %rsp, %rdi      /* %rdi -> pt_regs */
 .endm
 
Quoted 4 lines
Quoted 3 lines

--
2.54.0.rc2.544.gc7ae2d5bb8-goog

Yes, it should.

The macroization is weird and I'm somewhat confused about how it is done but I also don't really care because it isn't like frame pointer use is common on x86-64.

maintainer
#16 lore raw

Re: [PATCH v2 11/13] x86/entry/fred: encode frame pointer on entry

Quoted 45 lines · H. Peter Anvin
Quoted 40 lines · David Stevens

Setting aside the rest of the RFC, should this patch be merged? If someone tries to use CONFIG_UNWINDER_FRAME_POINTER on a system with FRED, they'll get a "kernel stack regs ... has bad 'bp' value" warning in the logs the first time the unwinder runs.

Thanks,
David

Quoted 31 lines · David Stevens

Add missing ENCODE_FRAME_POINTER macro invocation into FRED_ENTER macro, to prevent the unwinder from encountering a NULL stack frame pointer when CONFIG_UNWINDER_FRAME_POINTER is enabled

Fixes 14619d912b65 ("x86/fred: FRED entry/exit and dispatch code")

---
arch/x86/entry/entry_64_fred.S | 2 ++

1 file changed, 2 insertions(+)
arch/x86/entry/entry_64_fred.S +2 -0
index 894f7f16eb80..119b8214748e 100644
@@ -7,6 +7,7 @@
 #include <linux/kvm_types.h>
 
 #include <asm/asm.h>
+#include <asm/frame.h>
 #include <asm/fred.h>
 #include <asm/segment.h>
 
@@ -19,6 +20,7 @@
       UNWIND_HINT_END_OF_STACK
       ANNOTATE_NOENDBR
       PUSH_AND_CLEAR_REGS
+       ENCODE_FRAME_POINTER
       movq    %rsp, %rdi      /* %rdi -> pt_regs */

.endm

Quoted 8 lines
Quoted 4 lines
Quoted 3 lines

--
2.54.0.rc2.544.gc7ae2d5bb8-goog

Yes, it should.

The macroization is weird and I'm somewhat confused about how it is done but I also don't really care because it isn't like frame pointer use is common on x86-64.

I have the same patch in my private branch, but didn’t get motivated to send it.

merge bot Applied
#17 lore raw

[tip: x86/entry] x86/entry/fred: Encode frame pointer on entry

In reply to David Stevens
The following commit has been merged into the x86/entry branch of tip:

The following commit has been merged into the x86/entry branch of tip:

Commit-ID:     dab01c597f6bd40e0efe7da967b8374ca1971b79
Gitweb:        https://git.kernel.org/tip/dab01c597f6bd40e0efe7da967b8374ca1971b79
Author:        David Stevens <[email protected]>
AuthorDate:    Fri, 24 Apr 2026 12:14:54 -07:00
Committer:     Dave Hansen <[email protected]>
CommitterDate: Tue, 14 Jul 2026 11:08:12 -07:00

x86/entry/fred: Encode frame pointer on entry

Add missing ENCODE_FRAME_POINTER macro invocation into FRED_ENTER macro, to prevent the unwinder from encountering a NULL stack frame pointer when CONFIG_UNWINDER_FRAME_POINTER is enabled

Fixes 14619d912b65 ("x86/fred: FRED entry/exit and dispatch code")
---
 arch/x86/entry/entry_64_fred.S | 2 ++
 1 file changed, 2 insertions(+)
arch/x86/entry/entry_64_fred.S +2 -0
index 0d2768a..b98f894 100644
@@ -7,6 +7,7 @@
 #include <linux/kvm_types.h>
 
 #include <asm/asm.h>
+#include <asm/frame.h>
 #include <asm/fred.h>
 #include <asm/segment.h>
 
@@ -19,6 +20,7 @@
 	UNWIND_HINT_END_OF_STACK
 	ANNOTATE_NOENDBR
 	PUSH_AND_CLEAR_REGS
+	ENCODE_FRAME_POINTER
 	movq	%rsp, %rdi	/* %rdi -> pt_regs */
 .endm
 
#18 lore raw

PATCH v2 12/13 x86: Add support for dynamic kernel stacks via FRED

In reply to David Stevens

Add support for dynamic kernel stack faults by handling #PFs from CPL 0 on stack level 1. Since we can't sleep while on a per-CPU stack, any page faults that didn't originate in an atomic context need to be bounced back to the originating stack.

With dynamic kernel stacks, the processor pushing data onto the kernel thread stack can cause a page fault. The SDM says in the #DF section that the processor should be able to handle these exceptions serially. However, this does not seem to actually be handled reliably.

With KVM, I've observed timer interrupts dropped. The corresponding bit in VIRR is cleared and the ISR bit in the APIC is set before the #PF is delivered, but the interrupt handler is not invoked after the kernel stack fault is resolved. On bare metal, I've observed frequent hangs due to threads getting stuck on folio_wait_bit_common. I haven't traced this to an exact interrupt being lost, but moving interrupts to stack level 1 reduces boot failures from >10% to 0 in 1000s of attempts.

To work around this, external interrupts are also moved to stack level 1, and unconditionally bounced back to the originating stack.

Bouncing page faults and external interrupts through stack level 1 while in CPL 0 adds a small but non-trivial overhead to those paths. The shared entry point for events received in CPL 0 also becomes slightly more expensive, due to the need to detect page faults and external interrupts.

Since enabling HAVE_ARCH_DYNAMIC_STACK requires unconditional support, enabling the config is done in the next patch that adds dynamic stack support for traditional interrupt delivery.

---
 arch/x86/entry/entry_64_fred.S    | 55 +++++++++++++++++++++++++++++++
 arch/x86/include/asm/pgtable_64.h | 36 ++++++++++++++++++++
 arch/x86/include/asm/traps.h      |  5 +++
 arch/x86/kernel/fred.c            | 20 ++++++++---
 arch/x86/mm/dump_pagetables.c     | 14 +++++---
 arch/x86/mm/fault.c               | 53 +++++++++++++++++++++++++++++
 6 files changed, 174 insertions(+), 9 deletions(-)
arch/x86/entry/entry_64_fred.S +55 -0
index 119b8214748e..7202655ef662 100644
@@ -54,7 +54,62 @@ SYM_CODE_END(asm_fred_entrypoint_user)
 	.org asm_fred_entrypoint_user + 256, 0xcc
 SYM_CODE_START_NOALIGN(asm_fred_entrypoint_kernel)
 	FRED_ENTER
+
+#ifdef CONFIG_DYNAMIC_STACK
+	/* Extract event type and vector from augmented SS. */
+	movl	(SS + 4)(%rsp), %esi
+	andl	$0x000f00ff, %esi
+
+	/* Check if event type is hardware exception and vector is #PF. */
+	cmpl	$0x0003000e, %esi
+	jne	.Lcheck_for_extint
+
+	call	handle_dynamic_stack_kernel_faults
+	testq	%rax, %rax
+	jz	.Lentrypoint_done
+	cmpq	%rax, %rsp
+	je	.Lskip_stack_switch
+	jmp	.Ldo_stack_switch
+
+.Lcheck_for_extint:
+	/* Check if event type is external interrupt. */
+	andl	$0xf0000, %esi
+	testl	%esi, %esi
+	jne	.Lcall_primary_entry
+	call	switch_to_kstack
+
+.Ldo_stack_switch:
+#ifdef CONFIG_DEBUG_ENTRY
+	/*
+	 * We should only do a stack switch for an external interrupt or a page
+	 * fault in a non-atomic context. These should only ever happen in user
+	 * space or from a regular kernel stack (i.e. CSL == 0).
+	 */
+	movw	(CS + 2)(%rsp), %si
+	testw	$0x3, %si
+	jz	.Lcsl_ok
+	ud2
+.Lcsl_ok:
+#endif
+	movq	%rax, %rsp
+
+	UNWIND_HINT_REGS
+	ENCODE_FRAME_POINTER
+
+	mov	$MSR_IA32_FRED_CONFIG, %ecx
+	rdmsr
+	andl	$~0x3, %eax
+	wrmsr
+
+	movq	%rsp, %rdi
+#endif
+
+.Lskip_stack_switch:
+	movq	%rsp, %rdi
+.Lcall_primary_entry:
 	call	fred_entry_from_kernel
+
+.Lentrypoint_done:
 	FRED_EXIT
 	ERETS
 SYM_CODE_END(asm_fred_entrypoint_kernel)
arch/x86/include/asm/pgtable_64.h +36 -0
index ce45882ccd07..fbb042c89d13 100644
@@ -237,6 +237,42 @@ static inline void native_pgd_clear(pgd_t *pgd)
 #define __swp_entry_to_pte(x)		(__pte((x).val))
 #define __swp_entry_to_pmd(x)		(__pmd((x).val))
 
+#ifdef CONFIG_DYNAMIC_STACK
+
+/*
+ * Skip the present bit. And skip dirty and accessed bits due to
+ * erratum where they can be incorrectly set on non-present ptes.
+ *
+ * Also skip bit 8, which is used for pte_present for PROT_NONE. This
+ * isn't necessary in the strictest sense since PROT_NONE doesn't apply
+ * to kernel PTEs, but it's easier to let pte_present just continue
+ * to work.
+ */
+#define KPTE_AVAILABLE_DATA_BITS 58
+
+static inline pte_t make_data_kpte(unsigned long val)
+{
+	unsigned long low_part, mid_part, high_part;
+
+	low_part = (val & 0xf) << 1;
+	mid_part = (val & 0x10) << 3;
+	high_part = (val & ~0x1f) << 4;
+
+	return __pte(low_part | mid_part | high_part);
+}
+
+static inline unsigned long unpack_data_kpte(pte_t pte)
+{
+	unsigned long val = pte_val(pte), high_part, mid_part, low_part;
+
+	low_part = (val >> 1) & 0xf;
+	mid_part = (val >> 3) & 0x10;
+	high_part = (val >> 4) & ~0x1f;
+
+	return low_part | mid_part | high_part;
+}
+#endif /* CONFIG_DYNAMIC_STACK */
+
 extern void cleanup_highmap(void);
 
 #define HAVE_ARCH_UNMAPPED_AREA
arch/x86/include/asm/traps.h +5 -0
index 3f24cc472ce9..6b55eb91aea6 100644
@@ -15,6 +15,11 @@ asmlinkage __visible notrace struct pt_regs *sync_regs(struct pt_regs *eregs);
 asmlinkage __visible notrace
 struct pt_regs *fixup_bad_iret(struct pt_regs *bad_regs);
 asmlinkage __visible noinstr struct pt_regs *vc_switch_off_ist(struct pt_regs *eregs);
+
+#ifdef CONFIG_DYNAMIC_STACK
+asmlinkage __visible noinstr unsigned long switch_to_kstack(struct pt_regs *regs);
+asmlinkage __visible noinstr unsigned long handle_dynamic_stack_kernel_faults(struct pt_regs *regs);
+#endif
 #endif
 
 extern int ibt_selftest(void);
arch/x86/kernel/fred.c +15 -5
index e736b19e18de..01d727420d1f 100644
@@ -9,6 +9,8 @@
 
 /* #DB in the kernel would imply the use of a kernel debugger. */
 #define FRED_DB_STACK_LEVEL		1UL
+#define FRED_PF_STACK_LEVEL		1UL
+#define FRED_INT_STACK_LEVEL		1UL
 #define FRED_NMI_STACK_LEVEL		2UL
 #define FRED_MC_STACK_LEVEL		2UL
 /*
@@ -25,6 +27,11 @@
 DEFINE_PER_CPU(unsigned long, fred_rsp0);
 EXPORT_PER_CPU_SYMBOL(fred_rsp0);
 
+#define FRED_CONFIG_VAL(int_stklvl) \
+	(FRED_CONFIG_REDZONE /* Reserve for CALL emulation */ | \
+	 FRED_CONFIG_INT_STKLVL(int_stklvl) | \
+	 FRED_CONFIG_ENTRYPOINT(asm_fred_entrypoint_user))
+
 void cpu_init_fred_exceptions(void)
 {
 	/* When FRED is enabled by default, remove this log message */
@@ -44,11 +51,7 @@ void cpu_init_fred_exceptions(void)
 	 */
 	loadsegment(ss, __KERNEL_DS);
 
-	wrmsrq(MSR_IA32_FRED_CONFIG,
-	       /* Reserve for CALL emulation */
-	       FRED_CONFIG_REDZONE |
-	       FRED_CONFIG_INT_STKLVL(0) |
-	       FRED_CONFIG_ENTRYPOINT(asm_fred_entrypoint_user));
+	wrmsrq(MSR_IA32_FRED_CONFIG, FRED_CONFIG_VAL(0));
 
 	wrmsrq(MSR_IA32_FRED_STKLVLS, 0);
 
@@ -84,8 +87,15 @@ void cpu_init_fred_rsps(void)
 	       FRED_STKLVL(X86_TRAP_DB,  FRED_DB_STACK_LEVEL) |
 	       FRED_STKLVL(X86_TRAP_NMI, FRED_NMI_STACK_LEVEL) |
 	       FRED_STKLVL(X86_TRAP_MC,  FRED_MC_STACK_LEVEL) |
+#ifdef CONFIG_DYNAMIC_STACK
+	       FRED_STKLVL(X86_TRAP_PF,  FRED_PF_STACK_LEVEL) |
+#endif
 	       FRED_STKLVL(X86_TRAP_DF,  FRED_DF_STACK_LEVEL));
 
+#ifdef CONFIG_DYNAMIC_STACK
+	wrmsrq(MSR_IA32_FRED_CONFIG, FRED_CONFIG_VAL(FRED_INT_STACK_LEVEL));
+#endif
+
 	/* The FRED equivalents to IST stacks... */
 	wrmsrq(MSR_IA32_FRED_RSP1, __this_cpu_ist_top_va(DB));
 	wrmsrq(MSR_IA32_FRED_RSP2, __this_cpu_ist_top_va(NMI));
arch/x86/mm/dump_pagetables.c +10 -4
index 2afa7a23340e..5c33c33e93fe 100644
@@ -306,11 +306,17 @@ static void note_page(struct ptdump_state *pt_st, unsigned long addr, int level,
 	static const char units[] = "BKMGTPE";
 	struct seq_file *m = st->seq;
 
-	new_prot = val & PTE_FLAGS_MASK;
-	if (!val)
+	/* Ignore prot/eff from data kptes. */
+	if (val & _PAGE_PRESENT || addr < address_markers[KERNEL_SPACE_NR].start_address) {
+		new_prot = val & PTE_FLAGS_MASK;
+		if (!val)
+			new_eff = 0;
+		else
+			new_eff = st->prot_levels[level];
+	} else {
+		new_prot = 0;
 		new_eff = 0;
-	else
-		new_eff = st->prot_levels[level];
+	}
 
 	/*
 	 * If we have a "break" in the series, we need to flush the state that
arch/x86/mm/fault.c +53 -0

59 lines not on this page · raw message

Signature · 1 line
2.54.0.rc2.544.gc7ae2d5bb8-goog
maintainer
#19 lore raw

Re: [PATCH v2 12/13] x86: Add support for dynamic kernel stacks via FRED

On Fri, Apr 24 2026 at 12:14, David Stevens wrote:

As promised, I've came around to look at this part too.

arch/x86/mm/fault.c handle_page_fault(struct pt_regs *regs, unsigned long error_code, · line 1488

Quoted 6 lines
arch/x86/mm/fault.c +6 -0
+#ifdef CONFIG_DYNAMIC_STACK
+
+static noinstr unsigned long copy_stack_data(struct pt_regs *regs)
+{
+	unsigned long new_sp;
+	unsigned long data_len;

Just a minor issue. Please read and comply with the coding standards documented in https://docs.kernel.org/process/maintainer-tip.html

Quoted 16 lines
arch/x86/mm/fault.c +16 -0
+
+	new_sp = regs->sp - (FRED_CONFIG_REDZONE_AMOUNT << 6);
+	new_sp &= FRED_STACK_FRAME_RSP_MASK;
+	data_len = sizeof(struct fred_frame);
+	new_sp -= data_len;
+
+	memcpy((void *)new_sp, regs, data_len);
+
+	return new_sp;
+}
+
+__visible noinstr unsigned long switch_to_kstack(struct pt_regs *regs)
+{
+	return copy_stack_data(regs);
+}
+

arch/x86/mm/fault.c handle_page_fault(struct pt_regs *regs, unsigned long error_code, · line 1514

Quoted 10 lines
arch/x86/mm/fault.c +10 -0
+#define ALIGN_TO_STACK(addr) ((addr) & ~(THREAD_ALIGN - 1))
+
+__visible noinstr unsigned long handle_dynamic_stack_kernel_faults(struct pt_regs *regs)
+{
+	unsigned long address;
+	struct task_struct *tsk;
+	bool on_stack;
+
+	address = fred_event_data(regs);
+	if (fault_in_kernel_space(address) && !in_nmi()) {

Assume the following scenario:

CPU runs in user space
NMI is raised
NMI runs on SL0
NMI execution triggers the stack guard page

Don't tell me that's unlikely. As discussed before unlikely does not exist. You just have to enable enough debug muck, build with a compiler which aggressively out of lines code (hint CLANG) and then end up in a deep perf/trace/bpf callchain.

arch/x86/mm/fault.c handle_page_fault(struct pt_regs *regs, unsigned long error_code, · line 1527

Quoted 13 lines
arch/x86/mm/fault.c +13 -0
+		tsk = task_from_stack_address(address);
+
+		if (tsk && dynamic_stack_fault(tsk, address, &on_stack)) {
+			WARN_ON_ONCE(tsk != current &&
+				     ALIGN_TO_STACK(regs->sp) != ALIGN_TO_STACK(address));
+			return 0;
+		}
+	}
+
+	/*
+	 * The regular fault handler won't sleep when executing in an
+	 * atomic context, so we can complete the #PF directly on the
+	 * #PF stack.

Q: What guarantees you that in_atomic() is giving you always the right answer?
A: Nothing

Why?

If there is a #PF (not caused by the thread stack guard) on a SL>0 stack _before_ the kernel reached the point where it increments preempt_count, then in_atomic() returns false.

As a consequence you copy stack data around to the same place, which should be benign, but it is well understood that memcpy() source and destination areas _must_ not overlap. That's UB, no?

I know that should not happen, but that doesn't make it less UB :)

arch/x86/mm/fault.c handle_page_fault(struct pt_regs *regs, unsigned long error_code, · line 1532

Quoted 5 lines
arch/x86/mm/fault.c +5 -0
+	 */
+	if (in_atomic())
+		return (unsigned long)regs;
+	else
+		return copy_stack_data(regs);

Thanks,

tglx

#20 lore raw

Re: [PATCH v2 12/13] x86: Add support for dynamic kernel stacks via FRED

Quoted 4 lines · Thomas Gleixner

On Fri, Apr 24 2026 at 12:14, David Stevens wrote:

As promised, I've came around to look at this part too.

Thanks for the detailed review.

Quoted 44 lines

arch/x86/mm/fault.c handle_page_fault(struct pt_regs *regs, unsigned long error_code, · line 1488

Quoted 6 lines
arch/x86/mm/fault.c +6 -0
+#ifdef CONFIG_DYNAMIC_STACK
+
+static noinstr unsigned long copy_stack_data(struct pt_regs *regs)
+{
+     unsigned long new_sp;
+     unsigned long data_len;

Just a minor issue. Please read and comply with the coding standards documented in https://docs.kernel.org/process/maintainer-tip.html

Quoted 16 lines
arch/x86/mm/fault.c +16 -0
+
+     new_sp = regs->sp - (FRED_CONFIG_REDZONE_AMOUNT << 6);
+     new_sp &= FRED_STACK_FRAME_RSP_MASK;
+     data_len = sizeof(struct fred_frame);
+     new_sp -= data_len;
+
+     memcpy((void *)new_sp, regs, data_len);
+
+     return new_sp;
+}
+
+__visible noinstr unsigned long switch_to_kstack(struct pt_regs *regs)
+{
+     return copy_stack_data(regs);
+}
+

arch/x86/mm/fault.c handle_page_fault(struct pt_regs *regs, unsigned long error_code, · line 1514

Quoted 10 lines
arch/x86/mm/fault.c +10 -0
+#define ALIGN_TO_STACK(addr) ((addr) & ~(THREAD_ALIGN - 1))
+
+__visible noinstr unsigned long handle_dynamic_stack_kernel_faults(struct pt_regs *regs)
+{
+     unsigned long address;
+     struct task_struct *tsk;
+     bool on_stack;
+
+     address = fred_event_data(regs);
+     if (fault_in_kernel_space(address) && !in_nmi()) {

Assume the following scenario:

CPU runs in user space
NMI is raised
NMI runs on SL0
NMI execution triggers the stack guard page

Quoted 4 lines

Don't tell me that's unlikely. As discussed before unlikely does not exist. You just have to enable enough debug muck, build with a compiler which aggressively out of lines code (hint CLANG) and then end up in a deep perf/trace/bpf callchain.

That is definitely a situation that could happen, I hadn't thought of it. Luckily, I don't think we need NMI-safe re-entrancy to handle this, since an NMI on SL0 couldn't have interrupted a dynamic stack fault.

Quoted 23 lines

arch/x86/mm/fault.c handle_page_fault(struct pt_regs *regs, unsigned long error_code, · line 1527

Quoted 13 lines
arch/x86/mm/fault.c +13 -0
+             tsk = task_from_stack_address(address);
+
+             if (tsk && dynamic_stack_fault(tsk, address, &on_stack)) {
+                     WARN_ON_ONCE(tsk != current &&
+                                  ALIGN_TO_STACK(regs->sp) != ALIGN_TO_STACK(address));
+                     return 0;
+             }
+     }
+
+     /*
+      * The regular fault handler won't sleep when executing in an
+      * atomic context, so we can complete the #PF directly on the
+      * #PF stack.

Q: What guarantees you that in_atomic() is giving you always the right answer?
A: Nothing

Why?

If there is a #PF (not caused by the thread stack guard) on a SL>0 stack _before_ the kernel reached the point where it increments preempt_count, then in_atomic() returns false.

Quoted 5 lines

As a consequence you copy stack data around to the same place, which should be benign, but it is well understood that memcpy() source and destination areas _must_ not overlap. That's UB, no?

I know that should not happen, but that doesn't make it less UB :)

That sort of fault would probably end up either with a page_fault_oops() or risk a context switch with SL>0, but we shouldn't make debugging harder with UB.

Thanks,
David

Quoted 10 lines

arch/x86/mm/fault.c handle_page_fault(struct pt_regs *regs, unsigned long error_code, · line 1532

Quoted 5 lines
arch/x86/mm/fault.c +5 -0
+      */
+     if (in_atomic())
+             return (unsigned long)regs;
+     else
+             return copy_stack_data(regs);

Thanks,

tglx

maintainer
#21 lore raw

Re: [PATCH v2 12/13] x86: Add support for dynamic kernel stacks via FRED

Quoted 5 lines · David Stevens

On Fri, Jun 26, 2026 at 3:39 PM Thomas Gleixner <[email protected]> wrote:
That is definitely a situation that could happen, I hadn't thought of it. Luckily, I don't think we need NMI-safe re-entrancy to handle this, since an NMI on SL0 couldn't have interrupted a dynamic stack fault.

Correct.

Quoted 9 lines
Quoted 5 lines

As a consequence you copy stack data around to the same place, which should be benign, but it is well understood that memcpy() source and destination areas _must_ not overlap. That's UB, no?

I know that should not happen, but that doesn't make it less UB :)

That sort of fault would probably end up either with a page_fault_oops() or risk a context switch with SL>0, but we shouldn't make debugging harder with UB.

Definitely not :)

You can avoid quite some of the problems right in the ASM entry code. Instead of checking event type/vector and then going to the stack switch logic, you can check the stack level on which the event happened first. If it's not 0, then there is nothing to do.

maintainer
#22 lore raw

Re: [PATCH v2 12/13] x86: Add support for dynamic kernel stacks via FRED

Quoted 19 lines · Thomas Gleixner
Quoted 5 lines · David Stevens

On Fri, Jun 26, 2026 at 3:39 PM Thomas Gleixner <[email protected]> wrote:
That is definitely a situation that could happen, I hadn't thought of it. Luckily, I don't think we need NMI-safe re-entrancy to handle this, since an NMI on SL0 couldn't have interrupted a dynamic stack fault.

Correct.

Quoted 9 lines
Quoted 5 lines

As a consequence you copy stack data around to the same place, which should be benign, but it is well understood that memcpy() source and destination areas _must_ not overlap. That's UB, no?

I know that should not happen, but that doesn't make it less UB :)

That sort of fault would probably end up either with a page_fault_oops() or risk a context switch with SL>0, but we shouldn't make debugging harder with UB.

Quoted 8 lines

Definitely not :)

You can avoid quite some of the problems right in the ASM entry code. Instead of checking event type/vector and then going to the stack switch logic, you can check the stack level on which the event happened first. If it's not 0, then there is nothing to do.

To be fair, for this *specific* case, memmove() does exist for a reason :)

maintainer
#23 lore raw

Re: [PATCH v2 12/13] x86: Add support for dynamic kernel stacks via FRED

Quoted 7 lines · H. Peter Anvin
Quoted 6 lines · Thomas Gleixner
Quoted 3 lines

That sort of fault would probably end up either with a page_fault_oops() or risk a context switch with SL>0, but we shouldn't make debugging harder with UB.

Definitely not :)

Quoted 8 lines
Quoted 6 lines

You can avoid quite some of the problems right in the ASM entry code. Instead of checking event type/vector and then going to the stack switch logic, you can check the stack level on which the event happened first. If it's not 0, then there is nothing to do.

To be fair, for this *specific* case, memmove() does exist for a reason :)

It does, but why bother doing actual work, if you know upfront that it's not required :)

maintainer
#24 lore raw

Re: [PATCH v2 12/13] x86: Add support for dynamic kernel stacks via FRED

Quoted 15 lines · Thomas Gleixner
Quoted 14 lines · H. Peter Anvin
Quoted 12 lines · Thomas Gleixner
Quoted 3 lines

That sort of fault would probably end up either with a page_fault_oops() or risk a context switch with SL>0, but we shouldn't make debugging harder with UB.

Definitely not :)

You can avoid quite some of the problems right in the ASM entry code. Instead of checking event type/vector and then going to the stack switch logic, you can check the stack level on which the event happened first. If it's not 0, then there is nothing to do.

Quoted 4 lines
Quoted 1 line

To be fair, for this *specific* case, memmove() does exist for a reason :)

It does, but why bother doing actual work, if you know upfront that it's not required :)

Of course.

Then there is the very open question if this thing has legs at all. I'm still interested in using SL1 for the interrupt stack as I think that could be a performance boost.

maintainer
#25 lore raw

Re: [PATCH v2 12/13] x86: Add support for dynamic kernel stacks via FRED

Quoted 5 lines · H. Peter Anvin
Quoted 4 lines · Thomas Gleixner
Quoted 2 lines · H. Peter Anvin

To be fair, for this *specific* case, memmove() does exist for a reason :)

Quoted 7 lines
Quoted 2 lines

It does, but why bother doing actual work, if you know upfront that it's not required :)

Of course.

Then there is the very open question if this thing has legs at all.

The jury is still out on that :)

Quoted 2 lines

I'm still interested in using SL1 for the interrupt stack as I think that could be a performance boost.

I'm not convinced that it will give you a measurable boost.

It spares the trivial stack switch to/from the interrupt stack in call_on_stack() and the hardirq_stack_inuse logic around it. With an interrupt rate of 50KHz and 10 instructions spared, that gives you 0.025% performance win on a 2GHz machine.

But if need_resched() is set the return path has to do the full stack pivot to the SL0 stack. That's a memcpy() of sizeof(fred_frame) + the extra stack consumption including common_interrupt(), which accumulates to ~256 bytes, i.e. four cache lines. Plus the FRED_CONFIG MSR write.

IOW, if need_resched() is set, it's a guaranteed loss and latency sensitive scenarios including RT will notice.

Thanks,

tglx

#26 lore raw

PATCH v2 13/13 x86: Add support for dynamic kernel stacks via IST

In reply to David Stevens

On hardware that doesn't support FRED, use ISTs to support dynamic kernel stacks. In the same way as we do when using FRED, any regular #PF gets manually moved back onto the original stack. Additionally, we take the similar approach as we do with FRED to avoid issues with interrupt re-delivery and handle external interrupts on an IST stack.

The fact that IST stacks aren't reentrant means we have to be very careful to avoid triggering a #PF while the #PF IST is being used. Since NMIs can trigger #PFs, we have the NMI handler temporarily install a secondary #PF IST stack if it detects it came from the #PF IST stack, to avoid clobbering that stack. Note that although iret unmasking of NMIs can cause us to get a second NMI while an NMI is on the #PF IST stack, the actual handling of that secondary NMI will be delayed until after the original NMI (and thus the #PF) is resolved. As such, one extra #PF IST stack is sufficient to resolve reentrancy issues with respect to NMIs.

For #DB exceptions, we make sure that all code that executes on the #PF IST stack is noinstr. Unfortunately this is not 100% bulletproof, since the handler needs to access data outside of cpu_entry_area (e.g. current, current's stack, vmap stack page tables), and the user could have set hardware breakpoints on accesses to those addresses. Rather than handle this edge case that should only occur during manual debugging, we just detect reentrancy on the #PF IST and abort.

It is possible for #MCE to occur on the #PF IST stack, but the #MCE handler shouldn't generate new #PFs. The reentrancy check on the #PF stack will trigger if any recoverable #MCEs do generate #PFs - if there are actually reports of it happening, we can address it then.

Bouncing all #PF and external interrupts through IST stacks adds some overhead. However, such events from userspace already had to bounce through the CPU entry stack, so introducing ISTs only adds notable overhead for #PFs and external interrupts that occur while in CPL 0.

---
 arch/x86/Kconfig                      |  1 +
 arch/x86/entry/entry_64.S             | 49 +++++++++++++++++--
 arch/x86/include/asm/cpu_entry_area.h | 18 +++++++
 arch/x86/include/asm/idtentry.h       | 38 ++++++++++++++-
 arch/x86/include/asm/page_64_types.h  | 10 +++-
 arch/x86/include/asm/processor.h      |  6 +++
 arch/x86/kernel/cpu/common.c          | 11 +++++
 arch/x86/kernel/dumpstack_64.c        | 10 +++-
 arch/x86/kernel/idt.c                 | 57 +++++++++++++---------
 arch/x86/kernel/nmi.c                 |  9 ++++
 arch/x86/lib/usercopy.c               |  9 ++++
 arch/x86/mm/cpu_entry_area.c          | 17 +++++++
 arch/x86/mm/fault.c                   | 70 ++++++++++++++++++++++-----
 13 files changed, 262 insertions(+), 43 deletions(-)
arch/x86/Kconfig +1 -0
index e2df1b147184..182fda721b0d 100644
@@ -212,6 +212,7 @@ config X86
 	select HAVE_ARCH_USERFAULTFD_WP         if X86_64 && USERFAULTFD
 	select HAVE_ARCH_USERFAULTFD_MINOR	if X86_64 && USERFAULTFD
 	select HAVE_ARCH_VMAP_STACK		if X86_64
+	select HAVE_ARCH_DYNAMIC_STACK		if X86_64 && !XEN_PV
 	select HAVE_ARCH_RANDOMIZE_KSTACK_OFFSET
 	select HAVE_ARCH_WITHIN_STACK_FRAMES
 	select HAVE_ASM_MODVERSIONS
arch/x86/entry/entry_64.S +45 -4

89 lines not on this page · raw message

arch/x86/include/asm/cpu_entry_area.h +18 -0
index 462fc34f1317..5bce3259edee 100644
@@ -26,6 +26,12 @@
 	char	DB_stack[EXCEPTION_STKSZ];			\
 	char	MCE_stack_guard[guardsize];			\
 	char	MCE_stack[EXCEPTION_STKSZ];			\
+	char	PF_stack_guard[guardsize];			\
+	char	PF_stack[EXCEPTION_STKSZ];			\
+	char	PF2_stack_guard[guardsize];			\
+	char	PF2_stack[EXCEPTION_STKSZ];			\
+	char	UDI_stack_guard[guardsize];			\
+	char	UDI_stack[EXCEPTION_STKSZ];			\
 	char	VC_stack_guard[guardsize];			\
 	char	VC_stack[optional_stack_size];			\
 	char	VC2_stack_guard[guardsize];			\
@@ -50,6 +56,9 @@ enum exception_stack_ordering {
 	ESTACK_NMI,
 	ESTACK_DB,
 	ESTACK_MCE,
+	ESTACK_PF,
+	ESTACK_PF2,
+	ESTACK_UDI,
 	ESTACK_VC,
 	ESTACK_VC2,
 	N_EXCEPTION_STACKS
@@ -144,6 +153,15 @@ static __always_inline struct entry_stack *cpu_entry_stack(int cpu)
 	return &get_cpu_entry_area(cpu)->entry_stack_page.stack;
 }
 
+#ifdef CONFIG_DYNAMIC_STACK
+bool is_pf_ist_stack(unsigned long addr);
+#else
+static inline bool is_pf_ist_stack(unsigned long addr)
+{
+	return false;
+}
+#endif
+
 #define __this_cpu_ist_top_va(name)					\
 	CEA_ESTACK_TOP(__this_cpu_read(cea_exception_stacks), name)
 
arch/x86/include/asm/idtentry.h +36 -2
index 42bf6a58ec36..d8c846d28a1d 100644
@@ -163,6 +163,16 @@ noinstr void fred_##func(struct pt_regs *regs)
 #define DECLARE_IDTENTRY_RAW_ERRORCODE(vector, func)			\
 	DECLARE_IDTENTRY_ERRORCODE(vector, func)
 
+/**
+ * DECLARE_IDTENTRY_PF - Declare functions for page fault entry point
+ * @vector:	Vector number (ignored for C)
+ * @func:	Function name of the entry point
+ *
+ * Maps to @DECLARE_IDTENTRY_ERRORCODE().
+ */
+#define DECLARE_IDTENTRY_PF(vector, func)			\
+	DECLARE_IDTENTRY_RAW_ERRORCODE(vector, func)
+
 /**
  * DEFINE_IDTENTRY_RAW_ERRORCODE - Emit code for raw IDT entry points
  * @func:	Function name of the entry point
@@ -391,6 +401,15 @@ static __always_inline void __##func(struct pt_regs *regs)
 #define DEFINE_IDTENTRY_DF(func)					\
 	DEFINE_IDTENTRY_RAW_ERRORCODE(func)
 
+/**
+ * DEFINE_IDTENTRY_PF - Emit code for page fault
+ * @func:	Function name of the entry point
+ *
+ * Maps to DEFINE_IDTENTRY_RAW_ERRORCODE
+ */
+#define DEFINE_IDTENTRY_PF(func)					\
+	DEFINE_IDTENTRY_RAW_ERRORCODE(func)
+
 /**
  * DEFINE_IDTENTRY_VC_KERNEL - Emit code for VMM communication handler
  *			       when raised from kernel mode
@@ -480,6 +499,15 @@ void fred_install_sysvec(unsigned int vector, const idtentry_t function);
 #define DECLARE_IDTENTRY_ERRORCODE(vector, func)			\
 	idtentry vector asm_##func func has_error_code=1
 
+#ifdef CONFIG_DYNAMIC_STACK
+#define DECLARE_IDTENTRY_PF(vector, func)				\
+	idtentry vector asm_##func func has_error_code=1		\
+	kernel_reentry_fn=handle_dynamic_stack_kernel_faults
+#else
+#define DECLARE_IDTENTRY_PF(vector, func)				\
+	DECLARE_IDTENTRY_RAW_ERRORCODE(vector, func)
+#endif
+
 /* Special case for 32bit IRET 'trap'. Do not emit ASM code */
 #define DECLARE_IDTENTRY_SW(vector, func)
 
@@ -494,8 +522,14 @@ void fred_install_sysvec(unsigned int vector, const idtentry_t function);
 	idtentry_irq vector func
 
 /* System vector entries */
+#ifdef CONFIG_DYNAMIC_STACK
+#define DECLARE_IDTENTRY_SYSVEC(vector, func)				\
+	idtentry vector asm_##func func has_error_code=0		\
+	kernel_reentry_fn=switch_to_kstack
+#else
 #define DECLARE_IDTENTRY_SYSVEC(vector, func)				\
 	DECLARE_IDTENTRY(vector, func)
+#endif
 
 #ifdef CONFIG_X86_64
 # define DECLARE_IDTENTRY_MCE(vector, func)				\
@@ -615,7 +649,7 @@ DECLARE_IDTENTRY_ERRORCODE(X86_TRAP_AC,	exc_alignment_check);
 /* Raw exception entries which need extra work */
 DECLARE_IDTENTRY_RAW(X86_TRAP_UD,		exc_invalid_op);
 DECLARE_IDTENTRY_RAW(X86_TRAP_BP,		exc_int3);
-DECLARE_IDTENTRY_RAW_ERRORCODE(X86_TRAP_PF,	exc_page_fault);
+DECLARE_IDTENTRY_PF(X86_TRAP_PF,		exc_page_fault);
 
 #if defined(CONFIG_IA32_EMULATION)
 DECLARE_IDTENTRY_RAW(IA32_SYSCALL_VECTOR,	int80_emulation);
@@ -699,7 +733,7 @@ DECLARE_IDTENTRY_SYSVEC(X86_PLATFORM_IPI_VECTOR,	sysvec_x86_platform_ipi);
 #endif
 
 #ifdef CONFIG_SMP
-DECLARE_IDTENTRY(RESCHEDULE_VECTOR,			sysvec_reschedule_ipi);
+DECLARE_IDTENTRY_SYSVEC(RESCHEDULE_VECTOR,		sysvec_reschedule_ipi);
 DECLARE_IDTENTRY_SYSVEC(REBOOT_VECTOR,			sysvec_reboot);
 DECLARE_IDTENTRY_SYSVEC(CALL_FUNCTION_SINGLE_VECTOR,	sysvec_call_function_single);
 DECLARE_IDTENTRY_SYSVEC(CALL_FUNCTION_VECTOR,		sysvec_call_function);
arch/x86/include/asm/page_64_types.h +9 -1
index 7400dab373fe..b0b60f83a531 100644
@@ -28,7 +28,15 @@
 #define	IST_INDEX_NMI		1
 #define	IST_INDEX_DB		2
 #define	IST_INDEX_MCE		3
-#define	IST_INDEX_VC		4
+#define	IST_INDEX_PF		4
+#define	IST_INDEX_UDI		5
+#define	IST_INDEX_VC		6
+
+/*
+ * Offset used for some IST stacks to reserve a slot for re-entry
+ * canary. At the very top of the stack for cache friendliness.
+ */
+#define IST_ENTRY_OFFSET	8
 
 /*
  * Set __PAGE_OFFSET to the most negative possible address +
arch/x86/include/asm/processor.h +6 -0
index a24c7805acdb..fa790731dea0 100644
@@ -573,6 +573,12 @@ static inline void load_sp0(unsigned long sp0)
 
 #endif /* CONFIG_PARAVIRT_XXL */
 
+#ifdef CONFIG_DYNAMIC_STACK
+void install_nmi_pf_stack(bool use_nmi_pf_stack);
+#else
+static inline void install_nmi_pf_stack(bool use_nmi_pf_stack) {}
+#endif
+
 unsigned long __get_wchan(struct task_struct *p);
 
 extern void select_idle_routine(void);
arch/x86/kernel/cpu/common.c +11 -0
index ec0670114efa..d90a01e2fdd2 100644
@@ -2377,6 +2377,8 @@ static inline void tss_setup_ist(struct tss_struct *tss)
 	tss->x86_tss.ist[IST_INDEX_NMI] = __this_cpu_ist_top_va(NMI);
 	tss->x86_tss.ist[IST_INDEX_DB] = __this_cpu_ist_top_va(DB);
 	tss->x86_tss.ist[IST_INDEX_MCE] = __this_cpu_ist_top_va(MCE);
+	tss->x86_tss.ist[IST_INDEX_PF] = __this_cpu_ist_top_va(PF) - IST_ENTRY_OFFSET;
+	tss->x86_tss.ist[IST_INDEX_UDI] = __this_cpu_ist_top_va(UDI) - IST_ENTRY_OFFSET;
 	/* Only mapped when SEV-ES is active */
 	tss->x86_tss.ist[IST_INDEX_VC] = __this_cpu_ist_top_va(VC);
 }
@@ -2665,3 +2667,12 @@ void __init arch_cpu_finalize_init(void)
 	 */
 	mem_encrypt_init();
 }
+
+#ifdef CONFIG_DYNAMIC_STACK
+noinstr void install_nmi_pf_stack(bool use_nmi_pf_stack)
+{
+	unsigned long stack = use_nmi_pf_stack ? __this_cpu_ist_top_va(PF2)
+					       : __this_cpu_ist_top_va(PF);
+	this_cpu_write(cpu_tss_rw.x86_tss.ist[IST_INDEX_PF], stack - IST_ENTRY_OFFSET);
+}
+#endif
arch/x86/kernel/dumpstack_64.c +8 -2
index 6c5defd6569a..6784d31d3eb3 100644
@@ -24,13 +24,16 @@ static const char * const exception_stack_names[] = {
 		[ ESTACK_NMI	]	= "NMI",
 		[ ESTACK_DB	]	= "#DB",
 		[ ESTACK_MCE	]	= "#MC",
+		[ ESTACK_PF	]	= "#PF",
+		[ ESTACK_PF2	]	= "#PF2",
+		[ ESTACK_UDI	]	= "#UDI",
 		[ ESTACK_VC	]	= "#VC",
 		[ ESTACK_VC2	]	= "#VC2",
 };
 
 const char *stack_type_name(enum stack_type type)
 {
-	BUILD_BUG_ON(N_EXCEPTION_STACKS != 6);
+	BUILD_BUG_ON(N_EXCEPTION_STACKS != 9);
 
 	if (type == STACK_TYPE_TASK)
 		return "TASK";
@@ -87,6 +90,9 @@ struct estack_pages estack_pages[CEA_ESTACK_PAGES] ____cacheline_aligned = {
 	EPAGERANGE(NMI),
 	EPAGERANGE(DB),
 	EPAGERANGE(MCE),
+	EPAGERANGE(PF),
+	EPAGERANGE(PF2),
+	EPAGERANGE(UDI),
 	EPAGERANGE(VC),
 	EPAGERANGE(VC2),
 };
@@ -98,7 +104,7 @@ static __always_inline bool in_exception_stack(unsigned long *stack, struct stac
 	struct pt_regs *regs;
 	unsigned int k;
 
-	BUILD_BUG_ON(N_EXCEPTION_STACKS != 6);
+	BUILD_BUG_ON(N_EXCEPTION_STACKS != 9);
 
 	begin = (unsigned long)__this_cpu_read(cea_exception_stacks);
 	/*
arch/x86/kernel/idt.c +35 -22

128 lines not on this page · raw message

arch/x86/kernel/nmi.c +9 -0
index 3d239ed12744..a2444b9d5b71 100644
@@ -37,6 +37,7 @@
 #include <asm/microcode.h>
 #include <asm/sev.h>
 #include <asm/fred.h>
+#include <asm/cpu_entry_area.h>
 
 #define CREATE_TRACE_POINTS
 #include <trace/events/nmi.h>
@@ -581,6 +582,11 @@ DEFINE_IDTENTRY_RAW(exc_nmi)
 	if (IS_ENABLED(CONFIG_NMI_CHECK_CPU) && ignore_nmis) {
 		WRITE_ONCE(nsp->idt_ignored, nsp->idt_ignored + 1);
 	} else if (!ignore_nmis) {
+		bool protect_pf_ist_stack = is_pf_ist_stack(regs->sp);
+
+		if (protect_pf_ist_stack)
+			install_nmi_pf_stack(true);
+
 		if (IS_ENABLED(CONFIG_NMI_CHECK_CPU)) {
 			WRITE_ONCE(nsp->idt_nmi_seq, nsp->idt_nmi_seq + 1);
 			WARN_ON_ONCE(!(nsp->idt_nmi_seq & 0x1));
@@ -590,6 +596,9 @@ DEFINE_IDTENTRY_RAW(exc_nmi)
 			WRITE_ONCE(nsp->idt_nmi_seq, nsp->idt_nmi_seq + 1);
 			WARN_ON_ONCE(nsp->idt_nmi_seq & 0x1);
 		}
+
+		if (protect_pf_ist_stack)
+			install_nmi_pf_stack(false);
 	}
 
 	irqentry_nmi_exit(regs, irq_state);
arch/x86/lib/usercopy.c +9 -0
index 24b48af27417..75b9f851f428 100644
@@ -9,6 +9,7 @@
 #include <linux/instrumented.h>
 
 #include <asm/tlbflush.h>
+#include <asm/cpu_entry_area.h>
 
 /**
  * copy_from_user_nmi - NMI safe copy from user
@@ -39,6 +40,14 @@ copy_from_user_nmi(void *to, const void __user *from, unsigned long n)
 	if (!nmi_uaccess_okay())
 		return n;
 
+	/*
+	 * IST stacks aren't reentrant, so bail before the possibility of
+	 * a #PF. While on the #PF IST stack, we should only need this
+	 * function for stack dumps (WARN/panic/etc).
+	 */
+	if (is_pf_ist_stack(current_stack_pointer))
+		return n;
+
 	/*
 	 * Even though this function is typically called from NMI/IRQ context
 	 * disable pagefaults so that its behaviour is consistent even when
arch/x86/mm/cpu_entry_area.c +17 -0
index 575f863f3c75..97ac91c109ed 100644
@@ -156,6 +156,12 @@ static void __init percpu_setup_exception_stacks(unsigned int cpu)
 	cea_map_stack(DB);
 	cea_map_stack(MCE);
 
+	if (IS_ENABLED(CONFIG_DYNAMIC_STACK)) {
+		cea_map_stack(PF);
+		cea_map_stack(PF2);
+		cea_map_stack(UDI);
+	}
+
 	if (IS_ENABLED(CONFIG_AMD_MEM_ENCRYPT)) {
 		if (cc_platform_has(CC_ATTR_GUEST_STATE_ENCRYPT)) {
 			cea_map_stack(VC);
@@ -173,6 +179,17 @@ static void __init percpu_setup_exception_stacks(unsigned int cpu)
 }
 #endif
 
+#ifdef CONFIG_DYNAMIC_STACK
+bool noinstr is_pf_ist_stack(unsigned long addr)
+{
+	struct cea_exception_stacks *cs = __this_cpu_read(cea_exception_stacks);
+	unsigned long top = CEA_ESTACK_TOP(cs, PF2);
+	unsigned long bot = CEA_ESTACK_BOT(cs, PF);
+
+	return addr >= bot && addr < top;
+}
+#endif
+
 /* Setup the fixmap mappings only once per-processor */
 static void __init setup_cpu_entry_area(unsigned int cpu)
 {
arch/x86/mm/fault.c +58 -12

106 lines not on this page · raw message

Signature · 1 line
2.54.0.rc2.544.gc7ae2d5bb8-goog
#27 lore raw
In reply to David Stevens
Quoted 2 lines · David Stevens

The question is then: is this approach something that is fundamentally untenable in the kernel

Yes. Fundamentally untenable.

Not allowing stack faults has been a wonderful simplification. It's one of those things that just plain makes the kernel easier to maintain. Saving low single digits of system memory is not exactly making me eager to go back to the harder-to-maintain days.

I seriously doubt that this 1% is the lowest hanging fruit for memory bloat on these systems. ;)

#28 lore raw
Quoted 6 lines · Dave Hansen
Quoted 2 lines · David Stevens

The question is then: is this approach something that is fundamentally untenable in the kernel

Yes. Fundamentally untenable.

Quoted 7 lines

Not allowing stack faults has been a wonderful simplification. It's one of those things that just plain makes the kernel easier to maintain. Saving low single digits of system memory is not exactly making me eager to go back to the harder-to-maintain days.

I seriously doubt that this 1% is the lowest hanging fruit for memory bloat on these systems. ;)

This true until, in a fleet of millions of machines, you encounter a one-in-a-billion chance of a stack overflow. You are then forced to double the statically allocated kernel stacks on every machine, paying a memory tax even though 99.999..% of threads never exceed 4K. This overhead accumulates to petabytes of wasted capacity.

Pasha

#29 lore raw
Quoted 11 lines · Pasha Tatashin
Quoted 10 lines · Dave Hansen
Quoted 2 lines · David Stevens

The question is then: is this approach something that is fundamentally untenable in the kernel

Yes. Fundamentally untenable.

Not allowing stack faults has been a wonderful simplification. It's one of those things that just plain makes the kernel easier to maintain. Saving low single digits of system memory is not exactly making me eager to go back to the harder-to-maintain days.

Quoted 7 lines
Quoted 2 lines

I seriously doubt that this 1% is the lowest hanging fruit for memory bloat on these systems. 😉

This true until, in a fleet of millions of machines, you encounter a one-in-a-billion chance of a stack overflow. You are then forced to double the statically allocated kernel stacks on every machine, paying a memory tax even though 99.999..% of threads never exceed 4K. This overhead accumulates to petabytes of wasted capacity.

I don't disagree with you. But, at that point, you're picking your
poison: bugs dynamic kernel stacks versus crashes from stack overflows.

At some point, I might be able to be talked into dynamic stack as a FRED-only feature. But FRED isn't widespread enough to go to the trouble today. I'm sure the folks who want this also don't want to wait until all the devices in the field have FRED because that even *longer* off.

So maybe this is one of those things that folks just need to deploy out-of-tree for a couple of years, come back with some data to show us that we were just paranoid, and we'll look at it again.

#30 lore raw
Quoted 20 lines · Dave Hansen
Quoted 11 lines · Pasha Tatashin
Quoted 10 lines · Dave Hansen
Quoted 2 lines · David Stevens

The question is then: is this approach something that is fundamentally untenable in the kernel

Yes. Fundamentally untenable.

Not allowing stack faults has been a wonderful simplification. It's one of those things that just plain makes the kernel easier to maintain. Saving low single digits of system memory is not exactly making me eager to go back to the harder-to-maintain days.

Quoted 7 lines
Quoted 2 lines

I seriously doubt that this 1% is the lowest hanging fruit for memory bloat on these systems. 😉

This true until, in a fleet of millions of machines, you encounter a one-in-a-billion chance of a stack overflow. You are then forced to double the statically allocated kernel stacks on every machine, paying a memory tax even though 99.999..% of threads never exceed 4K. This overhead accumulates to petabytes of wasted capacity.

Quoted 7 lines

I don't disagree with you. But, at that point, you're picking your
poison: bugs dynamic kernel stacks versus crashes from stack overflows.

At some point, I might be able to be talked into dynamic stack as a FRED-only feature. But FRED isn't widespread enough to go to the trouble today. I'm sure the folks who want this also don't want to wait until all the devices in the field have FRED because that even *longer* off.

Why does this need to be FRED only? True, the lack of reentrancy with IST stacks complicates a few situations. That adds some complexity beyond what's needed for FRED-only support, but the additional complexity doesn't really seem like a hard blocker, at least if we accept the complexity of kernel stack faults for FRED.

-David

#31 lore raw
In reply to Pasha Tatashin
Quoted 12 lines · On Fri, 24 Apr 2026 21:35:20 +0000 Pasha Tatashin <[email protected]>…
Quoted 11 lines · Dave Hansen
Quoted 2 lines · David Stevens

The question is then: is this approach something that is fundamentally untenable in the kernel

Yes. Fundamentally untenable.

Not allowing stack faults has been a wonderful simplification. It's one of those things that just plain makes the kernel easier to maintain. Saving low single digits of system memory is not exactly making me eager to go back to the harder-to-maintain days.

Quoted 8 lines
Quoted 2 lines

I seriously doubt that this 1% is the lowest hanging fruit for memory bloat on these systems. ;)

This true until, in a fleet of millions of machines, you encounter a one-in-a-billion chance of a stack overflow. You are then forced to double the statically allocated kernel stacks on every machine, paying a memory tax even though 99.999..% of threads never exceed 4K. This overhead accumulates to petabytes of wasted capacity.

And then you hit a stack fault in some path where you can't sleep and there isn't any available kernel memory.

An alternative idea is to arrange for some system calls to sleep in userspace, so when the thread is woken it re-executes the system call. It then makes sense to assign the kernel stack to the process when it enters the kernel.
That might mean that you don't need a kernel stack for all the threads sleeping in futex() - it might even be possible to do the retry in userspace saving the second kernel entry most of the time. It is all 'hard and difficult' though.

The easier solution is to rewrite the system code so it doesn't have 1000s of threads :-)

David

Quoted 3 lines

Pasha

#32 lore raw
Quoted 18 lines · David Laight
Quoted 15 lines · On Fri, 24 Apr 2026 21:35:20 +0000 Pasha Tatashin <[email protected]>…
Quoted 13 lines · Dave Hansen
Quoted 2 lines · David Stevens

The question is then: is this approach something that is fundamentally untenable in the kernel

Yes. Fundamentally untenable.

Not allowing stack faults has been a wonderful simplification. It's one of those things that just plain makes the kernel easier to maintain. Saving low single digits of system memory is not exactly making me eager to go back to the harder-to-maintain days.

I seriously doubt that this 1% is the lowest hanging fruit for memory bloat on these systems. ;)

Quoted 8 lines
Quoted 5 lines

This true until, in a fleet of millions of machines, you encounter a one-in-a-billion chance of a stack overflow. You are then forced to double the statically allocated kernel stacks on every machine, paying a memory tax even though 99.999..% of threads never exceed 4K. This overhead accumulates to petabytes of wasted capacity.

And then you hit a stack fault in some path where you can't sleep and there isn't any available kernel memory.

Well, at least if we hit this rare case, we can simply double a buffer of pre-reserved stack memory per CPU. This still saves significant memory compared to wasting it on every single thread.

Quoted 8 lines

An alternative idea is to arrange for some system calls to sleep in userspace, so when the thread is woken it re-executes the system call. It then makes sense to assign the kernel stack to the process when it enters the kernel.
That might mean that you don't need a kernel stack for all the threads sleeping in futex() - it might even be possible to do the retry in userspace saving the second kernel entry most of the time. It is all 'hard and difficult' though.

I was thinking about a similar approach as well—sort of multiplexing the kernel stacks. But honestly, when trying to cover all the edge cases, I didn't find it to be any better or easier than just using dynamic kernel stacks.

An alternative approach, which was proposed at LSFMM by Willy, is to add an explicit deep stack calls. When we enter a path that we know is exceptionally deep, only then do we extend the stack, keeping the default (say, 8K) everywhere else.

Quoted 2 lines

The easier solution is to rewrite the system code so it doesn't have 1000s of threads :-)

That ship sailed in the early 90s of the previous millennium. Nowadays, we have high end workstations with almost 200 hardware threads. Rewriting system code to reduce thread counts simply isn't an option for our storage machines, which have millions of threads per unit.

+CC Matthew Wilcox

#33 lore raw
In reply to David Laight
Quoted 5 lines · David Laight
Quoted 5 lines

This true until, in a fleet of millions of machines, you encounter a one-in-a-billion chance of a stack overflow. You are then forced to double the statically allocated kernel stacks on every machine, paying a memory tax even though 99.999..% of threads never exceed 4K. This overhead accumulates to petabytes of wasted capacity.

Quoted 7 lines

And then you hit a stack fault in some path where you can't sleep and there isn't any available kernel memory.

An alternative idea is to arrange for some system calls to sleep in userspace, so when the thread is woken it re-executes the system call. It then makes sense to assign the kernel stack to the process when it enters the kernel.

There are probably other ways to do this without handling exceptions.

For instance, let's say you always *map* 16k of stack for each process. But, after context switching out, you take a look at 4x8b pte_t's that were mapping the kernel stack. If the _PAGE_ACCESSED bit is clear, you can just clear _PAGE_PRESENT and reclaim the page.

If you don't want the overhead in the normal context switch path, you reclaim in a shrinker, at the cost of needing locking to coordinate with the scheduler.

A simple rule would be: a thread that ever accesses a page gets to keep it forever. They're never reclaimed after being accessed, only before.

For that, the worst case is that you go to schedule a new thread and can't allocate memory fill in the 4 pte_t's. You can't run it until you or some other CPU goes and does some reclaim.

Needing memory in the middle of schedule() is generally a no-go. But its a lot better than not being able to continue _execution_ of a kernel thread at *ALL*, possibly in a non-preemptible context, like when you do it in a #PF.

Basically, I think there's a way to do this that limits the kernel blast radius to _mostly_ being a core mm problem.

What else has been considered before the #PF-based mechanism?

#34 lore raw

On Thu, Jun 18, 2026 at 5:29 PM Dave Hansen <[email protected]> wrote:

Thanks for the thoughts, Dave

Quoted 21 lines · David Laight
Quoted 5 lines
Quoted 5 lines

This true until, in a fleet of millions of machines, you encounter a one-in-a-billion chance of a stack overflow. You are then forced to double the statically allocated kernel stacks on every machine, paying a memory tax even though 99.999..% of threads never exceed 4K. This overhead accumulates to petabytes of wasted capacity.

Quoted 7 lines

And then you hit a stack fault in some path where you can't sleep and there isn't any available kernel memory.

An alternative idea is to arrange for some system calls to sleep in userspace, so when the thread is woken it re-executes the system call. It then makes sense to assign the kernel stack to the process when it enters the kernel.

There are probably other ways to do this without handling exceptions.

For instance, let's say you always *map* 16k of stack for each process. But, after context switching out, you take a look at 4x8b pte_t's that were mapping the kernel stack. If the _PAGE_ACCESSED bit is clear, you can just clear _PAGE_PRESENT and reclaim the page.

Quoted 6 lines

If you don't want the overhead in the normal context switch path, you reclaim in a shrinker, at the cost of needing locking to coordinate with the scheduler.

A simple rule would be: a thread that ever accesses a page gets to keep it forever. They're never reclaimed after being accessed, only before.

That's an interesting take; but it's a one-way latch, right? How do we know that task won't dive deeper, later?

Quoted 9 lines

For that, the worst case is that you go to schedule a new thread and can't allocate memory fill in the 4 pte_t's. You can't run it until you or some other CPU goes and does some reclaim.

Needing memory in the middle of schedule() is generally a no-go. But its a lot better than not being able to continue _execution_ of a kernel thread at *ALL*, possibly in a non-preemptible context, like when you do it in a #PF.

Quoted 4 lines

Basically, I think there's a way to do this that limits the kernel blast radius to _mostly_ being a core mm problem.

What else has been considered before the #PF-based mechanism?

The only other way to know on-demand when to increase the stack size is through stack probing, which I've ruled out without further consideration due to performance.

Then there is a class of solutions to explicitly grow / run certain code paths on larger stacks. Though instrumentation may help, others have described it as playing whack-a-mole.

Then there are solutions that use a shared pool of kernel stacks, blocking userspace until one becomes available. Very disruptive.

I personally haven't explored any of these in great depth.

To me, handling this on-demand in #PF, though technically challenging, offered (1) the most memory savings, (2) the least disruption to userspace, and (3) (ironically, expected to be) the most maintainable, general solution with the least perf impact.

Happy to consider other ideas, and again, I appreciate your time and thoughts.

Best,
Zach

#35 lore raw
In reply to Dave Hansen
Quoted 17 lines · Dave Hansen
Quoted 5 lines · David Laight
Quoted 5 lines

This true until, in a fleet of millions of machines, you encounter a one-in-a-billion chance of a stack overflow. You are then forced to double the statically allocated kernel stacks on every machine, paying a memory tax even though 99.999..% of threads never exceed 4K. This overhead accumulates to petabytes of wasted capacity.

Quoted 7 lines

And then you hit a stack fault in some path where you can't sleep and there isn't any available kernel memory.

An alternative idea is to arrange for some system calls to sleep in userspace, so when the thread is woken it re-executes the system call. It then makes sense to assign the kernel stack to the process when it enters the kernel.

There are probably other ways to do this without handling exceptions.

Quoted 8 lines

For instance, let's say you always *map* 16k of stack for each process. But, after context switching out, you take a look at 4x8b pte_t's that were mapping the kernel stack. If the _PAGE_ACCESSED bit is clear, you can just clear _PAGE_PRESENT and reclaim the page.

If you don't want the overhead in the normal context switch path, you reclaim in a shrinker, at the cost of needing locking to coordinate with the scheduler.

My understanding is that speculative execution can fill the TLB, but won't set access bits. Speculative execution of a function call could definitely put an apparently unused stack page into the TLB. In theory, I don't see anything preventing one CPU from speculatively accessing memory from another CPU's current stack. You definitely wouldn't want to do TLB shootdowns in the context switch path, so this would require a shrinker. I guess if you're batching shootdowns in a shrinker, it's probably not more expensive than swap on a per-page-freed basis.

Quoted 11 lines

A simple rule would be: a thread that ever accesses a page gets to keep it forever. They're never reclaimed after being accessed, only before.

For that, the worst case is that you go to schedule a new thread and can't allocate memory fill in the 4 pte_t's. You can't run it until you or some other CPU goes and does some reclaim.

Needing memory in the middle of schedule() is generally a no-go. But its a lot better than not being able to continue _execution_ of a kernel thread at *ALL*, possibly in a non-preemptible context, like when you do it in a #PF.

I don't think this is different from the current proposal from a memory allocation standpoint. Both proposals effectively maintain a pool of preallocated pages used to fill the current thread's stack. They vary substantially in when the pages are put into the page tables, but both need to allocate during schedule().

-David

Quoted 4 lines

Basically, I think there's a way to do this that limits the kernel blast radius to _mostly_ being a core mm problem.

What else has been considered before the #PF-based mechanism?

#36 lore raw
Quoted 9 lines · David Stevens
Quoted 4 lines

Needing memory in the middle of schedule() is generally a no-go. But its a lot better than not being able to continue _execution_ of a kernel thread at *ALL*, possibly in a non-preemptible context, like when you do it in a #PF.

I don't think this is different from the current proposal from a memory allocation standpoint. Both proposals effectively maintain a pool of preallocated pages used to fill the current thread's stack. They vary substantially in when the pages are put into the page tables, but both need to allocate during schedule().

I think you're saying: "Dave, you didn't solve all of our problems for us." I'd definitely agree. ;)

I thought I wrote it somewhere, but I either deleted it or it got ignored. I'll repeat: this PoC series has two big, big sticking points:

1. It requires allocation in very sticky contexts. It's theoretically any code that pushes on the stack. That's a *LOT* of the kernel. An allocation failure pretty much means the CPU thread is stuck.
2. Because those pushes happen almost anywhere, a #PF can happen almost anywhere, which widens the places #PF needs to be handled. Thus, the angst from the x86 maintainers.

I think I've at least hand-waved a potential path to getting rid of sticking point #2 in its entirety, and reducing the x86 maintainer angst.

My hand waving also reduces the scope of #1. It removes the need to allocate memory in some crazy interrupt-disabled region in the I/O driver interrupt handler holding a bunch of locks when a #MC happens during an NMI while kswapd was running.

So, yeah "both need to allocate during schedule()" is factually correct. But this PoC needs to allocate successfully *EVERYWHERE*. Virtually all kernel code paths, modulo some very very special areas.

Are you saying that as an engineering principle you see needing to guarantee allocation success of 12k at "virtually all kernel code paths" and "schedule()" as equivalent barriers to solving the problem at hand because they're both non-zero in size?

I suspect not. But it's kinda coming off that way. A bit of coaching for dealing with grumpy time-constrained maintainers: if they take their time to help you solve their problem, don't spend undue effort pointing out the engineering compromises in their proposals. Take more time to consider the engineering tradeoffs as opposed to simply arguing a lack of utter perfection.

But, really, my big takeaway from this thread is that the folks pushing dynamic kernel stacks have a very limited understanding of upstream or what its priorities are. Probably the single biggest obstacle here is going to be proving to the long-term maintainers that this isn't another dump and run operation. I suspect the x86 folks are going to be a bit more amenable in that territory than our mm friends. <cough>MGLRU<cough>

Either way, welcome to the party! If you want to come help upstream, there are always patches to review and always bugs to fix.

maintainer
#37 lore raw
In reply to Dave Hansen
Quoted 6 lines · Dave Hansen
Quoted 2 lines · David Stevens

The question is then: is this approach something that is fundamentally untenable in the kernel

Yes. Fundamentally untenable.

Quoted 7 lines

Not allowing stack faults has been a wonderful simplification. It's one of those things that just plain makes the kernel easier to maintain. Saving low single digits of system memory is not exactly making me eager to go back to the harder-to-maintain days.

I seriously doubt that this 1% is the lowest hanging fruit for memory bloat on these systems. ;)

It is worth noting that this was one of the VERY early design decisions that has shaped Linux from the beginning:

- No swapping of kernel memory
- Kernel stacks are statically allocated
- Physical RAM is mapped into the kernel at all times
- A "monolithic" kernel using function calls, not message passing
- A kernel interface that closely maps to the low-level application API (e.g. each user space thread is a kernel thread.)
- Kernel ABIs and APIs are subject to evolution; stability is only guaranteed in user space.

Those design decisions are, by and large, what has made Linux Linux: a relatively simple, highly performant, and reliable system.

-hpa

#38 lore raw
Quoted 5 lines · H. Peter Anvin

It is worth noting that this was one of the VERY early design decisions that has shaped Linux from the beginning:

- No swapping of kernel memory
- Kernel stacks are statically allocated

...

One other bit to add here: In the past, kernel faults on kernel memory have been allowed, like to populate vmalloc() page table entries into the parts of the page tables that are not shared across processes. Even *that* turned out to be too much of a pain even though it didn't involve allocation, and the kernel has been moving away from that.

#39 lore raw
Quoted 7 lines · Dave Hansen
Quoted 5 lines · H. Peter Anvin

It is worth noting that this was one of the VERY early design decisions that has shaped Linux from the beginning:

- No swapping of kernel memory
- Kernel stacks are statically allocated

Quoted 8 lines

...

One other bit to add here: In the past, kernel faults on kernel memory have been allowed, like to populate vmalloc() page table entries into the parts of the page tables that are not shared across processes. Even *that* turned out to be too much of a pain even though it didn't involve allocation, and the kernel has been moving away from that.

Dave,

Necroing this thread, as the potential aggregate savings continue to stand out for us on the datacenter side (whereas David is motivated separately, from the consumer device side).

I certainly empathize with your position, and hesitation to give up such a nice simplification just to invite new headaches.

However, I'd still like to work with you to understand what feasible path forward you see, hoping you can proactively steer us away from some of the bigger headaches.

I think we are fine being forward-looking, and only supporting this for FRED (which is on our doorstep). That said, understanding the issues you foresee with the IST approach would still be valuable, as it might save us internal trouble should we choose to carry it temporarily to bridge the gap with FRED.

Overall, are there any particular painpoints you'd like to see flushed out, first? How would you like to proceed? Would explicitly marking this as an experimental config, in the interim, be more attractive?

Thanks, and I appreciate any help or guidance here.

Best,
Zach

#40 lore raw
Quoted 2 lines · Zach O'Keefe

Overall, are there any particular painpoints you'd like to see flushed out, first?

Handing exceptions in the kernel is hard. Period. That's the pain point. Just look at NMIs, #VC, #MC and the rest of that mess. Just look at how we've moved away from ever taking random page faults in the kernel. Or, heck, randomly taking faults at *all*. We've concentrated them in very specific places, not in general code.

Now you're arguing that the kernel can pretty much take a fault *AND* allocate memory reliably at any point*.

I just don't see the collateral in this series to justify that claim.

The NMI entry code is a disaster because NMIs can happen anywhere. The #VC code is a disaster because #VCs can happen anywhere. Once #PF can happen anywhere*, why won't #PF become a disaster?

It would be a completely different story if there was a track record of finding and fixing bugs in the x86 entry code from the authors of this series. But I don't think I've ever seen a single email from your folks before this, much less a review tag or a patch. I'd be much happier if you got Andy L's blessing on this, for example.

Quoted 2 lines

How would you like to proceed? Would explicitly marking this as an experimental config, in the interim, be more attractive?

No.

The enemy here is complexity. *Maintenance* complexity. Being able to compile out some of the complexity helps with debugging. But it doesn't help maintaining the code.

--

* #PF on stack accesses isn't *quite* as bad as NMI or #VC, I'll give you that. But it's still pretty darn bad.

maintainer
#41 lore raw
Quoted 10 lines · Dave Hansen
Quoted 2 lines · Zach O'Keefe

Overall, are there any particular painpoints you'd like to see flushed out, first?

Handing exceptions in the kernel is hard. Period. That's the pain point. Just look at NMIs, #VC, #MC and the rest of that mess. Just look at how we've moved away from ever taking random page faults in the kernel. Or, heck, randomly taking faults at *all*. We've concentrated them in very specific places, not in general code.

Quoted 5 lines

Now you're arguing that the kernel can pretty much take a fault *AND* allocate memory reliably at any point*.

I just don't see the collateral in this series to justify that claim.

That is most definitely the zeroth-order thing. Extraordinary claims require extraordinary evidence, and this is certainly an extraordinary claim. In addition to the *massive* maintainability issue, you also have to consider the additional overheads you will now have to deal with in order to avoid deadlocks.

Almost every OS that have attempted to swap out kernel stacks have been known to suffer from deadlocks under very high memory load.

Quoted 6 lines

The NMI entry code is a disaster because NMIs can happen anywhere. The #VC code is a disaster because #VCs can happen anywhere. Once #PF can happen anywhere*, why won't #PF become a disaster?
[...]
* #PF on stack accesses isn't *quite* as bad as NMI or #VC, I'll give you that. But it's still pretty darn bad.

In some ways, they are actually *worse*.

#PFs need to be able to sleep, because the common case for a #PF in the kernel is that it touched user space. This means #PF needs to be using IST/SL 0. However, this is obviously incompatible with handling #PFs on the kernel stack itself, so now it needs a stack switch. In the common case, it will then need to demote the #PF back onto the normal execution stack, which is complex in its own right.

Now, if you are on a pre-FRED system, the IST entries don't nest, so you absolutely have to make sure you can't get there again through any means whatsoever. With FRED, it isn't quite so dire, but it will still give you lots of fun if that interrupt is one which would like to be demoted off the IRQ stack.

Quoted 6 lines

It would be a completely different story if there was a track record of finding and fixing bugs in the x86 entry code from the authors of this series. But I don't think I've ever seen a single email from your folks before this, much less a review tag or a patch. I'd be much happier if you got Andy L's blessing on this, for example.

Quoted 7 lines
Quoted 2 lines

How would you like to proceed? Would explicitly marking this as an experimental config, in the interim, be more attractive?

No.

The enemy here is complexity. *Maintenance* complexity. Being able to compile out some of the complexity helps with debugging. But it doesn't help maintaining the code.

Indeed. Paravirtualization is a great example of how this works. The PV hooks in the kernel are still a maintenance nightmare 20 years after they were introduced, and mostly that cost is not borne by the people who introduced and benefited from them.

-hpa

#42 lore raw
Quoted 12 lines · H. Peter Anvin
Quoted 10 lines · Dave Hansen
Quoted 2 lines · Zach O'Keefe

Overall, are there any particular painpoints you'd like to see flushed out, first?

Handing exceptions in the kernel is hard. Period. That's the pain point. Just look at NMIs, #VC, #MC and the rest of that mess. Just look at how we've moved away from ever taking random page faults in the kernel. Or, heck, randomly taking faults at *all*. We've concentrated them in very specific places, not in general code.

Quoted 8 lines
Quoted 5 lines

Now you're arguing that the kernel can pretty much take a fault *AND* allocate memory reliably at any point*.

I just don't see the collateral in this series to justify that claim.

That is most definitely the zeroth-order thing. Extraordinary claims require extraordinary evidence, and this is certainly an extraordinary claim.

I do acknowledge that there is currently a lack of evidence - this is an RFC after all. The question is whether it is possible in principle to produce sufficient evidence. From the Android side of Google, we are willing to carry the RFC patches downstream for a while to build a case for merging them upstream. However, there needs to be at least a possibility of success before we undertake that work. If upstream's position is that dynamic stacks are no good, full stop, and will absolutely never happen, then there's no point in us trying to pursue this avenue further. And I assume those from the datacenter side of the company are in a similar position.

-David

Quoted 48 lines

In addition to the *massive* maintainability issue, you also have to consider the additional overheads you will now have to deal with in order to avoid deadlocks.

Almost every OS that have attempted to swap out kernel stacks have been known to suffer from deadlocks under very high memory load.

Quoted 6 lines

The NMI entry code is a disaster because NMIs can happen anywhere. The #VC code is a disaster because #VCs can happen anywhere. Once #PF can happen anywhere*, why won't #PF become a disaster?
[...]
* #PF on stack accesses isn't *quite* as bad as NMI or #VC, I'll give you that. But it's still pretty darn bad.

In some ways, they are actually *worse*.

#PFs need to be able to sleep, because the common case for a #PF in the kernel is that it touched user space. This means #PF needs to be using IST/SL 0. However, this is obviously incompatible with handling #PFs on the kernel stack itself, so now it needs a stack switch. In the common case, it will then need to demote the #PF back onto the normal execution stack, which is complex in its own right.

Now, if you are on a pre-FRED system, the IST entries don't nest, so you absolutely have to make sure you can't get there again through any means whatsoever. With FRED, it isn't quite so dire, but it will still give you lots of fun if that interrupt is one which would like to be demoted off the IRQ stack.

Quoted 6 lines

It would be a completely different story if there was a track record of finding and fixing bugs in the x86 entry code from the authors of this series. But I don't think I've ever seen a single email from your folks before this, much less a review tag or a patch. I'd be much happier if you got Andy L's blessing on this, for example.

Quoted 7 lines
Quoted 2 lines

How would you like to proceed? Would explicitly marking this as an experimental config, in the interim, be more attractive?

No.

The enemy here is complexity. *Maintenance* complexity. Being able to compile out some of the complexity helps with debugging. But it doesn't help maintaining the code.

Indeed. Paravirtualization is a great example of how this works. The PV hooks in the kernel are still a maintenance nightmare 20 years after they were introduced, and mostly that cost is not borne by the people who introduced and benefited from them.

-hpa

maintainer
#43 lore raw
Quoted 4 lines · David Stevens
Quoted 3 lines

That is most definitely the zeroth-order thing. Extraordinary claims require extraordinary evidence, and this is certainly an extraordinary claim.

Quoted 11 lines

I do acknowledge that there is currently a lack of evidence - this is an RFC after all. The question is whether it is possible in principle to produce sufficient evidence. From the Android side of Google, we are willing to carry the RFC patches downstream for a while to build a case for merging them upstream. However, there needs to be at least a possibility of success before we undertake that work. If upstream's position is that dynamic stacks are no good, full stop, and will absolutely never happen, then there's no point in us trying to pursue this avenue further. And I assume those from the datacenter side of the company are in a similar position.

The answer is pretty much that you would have to present *very* impressive-looking evidence. There is very little that's completely absolute, but you definitely have a tall hill to climb on this one.

Keep also in mind that there are also people who claim that our current page sizes are much too small, and that the kernel should be doing 16K or 64K pages. At that point this more or less evaporates, too.

-hpa

maintainer
#44 lore raw
In reply to Dave Hansen
Quoted 10 lines · Dave Hansen
Quoted 2 lines · Zach O'Keefe

Overall, are there any particular painpoints you'd like to see flushed out, first?

Handing exceptions in the kernel is hard. Period. That's the pain point. Just look at NMIs, #VC, #MC and the rest of that mess. Just look at how we've moved away from ever taking random page faults in the kernel. Or, heck, randomly taking faults at *all*. We've concentrated them in very specific places, not in general code.

Quoted 4 lines

Now you're arguing that the kernel can pretty much take a fault *AND* allocate memory reliably at any point*.

I just don't see the collateral in this series to justify that claim.

There is none because it's simply impossible to guarantee and when reading through the series even a CPU hotplug operation happily continues with success when the stack page cache of the upcoming CPU can't be filled....

Quoted 3 lines

The NMI entry code is a disaster because NMIs can happen anywhere. The #VC code is a disaster because #VCs can happen anywhere. Once #PF can happen anywhere*, why won't #PF become a disaster?

It's already a disaster. See kvm_handle_async_pf() and the cute issues vs. taking a #PF in NMI or some other IST handler.

Quoted 6 lines

It would be a completely different story if there was a track record of finding and fixing bugs in the x86 entry code from the authors of this series. But I don't think I've ever seen a single email from your folks before this, much less a review tag or a patch. I'd be much happier if you got Andy L's blessing on this, for example.

Quoted 7 lines
Quoted 2 lines

How would you like to proceed? Would explicitly marking this as an experimental config, in the interim, be more attractive?

No.

The enemy here is complexity. *Maintenance* complexity. Being able to compile out some of the complexity helps with debugging. But it doesn't help maintaining the code.

Correct.

Aside of that the part which worries me most is the IDT hackery. That's fragile as hell and full of unvalidated assumptions. Reading "should not happen" several times in a changelog doesn't make me more confident.

"It is possible for #MCE to occur on the #PF IST stack, but the #MCE
handler shouldn't generate new #PFs. The reentrancy check on the #PF stack will trigger if any recoverable #MCEs do generate #PFs - if there are actually reports of it happening, we can address it then."

Seriously?

We don't wait until the report comes in because the report won't even happen in the worst case:

#PF on IST
...
cmp 0, reentrance
jne abort

#MC
...
#PF rewinds #PF IST
cmp 0, reentrance
jne abort <- Not taken because #MC happened before
it could be set.

IST is fundamentally not suitable for this and I'm sure there are more holes in this.

I haven't looked at the FRED side of affairs yet in detail, but the handwavy explanation about external interrupts having to be moved to stack level 1 and unconditionally bounced back does not really make it appealing. I agree that chapter 8.3.4 in the SDM volume 3 is not really helpful, but papering over the problem without understanding the root cause is not cutting it. If it's a genuine FRED hardware issue, then this needs to be understood and documented.

The x86 folks have spent a lot of time to make the horrific x86 interrupt and exception handling solid and therefore have zero interest to deal with the fallout of something based on "shouldn't happen" assumptions. Either it can prove correctness under all circumstances or not.

I understand the save tons of memory accross a fleet argument, but a large fleet is also a guarantee to trigger all the "should not happen and impropable" issues which are gracefully handwaved away. That's a truly bad tradeoff as it ends up in non-decodable bug reports. What's worse the have to be handled by the maintainers and not necessarily by those who implemented it.

Thanks,

tglx

#45 lore raw
Quoted 43 lines

Aside of that the part which worries me most is the IDT hackery. That's fragile as hell and full of unvalidated assumptions. Reading "should not happen" several times in a changelog doesn't make me more confident.

"It is possible for #MCE to occur on the #PF IST stack, but the #MCE
handler shouldn't generate new #PFs. The reentrancy check on the #PF stack will trigger if any recoverable #MCEs do generate #PFs - if there are actually reports of it happening, we can address it then."

Seriously?

We don't wait until the report comes in because the report won't even happen in the worst case:

#PF on IST
...
cmp 0, reentrance
jne abort

#MC
...
#PF rewinds #PF IST
cmp 0, reentrance
jne abort <- Not taken because #MC happened before
it could be set.

IST is fundamentally not suitable for this and I'm sure there are more holes in this.

I haven't looked at the FRED side of affairs yet in detail, but the handwavy explanation about external interrupts having to be moved to stack level 1 and unconditionally bounced back does not really make it appealing. I agree that chapter 8.3.4 in the SDM volume 3 is not really helpful, but papering over the problem without understanding the root cause is not cutting it. If it's a genuine FRED hardware issue, then this needs to be understood and documented.

The x86 folks have spent a lot of time to make the horrific x86 interrupt and exception handling solid and therefore have zero interest to deal with the fallout of something based on "shouldn't happen" assumptions. Either it can prove correctness under all circumstances or not.

Quoted 6 lines

I understand the save tons of memory accross a fleet argument, but a large fleet is also a guarantee to trigger all the "should not happen and impropable" issues which are gracefully handwaved away. That's a truly bad tradeoff as it ends up in non-decodable bug reports. What's worse the have to be handled by the maintainers and not necessarily by those who implemented it.

Thanks Dave / Thomas / Hans ; I appreciate your time taking a look at this.

As Dave previously pointed out, I'll admit to some ignorance regarding the subtle nuances of x86 interrupt / exception handling. Counter to my goals here, that code has "just worked," so attention and time have been spent elsewhere. We'll undoubtedly need help making things solid and avoiding previous pitfalls. As David mentions, this is an RFC.

While it seems common opinion that the IST-based solution is fragile, what of FRED? It seems like this is exactly the kind of support needed to avoid some of the aforementioned sw "mess" in various x86 exception handling paths. I agree that it's less-than-ideal that we are forced to downgrade exception levels in the common #PF case, but is that an unsurmountable problem? Pardon my ignorance.

Lastly, I just want to clarify what folks have meant by "extraordinary claims" or "evidence". Aside from the above discussion on FRED exception handling, the "only" other part of this is the allocation. Are people concerned about memory unavailability, deadlocking-type issues, or something else? We have considerable design freedom here to avoid certain classes of unreliability, but—barring any clever tricks—I don't know if the allocation can be guaranteed to succeed in all conceivable circumstances. I want to ensure that reality does not present a hard blocker.

Again, thanks everyone for the time and help,

Best,
Zach

maintainer
#46 lore raw

Zach!

Quoted 6 lines · Zach O'Keefe

While it seems common opinion that the IST-based solution is fragile, what of FRED? It seems like this is exactly the kind of support needed to avoid some of the aforementioned sw "mess" in various x86 exception handling paths. I agree that it's less-than-ideal that we are forced to downgrade exception levels in the common #PF case, but is that an unsurmountable problem? Pardon my ignorance.

The #PF path is considered perfomance critical. But how much the downgrade matters needs actual numbers to analyze under various workload scenarios.

I've not seen numbers to that effect anywhere. The only numbers provided are marketing material about the memory savings on a freshly booted idle machine. There are _zero_ numbers about the actual real world savings, but claims about the PETABYTE savings possible.

Seriously?

Quoted 3 lines

Lastly, I just want to clarify what folks have meant by "extraordinary claims" or "evidence". Aside from the above discussion on FRED exception handling, the "only" other part of this is the allocation.

Clearly anything which is explained with "shouldn't happen" and "unlikely". At cloud scale nothing is unlikely anymore. That's simply the reality of statistical math.

As I pointed out before the same applies to the unexplained upgrade/downgrade game with external interrupts. Such issues cannot be papered over without understanding the root cause as from decades long experience they come inevitably back some time down the road. Cloud scale even guarantees that.

Quoted 6 lines

Are people concerned about memory unavailability, deadlocking-type issues, or something else? We have considerable design freedom here to avoid certain classes of unreliability, but—barring any clever tricks—I don't know if the allocation can be guaranteed to succeed in all conceivable circumstances. I want to ensure that reality does not present a hard blocker.

First of all the failure scenario has to be clearly defined.

Right now, if I'm reading the patches correctly this simply can end up killing the wrong tasks/processes just because an OOM situation results in a depletion of the per CPU cache and the very wrong task which runs into the deep call stack situation ends up in the creek without a paddle.

Given that you even fail to abort a CPU bringup when the allocation of the per CPU stack page cache fails, makes it pretty clear that there has been spent exactly zero thoughts about this problem.

Why the heck does this cache refill call have to be unconditionally in __schedule() where preemption is disabled and therefore GFP_ATOMIC is mandatory? I know "Works for me" (most of the time).

And just because I was looking at the patch in question I found this other insanity:

Review

Quoted 8 lines
(quoted diff) +8 -0
+	/*
+	 * Most likely we faulted in the page right next to the last mapped
+	 * page in the stack, however, it is possible (but very unlikely) that
+	 * the faulted page is actually skips some pages in the stack. Make sure
+	 * we do not create  more than one holes in the stack, and map every
+	 * page between the current fault  address and the last page that is
+	 * mapped in the stack.
+	 */

Can anyone with a sane mind and the most minimal understanding of the kernel's inner working explain to me how the kernel can skip "some pages" on the stack?

If the kernel skips a whole page or more then there is a serious bug somewhere. I might be missing something, but again the "very unlikely" wording which handwaves about it is just disgustingly useless.

I disagree with Dave on the RFC status of this series. It's not even close to RFC, it's at PoC status.

Thanks,

tglx

#47 lore raw
Quoted 69 lines · Thomas Gleixner

Zach!

Quoted 6 lines · Zach O'Keefe

While it seems common opinion that the IST-based solution is fragile, what of FRED? It seems like this is exactly the kind of support needed to avoid some of the aforementioned sw "mess" in various x86 exception handling paths. I agree that it's less-than-ideal that we are forced to downgrade exception levels in the common #PF case, but is that an unsurmountable problem? Pardon my ignorance.

The #PF path is considered perfomance critical. But how much the downgrade matters needs actual numbers to analyze under various workload scenarios.

I've not seen numbers to that effect anywhere. The only numbers provided are marketing material about the memory savings on a freshly booted idle machine. There are _zero_ numbers about the actual real world savings, but claims about the PETABYTE savings possible.

Seriously?

Quoted 3 lines

Lastly, I just want to clarify what folks have meant by "extraordinary claims" or "evidence". Aside from the above discussion on FRED exception handling, the "only" other part of this is the allocation.

Clearly anything which is explained with "shouldn't happen" and "unlikely". At cloud scale nothing is unlikely anymore. That's simply the reality of statistical math.

As I pointed out before the same applies to the unexplained upgrade/downgrade game with external interrupts. Such issues cannot be papered over without understanding the root cause as from decades long experience they come inevitably back some time down the road. Cloud scale even guarantees that.

Quoted 6 lines

Are people concerned about memory unavailability, deadlocking-type issues, or something else? We have considerable design freedom here to avoid certain classes of unreliability, but—barring any clever tricks—I don't know if the allocation can be guaranteed to succeed in all conceivable circumstances. I want to ensure that reality does not present a hard blocker.

First of all the failure scenario has to be clearly defined.

Right now, if I'm reading the patches correctly this simply can end up killing the wrong tasks/processes just because an OOM situation results in a depletion of the per CPU cache and the very wrong task which runs into the deep call stack situation ends up in the creek without a paddle.

Given that you even fail to abort a CPU bringup when the allocation of the per CPU stack page cache fails, makes it pretty clear that there has been spent exactly zero thoughts about this problem.

Why the heck does this cache refill call have to be unconditionally in __schedule() where preemption is disabled and therefore GFP_ATOMIC is mandatory? I know "Works for me" (most of the time).
And just because I was looking at the patch in question I found this other insanity:

Review

Quoted 8 lines
(quoted diff) +8 -0
+     /*
+      * Most likely we faulted in the page right next to the last mapped
+      * page in the stack, however, it is possible (but very unlikely) that
+      * the faulted page is actually skips some pages in the stack. Make sure
+      * we do not create  more than one holes in the stack, and map every
+      * page between the current fault  address and the last page that is
+      * mapped in the stack.
+      */
Quoted 7 lines

Can anyone with a sane mind and the most minimal understanding of the kernel's inner working explain to me how the kernel can skip "some pages" on the stack?

If the kernel skips a whole page or more then there is a serious bug somewhere. I might be missing something, but again the "very unlikely" wording which handwaves about it is just disgustingly useless.

FRAME_WARN accepts values up to 8192 bytes, and it can always be ignored or simply disabled. If a stack frame is larger than 4k, then it's entirely possible for the code and compiler to align in a way where the first access in the frame skips a page in the stack. I think we agree that such code would be highly suspect and (hopefully) would only exist in out-of-tree drivers. But it's something the kernel build system accepts today. Dynamic kernel stacks suddenly turning that into a runtime kernel panic seems like exactly the sort of edge case that we would get yelled at for not addressing.

-David

Quoted 6 lines

I disagree with Dave on the RFC status of this series. It's not even close to RFC, it's at PoC status.

Thanks,

tglx

maintainer
#48 lore raw
Quoted 7 lines · David Stevens
Quoted 3 lines · Thomas Gleixner

If the kernel skips a whole page or more then there is a serious bug somewhere. I might be missing something, but again the "very unlikely" wording which handwaves about it is just disgustingly useless.

FRAME_WARN accepts values up to 8192 bytes, and it can always be ignored or simply disabled. If a stack frame is larger than 4k, then

We should limit that to something sane.

Quoted 4 lines

it's entirely possible for the code and compiler to align in a way where the first access in the frame skips a page in the stack. I think we agree that such code would be highly suspect and (hopefully) would only exist in out-of-tree drivers.

We don't care about out of tree drivers.

Quoted 4 lines

But it's something the kernel build system accepts today. Dynamic kernel stacks suddenly turning that into a runtime kernel panic seems like exactly the sort of edge case that we would get yelled at for not addressing.

That's a good thing. If it breaks in tree code then those people have finally an incentive to fix the warnings sent by the robots which they ignored for a long time. If it breaks out of tree code then *SHRUG*.

We guarantee not to break user space, but we don't guarantee anything for out of tree kernel hacks.

Thanks,

tglx

#49 lore raw
In reply to Thomas Gleixner

On Fri, Jun 19, 2026 at 2:59 PM Thomas Gleixner <[email protected]> wrote:

Thomas, thanks again for taking the time to look into this and help out.

Quoted 10 lines

Zach!

Quoted 6 lines · Zach O'Keefe

While it seems common opinion that the IST-based solution is fragile, what of FRED? It seems like this is exactly the kind of support needed to avoid some of the aforementioned sw "mess" in various x86 exception handling paths. I agree that it's less-than-ideal that we are forced to downgrade exception levels in the common #PF case, but is that an unsurmountable problem? Pardon my ignorance.

Quoted 3 lines

The #PF path is considered perfomance critical. But how much the downgrade matters needs actual numbers to analyze under various workload scenarios.

Ya, that's my concern as well, as I don't have a good intuition for how perf critical kernel #PF is for real workloads. If this is your primary concern, I'll take that as a _good_ thing ; i.e. there's nothing architecturally stopping us from doing this downgrade safely. We'll still need the analysis, but that can be a later stage -- we're more than happy to get this data for all.

Quoted 6 lines

I've not seen numbers to that effect anywhere. The only numbers provided are marketing material about the memory savings on a freshly booted idle machine. There are _zero_ numbers about the actual real world savings, but claims about the PETABYTE savings possible.

Seriously?

This is actually the most understood aspect. With O(100B) active tasks fleetwide at any point, it only takes an average savings of O(10KiB) per task to get to 1PiB. At least for our fleet, we know the % of tasks that use only 4KiB, 8KiB, or require the full 16KiB, and the math confirms that we expect O(PiB) aggregate savings. The % of stacks requiring the full 16KiB is minuscule, but it still occurs at a rate higher than what we can tolerate for SO panics. Given the vast majority of stacks never exceed the first 4KiB, this enables the significant opportunity.

Quoted 52 lines
Quoted 3 lines

Lastly, I just want to clarify what folks have meant by "extraordinary claims" or "evidence". Aside from the above discussion on FRED exception handling, the "only" other part of this is the allocation.

Clearly anything which is explained with "shouldn't happen" and "unlikely". At cloud scale nothing is unlikely anymore. That's simply the reality of statistical math.

As I pointed out before the same applies to the unexplained upgrade/downgrade game with external interrupts. Such issues cannot be papered over without understanding the root cause as from decades long experience they come inevitably back some time down the road. Cloud scale even guarantees that.

Quoted 6 lines

Are people concerned about memory unavailability, deadlocking-type issues, or something else? We have considerable design freedom here to avoid certain classes of unreliability, but—barring any clever tricks—I don't know if the allocation can be guaranteed to succeed in all conceivable circumstances. I want to ensure that reality does not present a hard blocker.

First of all the failure scenario has to be clearly defined.

Right now, if I'm reading the patches correctly this simply can end up killing the wrong tasks/processes just because an OOM situation results in a depletion of the per CPU cache and the very wrong task which runs into the deep call stack situation ends up in the creek without a paddle.

Given that you even fail to abort a CPU bringup when the allocation of the per CPU stack page cache fails, makes it pretty clear that there has been spent exactly zero thoughts about this problem.

Why the heck does this cache refill call have to be unconditionally in __schedule() where preemption is disabled and therefore GFP_ATOMIC is mandatory? I know "Works for me" (most of the time).

And just because I was looking at the patch in question I found this other insanity:

Review

Quoted 8 lines
(quoted diff) +8 -0
+     /*
+      * Most likely we faulted in the page right next to the last mapped
+      * page in the stack, however, it is possible (but very unlikely) that
+      * the faulted page is actually skips some pages in the stack. Make sure
+      * we do not create  more than one holes in the stack, and map every
+      * page between the current fault  address and the last page that is
+      * mapped in the stack.
+      */

Can anyone with a sane mind and the most minimal understanding of the kernel's inner working explain to me how the kernel can skip "some pages" on the stack?

Quoted 6 lines

If the kernel skips a whole page or more then there is a serious bug somewhere. I might be missing something, but again the "very unlikely" wording which handwaves about it is just disgustingly useless.

I disagree with Dave on the RFC status of this series. It's not even close to RFC, it's at PoC status.

Absolutely understood. I'm more interested in constructively working together (as we can see, we'll need your help) to figure out how the x86 experts want to approach this vs discussing _this_ series. Perhaps it was my mistake to necro this thread instead of starting a new, general discussion. Appologies.

To that end, how would you like to proceed? You may understand the x86 complexities better than anyone, so hopefully you can guide this in the right direction. How would you like us to approach this?

Thanks again for your time, help, and support,
Zach

Quoted 10 lines

Thanks,

tglx

maintainer
#50 lore raw
Quoted 107 lines · Zach O'Keefe

On Fri, Jun 19, 2026 at 2:59 PM Thomas Gleixner <[email protected]> wrote:

Thomas, thanks again for taking the time to look into this and help out.

Quoted 10 lines

Zach!

Quoted 6 lines · Zach O'Keefe

While it seems common opinion that the IST-based solution is fragile, what of FRED? It seems like this is exactly the kind of support needed to avoid some of the aforementioned sw "mess" in various x86 exception handling paths. I agree that it's less-than-ideal that we are forced to downgrade exception levels in the common #PF case, but is that an unsurmountable problem? Pardon my ignorance.

Quoted 3 lines

The #PF path is considered perfomance critical. But how much the downgrade matters needs actual numbers to analyze under various workload scenarios.

Ya, that's my concern as well, as I don't have a good intuition for how perf critical kernel #PF is for real workloads. If this is your primary concern, I'll take that as a _good_ thing ; i.e. there's nothing architecturally stopping us from doing this downgrade safely. We'll still need the analysis, but that can be a later stage -- we're more than happy to get this data for all.

Quoted 6 lines

I've not seen numbers to that effect anywhere. The only numbers provided are marketing material about the memory savings on a freshly booted idle machine. There are _zero_ numbers about the actual real world savings, but claims about the PETABYTE savings possible.

Seriously?

This is actually the most understood aspect. With O(100B) active tasks fleetwide at any point, it only takes an average savings of O(10KiB) per task to get to 1PiB. At least for our fleet, we know the % of tasks that use only 4KiB, 8KiB, or require the full 16KiB, and the math confirms that we expect O(PiB) aggregate savings. The % of stacks requiring the full 16KiB is minuscule, but it still occurs at a rate higher than what we can tolerate for SO panics. Given the vast majority of stacks never exceed the first 4KiB, this enables the significant opportunity.

Quoted 52 lines
Quoted 3 lines

Lastly, I just want to clarify what folks have meant by "extraordinary claims" or "evidence". Aside from the above discussion on FRED exception handling, the "only" other part of this is the allocation.

Clearly anything which is explained with "shouldn't happen" and "unlikely". At cloud scale nothing is unlikely anymore. That's simply the reality of statistical math.

As I pointed out before the same applies to the unexplained upgrade/downgrade game with external interrupts. Such issues cannot be papered over without understanding the root cause as from decades long experience they come inevitably back some time down the road. Cloud scale even guarantees that.

Quoted 6 lines

Are people concerned about memory unavailability, deadlocking-type issues, or something else? We have considerable design freedom here to avoid certain classes of unreliability, but—barring any clever tricks—I don't know if the allocation can be guaranteed to succeed in all conceivable circumstances. I want to ensure that reality does not present a hard blocker.

First of all the failure scenario has to be clearly defined.

Right now, if I'm reading the patches correctly this simply can end up killing the wrong tasks/processes just because an OOM situation results in a depletion of the per CPU cache and the very wrong task which runs into the deep call stack situation ends up in the creek without a paddle.

Given that you even fail to abort a CPU bringup when the allocation of the per CPU stack page cache fails, makes it pretty clear that there has been spent exactly zero thoughts about this problem.

Why the heck does this cache refill call have to be unconditionally in __schedule() where preemption is disabled and therefore GFP_ATOMIC is mandatory? I know "Works for me" (most of the time).

And just because I was looking at the patch in question I found this other insanity:

Review

Quoted 8 lines
(quoted diff) +8 -0
+     /*
+      * Most likely we faulted in the page right next to the last mapped
+      * page in the stack, however, it is possible (but very unlikely) that
+      * the faulted page is actually skips some pages in the stack. Make sure
+      * we do not create  more than one holes in the stack, and map every
+      * page between the current fault  address and the last page that is
+      * mapped in the stack.
+      */

Can anyone with a sane mind and the most minimal understanding of the kernel's inner working explain to me how the kernel can skip "some pages" on the stack?

Quoted 6 lines

If the kernel skips a whole page or more then there is a serious bug somewhere. I might be missing something, but again the "very unlikely" wording which handwaves about it is just disgustingly useless.

I disagree with Dave on the RFC status of this series. It's not even close to RFC, it's at PoC status.

Absolutely understood. I'm more interested in constructively working together (as we can see, we'll need your help) to figure out how the x86 experts want to approach this vs discussing _this_ series. Perhaps it was my mistake to necro this thread instead of starting a new, general discussion. Appologies.

Quoted 19 lines

To that end, how would you like to proceed? You may understand the x86 complexities better than anyone, so hopefully you can guide this in the right direction. How would you like us to approach this?

Thanks again for your time, help, and support,
Zach

Quoted 10 lines

Thanks,

tglx

1 PiB for a fleet only makes sense in the context of the size of that fleet.

But it's more than that.

You WILL slow down the general case by this stuff, and so how much actual gain does this imply? What is the mark needed to even get to a break-even point?

To be honest, this and the multikernel proposal are the worst motivated massive changes for no demonstrated value I have seen in a very, very long time.

#51 lore raw

Hey Hans,

Quoted 1 line

1 PiB for a fleet only makes sense in the context of the size of that fleet.

Of course! But that _is_ a real fleet, not a hypothetical one -- and I don't see too many news articles about hyperscalers downsizing :)

Quoted 3 lines

But it's more than that.

You WILL slow down the general case by this stuff, and so how much actual gain does this imply? What is the mark needed to even get to a break-even point?

You're absolutely right! Per Thomas' earlier comment, performance analysis is due. This is a chicken-before-the-egg problem: we can't get the data until we have a PoC to work with. We are looking to engage with upstream early in the project, to determine what a PoC ought to look like. It benefits no one to invest time and money doing the reverse. I'd be more than delighted to get these numbers, once we have a feasible (if conditional) path forward.

Quoted 1 line

To be honest, this and the multikernel proposal are the worst motivated massive changes for no demonstrated value I have seen in a very, very long time.

;) Glad I get to be memorable!

Appreciate your thoughts, and have a great weekend, Zach

maintainer
#52 lore raw
In reply to Zach O'Keefe
Quoted 11 lines · Zach O'Keefe
Quoted 3 lines · Thomas Gleixner

The #PF path is considered perfomance critical. But how much the downgrade matters needs actual numbers to analyze under various workload scenarios.

Ya, that's my concern as well, as I don't have a good intuition for how perf critical kernel #PF is for real workloads. If this is your primary concern, I'll take that as a _good_ thing ; i.e. there's nothing architecturally stopping us from doing this downgrade safely. We'll still need the analysis, but that can be a later stage -- we're more than happy to get this data for all.

No. That's not a later stage optional requirement.

You have a PoC which works for you otherwise you wouldn't have posted it. So you can trivially microbenchmark the costs of the up/downgrade. And that's critical information for us but also for you. If the costs are significant then you really have to think about the tradeoffs.

Care to read Documentation/process/* carefully? It applies to you as it applies to anyone else.

Quoted 7 lines
Quoted 6 lines

I've not seen numbers to that effect anywhere. The only numbers provided are marketing material about the memory savings on a freshly booted idle machine. There are _zero_ numbers about the actual real world savings, but claims about the PETABYTE savings possible.

Seriously?

Quoted 9 lines

This is actually the most understood aspect. With O(100B) active tasks fleetwide at any point, it only takes an average savings of O(10KiB) per task to get to 1PiB. At least for our fleet, we know the % of tasks that use only 4KiB, 8KiB, or require the full 16KiB, and the math confirms that we expect O(PiB) aggregate savings. The % of stacks requiring the full 16KiB is minuscule, but it still occurs at a rate higher than what we can tolerate for SO panics. Given the vast majority of stacks never exceed the first 4KiB, this enables the significant opportunity.

I know that the potential savings are well understood and my understanding of math is sufficient to calculate how much tasks and average saving it takes to save 1PiB on a fleet.

That's a no-brainer, but this is an aggregate saving, which sounds WOW but does not tell much about anything else.

1) What's the actual percentage of savings in relation to the overall memory?

2) Does the saving allow you to get more stuff done on a machine, pack more threads on it?

3) Can you actually downsize the memory on the machines?

4) What is the performance tradeoff for that?

IOW, you fail to tell what the actual benefit of such an intrusive change is. Just boasting an aggregate Petabyte number does not tell anything at all.

Let me give you a trivial example with a scenario which I have access to:

    256  CPUs
    256  GiB Memory
    64k  Threads

Let's assume the full saving of 12k per thread. That sums up to

      64k * 12k = 768MB of memory

which is 0.29% of the total 256 GiB of memory. Not so impressive as the petabyte aggregate number, right?

The workload consumes about 80% of the overall memory and is already constraint on close to 100% CPU utilization.

Now let's assume that the runtime overhead of this amounts to 1% then this is a net loss.

Let me turn that around and use a made up example assuming the 1Mio threads per compute unit taken from some reply in this thread.

Now the full saving of 12k per thread amounts to:

    1M * 12k = 12G

which is 4.7% of the overall available memory. Agreed that's a substantial number.

That 12G saving does not do anything in terms of hardware downsizing.

The only way that has a benefit is when the system is constraint by overall memory consumption, but has quite some compute capacity left.

IOW, if 1M threads hit the memory limit that means that the savings in kernel stack consumed memory allows you to add about 4% (~40k) more threads. If that ups the CPU utilization accordingly then yes, I can see the benefit. But TBH, if that's the case then you are trying to fix a user space implementation problem in the kernel.

That said you really have to describe the scenarios where there is a benefit and I do not buy this "fleet level" argument at all because there is no single fleet which has a uniform workload distribution.

Aside of that. If your argument holds that there are only a few scenarios which require a deep stack, then we are better off to identify them and fix them up rather than trying to hack around the occacional insanity of deep stack usage by adding complexity for complexity sake.

As you say that you have numbers of your fleet which confirm that the vast majority of the stack depth is below 4k, you can surely figure out the information which call chains are actually exceeding the limit.

I prefer to fix such shitty code and downgrade the stacksize in general instead of papering over the underlying issues which probably have been ignored for years if not decades.

Have you ever thought about that instead of adding complexity with a dubious value?

Thanks,

tglx

#53 lore raw

On Sat, Jun 20, 2026 at 4:34 PM Thomas Gleixner <[email protected]> wrote:

Thomas, thanks for taking the time, as always, for such a thoughtful response.

Quoted 103 lines · Zach O'Keefe
Quoted 11 lines · Thomas Gleixner
Quoted 3 lines

The #PF path is considered perfomance critical. But how much the downgrade matters needs actual numbers to analyze under various workload scenarios.

Ya, that's my concern as well, as I don't have a good intuition for how perf critical kernel #PF is for real workloads. If this is your primary concern, I'll take that as a _good_ thing ; i.e. there's nothing architecturally stopping us from doing this downgrade safely. We'll still need the analysis, but that can be a later stage -- we're more than happy to get this data for all.

No. That's not a later stage optional requirement.

You have a PoC which works for you otherwise you wouldn't have posted it. So you can trivially microbenchmark the costs of the up/downgrade. And that's critical information for us but also for you. If the costs are significant then you really have to think about the tradeoffs.

Care to read Documentation/process/* carefully? It applies to you as it applies to anyone else.

Quoted 7 lines
Quoted 6 lines

I've not seen numbers to that effect anywhere. The only numbers provided are marketing material about the memory savings on a freshly booted idle machine. There are _zero_ numbers about the actual real world savings, but claims about the PETABYTE savings possible.

Seriously?

Quoted 9 lines

This is actually the most understood aspect. With O(100B) active tasks fleetwide at any point, it only takes an average savings of O(10KiB) per task to get to 1PiB. At least for our fleet, we know the % of tasks that use only 4KiB, 8KiB, or require the full 16KiB, and the math confirms that we expect O(PiB) aggregate savings. The % of stacks requiring the full 16KiB is minuscule, but it still occurs at a rate higher than what we can tolerate for SO panics. Given the vast majority of stacks never exceed the first 4KiB, this enables the significant opportunity.

I know that the potential savings are well understood and my understanding of math is sufficient to calculate how much tasks and average saving it takes to save 1PiB on a fleet.

That's a no-brainer, but this is an aggregate saving, which sounds WOW but does not tell much about anything else.

1) What's the actual percentage of savings in relation to the overall memory?

2) Does the saving allow you to get more stuff done on a machine, pack more threads on it?

3) Can you actually downsize the memory on the machines?

4) What is the performance tradeoff for that?

IOW, you fail to tell what the actual benefit of such an intrusive change is. Just boasting an aggregate Petabyte number does not tell anything at all.

Let me give you a trivial example with a scenario which I have access to:

    256  CPUs
    256  GiB Memory
    64k  Threads

Let's assume the full saving of 12k per thread. That sums up to

      64k * 12k = 768MB of memory

which is 0.29% of the total 256 GiB of memory. Not so impressive as the petabyte aggregate number, right?

The workload consumes about 80% of the overall memory and is already constraint on close to 100% CPU utilization.

Now let's assume that the runtime overhead of this amounts to 1% then this is a net loss.

Let me turn that around and use a made up example assuming the 1Mio threads per compute unit taken from some reply in this thread.

Now the full saving of 12k per thread amounts to:

    1M * 12k = 12G

which is 4.7% of the overall available memory. Agreed that's a substantial number.

That 12G saving does not do anything in terms of hardware downsizing.

The only way that has a benefit is when the system is constraint by overall memory consumption, but has quite some compute capacity left.

IOW, if 1M threads hit the memory limit that means that the savings in kernel stack consumed memory allows you to add about 4% (~40k) more threads. If that ups the CPU utilization accordingly then yes, I can see the benefit. But TBH, if that's the case then you are trying to fix a user space implementation problem in the kernel.

Quoted 3 lines

That said you really have to describe the scenarios where there is a benefit and I do not buy this "fleet level" argument at all because there is no single fleet which has a uniform workload distribution.

These are good thoughts, thank you. Perhaps I've been too biased by our particular environment—apologies for that.

We (mostly) punt this problem to cluster-level scheduling, which ironically exploits this non-uniformity of workload dynamics to appropriately bin-pack machines and materialize these small savings.

In the general case, I guess a lot hinges on that overhead cost -- in the best (memory-constrained) case.

Quoted 9 lines

Aside of that. If your argument holds that there are only a few scenarios which require a deep stack, then we are better off to identify them and fix them up rather than trying to hack around the occacional insanity of deep stack usage by adding complexity for complexity sake.

As you say that you have numbers of your fleet which confirm that the vast majority of the stack depth is below 4k, you can surely figure out the information which call chains are actually exceeding the limit.

Quoted 6 lines

I prefer to fix such shitty code and downgrade the stacksize in general instead of papering over the underlying issues which probably have been ignored for years if not decades.

Have you ever thought about that instead of adding complexity with a dubious value?

Yeah, and that is certainly an alternative path we can explore. I was _hoping_ to be able to maximize the savings here, via the >90% case where 4KiB is sufficient. If we instead play whack-a-mole with the handful of cases requiring > 8KiB, the best case is we can move back to 8KiB stacks. Ironically, I was thinking going this route would be _more_ of a maintenance burden vs having a generalized solution ;)

To Yourself, Dave, Peter --

I do appreciate all your thoughts and assistance. I'll likely take a few days to collect some my thoughts, and take a harder look at some alternative paths.

Thanks again, and have a great day,
Zach

Quoted 5 lines

Thanks,

tglx

#54 lore raw
Quoted 104 lines · Zach O'Keefe

On Sat, Jun 20, 2026 at 4:34 PM Thomas Gleixner <[email protected]> wrote:

Thomas, thanks for taking the time, as always, for such a thoughtful response.

Quoted 93 lines · Zach O'Keefe
Quoted 7 lines

Ya, that's my concern as well, as I don't have a good intuition for how perf critical kernel #PF is for real workloads. If this is your primary concern, I'll take that as a _good_ thing ; i.e. there's nothing architecturally stopping us from doing this downgrade safely. We'll still need the analysis, but that can be a later stage -- we're more than happy to get this data for all.

No. That's not a later stage optional requirement.

You have a PoC which works for you otherwise you wouldn't have posted it. So you can trivially microbenchmark the costs of the up/downgrade. And that's critical information for us but also for you. If the costs are significant then you really have to think about the tradeoffs.

Care to read Documentation/process/* carefully? It applies to you as it applies to anyone else.

Quoted 10 lines

This is actually the most understood aspect. With O(100B) active tasks fleetwide at any point, it only takes an average savings of O(10KiB) per task to get to 1PiB. At least for our fleet, we know the % of tasks that use only 4KiB, 8KiB, or require the full 16KiB, and the math confirms that we expect O(PiB) aggregate savings. The % of stacks requiring the full 16KiB is minuscule, but it still occurs at a rate higher than what we can tolerate for SO panics. Given the vast majority of stacks never exceed the first 4KiB, this enables the significant opportunity.

I know that the potential savings are well understood and my understanding of math is sufficient to calculate how much tasks and average saving it takes to save 1PiB on a fleet.

That's a no-brainer, but this is an aggregate saving, which sounds WOW but does not tell much about anything else.

1) What's the actual percentage of savings in relation to the overall memory?

2) Does the saving allow you to get more stuff done on a machine, pack more threads on it?

3) Can you actually downsize the memory on the machines?

4) What is the performance tradeoff for that?

IOW, you fail to tell what the actual benefit of such an intrusive change is. Just boasting an aggregate Petabyte number does not tell anything at all.

Let me give you a trivial example with a scenario which I have access to:

    256  CPUs
    256  GiB Memory
    64k  Threads

Let's assume the full saving of 12k per thread. That sums up to

      64k * 12k = 768MB of memory

which is 0.29% of the total 256 GiB of memory. Not so impressive as the petabyte aggregate number, right?

The workload consumes about 80% of the overall memory and is already constraint on close to 100% CPU utilization.

Now let's assume that the runtime overhead of this amounts to 1% then this is a net loss.

Let me turn that around and use a made up example assuming the 1Mio threads per compute unit taken from some reply in this thread.

Now the full saving of 12k per thread amounts to:

    1M * 12k = 12G

which is 4.7% of the overall available memory. Agreed that's a substantial number.

That 12G saving does not do anything in terms of hardware downsizing.

The only way that has a benefit is when the system is constraint by overall memory consumption, but has quite some compute capacity left.

IOW, if 1M threads hit the memory limit that means that the savings in kernel stack consumed memory allows you to add about 4% (~40k) more threads. If that ups the CPU utilization accordingly then yes, I can see the benefit. But TBH, if that's the case then you are trying to fix a user space implementation problem in the kernel.

Quoted 3 lines

That said you really have to describe the scenarios where there is a benefit and I do not buy this "fleet level" argument at all because there is no single fleet which has a uniform workload distribution.

These are good thoughts, thank you. Perhaps I've been too biased by our particular environment—apologies for that.

Quoted 22 lines

We (mostly) punt this problem to cluster-level scheduling, which ironically exploits this non-uniformity of workload dynamics to appropriately bin-pack machines and materialize these small savings.

In the general case, I guess a lot hinges on that overhead cost -- in the best (memory-constrained) case.

Quoted 15 lines

Aside of that. If your argument holds that there are only a few scenarios which require a deep stack, then we are better off to identify them and fix them up rather than trying to hack around the occacional insanity of deep stack usage by adding complexity for complexity sake.

As you say that you have numbers of your fleet which confirm that the vast majority of the stack depth is below 4k, you can surely figure out the information which call chains are actually exceeding the limit.

I prefer to fix such shitty code and downgrade the stacksize in general instead of papering over the underlying issues which probably have been ignored for years if not decades.

Have you ever thought about that instead of adding complexity with a dubious value?

There was some (hallway?) talk at LSF/MM about possibly removing direct reclaim, similar to how other operating systems handle it. Now, I don't know how feasible it is (I guess devil is in the detail ;) ), or any details how that would work, but direct reclaim was repeatedly called out as one of the main reasons we can get huge stacks.

So I guess direct reclaim (incl. compaction) is one of the main problematic pieces. Are we aware of other scenarios where we (easily) trigger consumption of larger stacks?

Wild idea: as a first step to test the waters, use smaller stacks on selected kernel threads and disallow direct reclaim/compaction if the stack for the thread is small?

Signature · 3 lines
Cheers,

David
#55 lore raw

On Tue, 23 Jun 2026 09:50:00 +0200
"David Hildenbrand (Arm)" <[email protected]> wrote:

...

Quoted 9 lines

There was some (hallway?) talk at LSF/MM about possibly removing direct reclaim, similar to how other operating systems handle it. Now, I don't know how feasible it is (I guess devil is in the detail ;) ), or any details how that would work, but direct reclaim was repeatedly called out as one of the main reasons we can get huge stacks.

So I guess direct reclaim (incl. compaction) is one of the main problematic pieces. Are we aware of other scenarios where we (easily) trigger consumption of larger stacks?

I suspect some printk in error paths could get problematic. Especially if they involve direct writes to a non-trivial console (does that happen any more??).
How much stack does dump_stack() need?

Quoted 3 lines

Wild idea: as a first step to test the waters, use smaller stacks on selected kernel threads and disallow direct reclaim/compaction if the stack for the thread is small?

Or if there isn't much stack space left?

Isn't reclaim one of the places that is actually likely to exceed the stack limit because it is unusual and won't be exercised at most of the kmalloc() call sites?
One you get to the slow bits (especially ones that do IO) it is unlikely that a context-switch to a special worker thread will make much difference. (You might need to boost the thread priority based on the priority of the waiting process.)

David

#56 lore raw
Quoted 10 lines · David Laight

On Tue, 23 Jun 2026 09:50:00 +0200
"David Hildenbrand (Arm)" <[email protected]> wrote:

...

Quoted 6 lines

There was some (hallway?) talk at LSF/MM about possibly removing direct reclaim, similar to how other operating systems handle it. Now, I don't know how feasible it is (I guess devil is in the detail ;) ), or any details how that would work, but direct reclaim was repeatedly called out as one of the main reasons we can get huge stacks.

Quoted 7 lines
Quoted 3 lines

So I guess direct reclaim (incl. compaction) is one of the main problematic pieces. Are we aware of other scenarios where we (easily) trigger consumption of larger stacks?

I suspect some printk in error paths could get problematic. Especially if they involve direct writes to a non-trivial console (does that happen any more??).

Good question!

Quoted 8 lines

How much stack does dump_stack() need?

Quoted 3 lines

Wild idea: as a first step to test the waters, use smaller stacks on selected kernel threads and disallow direct reclaim/compaction if the stack for the thread is small?

Or if there isn't much stack space left?

Quoted 7 lines

Isn't reclaim one of the places that is actually likely to exceed the stack limit because it is unusual and won't be exercised at most of the kmalloc() call sites?
One you get to the slow bits (especially ones that do IO) it is unlikely that a context-switch to a special worker thread will make much difference. (You might need to boost the thread priority based on the priority of the waiting process.)

I think that was exactly the idea of removing direct reclaim: instead of doing reclaim yourself, you trigger another (reclaim) thread (like kswapd) and wait for it to finish.

Signature · 3 lines
Cheers,

David
maintainer
#57 lore raw
In reply to Zach O'Keefe
Quoted 11 lines · Zach O'Keefe
Quoted 3 lines · Thomas Gleixner

That said you really have to describe the scenarios where there is a benefit and I do not buy this "fleet level" argument at all because there is no single fleet which has a uniform workload distribution.

These are good thoughts, thank you. Perhaps I've been too biased by our particular environment—apologies for that.

We (mostly) punt this problem to cluster-level scheduling, which ironically exploits this non-uniformity of workload dynamics to appropriately bin-pack machines and materialize these small savings.

That makes sense.

Quoted 2 lines

In the general case, I guess a lot hinges on that overhead cost -- in the best (memory-constrained) case.

Right, we need to look at the wide range of use cases and avoid to create niche solutions which impose maintenance costs for everyone.

Quoted 16 lines
Quoted 9 lines

Aside of that. If your argument holds that there are only a few scenarios which require a deep stack, then we are better off to identify them and fix them up rather than trying to hack around the occacional insanity of deep stack usage by adding complexity for complexity sake.

As you say that you have numbers of your fleet which confirm that the vast majority of the stack depth is below 4k, you can surely figure out the information which call chains are actually exceeding the limit.

Quoted 6 lines

I prefer to fix such shitty code and downgrade the stacksize in general instead of papering over the underlying issues which probably have been ignored for years if not decades.

Have you ever thought about that instead of adding complexity with a dubious value?

Quoted 6 lines

Yeah, and that is certainly an alternative path we can explore. I was _hoping_ to be able to maximize the savings here, via the >90% case where 4KiB is sufficient. If we instead play whack-a-mole with the handful of cases requiring > 8KiB, the best case is we can move back to 8KiB stacks. Ironically, I was thinking going this route would be _more_ of a maintenance burden vs having a generalized solution ;)

It's actually not. The point is that we lazily punted to 16K stacks more than 10 years ago. If you read the changelog carefully there is an implicit request to watch out for stack usage and to fix the offenders. That's obviously not worth the electrons it wasted to commit it.

Software people are really good in not ignoring such things as long as they do not explode in their face. :)

So as you already confirmed that the number of offenders is really low, it'd be definitely a worthwhile effort to fix that for everyone and also make the compiler people to get their act together :)

Once we got there the complexity of this becomes smaller and we might have a common goal to get the other 90% solved.

Thanks,

tglx

#58 lore raw
Quoted 3 lines · Thomas Gleixner

So as you already confirmed that the number of offenders is really low, it'd be definitely a worthwhile effort to fix that for everyone and also make the compiler people to get their act together 🙂

One other thing comes to mind...

We have debugging options for looking at max stack depth, like CONFIG_DEBUG_STACK_USAGE. But no distros seem to turn that on. There's also some ftrace stuff, but I assume none of that is used at runtime either.

Do we need some max stack depth instrumentation that folks will actually turn on? It would be nice if we spat out warnings when folks went through 12k (or 8k) of stack so there was a chance of closing the loop and getting feedback from them.

It could be done with really little overhead if the vmalloc()'d stacks set Accessed=0 on their PTEs and then checked them near vfree(). The PTEs are already getting touched there, so the cachelines should be hot anyway.

The granularity would only be 4k, but it would be so cheap that we could turn it on universally. It would also be 100% deterministic.

There are, of course, more games that could be played with stack depth checks on normal interrupts or NMIs. But those would be less deterministic.

Thoughts?

maintainer
#59 lore raw
Quoted 21 lines · Dave Hansen
Quoted 3 lines · Thomas Gleixner

So as you already confirmed that the number of offenders is really low, it'd be definitely a worthwhile effort to fix that for everyone and also make the compiler people to get their act together 🙂

One other thing comes to mind...

We have debugging options for looking at max stack depth, like CONFIG_DEBUG_STACK_USAGE. But no distros seem to turn that on. There's also some ftrace stuff, but I assume none of that is used at runtime either.

Do we need some max stack depth instrumentation that folks will actually turn on? It would be nice if we spat out warnings when folks went through 12k (or 8k) of stack so there was a chance of closing the loop and getting feedback from them.

It could be done with really little overhead if the vmalloc()'d stacks set Accessed=0 on their PTEs and then checked them near vfree(). The PTEs are already getting touched there, so the cachelines should be hot anyway.

Quoted 7 lines

The granularity would only be 4k, but it would be so cheap that we could turn it on universally. It would also be 100% deterministic.

There are, of course, more games that could be played with stack depth checks on normal interrupts or NMIs. But those would be less deterministic.

Thoughts?

Having a cheap stack depth check warning would seem to be a good thing.

#60 lore raw
In reply to Dave Hansen
Quoted 16 lines · Dave Hansen
Quoted 3 lines · Thomas Gleixner

So as you already confirmed that the number of offenders is really low, it'd be definitely a worthwhile effort to fix that for everyone and also make the compiler people to get their act together 🙂

One other thing comes to mind...

We have debugging options for looking at max stack depth, like CONFIG_DEBUG_STACK_USAGE. But no distros seem to turn that on. There's also some ftrace stuff, but I assume none of that is used at runtime either.

Do we need some max stack depth instrumentation that folks will actually turn on? It would be nice if we spat out warnings when folks went through 12k (or 8k) of stack so there was a chance of closing the loop and getting feedback from them.

Quoted 7 lines

It could be done with really little overhead if the vmalloc()'d stacks set Accessed=0 on their PTEs and then checked them near vfree(). The PTEs are already getting touched there, so the cachelines should be hot anyway.

The granularity would only be 4k, but it would be so cheap that we could turn it on universally. It would also be 100% deterministic.

But it wouldn't tell you what was the large stack? Or we could try to print it inaccurately, and hope the non-overwritten deep part gives us enough insight?

Quoted 4 lines

There are, of course, more games that could be played with stack depth checks on normal interrupts or NMIs. But those would be less deterministic.

Thoughts?

maintainer
#61 lore raw
Quoted 6 lines · Vlastimil Babka (SUSE)

But it wouldn't tell you what was the large stack? Or we could try to print it inaccurately, and hope the non-overwritten deep part gives us enough insight?

Quoted 2 lines

There are, of course, more games that could be played with stack depth checks on normal interrupts or NMIs. But those would be less deterministic.>

Less deterministic, but probably more informative, since it would be possible to actually get a stack trace.

-hpa

#62 lore raw

Thanks All.

Yes, we run with CONFIG_DEBUG_STACK_USAGE -- which allows us to put together information on max stack depth usage per host/task.

I'd like to create an upstream recipe for finding deep stacks statically by combining per-frame sizes from --fstack-usage with callgraphs generated using Dan Carpenter's smatch utility. In particular, smatch's selling point is the ability to track indirect calls (i.e., calls through vm_operations, super_operations, etc). While easy to set up, and working well out-of-the-box, this currently results in many false-positive callstack possibilities, so I'm trying to pare down the graph to get to some useable data. Additionally will need to know what exception-originating stacks can possibly be superimposed on the base callstack, for the given configuration -- for example, today, only a few places we'd expect to see #PF stack. Lastly, I've been analyzing perf-like data to authoritatively construct the call graphs as well -- or at least to use for seeding/filtering the static call graph.

Thanks,
Zach

#63 lore raw
Quoted 2 lines · Zach O'Keefe

Yes, we run with CONFIG_DEBUG_STACK_USAGE -- which allows us to put together information on max stack depth usage per host/task.

Oooh, I'm very glad you do that.

I wonder if we could ever cajole the distros into doing something similar.

#64 lore raw
In reply to Zach O'Keefe
Quoted 5 lines · On Mon, 29 Jun 2026 10:29:53 -0700 "Zach O'Keefe" <[email protected]> wrote:

Thanks All.

Yes, we run with CONFIG_DEBUG_STACK_USAGE -- which allows us to put together information on max stack depth usage per host/task.

Quoted 14 lines

I'd like to create an upstream recipe for finding deep stacks statically by combining per-frame sizes from --fstack-usage with callgraphs generated using Dan Carpenter's smatch utility. In particular, smatch's selling point is the ability to track indirect calls (i.e., calls through vm_operations, super_operations, etc). While easy to set up, and working well out-of-the-box, this currently results in many false-positive callstack possibilities, so I'm trying to pare down the graph to get to some useable data. Additionally will need to know what exception-originating stacks can possibly be superimposed on the base callstack, for the given configuration -- for example, today, only a few places we'd expect to see #PF stack. Lastly, I've been analyzing perf-like data to authoritatively construct the call graphs as well -- or at least to use for seeding/filtering the static call graph.

I've thought of trying to do that getting objtool to output the stack offset of every call (I think it ends up in the debug info) and using the IBT function hashes to group indirect calls together. But I've not got further than failing to compile a kernel with the required compiler options :-(

Unravelling the mess and breaking recursive call loops is a separate problem - which you seem to have hit.
(I've done it many, many years ago for an embedded system with no indirect calls of recursion.)

I think objtool already works out a lot of things you'd need. Getting it to output a file of:
'func_x at stack offset off calls func_y'
would give something to post-process.

David

Quoted 4 lines

Thanks,
Zach

#65 lore raw
In reply to Dave Hansen
Quoted 10 lines · Dave Hansen

It could be done with really little overhead if the vmalloc()'d stacks set Accessed=0 on their PTEs and then checked them near vfree(). The PTEs are already getting touched there, so the cachelines should be hot anyway.

The granularity would only be 4k, but it would be so cheap that we could turn it on universally. It would also be 100% deterministic.

There are, of course, more games that could be played with stack depth checks on normal interrupts or NMIs. But those would be less deterministic.

I mean, if we just memset64() the whole stack to 0x535441434b544f50 and then searched the stack for the lowest word that wasn't that value at process exit, we'd get a much more granular idea with vanishingly few false positives.

#66 lore raw
Quoted 9 lines · Matthew Wilcox
Quoted 8 lines · Dave Hansen

It could be done with really little overhead if the vmalloc()'d stacks set Accessed=0 on their PTEs and then checked them near vfree(). The PTEs are already getting touched there, so the cachelines should be hot anyway.

The granularity would only be 4k, but it would be so cheap that we could turn it on universally. It would also be 100% deterministic.

Quoted 6 lines
Quoted 2 lines

There are, of course, more games that could be played with stack depth checks on normal interrupts or NMIs. But those would be less deterministic.

I mean, if we just memset64() the whole stack to 0x535441434b544f50 and then searched the stack for the lowest word that wasn't that value at process exit, we'd get a much more granular idea with vanishingly few false positives.

What I was going for was some first pass check that would be so ridiculously cheap that it could be turned on everywhere. Those 4 PTEs are going to be touched in the exit code path anyway, so there's not even any additional cache impact.

But, yeah, if it needed to be more more fine-grained than pages, what you describe could 100% be done. But, at the cost of pulling in more cachelines. It's similar to what Vlastimil was pointing out: there's a lot of good info in the bottom of the stack long after it was popped. There is fine-graned usage data and probably some stack frame remnants that could point the finger at what code is to blame.

#67 lore raw
In reply to Dave Hansen
Quoted 16 lines · On Mon, 29 Jun 2026 09:02:08 -0700 Dave Hansen <[email protected]> wrote:
Quoted 3 lines · Thomas Gleixner

So as you already confirmed that the number of offenders is really low, it'd be definitely a worthwhile effort to fix that for everyone and also make the compiler people to get their act together 🙂

One other thing comes to mind...

We have debugging options for looking at max stack depth, like CONFIG_DEBUG_STACK_USAGE. But no distros seem to turn that on. There's also some ftrace stuff, but I assume none of that is used at runtime either.

Do we need some max stack depth instrumentation that folks will actually turn on? It would be nice if we spat out warnings when folks went through 12k (or 8k) of stack so there was a chance of closing the loop and getting feedback from them.

Quoted 4 lines

It could be done with really little overhead if the vmalloc()'d stacks set Accessed=0 on their PTEs and then checked them near vfree(). The PTEs are already getting touched there, so the cachelines should be hot anyway.

For testing purposes you'd probably want to do the check in other places. Possibly slow paths that might be called with a deep stack as well as context switch and return to uses.
I guess you could copy the stack (to global data) so that it could be dumped for diagnostics (finding the relevant stack trace is left as an exercise to the reader).
Similarly it might be worth allocating an extra 4k page so that stack could be offset so the warning happens at (say) 12k.

David

Quoted 9 lines

The granularity would only be 4k, but it would be so cheap that we could turn it on universally. It would also be 100% deterministic.

There are, of course, more games that could be played with stack depth checks on normal interrupts or NMIs. But those would be less deterministic.

Thoughts?

#68 lore raw
In reply to Zach O'Keefe
Quoted 7 lines

However, I'd still like to work with you to understand what feasible path forward you see, hoping you can proactively steer us away from some of the bigger headaches.

I think we are fine being forward-looking, and only supporting this for FRED (which is on our doorstep). That said, understanding the issues you foresee with the IST approach

For IST, read
https://docs.google.com/document/d/1hWejnyDkjRRAW-JEsRjA5c9CKLOPc6VKJQsuvODlQEI/edit?usp=sharing

This was written prior to the first public drafts of FRED and SEE specs, and FRED evolved quite a lot from the first draft to where we are today.

Quoted 3 lines

would still be valuable, as
it might save us internal trouble should we choose to carry it temporarily to bridge the gap with FRED.

A lot has been said about the invasiveness and tradeoffs.  I don't wish to dwell on or rehash that.

There is one critical point which has not been made as far as I can tell:

In x86, if delivering an interrupt (NMI or INTR) causes an exception (any exception, not just pagefault), the original interrupt is lost.

If you are aware of this, then in principle you can look at the LAPIC state to figure out when an INTR was missed.  However there is no ability at all to figure out when an NMI was missed.  Worse, it is both vendor and IDT-vs-FRED specific as to whether NMIs are blocked following a missed NMI, and that too is state you cannot recover.

If you wish to make dynamic stacks works, then you *must* find a non-fault based approach on x86.

~Andrew

maintainer
#69 lore raw
Quoted 32 lines · Andrew Cooper
Quoted 7 lines

However, I'd still like to work with you to understand what feasible path forward you see, hoping you can proactively steer us away from some of the bigger headaches.

I think we are fine being forward-looking, and only supporting this for FRED (which is on our doorstep). That said, understanding the issues you foresee with the IST approach

For IST, read
https://docs.google.com/document/d/1hWejnyDkjRRAW-JEsRjA5c9CKLOPc6VKJQsuvODlQEI/edit?usp=sharing

This was written prior to the first public drafts of FRED and SEE specs, and FRED evolved quite a lot from the first draft to where we are today.

Quoted 3 lines

would still be valuable, as
it might save us internal trouble should we choose to carry it temporarily to bridge the gap with FRED.

A lot has been said about the invasiveness and tradeoffs.  I don't wish to dwell on or rehash that.

There is one critical point which has not been made as far as I can tell:

In x86, if delivering an interrupt (NMI or INTR) causes an exception (any exception, not just pagefault), the original interrupt is lost.

If you are aware of this, then in principle you can look at the LAPIC state to figure out when an INTR was missed.  However there is no ability at all to figure out when an NMI was missed.  Worse, it is both vendor and IDT-vs-FRED specific as to whether NMIs are blocked following a missed NMI, and that too is state you cannot recover.

Quoted 4 lines

If you wish to make dynamic stacks works, then you *must* find a non-fault based approach on x86.

~Andrew

Yes, this is why I said interrupts would absolutely need to be on a separate stack.

As one of the FRED architects: there was a lot of desire to fix every single little bit of the systems architecture with the first release of FRED, but in that case it would simply never have gotten released, so a key criterion became if some specific improvement would tie fundamentally to FRED or if it can be done separately (possibly specifically in FRED mode.)

At the time, there was absolutely no interest in paging kernel stacks from any OS, and I think you will find when you dig into it that it might very well mean having to promote most, if not all, exceptions onto an SL1 stack in the end.

#70 lore raw
In reply to Zach O'Keefe

On Thu, 18 Jun 2026 07:50:26 -0700
"Zach O'Keefe" <[email protected]> wrote:

...

Quoted 5 lines

Dave,

Necroing this thread, as the potential aggregate savings continue to stand out for us on the datacenter side (whereas David is motivated separately, from the consumer device side).

What are the other kernel memory costs associated with each thread? While thinks like ps used to assume it was just 4k, it won't be any more. Even for user threads many things that used to be per-process aren't any more either.
But just the task structure and FP save area are non-trivial.

David L

#71 lore raw
In reply to H. Peter Anvin
Quoted 27 lines · H. Peter Anvin
Quoted 6 lines · Dave Hansen
Quoted 2 lines · David Stevens

The question is then: is this approach something that is fundamentally untenable in the kernel

Yes. Fundamentally untenable.

Quoted 7 lines

Not allowing stack faults has been a wonderful simplification. It's one of those things that just plain makes the kernel easier to maintain. Saving low single digits of system memory is not exactly making me eager to go back to the harder-to-maintain days.

I seriously doubt that this 1% is the lowest hanging fruit for memory bloat on these systems. ;)

It is worth noting that this was one of the VERY early design decisions that has shaped Linux from the beginning:

- No swapping of kernel memory
- Kernel stacks are statically allocated
- Physical RAM is mapped into the kernel at all times
- A "monolithic" kernel using function calls, not message passing
- A kernel interface that closely maps to the low-level application API (e.g. each user space thread is a kernel thread.)
- Kernel ABIs and APIs are subject to evolution; stability is only guaranteed in user space.

Quoted 2 lines

Those design decisions are, by and large, what has made Linux Linux: a relatively simple, highly performant, and reliable system.

I think there is a bit of survivorship bias in that list. Originally, there were many other foundational assumptions that have since evolved as hardware and requirements scaled.

For example, there were assumptions about no dynamic hardware reconfiguration (no memory/CPU hot-plug), uniform memory access (no NUMA), and fixed page sizes (no THP or HugeTLB). All of those have changed, and you, better than most, know of many other such examples.

A more recent example is PREEMPT_RT: the Linux kernel was originally designed to be non-preemptible.

Even the assumptions in your list, such as "physical RAM is mapped into the kernel at all times," are evolving: emulated pmem is not mapped, and guestmemfd plans to allow unmapping memory from the direct map for security reasons.

Aside from trying our best not to break user space and allowing the internal kernel API to evolve, the other items are architectural decisions that can and should adapt to new requirements.

We now have machines with thousands of hardware threads. Running millions of software threads on such machines is a practical reality, and at fleet scales, statically allocating kernel stacks for all of them wastes a massive amount of memory.

The proposed solution won't affect Linux as a whole. It can be optionally enabled for targeted configurations. Additionally, the max stack size is still statically set; it simply isn't populated until actually used.

Pasha