> arena_vm_fault() allocated the page while holding arena->spinlock, so it
> could only use the non-blocking allocator, which never reclaims. Once the
> memcg is at memory.max that allocation just fails, the fault turns into
> VM_FAULT_SIGSEGV, and the process gets a SIGSEGV on a perfectly valid
> arena address. Hitting memory.max is routine (e.g. page cache from
> reading a big file), so this kills innocent processes over memory that
> reclaim could have freed.
>
> Rework the fault handler:
>
> - Preallocate the page before taking the lock, like do_anonymous_page()
>   does, so it can sleep and reclaim, instead of turning a routine
>   memory.max into a fake segfault. The allocation uses
>   __GFP_RETRY_MAYFAIL so it never invokes the OOM killer: the page is
>   charged to the map's memcg, which need not be the faulting task's, so
>   an OOM there could kill unrelated tasks in the map's cgroup while a
>   foreign faulter could never be its victim. Like the other bpf map
>   allocators it places the page on the map's numa_node rather than
>   following the task's mempolicy; arena memory is shared, so the map's
>   node is the right placement policy.

This isn't a bug, but since arena_map_alloc() rejects BPF_F_NUMA_NODE,
map->numa_node is always NUMA_NO_NODE here. Is the numa_node rationale in
the changelog saying more than 'unchanged, still the local node'?

> - On allocation failure fall through to the locked recheck rather than
>   failing right away: a page a concurrent allocator installed meanwhile
>   is used, otherwise the non-blocking fallback fails and we return
>   VM_FAULT_SIGBUS. Not VM_FAULT_OOM: nothing ran the OOM killer, and the
>   fault path would just retry it forever.
>
> - A lockless probe skips that preallocation when a page is already mapped
>   (e.g. allocated by the bpf program), so the common case wastes no
>   allocation. The rare race where such a page is freed before we take the
>   lock falls back to the non-blocking allocator under the lock.
>
> - Return VM_FAULT_SIGBUS for the other non-recoverable errors (lock
>   failure, range-tree and page-table failures) instead of
>   VM_FAULT_SIGSEGV; only BPF_F_SEGV_ON_FAULT, and a scratch-page hole
>   under that flag, is a real user addressing error and keeps
>   VM_FAULT_SIGSEGV.

Small changelog nit: the lock-failure path already returned VM_FAULT_SIGBUS
before this patch -- would it be clearer to list only the range-tree/
page-table/allocation failures as the ones changing from VM_FAULT_SIGSEGV?

> - Tidy up the error labels.
>
> Reviewed-by: Emil Tsalapatis <[email protected]>
> Signed-off-by: Jiayuan Chen <[email protected]>

> diff --git a/kernel/bpf/arena.c b/kernel/bpf/arena.c
> index 7b6847200b431..2d0efc0232afd 100644
> --- a/kernel/bpf/arena.c
> +++ b/kernel/bpf/arena.c

[ ... ]

> @@ -489,59 +490,108 @@ static vm_fault_t arena_vm_fault(struct vm_fault *vmf)
>       kbase = bpf_arena_get_kern_vm_start(arena);
>       kaddr = kbase + (u32)(vmf->address);
>
> -     if (raw_res_spin_lock_irqsave(&arena->spinlock, flags))
> +     page = vmalloc_to_page((void *)kaddr);
> +     if (!page && !(arena->map.map_flags & BPF_F_SEGV_ON_FAULT)) {
> +             /*
> +              * Preallocate outside the lock so the allocation can sleep and
> +              * reclaim, which the non-blocking allocator under
> +              * arena->spinlock cannot. __GFP_RETRY_MAYFAIL: reclaim, but
> +              * never OOM-kill in the map's memcg, which need not be the
> +              * faulting task's. On failure, fallthrough: the locked
> +              * recheck below picks up a page a concurrent allocator may
> +              * have installed meanwhile, and otherwise the non-blocking
> +              * fallback fails and we return VM_FAULT_SIGBUS. Not
> +              * VM_FAULT_OOM: nothing ran the OOM killer, and the fault path
> +              * would just retry it forever.
> +              */
> +             bpf_map_memcg_enter(&arena->map, &old_memcg, &new_memcg);
> +             new_page = alloc_pages_node(map->numa_node,
> +                                         GFP_KERNEL | __GFP_ZERO |
> +                                         __GFP_ACCOUNT | __GFP_NOWARN |
> +                                         __GFP_RETRY_MAYFAIL, 0);
> +             bpf_map_memcg_exit(old_memcg, new_memcg);
> +     }
> +
> +     if (raw_res_spin_lock_irqsave(&arena->spinlock, flags)) {
>               /*
>                * A failed lock means a possible deadlock was detected. Don't
>                * return VM_FAULT_RETRY: this handler never took mmap_lock, but
>                * the fault path would re-take it on retry and deadlock. Fail.
>                */
> +             if (new_page)
> +                     free_pages_nolock(new_page, 0);
>               return VM_FAULT_SIGBUS;
> +     }

[ ... ]


---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/35327041280

Reply via email to