Skip to content

scx_rustsched: Let callbacks hold kernel objects the kernel changes - #3980

Open
arighi wants to merge 2 commits into
mainfrom
rustsched-unsafecell-handles
Open

arighi wants to merge 2 commits into
mainfrom
rustsched-unsafecell-handles

Conversation

@arighi

@arighi arighi commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

The handle types that scx_rustsched callbacks receive (Task, Cgroup and Timer) wrap kernel memory without an UnsafeCell. Rust therefore treats a &Task argument as read-only and unaliased, and LLVM is free to assume nothing changes the task while the callback holds it. The kernel and the kfuncs do change it, so this is undefined behaviour.

The fix wraps the kernel structure in an UnsafeCell, which keeps the layout and removes the read-only and no-alias assumptions. For Timer the cell goes around the words inside bpf_timer, because the verifier finds a timer in a map value by the struct name bpf_timer.

Task and Cgroup wrap the kernel's task_struct and cgroup without an
UnsafeCell, so rustc marks a &Task or &Cgroup argument noalias readonly.
The kernel and the kfuncs change both while a callback holds them. LLVM
may then delete a store made through as_ptr() and reuse a field read
from before a kfunc call.

Both happen. A plain store to p->scx.disallow from a non-inlined helper
taking &Task was compiled away, and a plain read of the slice after
scx_bpf_task_set_slice() returned the value read before the call. The
library reads fields with read_volatile and writes through kfuncs, so
no scheduler in the tree is affected yet.

Wrap both in UnsafeCell, which keeps the layout (it is
repr(transparent)) and drops the readonly and noalias attributes.
as_ptr() now returns the cell's pointer instead of casting away const.

Signed-off-by: Andrea Righi <arighi@nvidia.com>
Timer has the problem c2fd7d9 ("scx_rustsched: Let callbacks change
the task and cgroup they are passed") fixed for Task and Cgroup. A timer
callback holds a &Timer while bpf_timer_init(), bpf_timer_cancel() or a
map delete on another CPU can change the timer's words, and start()
casts the shared reference to a mutable pointer. Rust never reads or
writes those words itself, so nothing can be miscompiled today. This
only removes the undefined behaviour.

Wrap the words of bpf_timer in an UnsafeCell rather than the struct
itself. The verifier finds the timer in a map value by the struct name
bpf_timer, which a wrapping UnsafeCell<bpf_timer> would hide. The layout
and the generated code stay the same.

Signed-off-by: Andrea Righi <arighi@nvidia.com>
@arighi
arighi requested review from etsal, htejun and sirlucjan November 12, 2026 22:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants