Dynamic Kernel Stacks
Subsystems (11)
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
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.
--- kernel/fork.c | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-)
kernel/fork.c +3 -5
@@ -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
PATCH v2 2/13 fork: Don't assume fully populated stack during reuse
In reply to David StevensIn 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
@@ -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
PATCH v2 3/13 fork: Move vm_stack to the beginning of the stack
In reply to David StevensThe 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
@@ -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
PATCH v2 4/13 fork: separate vmap stack allocation and free calls
In reply to David StevensFrom: 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.
--- kernel/fork.c | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-)
kernel/fork.c +24 -16
@@ -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
PATCH v2 5/13 mm/vmalloc: Add a get_vm_area_node() and vmap_pages_range() public functions
In reply to David StevensFrom: 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.
--- include/linux/vmalloc.h | 14 ++++++++++++++ mm/vmalloc.c | 25 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+)
include/linux/vmalloc.h +14 -0
@@ -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 0ULstatic 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
@@ -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
PATCH v2 6/13 fork: Move vmap stack freeing to work queue
In reply to David StevensFor 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
@@ -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
PATCH v2 7/13 fork: Dynamic Kernel Stacks
In reply to David StevensFrom: 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.
--- 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
@@ -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
@@ -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
@@ -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
@@ -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
PATCH v2 8/13 task_stack.h: Add stack_not_used() support for dynamic stack
In reply to David StevensFrom: 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.
--- arch/Kconfig | 1 - kernel/exit.c | 22 ++++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-)
arch/Kconfig +0 -1
@@ -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
@@ -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
PATCH v2 9/13 fork: Dynamic Kernel Stack accounting
In reply to David StevensFrom: 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
--- 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
@@ -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
@@ -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
@@ -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 , +#endifNR_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
@@ -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
PATCH v2 10/13 fork: Store task pointer in unpopulated stack ptes
In reply to David StevensStore 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
@@ -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
@@ -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
Thomas Gleixner
maintainerRe: [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_structpointer 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...
Thomas Gleixner
maintainerRe: [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_structpointer 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
PATCH v2 11/13 x86/entry/fred: encode frame pointer on entry
In reply to David StevensAdd 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
--- arch/x86/entry/entry_64_fred.S | 2 ++ 1 file changed, 2 insertions(+)
arch/x86/entry/entry_64_fred.S +2 -0
@@ -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
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_POINTERmacro invocation intoFRED_ENTERmacro, to prevent the unwinder from encountering a NULL stack frame pointer whenCONFIG_UNWINDER_FRAME_POINTERis enabledFixes 14619d912b65 ("x86/fred: FRED entry/exit and dispatch code")Signed-off-by David Stevens <[email protected]>--- arch/x86/entry/entry_64_fred.S | 2 ++ 1 file changed, 2 insertions(+)arch/x86/entry/entry_64_fred.S +2
-1@@ -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
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_POINTERon 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,
DavidQuoted 31 lines · David Stevens
Add missing
ENCODE_FRAME_POINTERmacro invocation intoFRED_ENTERmacro, to prevent the unwinder from encountering a NULL stack frame pointer whenCONFIG_UNWINDER_FRAME_POINTERis enabledFixes 14619d912b65 ("x86/fred: FRED entry/exit and dispatch code")Signed-off-by David Stevens <[email protected]>--- arch/x86/entry/entry_64_fred.S | 2 ++ 1 file changed, 2 insertions(+)arch/x86/entry/entry_64_fred.S +2
-0@@ -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.
Xin Li
maintainerRe: [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_POINTERon 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,
DavidQuoted 31 lines · David Stevens
Add missing
ENCODE_FRAME_POINTERmacro invocation intoFRED_ENTERmacro, to prevent the unwinder from encountering a NULL stack frame pointer whenCONFIG_UNWINDER_FRAME_POINTERis enabledFixes 14619d912b65 ("x86/fred: FRED entry/exit and dispatch code")Signed-off-by David Stevens <[email protected]>---
arch/x86/entry/entry_64_fred.S| 2 ++1 file changed, 2 insertions(+)arch/x86/entry/entry_64_fred.S +2
-0@@ -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-googYes, 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.
[tip: x86/entry] x86/entry/fred: Encode frame pointer on entry
In reply to David StevensThe 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
--- arch/x86/entry/entry_64_fred.S | 2 ++ 1 file changed, 2 insertions(+)
arch/x86/entry/entry_64_fred.S +2 -0
@@ -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
PATCH v2 12/13 x86: Add support for dynamic kernel stacks via FRED
In reply to David StevensAdd 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
@@ -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
@@ -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
@@ -15,6 +15,11 @@ asmlinkage __visible notrace struct pt_regs *sync_regs(struct pt_regs *eregs); asmlinkage __visible notracestruct 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
@@ -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
@@ -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
Thomas Gleixner
maintainerRe: [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
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: NothingWhy?
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, thenin_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
Thomas Gleixner
maintainerRe: [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.
H. Peter Anvin
maintainerRe: [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 :)
Thomas Gleixner
maintainerRe: [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 :)
H. Peter Anvin
maintainerRe: [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.
Thomas Gleixner
maintainerRe: [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
PATCH v2 13/13 x86: Add support for dynamic kernel stacks via IST
In reply to David StevensOn 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 stacstruct 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
@@ -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
@@ -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
@@ -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
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 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
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.
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
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
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 infutex()- 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
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?
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_ACCESSEDbit is clear, you can just clear_PAGE_PRESENTand 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
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_ACCESSEDbit is clear, you can just clear_PAGE_PRESENTand 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?
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.
H. Peter Anvin
maintainerQuoted 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
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.
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
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.
H. Peter Anvin
maintainerQuoted 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
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
H. Peter Anvin
maintainerQuoted 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
Thomas Gleixner
maintainerQuoted 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
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
Thomas Gleixner
maintainerZach!
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
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 thereforeGFP_ATOMICis 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
Thomas Gleixner
maintainerQuoted 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_WARNaccepts 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
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 thereforeGFP_ATOMICis 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
H. Peter Anvin
maintainerQuoted 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 thereforeGFP_ATOMICis 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,
ZachQuoted 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.
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
Thomas Gleixner
maintainerQuoted 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 ThreadsLet'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
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 ThreadsLet's assume the full saving of 12k per thread. That sums up to
64k * 12k = 768MB of memorywhich 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 = 12Gwhich 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
David Hildenbrand (Arm)
maintainerQuoted 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 ThreadsLet's assume the full saving of 12k per thread. That sums up to
64k * 12k = 768MB of memorywhich 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 = 12Gwhich 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
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
David Hildenbrand (Arm)
maintainerQuoted 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
Thomas Gleixner
maintainerQuoted 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
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?
H. Peter Anvin
maintainerQuoted 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.
Vlastimil Babka (SUSE)
reviewerQuoted 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?
H. Peter Anvin
maintainerQuoted 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
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
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.
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
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.
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 to0x535441434b544f50and 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.
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?
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
H. Peter Anvin
maintainerQuoted 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=sharingThis 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.
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
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