BPF List
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Emil Tsalapatis" <emil@etsalapatis.com>, bpf@vger.kernel.org
Subject: Re: [PATCH bpf-next v4 6/9] selftests/bpf: libarena: Handle NMI reentrancy
Date: Mon, 09 Nov 2026 18:42:31 +0000	[thread overview]
Message-ID: <sashiko-outbox-205433@kernel.org> (raw)
In-Reply-To: <20261109182055.7581-7-emil@etsalapatis.com>

Thank you for your contribution! Sashiko AI review found 3 potential
issues to consider:
- [Medium] selftests/bpf: buddy_alloc_memory() grows allocator in NMI
  context
- [Medium] selftests/bpf: unvalidated write and error propagation in
  deferred free
- [Low] selftests/bpf: deferred frees stay unpoisoned in
  buddy_deferred_free_start()
--

commit 88f6bdc1e521e6f215ca4ea48a935038a6d8cf00
Author: Emil Tsalapatis <emil@etsalapatis.com>

selftests/bpf: libarena: Handle NMI reentrancy

This commit switches the libarena buddy allocator to use trylock in NMI
context and defers frees to an intrusive list when the buddy lock cannot be
acquired.

> diff --git a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
> index 13c55cf53e524..4d4d21356935d 100644
> --- a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
> +++ b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
[ ... ]
> @@ -46,10 +47,14 @@ static int idx_is_allocated(struct buddy_chunk __arena *chunk, u64 idx, bool *al
>  static u64 arena_next_pow2(__u64 n);
>  static u64 size_to_order(size_t size);
>  static void __arena *buddy_alloc_memory(struct buddy __arena *buddy, size_t size,
> -					int order, size_t norder);
> +					int order, size_t norder, bool grow);
>  static int buddy_free_unlocked(struct buddy __arena *buddy, u64 addr);
> +static bool buddy_deferred_free_pending(struct buddy __arena *buddy);
> +static int buddy_deferred_free_complete(struct buddy __arena *buddy);
>  
> -#define buddy_lock(buddy, flags) (arena_spin_lock_irqsave(&(buddy)->lock, (flags)))
> +#define buddy_lock(buddy, flags)						\
> +	(bpf_in_nmi() ? arena_spin_trylock_irqsave(&(buddy)->lock, (flags)) :	\
> +			arena_spin_lock_irqsave(&(buddy)->lock, (flags)))
>  #define buddy_unlock(buddy, flags) (arena_spin_unlock_irqrestore(&(buddy)->lock, (flags)))

[Severity: Medium]
Can buddy_alloc_memory() still call buddy_chunk_get() to grow the allocator
when bpf_in_nmi() is true?

When an allocation in NMI context misses the per-cpu cache and existing
chunks, buddy_alloc_memory() calls buddy_chunk_get():

tools/testing/selftests/bpf/libarena/src/buddy.bpf.c:buddy_chunk_get() {
    ...
	ret = buddy_alloc_arena_vaddr(buddy, &vaddr);
	if (ret)
		return NULL;
    ...
	/* Unreserve the address space. */
	bpf_arena_free_pages(&arena, (void __arena *)vaddr,
			     BUDDY_CHUNK_PAGES);

	chunk = bpf_arena_alloc_pages(&arena, (void __arena *)vaddr,
				      BUDDY_CHUNK_PAGES, NUMA_NO_NODE, 0);
    ...
}

Because NMI programs are non-sleepable, bpf_arena_free_pages() defers
unreservation to a workqueue via irq_work instead of updating the arena
range tree synchronously.  The immediately following bpf_arena_alloc_pages()
at vaddr then always fails because the range is still marked reserved,
logging an allocation failure and leaking 1 MiB of buddy->vaddr address
space per attempt.  Should buddy_alloc_memory() avoid calling
buddy_chunk_get() when bpf_in_nmi() is true?

[ ... ]

> @@ -970,6 +977,13 @@ static void __arena *buddy_alloc_memory(struct buddy __arena *buddy, size_t size
>  	if (buddy_lock(buddy, flags))
>  		return NULL;
>  
> +	if (unlikely(buddy_deferred_free_pending(buddy))) {
> +		if (buddy_deferred_free_complete(buddy)) {
> +			buddy_unlock(buddy, flags);
> +			return NULL;
> +		}
> +	}
> +
>  	address = (u8 __arena *)buddy_alloc_from_existing_chunks(buddy, order, norder);
>  	buddy_unlock(buddy, flags);
>  	if (address)
> @@ -1176,10 +1190,90 @@ static __always_inline int buddy_free_unlocked(struct buddy __arena *buddy, u64
>  	return 0;
>  }
>  
> +static bool buddy_deferred_free_pending(struct buddy __arena *buddy)
> +{
> +	return buddy->deferred_free != NULL;
> +}
> +
> +static __always_inline void buddy_deferred_free_push(struct buddy __arena *buddy,
> +						   void __arena *first, void __arena *last)
> +{
> +	void __arena *head;
> +
> +	do {
> +		head = READ_ONCE(buddy->deferred_free);
> +		*(void __arena * __arena *)last = head;
> +	} while (cmpxchg((void __arena * __arena *)&buddy->deferred_free,
> +			 head, first) != head && can_loop);
> +}
> +
> +static void buddy_deferred_free_start(struct buddy __arena *buddy, void __arena *addr)
> +{
> +	/*
> +	 * Make a linked list by chaining the allocations into a linked list.
> +	 * Possible because allocations are 16 bytes minimum and so fit a pointer.
> +	 */
> +	asan_unpoison(addr, sizeof(void *));
> +	buddy_deferred_free_push(buddy, addr, addr);
> +}
> +
> +static void buddy_deferred_free_start_bulk(struct buddy __arena *buddy,
> +		 void __arena * __arena *addrs, size_t naddrs)
> +{
> +	void __arena *first, *last, *addr;
> +	unsigned int i;
> +
> +	/*
> +	 * Chain the allocations into a local linked list. Possible because
> +	 * allocations are 16 bytes minimum and so fit a pointer.
> +	 */
> +	first = NULL;
> +	last = NULL;
> +	for (i = zero; i < naddrs && can_loop; i++) {
> +		addr = addrs[i];
> +		if (!addr)
> +			continue;
> +
> +		asan_unpoison(addr, sizeof(void *));
> +
> +		if (!first)
> +			first = addr;
> +		else
> +			*(void __arena * __arena *)last = addr;
> +
> +		last = addr;
> +	}
> +
> +	if (!first)
> +		return;
> +
> +	/* Splice the local list onto the deferred-free list. */
> +	buddy_deferred_free_push(buddy, first, last);
> +}

[Severity: Low]
Does buddy_deferred_free_start() leave deferred objects unpoisoned in the
asan shadow map until buddy_deferred_free_complete() runs?

Both buddy_deferred_free_start() and buddy_deferred_free_start_bulk() call
asan_unpoison(addr, sizeof(void *)) and leave the freed object (including
any padding bytes in the first 8 bytes when the allocation size was under 8
bytes) unpoisoned while it sits on buddy->deferred_free.  Use-after-free
accesses during that window will not be caught by asan, and writes to the
first 8 bytes can corrupt the intrusive list pointer without triggering an
asan violation.

> +
> +static int buddy_deferred_free_complete(struct buddy __arena *buddy)
> +{
> +	void __arena *head, *old;
> +	int ret = 0, err;
> +
> +	head = __sync_lock_test_and_set((void __arena * __arena *)&buddy->deferred_free,
> +					NULL);
> +
> +	while (head && can_loop) {
> +		old = head;
> +		head = *(void __arena * __arena *)head;
> +
> +		err = buddy_free_unlocked(buddy, (u64)old);
> +		ret = ret ?: err;
> +	}
> +
> +	return ret;
> +}
> +
>  __weak int buddy_free(struct buddy __arena *buddy, void __arena *addr)
>  {
>  	unsigned long flags;
> -	int ret;
> +	int ret, err;
>  
>  	if (!buddy)
>  		return -EINVAL;
> @@ -1193,20 +1287,28 @@ __weak int buddy_free(struct buddy __arena *buddy, void __arena *addr)
>  		return 0;
>  
>  	ret = buddy_lock(buddy, flags);
> -	if (ret)
> -		return ret;
> +	if (ret) {
> +		buddy_deferred_free_start(buddy, addr);
> +		return 0;
> +	}
>  
>  	ret = buddy_free_unlocked(buddy, (u64)addr);
>  
> +	/* Drain any frees stranded during NMI/lock failure. */
> +	if (unlikely(buddy_deferred_free_pending(buddy))) {
> +		err = buddy_deferred_free_complete(buddy);
> +		ret = ret ?: err;
> +	}
> +
>  	buddy_unlock(buddy, flags);
>  
>  	return ret;
>  }

[Severity: Medium]
Can buddy_deferred_free_start() and buddy_deferred_free_start_bulk()
overwrite memory before validating that the address is an allocated block?

Unlike buddy_cache_free(), which checks alignment and allocation state via
buddy_cache_get_order() before touching the block, the deferred-free paths
unpoison and write the next pointer directly into *(void __arena * __arena *)addr
when buddy_lock() fails.  Passing an unaligned, interior,already-freed, or
duplicate address while the lock is contended corrupts 8 bytes at addr or
creates a cycle in buddy->deferred_free.

In addition, when buddy_deferred_free_complete() later drains an invalid
entry, propagating its error causes buddy_alloc_memory() to abort and return
NULL for an unrelated allocation, and causes buddy_free() to return an error
even when the current caller's addr was freed without issue.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261109182055.7581-1-emil@etsalapatis.com?part=6

  reply	other threads:[~2026-11-09 18:42 UTC|newest]

Thread overview: 17+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-09 18:20 [PATCH bpf-next v4 0/9] bpf libarena allocator improvements Emil Tsalapatis
2026-11-09 18:20 ` [PATCH bpf-next v4 1/9] selftests/bpf: libarena: Add bulk buddy allocation helpers Emil Tsalapatis
2026-11-09 18:20 ` [PATCH bpf-next v4 2/9] selftests/bpf: Add a parallel buddy allocator benchmark Emil Tsalapatis
2026-11-09 18:20 ` [PATCH bpf-next v4 3/9] selftests/bpf: libarena: Make spinlock trylock match arena_spin_unlock Emil Tsalapatis
2026-11-09 18:29   ` sashiko-bot
2026-11-09 18:20 ` [PATCH bpf-next v4 4/9] selftests/bpf: libarena: Raise arena spinlock CPU limits Emil Tsalapatis
2026-11-09 18:32   ` sashiko-bot
2026-11-09 18:20 ` [PATCH bpf-next v4 5/9] selftests/bpf: libarena: Add buddy per-CPU caches Emil Tsalapatis
2026-11-09 18:38   ` sashiko-bot
2026-11-09 18:20 ` [PATCH bpf-next v4 6/9] selftests/bpf: libarena: Handle NMI reentrancy Emil Tsalapatis
2026-11-09 18:42   ` sashiko-bot [this message]
2026-11-09 19:05   ` bot+bpf-ci
2026-11-09 19:08     ` Emil Tsalapatis
2026-11-09 18:20 ` [PATCH bpf-next v4 7/9] selftests/bpf: libarena: Refill local caches in batches Emil Tsalapatis
2026-11-09 18:20 ` [PATCH bpf-next v4 8/9] selftests/bpf: libarena: Add a transfer cache Emil Tsalapatis
2026-11-09 18:20 ` [PATCH bpf-next v4 9/9] selftests/bpf: libarena: Cover buddy cache fast paths Emil Tsalapatis
2026-11-09 19:05   ` bot+bpf-ci

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=sashiko-outbox-205433@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=emil@etsalapatis.com \
    --cc=sashiko-reviews@lists.linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox