Skip to content

Merge operators aren't required to implement Send or Sync, allowing data races #1536

@BongoThirteen

Description

@BongoThirteen

The MergeOperator trait appears to be missing (at least) a Send bound, allowing non-Send types to be misused when data is merged on another thread. I'm aware that removal of the merge API is planned but this seems like a pretty serious soundness issue and should hopefully be easy to fix.

Expected result

This issue can be reproduced with the following code.

use std::hint::black_box;
use std::rc::Rc;
use std::thread;

use sled::Config;

fn main() {
    let config = Config::new().temporary(true);
    let db = config.open().unwrap();

    let cant_share = Rc::new(69);
    let cant_share_copy = Rc::clone(&cant_share);

    let bad_access = move |_: &[u8], _: Option<&[u8]>, _: &[u8]| {
        for _ in 0..10_000 {
            let cant_share_copy = Rc::clone(&cant_share_copy);
            black_box(&*cant_share_copy);
            drop(cant_share_copy);
        }
        None
    };

    db.set_merge_operator(bad_access);

    let db_copy = db.clone();

    let handle = thread::spawn(move || {
        db_copy.merge("test", b"anything".to_vec()).unwrap();
    });

    for _ in 0..10_000 {
        let cant_share_copy = Rc::clone(&cant_share);
        black_box(&*cant_share_copy);
        drop(cant_share_copy);
    }

    handle.join().unwrap();
}

Since the closure bad_access captures an Rc<i32>, it implements neither Send nor Sync and should not be usable as a merge operator in the concurrent database db. The code should not compile.

Actual result

The result of running the code is not predictable, but I observed the following errors in debug mode, indicating that undefined behavior is occurring.

$ cargo run
   Compiling sled_ub v0.1.0 ([...]/sled_ub)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/sled_ub`

thread 'main' (370158) panicked at [...]/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/rc.rs:3579:13:
unsafe precondition(s) violated: hint::assert_unchecked must never be called when the condition is false

This indicates a bug in the program. This Undefined Behavior check is optional, and cannot be relied on for safety.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread caused non-unwinding panic. aborting.
[1]    370158 IOT instruction (core dumped)  cargo run
$ cargo run
   Compiling sled_ub v0.1.0 ([...]/sled_ub)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/sled_ub`
[1]    376531 segmentation fault (core dumped)  cargo run

Sled version

$ cat Cargo.toml
[package]
name = "sled_ub"
version = "0.1.0"
edition = "2024"

[dependencies]
sled = "0.34.7"

Rust version

$ rustc --version
rustc 1.89.0 (29483883e 2025-08-04)

Operating system

I'm running Ubuntu 24.04 on 64-bit x86.

Backtrace

Running with RUST_BACKTRACE=full seems to confirm that the issue is caused by the non-Send closure.

$ RUST_BACKTRACE=full cargo run
[...]
  18:     0x5749ca7ee59c - core::hint::assert_unchecked::h31232f484bd204f4
                               at [...]/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ub_checks.rs:77:17
  19:     0x5749ca7ee59c - alloc::rc::RcInnerPtr::inc_strong::h78e74a5aefe294f8
                               at [...]/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/rc.rs:3567:13
  20:     0x5749ca7ee6f5 - <alloc::rc::Rc<T,A> as core::clone::Clone>::clone::he8ecaed6f4485f92
                               at [...]/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/rc.rs:2337:26
  21:     0x5749ca7f18c9 - sled_ub::main::{{closure}}::hb11d0918d7c5f48c
                               at [...]/sled_ub/src/main.rs:16:35
  22:     0x5749ca8d2c9d - <alloc::boxed::Box<F,A> as core::ops::function::Fn<Args>>::call::ha9d7004ecd973fe0
                               at [...]/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:1980:9
  23:     0x5749ca89a856 - sled::tree::Tree::merge_inner::ha722e930d94a23b1
                               at [...]/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sled-0.34.7/src/tree.rs:1043:23
  24:     0x5749ca7ef336 - sled::tree::Tree::merge::hf6aa52a95b96feca
                               at [...]/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sled-0.34.7/src/tree.rs:1008:37
[...]

Note that assert_unchecked failed within Rc::clone called by the closure invoked by Tree::merge_inner. The data race caused the reference count to underflow, as detected by the following standard library code, which could cause a use-after-free if not caught by debug checks.

// SAFETY: The reference count will never be zero when this is
// called.
unsafe {
    hint::assert_unchecked(strong != 0);
}

Thank you for your time.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions