BPF List
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Kaitao Cheng" <kaitao.cheng@linux.dev>
Cc: bpf@vger.kernel.org
Subject: Re: [RFC v3 1/3] block: Introduce the UFQ I/O scheduler
Date: Sat, 03 Oct 2026 04:45:53 +0000	[thread overview]
Message-ID: <20261003044553.BCA211F000FF@smtp.kernel.org> (raw)
In-Reply-To: <20261003042748.33795-3-kaitao.cheng@linux.dev>

Thank you for your contribution! Sashiko AI review found 11 potential issue(s) to consider:
- [Critical] block: ufq_bio_merge() destroys active target request leading to UAF
- [Critical] block: missing state validation in UFQ merges allows corruption of in-flight requests
- [Critical] bpf: unbounded stack buffer overflow in UFQ struct_ops via PTR_TO_BUF
- [High] bpf: bpf_request_try_merge() permanently leaks absorbed requests
- [High] block: ufq_insert_requests() re-inserts requests that absorbed a back-merge
- [High] bpf: incompatible reference contract in dispatch_req leads to UAF
- [High] block: TOCTOU Use-After-Free in UFQ merge and dispatch paths
- [High] bpf: bpf_request_bio_try_merge() allows double-merging of bios causing list corruption
- [High] bpf: bpf_request_try_merge() crashes on already-merged requests
- [Medium] bpf: spin_trylock() inside KF_SPINLOCK_SAFE causes Invalid wait context on PREEMPT_RT
- [Low] block: modpost failure due to missing EXPORT_SYMBOL_GPL for core functions
--

commit 439b7e4704107bb499b42ef24fd4ef9ddf9e15ea
Author: Kaitao Cheng <chengkaitao@kylinos.cn>

block: Introduce the UFQ I/O scheduler

Introduce IOSCHED_UFQ, a blk-mq elevator ("ufq: User-programmable
Flexible Queueing") whose policy is supplied by an eBPF program via
struct_ops (insert, dispatch, merge, finish, etc.).

> diff --git a/block/blk-mq.c b/block/blk-mq.c
> index e929f94b25c7f..aa8ae5bd4b6fd 100644
> --- a/block/blk-mq.c
> +++ b/block/blk-mq.c
> @@ -809,7 +809,7 @@ static void blk_mq_finish_request(struct request *rq)
>  	}
>  }
>  
> -static void __blk_mq_free_request(struct request *rq)
> +void __blk_mq_free_request(struct request *rq)
>  {

[Severity: Low]
Will this cause a modpost failure?

CONFIG_IOSCHED_UFQ can be compiled as a module, and it calls
__blk_mq_free_request(), bpf_attempt_merge(), and blk_attempt_bio_merge().
Since these core block layer functions are not exported via EXPORT_SYMBOL_GPL,
linking the module will fail during modpost.

> diff --git a/block/ufq-bpfops.c b/block/ufq-bpfops.c
> new file mode 100644
> index 0000000000000..5efcfcaa9cb8c
> --- /dev/null
> +++ b/block/ufq-bpfops.c
> [ ... ]
> +static bool bpf_ufq_is_valid_access(int off, int size,
> +				    enum bpf_access_type type,
> +				    const struct bpf_prog *prog,
> +				    struct bpf_insn_access_aux *info)
> +{
> [ ... ]
> +	if (size == sizeof(__u64) && prog->aux->attach_func_name &&
> +	    ((!strcmp(prog->aux->attach_func_name, "merge_req") && off == 16) ||
> +	     (!strcmp(prog->aux->attach_func_name, "merge_bio") && off == 24))) {
> +		if (!btf_ctx_access(off, size, type, prog, info))
> +			return false;
> +		info->reg_type = PTR_TO_BUF;
> +		return true;
> +	}

[Severity: Critical]
Does this allow an unbounded stack buffer overflow?

The verifier is told to treat the pointer as a variable-sized buffer
(PTR_TO_BUF) to allow pointer writes, but the subsystem fails to validate the
size against the actual variable size in the check_member callback. A
malicious BPF program can write out of bounds into the stack variables passed
to merge_req and merge_bio.

> diff --git a/block/ufq-iosched.c b/block/ufq-iosched.c
> new file mode 100644
> index 0000000000000..f7964ccfc1ee3
> --- /dev/null
> +++ b/block/ufq-iosched.c
> [ ... ]
> +static struct request *ufq_dispatch_request(struct blk_mq_hw_ctx *hctx)
> +{
> [ ... ]
> +		/*
> +		 * The BPF insert_req callback bumps the request's reference
> +		 * count; dispatch_req returns that same request with an extra
> +		 * reference held. The kernel must put that reference here,
> +		 * and the request's refcount is always greater than zero at
> +		 * this point.
> +		 */
> +		if (WARN_ON_ONCE(req_ref_put_and_test(rq))) {
> +			__blk_mq_free_request(rq);

[Severity: High]
Is the reference contract here incompatible with the BPF verifier?

ufq_dispatch_request() expects BPF to transfer an acquired reference.
However, BPF struct_ops do not support returning acquired references. To
bypass the verifier, a BPF program must return an unacquired pointer,
which causes the kernel to decrement the core reference count instead of
an extra one, freeing an active request prematurely.

> +			ufq_bpfops_put();
> +			return NULL;
> +		}
> +
> +		ctx = rq->mq_ctx;
> +		spin_lock(&ctx->lock);

[Severity: High]
Does this expose a Time-of-Check to Time-of-Use race?

The BPF-owned reference is dropped via req_ref_put_and_test() before
dereferencing rq->mq_ctx and acquiring the lock. If the request completes
concurrently during this window, the reference count drops to zero and it is
freed, resulting in a Use-After-Free.

> [ ... ]
> +static bool ufq_bio_merge(struct request_queue *q, struct bio *bio,
> +			  unsigned int nr_segs)
> +{
> [ ... ]
> +			if (rq) {
> +				spin_lock(&rq->mq_ctx->lock);
> +				if (!list_empty(&rq->queuelist)) {
> +					list_del_init(&rq->queuelist);
> +					atomic_dec(&ufq->rqs_count);
> +				}
> +				spin_unlock(&rq->mq_ctx->lock);
> +				/* merge_bio transfers an extra BPF-owned reference. */
> +				WARN_ON_ONCE(req_ref_put_and_test(rq));
> +				blk_mq_free_request(rq);

[Severity: Critical]
Does this cause a Use-After-Free by destroying the active target request?

If the BPF policy's merge_bio hook successfully merges a bio into a request,
it returns the target rq. The kernel then erroneously calls
blk_mq_free_request(rq), actively destroying the target request that is
still waiting for dispatch instead of just dropping the BPF reference.

> [ ... ]
> +static enum elv_merge ufq_try_insert_merge(struct request_queue *q,
> +					   struct request **new)
> +{
> [ ... ]
> +	ops = ufq_bpfops_tryget();
> +	if (ops && ops->merge_req) {
> +		target = ops->merge_req(q, rq, &merge_type);

[Severity: Critical]
Can this overflow the merge_type stack variable?

This points to the stack variable merge_type which is passed to the BPF
program via PTR_TO_BUF without proper bounds checking, leading to local
privilege escalation.

> +		type = (enum elv_merge)merge_type;
> +	}
> +
> +	if (target && WARN_ON_ONCE(req_ref_put_and_test(target))) {
> +		__blk_mq_free_request(target);

[Severity: High]
Does this introduce a Time-of-Check to Time-of-Use race?

The BPF-owned reference on the target request is dropped via
req_ref_put_and_test() before taking target->mq_ctx->lock and validating
the request. If the request completes concurrently during this window, the
reference count drops to zero and it is freed, resulting in a Use-After-Free
when dereferencing target->mq_ctx later in this function.

> +		ufq_bpfops_put();
> +		return ELEVATOR_NO_MERGE;
> +	}
> +
> +	if (type == ELEVATOR_NO_MERGE || !target) {
> +		if (ops)
> +			ufq_bpfops_put();
> +		return ELEVATOR_NO_MERGE;
> +	} else if (type == ELEVATOR_FRONT_MERGE) {
> +		if (rq->mq_ctx != target->mq_ctx || rq->mq_hctx != target->mq_hctx)
> +			goto rollback;
> +		spin_lock(&target->mq_ctx->lock);

[Severity: Critical]
Is there a missing state validation here?

The BPF merge_req callback blindly returns an arbitrary target request. There
is no verification that the target request belongs to the correct queue, nor
that its state is MQ_RQ_IDLE. Modifying an in-flight request concurrently
with active DMA transfers corrupts data.

> [ ... ]
> +static void ufq_insert_requests(struct blk_mq_hw_ctx *hctx,
> +			       struct list_head *list,
> +			       blk_insert_t flags)
> +{
> [ ... ]
> +		if (ops && rq && ops->insert_req) {
> +			rq->elv.priv[0] = (void *)((uintptr_t)rq->elv.priv[0]
> +				  | UFQ_PRIV_IN_BPF);
> +			ret = ops->insert_req(q, rq, flags);

[Severity: High]
Does this cause double-insertion for requests that absorbed a back-merge?

If ufq_try_insert_merge() results in an ELEVATOR_BACK_MERGE, rq is updated
to point to the target request. This function then unconditionally calls
ops->insert_req() on this pre-existing target request, corrupting the BPF
scheduler's internal tracking.

> diff --git a/block/ufq-kfunc.c b/block/ufq-kfunc.c
> new file mode 100644
> index 0000000000000..843aeb0e67cff
> --- /dev/null
> +++ b/block/ufq-kfunc.c
> @@ -0,0 +1,169 @@
> [ ... ]
> +__bpf_kfunc bool bpf_request_bio_try_merge(struct request *rq, struct bio *bio,
> +					   unsigned int nr_segs)
> +{
> [ ... ]
> +	/*
> +	 * KF_SPINLOCK_SAFE callers (BPF merge_bio) often already hold a scheduler
> +	 * lock taken with irqsave (e.g. PFQ dd->lock).  Blocking on ctx->lock
> +	 * nesting that order deadlocks / hard-locks under load.  Trylock: skip
> +	 * the merge if ctx is busy; bio will be issued as a new request.
> +	 */
> +	if (!spin_trylock(&ctx->lock))
> +		goto out;

[Severity: Medium]
Will this cause an Invalid wait context warning on PREEMPT_RT?

This kfunc is annotated with KF_SPINLOCK_SAFE, meaning it can execute with
interrupts disabled or a raw_spinlock_t held. Calling spin_trylock() inside
this context violates strict lock nesting rules, as it maps to an rt_mutex
operation on PREEMPT_RT.

> +
> +	merged = blk_attempt_bio_merge(q, rq, bio, nr_segs, true) == BIO_MERGE_OK;

[Severity: Critical]
Is there missing state validation for the target request here?

The kfunc modifies the target request's internal structures without verifying
that its state is MQ_RQ_IDLE, which can corrupt data if the request is already
in-flight.

[Severity: High]
Can a BPF program trigger double-merging of bios here?

Because sector proximity checks will still pass and there is no safeguard
against already-merged bios, calling this kfunc multiple times with the
same bio will link it into the biotail of multiple requests, corrupting
the bi_next pointers.

> [ ... ]
> +__bpf_kfunc struct request *bpf_request_try_merge(struct request *rq, struct request *next)
> +{
> [ ... ]
> +	free = bpf_attempt_merge(q, rq, next);

[Severity: High]
Can this code crash by dereferencing a NULL pointer?

If bpf_request_try_merge() successfully absorbs a request, the block layer
sets next->bio = NULL. If the BPF program erroneously calls this kfunc
again on the already-absorbed request, attempt_merge() will
unconditionally dereference next->bio->bi_write_hint.

> [ ... ]
> +BTF_KFUNCS_START(ufq_kfunc_set_ops)
> +BTF_ID_FLAGS(func, bpf_request_acquire, KF_ACQUIRE | KF_RET_NULL)
> +BTF_ID_FLAGS(func, bpf_request_release, KF_RELEASE)
> +BTF_ID_FLAGS(func, bpf_request_bio_try_merge, KF_SPINLOCK_SAFE)
> +BTF_ID_FLAGS(func, bpf_request_try_merge, KF_SPINLOCK_SAFE)

[Severity: High]
Does this code permanently leak the absorbed request?

bpf_request_try_merge() returns the redundant absorbed request, but without
the KF_ACQUIRE flag, the BPF verifier treats it as an unreferenced pointer.
The BPF program cannot legally free it via bpf_request_release(), and the
kernel omits freeing it, resulting in a permanent leak and eventual
exhaustion of block tags.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261003042748.33795-1-kaitao.cheng@linux.dev?part=1

  reply	other threads:[~2026-10-03  4:45 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-03  4:27 [RFC v3 0/3] block: Introduce a BPF-based I/O scheduler Kaitao Cheng
2026-10-03  4:27 ` [RFC v3 2/3] tools/ufq_iosched: add BPF example scheduler and build scaffolding Kaitao Cheng
2026-10-03  4:44   ` sashiko-bot
2026-10-03  4:27 ` [RFC v3 1/3] block: Introduce the UFQ I/O scheduler Kaitao Cheng
2026-10-03  4:45   ` sashiko-bot [this message]
2026-10-03  4:27 ` [RFC v3 3/3] tools/ufq_iosched: add PFQ eBPF " Kaitao Cheng
2026-10-03  4:42   ` sashiko-bot
2026-10-03  9:36 ` [RFC v3 0/3] block: Introduce a BPF-based " Alexei Starovoitov
2026-10-03 11:15   ` Kaitao Cheng

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=20261003044553.BCA211F000FF@smtp.kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=kaitao.cheng@linux.dev \
    --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