Skip to contentSkip to conversation

rust: sync: Make Guard::lock_ref unavailable if Lock is not Sync

PATCH In review Priya Bala Govindasamy 3 participants 4 messages Jun 29 – Aug 17

Subsystems (3)
lore
Share & tools
lore
#0 lore raw

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
#1 lore raw

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.

Fixes 8f65291dae0e ("rust: sync: Add accessor for the lock behind a given guard")
Assisted-by ChatGPT:gpt-5.4

---
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
index 10b6b5e9b024..91317d0d81b9 100644
@@ -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) -> &'a Lock<T, B> {
+    pub fn lock_ref(&self) -> &'a Lock<T, B>
+    where
+        Lock<T, B>: Sync,
+    {
         self.lock
     }
 
Signature · 1 line
2.34.1
maintainer Acked-by on v2 1/1
#2 lore raw

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: 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.

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_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.

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")
Assisted-by ChatGPT:gpt-5.4
Quoted 25 lines

---
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
index 10b6b5e9b024..91317d0d81b9 100644
@@ -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) -> &'a Lock<T, B> {
+    pub fn lock_ref(&self) -> &'a Lock<T, B>
+    where
+        Lock<T, B>: Sync,
+    {
         self.lock
     }
 
#3 lore raw

Re: [PATCH v2 1/1] Make Guard::lock_ref unavailable if Lock is not Sync

In reply to Priya Bala Govindasamy
kernel 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