From: sashiko-bot@kernel.org
To: "Austin Schuh" <austin.linux@gmail.com>
Cc: linux-rt-devel@lists.linux.dev, linux-kernel@vger.kernel.org
Subject: Re: [RFC PATCH 2/2] io_uring/futex: add persistent waits and registered wakes
Date: Thu, 22 Oct 2026 03:13:00 +0000 [thread overview]
Message-ID: <sashiko-outbox-180899@kernel.org> (raw)
In-Reply-To: <20261022024701.1574811-3-austin.linux@gmail.com>
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] io_uring/futex: NULL deref in io_futex_node_get() on !CONFIG_MMU
- [High] io_uring/futex: busy-spin on teardown from uncleared futex_pending
- [Medium] io_uring/futex: 32-bit truncation of SQE fields in io_futex_prep()
--
commit d39ae03333a1ccb677abc6626226b97d34d99fdf
Author: Austin Schuh <austin.linux@gmail.com>
io_uring/futex: add persistent waits and registered wakes
This commit adds persistent futex waits and registered futex wakes to
io_uring to avoid futex hash bucket lock contention on PREEMPT_RT. It
introduces registration opcodes for futex words and uses an RCU-walked node
table and per-ring pending bitmaps to notify waiting rings without taking
sleeping locks in the wake path.
> diff --git a/io_uring/futex.c b/io_uring/futex.c
> index 9203998654ffb..9f5050bfd3d44 100644
> --- a/io_uring/futex.c
> +++ b/io_uring/futex.c
[ ... ]
> +/*
> + * A key without FUT_OFF_INODE names its mm by pointer. The node keeps the
> + * mm_struct allocated so the pointer can't be reused for another mm while
> + * the node can still be found by it.
> + */
> +static bool io_futex_key_has_mm(union futex_key *key)
> +{
> + return !(key->both.offset & FUT_OFF_INODE);
> +}
> +
> +static struct io_futex_node *io_futex_node_get(u32 __user *uaddr,
> + unsigned int flags)
> +{
> + union futex_key key = FUTEX_KEY_INIT;
> + struct io_futex_node *node, *new = NULL;
> + struct io_futex_bucket *b;
> + int ret;
> +
> + /* Write access, as a waker needs: reading the word is not enough. */
> + ret = get_futex_key(uaddr, flags, &key, FUTEX_WRITE);
> + if (ret)
> + return ERR_PTR(ret);
> + b = io_futex_bucket(&key);
> +again:
> + spin_lock(&b->lock);
> + hlist_for_each_entry(node, &b->nodes, hnode) {
> + if (futex_match(&node->key, &key)) {
> + /* A node at zero has already left the bucket. */
> + refcount_inc(&node->ref);
> + spin_unlock(&b->lock);
> + kfree(new);
> + return node;
> + }
> + }
> + if (!new) {
> + /* Allocate only on a miss, and not under the lock. */
> + spin_unlock(&b->lock);
> + new = kzalloc_obj(*new, GFP_KERNEL_ACCOUNT);
> + if (!new)
> + return ERR_PTR(-ENOMEM);
> + goto again;
> + }
> + new->key = key;
> + new->bucket = b;
> + INIT_HLIST_HEAD(&new->members);
> + refcount_set(&new->ref, 1);
> + if (io_futex_key_has_mm(&key))
> + mmgrab(key.private.mm);
> + hlist_add_head(&new->hnode, &b->nodes);
> + spin_unlock(&b->lock);
> + return new;
> +}
> +
> +static void io_futex_node_put(struct io_futex_node *node)
> +{
> + if (!refcount_dec_and_lock(&node->ref, &node->bucket->lock))
> + return;
> + hlist_del(&node->hnode);
> + spin_unlock(&node->bucket->lock);
> + if (io_futex_key_has_mm(&node->key))
> + mmdrop(node->key.private.mm);
[Severity: High]
Can this trigger a NULL pointer dereference in io_futex_node_get() and
io_futex_node_put() on !CONFIG_MMU kernels?
On !CONFIG_MMU builds, FLAGS_SHARED is defined as 0x0000 in
kernel/futex/futex.h, so get_futex_key() in kernel/futex/core.c always takes
the !fshared path, leaves FUT_OFF_INODE unset in key->both.offset, and sets
key->private.mm to NULL:
kernel/futex/core.c:get_futex_key() {
if (!fshared) {
if (IS_ENABLED(CONFIG_MMU))
key->private.mm = mm;
else
key->private.mm = NULL;
key->private.address = address;
return 0;
}
}
Because io_futex_key_has_mm() only checks !(key->both.offset & FUT_OFF_INODE),
it returns true when CONFIG_MMU is disabled, causing io_futex_node_get() to
call mmgrab(NULL) and io_futex_node_put() to call mmdrop(NULL). Should
io_futex_key_has_mm() also check key->private.mm != NULL (or
IS_ENABLED(CONFIG_MMU))?
> + /* A registered wake may still be walking it. */
> + kfree_rcu(node, rcu);
> +}
[ ... ]
> @@ -127,23 +847,69 @@ bool io_futex_remove_all(struct io_ring_ctx *ctx, struct io_uring_task *tctx,
> int io_futex_prep(struct io_kiocb *req, const struct io_uring_sqe *sqe)
> {
> struct io_futex *iof = io_kiocb_to_cmd(req, struct io_futex);
> - u32 flags;
> + u32 flags, uflags;
>
> - if (unlikely(sqe->len || sqe->futex_flags || sqe->buf_index ||
> - sqe->file_index))
> + if (unlikely(sqe->len))
> return -EINVAL;
>
> + uflags = READ_ONCE(sqe->futex_flags);
> + iof->iou_flags = uflags;
> + iof->member = NULL;
> iof->uaddr = u64_to_user_ptr(READ_ONCE(sqe->addr));
> iof->futex_val = READ_ONCE(sqe->addr2);
> iof->futex_mask = READ_ONCE(sqe->addr3);
> flags = READ_ONCE(sqe->fd);
>
> + if (req->opcode == IORING_OP_FUTEX_WAIT) {
> + if (uflags & ~(IORING_FUTEX_PERSISTENT | IORING_FUTEX_REARM) ||
> + uflags == (IORING_FUTEX_PERSISTENT | IORING_FUTEX_REARM) ||
> + sqe->buf_index)
> + return -EINVAL;
> + /*
> + * A persistent wait's slot, as a direct descriptor's: index + 1,
> + * or IORING_FILE_INDEX_ALLOC. A re-arm names the slot the same
> + * way, as a close does.
> + */
> + iof->futex_nr = READ_ONCE(sqe->file_index);
> + if (!(uflags & (IORING_FUTEX_PERSISTENT | IORING_FUTEX_REARM)) !=
> + !iof->futex_nr)
> + return -EINVAL;
> + if (uflags & IORING_FUTEX_REARM) {
> + if (iof->uaddr || iof->futex_val || iof->futex_mask ||
> + flags || iof->futex_nr == IORING_FILE_INDEX_ALLOC)
> + return -EINVAL;
> + iof->futex_nr--;
> + return 0;
> + }
> + } else if (sqe->file_index) {
> + return -EINVAL;
> + } else if (uflags) {
> + /* Woken by slot: no word, no count, only the bitset. */
> + if (uflags != IORING_FUTEX_REGISTERED || iof->uaddr ||
> + iof->futex_val || flags || !iof->futex_mask ||
> + iof->futex_mask > U32_MAX)
> + return -EINVAL;
[Severity: Medium]
On 32-bit architectures, struct io_futex defines uaddr as void __user * and
futex_val and futex_mask as unsigned long:
struct io_futex {
struct file *file;
void __user *uaddr;
unsigned long futex_val;
unsigned long futex_mask;
...
};
When io_futex_prep() reads the 64-bit sqe->addr, sqe->addr2, and sqe->addr3
fields into iof->uaddr, iof->futex_val, and iof->futex_mask, doesn't that
truncate the upper 32 bits on 32-bit kernels before the validation checks
run?
In addition, on 32-bit architectures where unsigned long is 32 bits wide,
won't the check iof->futex_mask > U32_MAX in io_futex_prep() always be false
and trigger a compiler tautological-compare warning while failing to reject
non-zero upper 32 bits in sqe->addr3?
[ ... ]
> diff --git a/io_uring/tw.h b/io_uring/tw.h
> index 3ade5ad577fda..dc9125ebdd829 100644
> --- a/io_uring/tw.h
> +++ b/io_uring/tw.h
[ ... ]
> @@ -90,7 +91,8 @@ static inline int io_run_task_work(void)
>
> static inline bool io_local_work_pending(struct io_ring_ctx *ctx)
> {
> - return !mpscq_empty(&ctx->work_list);
> + return !mpscq_empty(&ctx->work_list) ||
> + atomic_read(&ctx->futex_pending);
> }
[Severity: High]
Could including ctx->futex_pending in io_local_work_pending() cause a busy-spin
in io_ring_ctx_wait_inflight() during ring teardown?
ctx->futex_pending is only cleared in __io_run_local_work(), which requires
ctx->submitter_task == current. During ring teardown, io_ring_ctx_cancel()
calls io_cancel_local_task_work(), which drains ctx->work_list under
uring_lock but does not clear ctx->futex_pending:
io_uring/tw.c:io_cancel_local_task_work() {
guard(mutex)(&ctx->uring_lock);
while (!mpscq_empty(&ctx->work_list)) {
...
}
io_submit_flush_completions(ctx);
}
If the ring fd is closed from a task other than ctx->submitter_task (or a
concurrent waker calls io_ring_futex_kick() during teardown) while an
uncancelable request such as storage I/O is still in flight,
io_ring_ctx_wait_inflight() sees io_local_work_pending(ctx) remain true on
every iteration:
io_uring/io_uring.c:io_ring_ctx_wait_inflight() {
/* don't sleep on work that's already there and that we can run */
if (ran_own && ((tctx && !mpscq_empty(&tctx->task_list)) ||
io_local_work_pending(ctx)))
__set_current_state(TASK_RUNNING);
else
schedule_timeout(1);
}
Wouldn't that skip schedule_timeout(1) and spin in a tight loop until all
in-flight requests complete unless io_cancel_local_task_work() also clears
ctx->futex_pending?
>
> static inline bool io_task_work_pending(struct io_ring_ctx *ctx)
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261022024701.1574811-1-austin.linux@gmail.com?part=2
prev parent reply other threads:[~2026-10-22 3:13 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-22 2:46 [RFC PATCH 0/2] io_uring: futex wakeups that a preempted lower-priority task can't delay Austin Schuh
2026-10-22 2:46 ` [RFC PATCH 1/2] io_uring: don't let a preempted task_work add hold back the wake up Austin Schuh
2026-10-22 2:46 ` [RFC PATCH 2/2] io_uring/futex: add persistent waits and registered wakes Austin Schuh
2026-10-22 3:13 ` sashiko-bot [this message]
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-180899@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=austin.linux@gmail.com \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-rt-devel@lists.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
all inboxes | Powered by JetHome®