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
next prev parent 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