BPF List
 help / color / mirror / Atom feed
From: Mykyta Yatsenko <mykyta.yatsenko5@gmail.com>
To: Yun Lu <luyun_611@163.com>,
	ast@kernel.org, daniel@iogearbox.net, andrii@kernel.org,
	eddyz87@gmail.com, memxor@gmail.com, martin.lau@linux.dev,
	song@kernel.org, yonghong.song@linux.dev, jolsa@kernel.org,
	emil@etsalapatis.com, ihor.solodrai@linux.dev
Cc: yatsenko@meta.com, bpf@vger.kernel.org
Subject: Re: [PATCH bpf-next v2 1/2] bpf: Fix task work scheduling and callback race
Date: Mon, 21 Sep 2026 16:54:04 +0100	[thread overview]
Message-ID: <47b04850-28e5-480b-a861-aed3670eea1d@gmail.com> (raw)
In-Reply-To: <20260921101453.69273-2-luyun_611@163.com>



On 9/21/26 11:14 AM, Yun Lu wrote:
> From: Yun Lu <luyun@kylinos.cn>
> 
> bpf_task_work_irq() publishes a task_work callback before changing the
> context from SCHEDULING to SCHEDULED.  The callback can therefore run on
> another CPU while the irq_work handler is still using the same scheduling
> round.
> 
> Nothing currently orders the handler's use of ctx->task against
> bpf_task_work_ctx_reset().  The callback can reset ctx->task and publish
> STANDBY before the handler resumes, allowing map-value deletion to make the
> handler pass NULL to task_work_cancel().  Publishing STANDBY this early
> also allows a new round to reuse the context and irq_work while the old
> handler is still running.
> 
> One concrete interleaving is:
> 
>   1. CPU 0 changes PENDING to SCHEDULING in bpf_task_work_irq() and
>      successfully publishes ctx->work with task_work_add(), but has not yet
>      attempted the SCHEDULING-to-SCHEDULED transition.
> 
>   2. The target task on CPU 1 runs the callback.  It changes SCHEDULING to
>      RUNNING, executes the BPF subprogram, then bpf_task_work_ctx_reset()
>      releases ctx->task and sets it to NULL.  The callback changes RUNNING
>      to STANDBY and drops its ctx reference.
> 
>   3. CPU 2 deletes the map value.  bpf_task_work_cancel_and_free() changes
>      STANDBY to FREED.  Since the old state is not SCHEDULED, it does not
>      queue cancellation and drops the map's ctx reference.
> 
>   4. CPU 0 resumes.  Its SCHEDULING-to-SCHEDULED cmpxchg observes FREED, so
>      bpf_task_work_cancel() calls task_work_cancel(NULL, &ctx->work).
> 
> If deletion changes the state to FREED before the callback runs, the
> callback instead takes its FREED exit and may drop the last ctx reference.
> The destroy path then resets ctx->task, producing the same NULL dereference
> when CPU 0 resumes.
> 
> A controlled reproducer that delays CPU 0 between task_work_add() and the
> state cmpxchg triggered the following fault on a v7.3-rc3 based x86-64 KVM
> guest:
> 
>   BUG: kernel NULL pointer dereference, address: 0000000000000890
>   #PF: supervisor read access in kernel mode
>   RIP: task_work_cancel+0xd/0xa0
>   RDI: 0000000000000000
>   CR2: 0000000000000890
>   Call Trace:
>    <IRQ>
>    bpf_task_work_irq+0x95/0x100
>    irq_work_run_list+0x4f/0x90
>    irq_work_run+0x18/0x50
>    __sysvec_irq_work+0x18/0xb0
>    sysvec_irq_work+0x66/0x80
>    </IRQ>
> 
> 0x890 is the task_struct::task_works offset in that build.  It is read by
> task_work_pending() through the NULL task argument.  The rcu_read_lock() in
> bpf_task_work_irq() only keeps ctx memory alive; it does not retain
> ctx->task.  A NULL check would still race with releasing the task
> reference.
> 
> Fix this by making the callback claim RUNNING before synchronizing with the
> scheduling irq_work.  RUNNING prevents map-value deletion from
> reinitializing the irq_work for asynchronous cancellation, so
> irq_work_sync() waits for the scheduling invocation that published the
> callback.  Only after that invocation returns may the callback execute the
> BPF subprogram, reset task/prog and publish STANDBY.
> 
> Pin the context with one temporary reference held by the scheduling
> handler.  If deletion wins and the callback takes its FREED exit first,
> this reference prevents destruction from resetting ctx->task before the
> handler completes its cancellation attempt.
> 
> An add failure has no callback to perform the synchronization.  Initialize
> the scheduling irq_work when the context is created and reject a new round
> while the failed invocation remains BUSY.  This prevents overlapping
> invocations from sharing the single BUSY bit and making a later
> irq_work_sync() return before its scheduling handler has finished.
> 
> The callback runs in task context after task_work_run() has released
> task->pi_lock, and existing task_work callbacks such as ____fput() may
> sleep.  Keep the Tasks Trace RCU read-side section across irq_work_sync()
> so the map value remains live until the callback finishes.  No context
> fields or states are added, and the existing cancellation and destruction
> paths are retained.
> 
> Fixes: 38aa7003e369 ("bpf: task work scheduling kfuncs")
> Signed-off-by: Yun Lu <luyun@kylinos.cn>
> ---
>  kernel/bpf/helpers.c | 46 ++++++++++++++++++++++++++++++++++++++------
>  1 file changed, 40 insertions(+), 6 deletions(-)
> 
> diff --git a/kernel/bpf/helpers.c b/kernel/bpf/helpers.c
> index b3cc5c8fc875..e5d5683626ea 100644
> --- a/kernel/bpf/helpers.c
> +++ b/kernel/bpf/helpers.c
> @@ -4469,6 +4469,14 @@ static void bpf_task_work_callback(struct callback_head *cb)
>  		bpf_task_work_ctx_put(ctx);
>  		return;
>  	}
> +	if (WARN_ON_ONCE(state != BPF_TW_SCHEDULING &&
> +			 state != BPF_TW_SCHEDULED)) {
> +		bpf_task_work_ctx_put(ctx);
> +		return;
> +	}

If this is an impossible condition, we should remove this hunk. 
If it is possible, we should remove WARN_ON_ONCE.

> +
> +	/* Do not release this round's resources until its scheduler is done. */
> +	irq_work_sync(&ctx->irq_work);

This looks like a perf problem, we are blocking the task work callback waiting 
for the irq_work. Instead we should try to make the irq_work safe after the 
task_work_add(), maybe take a task reference.
>  
>  	key = (void *)map_key_from_value(ctx->map, ctx->map_val, &idx);
>  
> @@ -4495,6 +4503,12 @@ static void bpf_task_work_irq(struct irq_work *irq_work)
>  		bpf_task_work_ctx_put(ctx);
>  		return;
>  	}
> +	/*
> +	 * Pin the ctx until this handler is done. The callback may observe
> +	 * FREED and drop its ref first, and destroy must not reset ctx->task
> +	 * before the cancellation attempt below.
> +	 */
> +	refcount_inc(&ctx->refcnt);
>  
>  	err = task_work_add(ctx->task, &ctx->work, ctx->mode);
>  	if (err) {
> @@ -4504,20 +4518,26 @@ static void bpf_task_work_irq(struct irq_work *irq_work)
>  		 * gone to FREED already, which is fine as we already cleaned up after ourselves
>  		 */
>  		(void)cmpxchg(&ctx->state, BPF_TW_SCHEDULING, BPF_TW_STANDBY);
> +		/*
> +		 * No callback was published, so drop both refs owned by this
> +		 * failed round: the callback ref and the scheduler's temporary ref.
> +		 */
> +		bpf_task_work_ctx_put(ctx);
>  		bpf_task_work_ctx_put(ctx);
>  		return;
>  	}
>  
>  	/*
> -	 * It's technically possible for just scheduled task_work callback to
> -	 * complete running by now, going SCHEDULING -> RUNNING and then
> -	 * dropping its ctx refcount. Instead of capturing an extra ref just
> -	 * to protect below ctx->state access, we rely on rcu_read_lock
> -	 * above to prevent kfree_rcu from freeing ctx before we return.
> +	 * The callback may already be running on the target task's CPU, but
> +	 * it waits for this invocation to finish before resetting task/prog
> +	 * or publishing STANDBY, and the temporary reference above keeps the
> +	 * ctx alive no matter how the other references are dropped here.
>  	 */
>  	state = cmpxchg(&ctx->state, BPF_TW_SCHEDULING, BPF_TW_SCHEDULED);
>  	if (state == BPF_TW_FREED)
>  		bpf_task_work_cancel(ctx); /* clean up if we switched into FREED state */
> +
> +	bpf_task_work_ctx_put(ctx);
>  }
>  
>  static struct bpf_task_work_ctx *bpf_task_work_fetch_ctx(struct bpf_task_work *tw,
> @@ -4537,6 +4557,7 @@ static struct bpf_task_work_ctx *bpf_task_work_fetch_ctx(struct bpf_task_work *t
>  	memset(ctx, 0, sizeof(*ctx));
>  	refcount_set(&ctx->refcnt, 1); /* map's own ref */
>  	ctx->state = BPF_TW_STANDBY;
> +	init_irq_work(&ctx->irq_work, bpf_task_work_irq);
>  
>  	old_ctx = cmpxchg(&twk->ctx, NULL, ctx);
>  	if (old_ctx) {
> @@ -4555,6 +4576,7 @@ static struct bpf_task_work_ctx *bpf_task_work_acquire_ctx(struct bpf_task_work
>  							   struct bpf_map *map)
>  {
>  	struct bpf_task_work_ctx *ctx;
> +	enum bpf_task_work_state state;
>  
>  	/*
>  	 * Sleepable BPF programs hold rcu_read_lock_trace but not
> @@ -4579,6 +4601,19 @@ static struct bpf_task_work_ctx *bpf_task_work_acquire_ctx(struct bpf_task_work
>  		bpf_task_work_ctx_put(ctx);
>  		return ERR_PTR(-EBUSY);
>  	}
> +	/*
> +	 * An add failure publishes STANDBY before its irq_work handler
> +	 * returns. Do not let a new round requeue the same irq_work until that
> +	 * handler has cleared BUSY. Otherwise two invocations can overlap on
> +	 * different CPUs; either tail can clear the shared BUSY bit and let a
> +	 * later irq_work_sync() return while the other invocation still runs.
> +	 */
> +	if (unlikely(irq_work_is_busy(&ctx->irq_work))) {
> +		state = cmpxchg(&ctx->state, BPF_TW_PENDING, BPF_TW_STANDBY);
> +		WARN_ON_ONCE(state != BPF_TW_PENDING && state != BPF_TW_FREED);

Let's remove this WARN_ON_ONCE(), we do not have asserts for state machine
states in other places.
> +		bpf_task_work_ctx_put(ctx);
> +		return ERR_PTR(-EBUSY);
> +	}
>  
>  	/*
>  	 * If no process or bpffs is holding a reference to the map, no new callbacks should be
> @@ -4628,7 +4663,6 @@ static int bpf_task_work_schedule(struct task_struct *task, struct bpf_task_work
>  	ctx->map = map;
>  	ctx->map_val = (void *)tw - map->record->task_work_off;
>  	init_task_work(&ctx->work, bpf_task_work_callback);
> -	init_irq_work(&ctx->irq_work, bpf_task_work_irq);
>  
>  	irq_work_queue(&ctx->irq_work);
>  	return 0;


  reply	other threads:[~2026-09-21 15:54 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-21 10:14 [PATCH bpf-next v2 0/2] bpf: Fix task work scheduling race Yun Lu
2026-09-21 10:14 ` [PATCH bpf-next v2 1/2] bpf: Fix task work scheduling and callback race Yun Lu
2026-09-21 15:54   ` Mykyta Yatsenko [this message]
2026-09-22  6:34     ` luyun
2026-09-21 10:14 ` [PATCH bpf-next v2 2/2] selftests/bpf: Add task work scheduling race test Yun Lu

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=47b04850-28e5-480b-a861-aed3670eea1d@gmail.com \
    --to=mykyta.yatsenko5@gmail.com \
    --cc=andrii@kernel.org \
    --cc=ast@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=daniel@iogearbox.net \
    --cc=eddyz87@gmail.com \
    --cc=emil@etsalapatis.com \
    --cc=ihor.solodrai@linux.dev \
    --cc=jolsa@kernel.org \
    --cc=luyun_611@163.com \
    --cc=martin.lau@linux.dev \
    --cc=memxor@gmail.com \
    --cc=song@kernel.org \
    --cc=yatsenko@meta.com \
    --cc=yonghong.song@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