From 90a0b155b9b1874438a243116673fa3dc2ea447a Mon Sep 17 00:00:00 2001 From: Berkant Koc Date: Thu, 14 May 2026 12:20:54 +0200 Subject: [PATCH 1/2] Add advisory for unbounded-spsc: Sender::send transmute UAF under tx/rx race The DISCONNECTED arm of Sender::send transmutes *mut Producer as a value-level Consumer, yielding OOB read inside the Sender frame and a fake-Arc drop on a non-allocated address. Reachable via TOCTOU race between Receiver::drop and Sender::send. --- crates/unbounded-spsc/RUSTSEC-0000-0000.md | 60 ++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 crates/unbounded-spsc/RUSTSEC-0000-0000.md diff --git a/crates/unbounded-spsc/RUSTSEC-0000-0000.md b/crates/unbounded-spsc/RUSTSEC-0000-0000.md new file mode 100644 index 0000000000..5f304571e7 --- /dev/null +++ b/crates/unbounded-spsc/RUSTSEC-0000-0000.md @@ -0,0 +1,60 @@ +```toml +[advisory] +id = "RUSTSEC-0000-0000" +package = "unbounded-spsc" +date = "2026-05-14" +categories = ["memory-corruption"] +keywords = ["transmute", "uaf", "race", "drop"] +informational = "unsound" + +[versions] +patched = [] +unaffected = [] + +[affected] +functions = { "unbounded_spsc::Sender::send" = ["<= 0.2.0"] } +``` + +# `Sender::send` pointer-as-value transmute causes OOB read and fake-Arc drop under tx/rx race + +`Sender::send` (single-file crate, `src/lib.rs:379-405` in 0.2.0 / commit `23a9ce7`) contains an `unsafe` block in the `DISCONNECTED` arm that transmutes the *pointer to* the producer instead of the producer value: + +```rust +let consumer: spsc::Consumer = std::mem::transmute(self.producer.get()); +``` + +`self.producer` has type `UnsafeCell>`. `UnsafeCell::::get(&self)` returns `*mut X` — a pointer-sized value. The author's intent (per the surrounding comment) was a value-level transmute (`Producer` and `Consumer` are layout-compatible newtypes around `Arc>`). The shipped code is one indirection off: it copies 8 bytes of pointer into the bytes of a `Consumer`, whose internal `Arc::ptr` is then the address of the producer field on the `Sender`, not the real `ArcInner>`. + +The subsequent `consumer.try_pop()` walks `Buffer` fields at offsets that lie inside the `Sender` struct (`send_new`, `inner`) — an OOB read of the Sender's own frame. The fake `Consumer`'s `Drop` then decrements bytes treated as `AtomicUsize::strong_count` and calls `dealloc(...)` on a non-allocated address. + +The branch is not reachable single-threaded: the receiver-drop and `connected = false` guard prevent it. The trigger is a TOCTOU race: the sender's `connected.load()` wins but the receiver's `counter.compare_exchange(_, DISCONNECTED, ...)` wins. Under contention the race reproduces in a few hundred trials. + +## Trigger + +```rust +use std::thread; +use unbounded_spsc::channel; + +for _ in 0..500 { + let (tx, rx) = channel::>(); + let h = thread::spawn(move || { + for _ in 0..10_000 { let _ = tx.send(Box::new(0xDEAD_BEEF)); } + }); + drop(rx); + let _ = h.join(); +} +// Release: SIGSEGV within a few iterations. +// ASan: stack-buffer-overflow inside fake-Consumer::try_pop. +``` + +## Smoking-gun upstream evidence + +`src/lib.rs:975` in the project's own test suite carries: + +``` +// TODO: failures +// - failed with assertion on line 394 in send fn +// assert!(second.is_none()) +``` + +That assertion lives in the same transmute block. The author has observed the symptom — `try_pop()` returning `Some` where logically there should be `None` — as a flaky test rather than recognising it as UB. From 57bfff3d83339c221576f27fc753378ab7f4c9de Mon Sep 17 00:00:00 2001 From: Berkant Koc Date: Sun, 17 May 2026 02:45:02 +0200 Subject: [PATCH 2/2] unbounded-spsc: maintainer ack, set patched_versions to >= 0.3.0 Maintainer @spearman published v0.3.0 on 2026-05-16 with the fix plus regression test race_disconnect_does_not_corrupt_sender_or_abort (commit 2714da2), accepted GHSA-6m57-8r3p-pqx6, and closed upstream issue #4. All prior versions (0.2.0, 0.1.x) are yanked on crates.io. - patched = [">= 0.3.0"] - aliases = ["GHSA-6m57-8r3p-pqx6"] - url = https://github.com/spearman/unbounded-spsc/issues/4 --- crates/unbounded-spsc/RUSTSEC-0000-0000.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/unbounded-spsc/RUSTSEC-0000-0000.md b/crates/unbounded-spsc/RUSTSEC-0000-0000.md index 5f304571e7..7f1063bf4f 100644 --- a/crates/unbounded-spsc/RUSTSEC-0000-0000.md +++ b/crates/unbounded-spsc/RUSTSEC-0000-0000.md @@ -3,12 +3,14 @@ id = "RUSTSEC-0000-0000" package = "unbounded-spsc" date = "2026-05-14" +url = "https://github.com/spearman/unbounded-spsc/issues/4" categories = ["memory-corruption"] keywords = ["transmute", "uaf", "race", "drop"] informational = "unsound" +aliases = ["GHSA-6m57-8r3p-pqx6"] [versions] -patched = [] +patched = [">= 0.3.0"] unaffected = [] [affected] @@ -17,6 +19,8 @@ functions = { "unbounded_spsc::Sender::send" = ["<= 0.2.0"] } # `Sender::send` pointer-as-value transmute causes OOB read and fake-Arc drop under tx/rx race +Maintainer acknowledged on 2026-05-15; v0.3.0 ships the fix plus a regression test (`race_disconnect_does_not_corrupt_sender_or_abort`), and 0.2.0 + all 0.1.x versions have been yanked on crates.io. + `Sender::send` (single-file crate, `src/lib.rs:379-405` in 0.2.0 / commit `23a9ce7`) contains an `unsafe` block in the `DISCONNECTED` arm that transmutes the *pointer to* the producer instead of the producer value: ```rust