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 v3 1/2] bpf: Fix task work scheduling and callback race
Date: Wed, 23 Sep 2026 18:46:46 +0100 [thread overview]
Message-ID: <03fe858d-b3dc-4244-9a4e-6177a0530344@gmail.com> (raw)
In-Reply-To: <20260922100042.136818-2-luyun_611@163.com>
On 9/22/26 11:00 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 taking a task reference in the scheduling irq_work before
> publishing the callback. Use the captured task for cancellation at the
> handler tail, so the task remains valid even if the callback or destroy
> path has reset ctx->task. Release the reference on every add success and
> failure path.
>
> The asynchronous cancellation path is queued only after deletion wins
> SCHEDULED-to-FREED. In that case the callback observes FREED and cannot
> reset ctx->task, while the map's transferred ctx reference prevents the
> destroy path from doing so. It can therefore safely pass ctx->task to the
> same cancellation helper. The two cancellation paths are mutually
> exclusive: the scheduling handler handles deletion from SCHEDULING, and
> the asynchronous path handles deletion from SCHEDULED.
>
> The callback or add-failure path can publish STANDBY before the scheduling
> irq_work returns. Initialize irq_work when the context is created and
> reject a new round while it is still BUSY. This prevents the old handler
> from consuming the new round's SCHEDULING state and publishing a false
> SCHEDULED state, and prevents invocations on different CPUs from
> overlapping on the same context.
>
> This keeps the task-work callback non-blocking and adds no context fields
> or states.
>
> Fixes: 38aa7003e369 ("bpf: task work scheduling kfuncs")
> Reported-by: Jackie Liu <liuyun01@kylinos.cn>
> Suggested-by: Mykyta Yatsenko <mykyta.yatsenko5@gmail.com>
> Signed-off-by: Yun Lu <luyun@kylinos.cn>
> ---
Looks good, the only thing that bothers a bit is
PENDING -> STANDBY transition when irq_work_is_busy(),
but overall looks like the cleanest solution.
Acked-by: Mykyta Yatsenko <yatsenko@meta.com>
> kernel/bpf/helpers.c | 39 ++++++++++++++++++++++++++++-----------
> 1 file changed, 28 insertions(+), 11 deletions(-)
>
> diff --git a/kernel/bpf/helpers.c b/kernel/bpf/helpers.c
> index b3cc5c8fc875..d8bea7538de5 100644
> --- a/kernel/bpf/helpers.c
> +++ b/kernel/bpf/helpers.c
> @@ -4437,7 +4437,8 @@ static void bpf_task_work_ctx_put(struct bpf_task_work_ctx *ctx)
> }
> }
>
> -static void bpf_task_work_cancel(struct bpf_task_work_ctx *ctx)
> +static void bpf_task_work_cancel(struct bpf_task_work_ctx *ctx,
> + struct task_struct *task)
> {
> /*
> * Scheduled task_work callback holds ctx ref, so if we successfully
> @@ -4445,7 +4446,7 @@ static void bpf_task_work_cancel(struct bpf_task_work_ctx *ctx)
> * cancel, callback will inevitably run or has already completed
> * running, and it would have taken care of its ctx ref itself.
> */
> - if (task_work_cancel(ctx->task, &ctx->work))
> + if (task_work_cancel(task, &ctx->work))
> bpf_task_work_ctx_put(ctx);
> }
>
> @@ -4486,6 +4487,7 @@ static void bpf_task_work_callback(struct callback_head *cb)
> static void bpf_task_work_irq(struct irq_work *irq_work)
> {
> struct bpf_task_work_ctx *ctx = container_of(irq_work, struct bpf_task_work_ctx, irq_work);
> + struct task_struct *task;
> enum bpf_task_work_state state;
> int err;
>
> @@ -4496,7 +4498,8 @@ static void bpf_task_work_irq(struct irq_work *irq_work)
> return;
> }
>
> - err = task_work_add(ctx->task, &ctx->work, ctx->mode);
> + task = get_task_struct(ctx->task);
> + err = task_work_add(task, &ctx->work, ctx->mode);
> if (err) {
> bpf_task_work_ctx_reset(ctx);
> /*
> @@ -4505,19 +4508,20 @@ static void bpf_task_work_irq(struct irq_work *irq_work)
> */
> (void)cmpxchg(&ctx->state, BPF_TW_SCHEDULING, BPF_TW_STANDBY);
> bpf_task_work_ctx_put(ctx);
> + put_task_struct(task);
> 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 have completed and reset ctx->task already. Keep the
> + * captured task alive until this invocation is done with cancellation.
> + * The RCU read-side section keeps ctx memory alive until then.
> */
> 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_cancel(ctx, task); /* clean up if we switched into FREED state */
> +
> + put_task_struct(task);
> }
>
> static struct bpf_task_work_ctx *bpf_task_work_fetch_ctx(struct bpf_task_work *tw,
> @@ -4537,6 +4541,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) {
> @@ -4579,6 +4584,18 @@ 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);
> }
> + /*
> + * The callback or add-failure path can publish STANDBY before the
> + * scheduling irq_work returns. Do not let a new round reuse the context
> + * until BUSY clears. Otherwise the old handler can consume the new
> + * round's SCHEDULING state and publish a false SCHEDULED state, and two
> + * invocations can overlap on different CPUs.
> + */
> + if (unlikely(irq_work_is_busy(&ctx->irq_work))) {
> + (void)cmpxchg(&ctx->state, BPF_TW_PENDING, BPF_TW_STANDBY);
> + 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 +4645,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;
> @@ -4777,7 +4793,8 @@ static void bpf_task_work_cancel_scheduled(struct irq_work *irq_work)
> {
> struct bpf_task_work_ctx *ctx = container_of(irq_work, struct bpf_task_work_ctx, irq_work);
>
> - bpf_task_work_cancel(ctx); /* this might put task_work callback's ref */
> + /* SCHEDULED -> FREED keeps ctx->task stable until this runs. */
> + bpf_task_work_cancel(ctx, ctx->task); /* this might put task_work callback's ref */
> bpf_task_work_ctx_put(ctx); /* and here we put map's own ref that was transferred to us */
> }
>
>
next prev parent reply other threads:[~2026-09-23 17:46 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-22 10:00 [PATCH bpf-next v3 0/2] bpf: Fix task work scheduling race Yun Lu
2026-09-22 10:00 ` [PATCH bpf-next v3 1/2] bpf: Fix task work scheduling and callback race Yun Lu
2026-09-23 17:46 ` Mykyta Yatsenko [this message]
2026-09-23 23:04 ` Alexei Starovoitov
2026-09-24 9:55 ` luyun
2026-09-22 10:00 ` [PATCH bpf-next v3 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=03fe858d-b3dc-4244-9a4e-6177a0530344@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