rust: sync: Make Guard::lock_ref unavailable if Lock is not Sync
Subsystems (3)
v2 cover rust: sync: Make Guard::lock_ref unavailable if Lock is not Sync
Dear Linux Kernel Maintainers,
We are developing a tool called FerroLens to detect potential unsound behavior in Rust code in the Linux kernel.
The tool internally uses an LLM to detect bugs.
We then perform manual analysis to verify these reports. FerroLens reported the following bug in rust/kernel/sync/lock.rs.
Guard::lock_ref() returns a reference to the Lock stored in the Guard. Guard is Sync if T is Sync. However, Lock is Sync only if T is Send. This means that a reference to Guard can be sent across threads and multiple threads can acquire a reference to the Lock,
even if Lock is not Sync.
The following scenario is possible:
If T is sync but not send,
Thread A: Creates a Lock<T>. Because T is not Send, Lock<T> is not Sync. It cannot be shared with other threads.
Thread A: Locks the mutex and gets a Guard<T>.
Since T is Sync, code says Guard<T> is Sync.
Thread A can now safely give a &Guard<T> to Thread B.
Thread B: calls guard.lock_ref(). This returns a &'a Lock<T>. Thread B now has a reference to the Lock. In this case,
even after Thread A drops the guard, Thread B still has that &Lock. Thread B can now call lock.lock() and get a &mut T.
This is unsound.
Here is a poc kernel module which demonstrates this problem:
use core::sync::atomic::{AtomicBool, Ordering};
use kernel::{
bindings, c_str,
error::from_err_ptr,
prelude::*,
sync::{
lock::{mutex::MutexBackend, Backend, Guard, Lock},
new_mutex, Arc,
},
types::NotThreadSafe,
};use pin_init::stack_pin_init;
module! {
type: LockRefPoc,
name: "lock_ref_poc",
authors: ["Priya Govindasamy"],
description: "Sample module demonstrating cross-thread Guard::lock_ref access",
license: "GPL",
}
struct LockRefPoc;
struct SyncButNotSend {
value: u32,
_not_send: NotThreadSafe,
}// SAFETY: Access to value is synchronized by the lock in this sample, and the marker field is // only used to prevent Send.
unsafe impl Sync for SyncButNotSend {}
struct HandoffState {
lock_ref_ready: AtomicBool,
guard_dropped: AtomicBool,
relock_done: AtomicBool,
}
struct ThreadBContext {
guard_addr: usize,
handoff: Arc<HandoffState>,
thread_id: u32,
}fn guard_matches_lock<T, B: Backend>(guard: &Guard<'_, T, B>, lock: &Lock<T, B>) -> bool {
core::ptr::eq(guard.lock_ref(), lock)
}unsafe extern "C" fn thread_b_main(data: *mut kernel::ffi::c_void) -> i32 {
// SAFETY: `data` comes from `Arc::into_raw` in `spawn_thread_b`, and the thread takes back
// ownership of exactly one reference.
let ctx = unsafe { Arc::from_raw(data.cast::<ThreadBContext>()) };
let guard: &Guard<'_, SyncButNotSend, MutexBackend> =
// SAFETY: Thread A passes the address of a live guard to thread B.
unsafe { &*(ctx.guard_addr as *const Guard<'_, SyncButNotSend, MutexBackend>) };
let leaked_lock = guard.lock_ref();
pr_info!(
"thread B obtained &Lock<SyncButNotSend, _> through &Guard\n",
);
ctx.handoff.lock_ref_ready.store(true, Ordering::Release);
while !ctx.handoff.guard_dropped.load(Ordering::Acquire) {
unsafe { bindings::msleep(20) };
}
let mut relocked = leaked_lock.lock();
relocked.value += 1;
pr_info!(
"thread B locked after thread A dropped the original guard; value={}\n",
relocked.value
);
ctx.handoff.relock_done.store(true, Ordering::Release);
0
}fn spawn_thread_b(guard_addr: usize, handoff: Arc<HandoffState>, thread_id: u32) -> Result {
let ctx = Arc::new(
ThreadBContext {
guard_addr,
handoff,
thread_id,
},
GFP_KERNEL,
)?;
let data = Arc::into_raw(ctx) as *mut kernel::ffi::c_void;
let task = match from_err_ptr(unsafe {
bindings::kthread_create_on_node(
Some(thread_b_main),
data,
bindings::NUMA_NO_NODE,
c_str!("lock_ref_thread_b/%u").as_char_ptr(),
thread_id,
)
}) {
Ok(task) => task,
Err(err) => {
// SAFETY: Thread creation failed, so the raw reference was not handed off.
unsafe { drop(Arc::from_raw(data.cast::<ThreadBContext>())) };
return Err(err);
}
};
// SAFETY: `task` is a valid sleeping kthread returned by `kthread_create_on_node`.
unsafe { bindings::wake_up_process(task) };
Ok(())
}unsafe extern "C" fn thread_a_main(_data: *mut kernel::ffi::c_void) -> i32 {
let handoff = match Arc::new(
HandoffState {
lock_ref_ready: AtomicBool::new(false),
guard_dropped: AtomicBool::new(false),
relock_done: AtomicBool::new(false),
},
GFP_KERNEL,
) {
Ok(h) => h,
Err(_) => return ENOMEM.to_errno(),
};
stack_pin_init!(let lock = new_mutex!(
SyncButNotSend {
value: 100,
_not_send: NotThreadSafe,
},
"lock_ref_poc.thread_a_lock"
));
let guard = lock.lock();
if !guard_matches_lock(&guard, &lock) {
pr_err!("thread A: guard.lock_ref() did not return the original lock\n");
return EINVAL.to_errno();
}
if let Err(err) = spawn_thread_b(
(&guard as *const Guard<'_, SyncButNotSend, MutexBackend>) as usize,
handoff.clone(),
2,
) {
return err.to_errno();
}
while !handoff.lock_ref_ready.load(Ordering::Acquire) {
unsafe { bindings::msleep(20) };
}
drop(guard);
pr_info!("thread A dropped the original guard after thread B captured &Lock\n");
handoff.guard_dropped.store(true, Ordering::Release);
while !handoff.relock_done.load(Ordering::Acquire) {
unsafe { bindings::msleep(20) };
}
0
}fn spawn_thread_a() -> Result {
let task = match from_err_ptr(unsafe {
bindings::kthread_create_on_node(
Some(thread_a_main),
core::ptr::null_mut(),
bindings::NUMA_NO_NODE,
c_str!("lock_ref_thread_a").as_char_ptr(),
)
}) {
Ok(task) => task,
Err(err) => return Err(err),
};
// SAFETY: `task` is a valid sleeping kthread returned by `kthread_create_on_node`.
unsafe { bindings::wake_up_process(task) };
Ok(())
}impl kernel::Module for LockRefPoc {
fn init(_module: &'static ThisModule) -> Result<Self> {
spawn_thread_a()?;
Ok(Self)
}
}impl Drop for LockRefPoc {
fn drop(&mut self) {
pr_info!("lock_ref_poc unloaded\n");
}
}Makefile:
obj-m += lock_ref_poc.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) LLVM=1 modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
Output:
[ 360.591802] lock_ref_poc: thread B obtained &Lock<SyncButNotSend, _> through &Guard [ 360.617600] lock_ref_poc: thread A dropped the original guard after thread B captured &Lock [ 360.641550] lock_ref_poc: thread B locked after thread A dropped the original guard; value=101 [ 362.580557] lock_ref_poc: lock_ref_poc unloaded
Priya Bala Govindasamy (1):
Make Guard::lock_ref unavailable if Lock is not Sync
rust/kernel/sync/lock.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-)
Signature · 1 line
2.34.1
PATCH v2 1/1 Make Guard::lock_ref unavailable if Lock is not Sync
Guard::lock_ref() returns a reference to the Lock stored in the Guard. Guard is Sync if T is Sync. However, Lock is Sync only if T is Send. This means that a reference to Guard can be sent across threads and multiple threads can acquire a reference to the Lock,
even if Lock is not Sync.
The following scenario is possible:
If T is sync but not send,
Thread A: Creates a Lock<T>. Because T is not Send, Lock<T> is not Sync. It cannot be shared with other threads.
Locks the mutex and gets a Guard<T>.
Since T is Sync, code says Guard<T> is Sync.
Thread A can now safely give a &Guard<T> to Thread B.
Thread B: calls guard.lock_ref(). This returns a &'a Lock<T>. Thread B now has a reference to the Lock. In this case,
even after Thread A drops the guard, Thread B still has that &Lock. Thread B can now call lock.lock() and get a &mut T.
This is unsound.
Fix this issue by adding a trait bound to Guard::lock_ref so that this method is available only when Lock<T, B> implements Sync. This ensures that multiple threads cannot acquire a reference to the Lock if it is not Sync.
---
changes in v2:
- Add a trait bound only to the Guard::lock_ref() method. Changes to the lock lifetime are not necessary.
rust/kernel/sync/lock.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-)
rust/kernel/sync/lock.rs +4 -1
@@ -234,7 +234,10 @@ impl<'a, T: ?Sized, B: Backend> Guard<'a, T, B> {/// // `g` originates from `l`. /// assert_held(&g, &l); /// ``` -pub fn lock_ref (&self) -> &'aLock <T ,B > { +pub fn lock_ref (&self) -> &'aLock <T ,B > +where +Lock <T ,B >:Sync , + { self.lock }
Signature · 1 line
2.34.1
Re: [PATCH v2 1/1] Make Guard::lock_ref unavailable if Lock is not Sync
Quoted 2 lines · Priya Bala Govindasamy
Guard::lock_ref()returns a reference to the Lock stored in the Guard. Guard is Sync if T is Sync. However, Lock is Sync only if T is Send.
Please quote code with ``
Quoted 4 lines
This means that a reference to Guard can be sent across threads and multiple threads can acquire a reference to the Lock, even if Lock is not Sync.
Quoted 12 lines
The following scenario is possible:
If T is sync but not send,
Thread A: Creates a Lock<T>. Because T is not Send, Lock<T> is not Sync. It cannot be shared with other threads.
Locks the mutex and gets a Guard<T>.
Since T is Sync, code says Guard<T> is Sync.
Thread A can now safely give a &Guard<T> to Thread B.
Thread B: callsguard.lock_ref(). This returns a &'a Lock<T>. Thread B now has a reference to the Lock. In this case,
even after Thread A drops the guard, Thread B still has that &Lock. Thread B can now calllock.lock()and get a &mut T.
This is unsound.
This is way too verbose. Please be concise. Something like
For l: Lock<T>, this means that send_across_thread(&l.lock()).lock_ref() is able to send &Lock<T> across threads despite T: !Sync.
should be more readable.
Quoted 5 lines
Fix this issue by adding a trait bound to
Guard::lock_refso that this method is available only when Lock<T, B> implements Sync. This ensures that multiple threads cannot acquire a reference to the Lock if it is not Sync.
Again, this is repeating the same thing with two different style. Typical LLM style verbosity. Just remove the second sentence and keep the first.
Quoted 7 lines
Fixes 8f65291dae0e ("rust: sync: Add accessor for the lock behind a given guard")Reported-by Dylan Zueck<[email protected]>Reported-by Yuan Tan<[email protected]>Signed-off-by Priya Bala Govindasamy<[email protected]>Assisted-by ChatGPT:gpt-5.4
Quoted 25 lines
---
changes in v2:
- Add a trait bound only to theGuard::lock_ref()method. Changes to the lock lifetime are not necessary.rust/kernel/sync/lock.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-)rust/kernel/sync/lock.rs +4
-1@@ -234,7 +234,10 @@ impl<'a, T: ?Sized, B: Backend> Guard<'a, T, B> {/// // `g` originates from `l`. /// assert_held(&g, &l); /// ``` -pub fn lock_ref (&self) -> &'aLock <T ,B > { +pub fn lock_ref (&self) -> &'aLock <T ,B > +where +Lock <T ,B >:Sync , + { self.lock }
kernel test robot
build botRe: [PATCH v2 1/1] Make Guard::lock_ref unavailable if Lock is not Sync
In reply to Priya Bala Govindasamykernel test robot noticed the following build errors:
Hi Priya,
kernel test robot noticed the following build errors:
[auto build test ERROR on tip/locking/core]
[also build test ERROR on rust/rust-next linus/master v7.2-rc7 next-20260814] [If your patch is applied to the wrong git tree, kindly drop us a note. And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]
url: https://github.com/intel-lab-lkp/linux/commits/Priya-Bala-Govindasamy/Make-Guard-lock_ref-unavailable-if-Lock-is-not-Sync/20260814-173656 base: tip/locking/core patch link: https://lore.kernel.org/r/878cfb22ab185874bc363e2b7910a6978af646ab.1782763889.git.pgovind2%40uci.edu patch subject: [PATCH v2 1/1] Make Guard::lock_ref unavailable if Lock is not Sync config: arm64-randconfig-003-20260817 (https://download.01.org/0day-ci/archive/20260817/[email protected]/config) compiler: clang version 17.0.6 (https://github.com/llvm/llvm-project 6009708b4367171ccdbf4b5905cb6a803753fe18) rustc: rustc 1.96.0 (ac68faa20 2026-05-25) reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20260817/[email protected]/reproduce)
If you fix the issue in a separate patch/commit (i.e. not just a new version of the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <[email protected]>
| Closes: https://lore.kernel.org/oe-kbuild-all/[email protected]/
All errors (new ones prefixed by >>):
>> error[E0277]: `T` cannot be sent between threads safely
--> rust/doctests_kernel_generated.rs:20824:33
|
20824 | assert!(core::ptr::eq(guard.lock_ref(), lock));
| ^^^^^^^^ T cannot be sent between threads safely
|
= note: required for Lock<T, B> to implement Sync
note: required by a bound in rust_doctest_kernel_alloc_allocator_rs_0::kernel::sync::lock::Guard::<'a, T, B>::lock_ref --> rust/kernel/sync/lock.rs:239:21
|
237 | pub fn lock_ref(&self) -> &'a Lock<T, B>
| -------- required by a bound in this associated function
238 | where
239 | Lock<T, B>: Sync,
| ^^^^ required by this bound in Guard::<'a, T, B>::lock_ref
help: consider restricting type parameter T with trait Send
|
20822 | fn assert_held<T: core::marker::Send, B: Backend>(guard: &Guard<'_, T, B>, lock: &Lock<T, B>) {
| ++++++++++++++++++++
--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki