CVE-2026-46690
unbounded-spsc: Sender::send pointer-as-value transmute causes OOB read and fake-Arc drop under TX/RX race
unbounded_spsc is an "unbounded" extension of bounded_spsc_queue. In versions 0.2.0 and prior, sender::send pointer-as-value transmute causes OOB read and fake-Arc drop under TX/RX race. At time of publication, there are no publicly available patches.
Attack profile
How easy it is to exploit
- Attack vector
- Local
- Complexity
- High
- Privileges needed
- Low
- User interaction
- None
What an attacker can affect
- Confidentiality
- Low
- Integrity
- Low
- Availability
- High
Exploit likelihood
What each source reports
CVE List Score used
5.8
unbounded-spsc: Sender::send pointer-as-value transmute causes OOB read and fake-Arc drop under TX/RX race
unbounded_spsc is an "unbounded" extension of bounded_spsc_queue. In versions 0.2.0 and prior, sender::send pointer-as-value transmute causes OOB read and fake-Arc drop under TX/RX race. At time of publication, there are no publicly available patches.
- Record
CVE-2026-46690- CVSS 3.1
- 5.8 Medium
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:H - Weakness
- CWE-125, CWE-415, CWE-704, CWE-787
- Updated
- 2026-11-02
EUVDEUVD-2026-36469
5.8
unbounded_spsc is an "unbounded" extension of bounded_spsc_queue. In versions 0.2.0 and prior, sender::send pointer-as-value transmute causes OOB read and fake-Arc drop under TX/RX race. At time of publication, there are no publicly available patches.
- Record
EUVD-2026-36469- CVSS 3.1
- 5.8 Medium
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:H - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-6m57-8r3p-pqx6
5.8
unbounded-spsc: Sender::send pointer-as-value transmute causes OOB read and fake-Arc drop under TX/RX race
Summary
Sender::send in src/lib.rs contains an unsafe block in the DISCONNECTED arm that transmutes a raw pointer (*mut Producer<T>) into the bytes of a value-level Consumer<T>. The author's intent, visible in the surrounding comment at lines 386-390, was a value transmute. The shipped code is one level of indirection off.
The resulting Consumer<T> has its internal Arc::ptr set to the address of the producer field on the Sender, not the real ArcInner<Buffer<T>>. Every subsequent consumer.try_pop() walks Buffer<T> fields at offsets that lie inside the Sender<T> struct (over send_new, inner) and adjacent memory, an out-of-bounds read. When the fake Consumer<T> is dropped at the end of the unsafe block, its Drop calls Arc::drop_in_place on a non-ArcInner address: it decrements bytes that the type system treats as strong_count: AtomicUsize but that are actually the real Arc::ptr value of the Sender, and at zero count it calls dealloc(Layout::for_value(...)) on an address the allocator never returned.
Reachable from 100% safe Rust through the canonical channel pattern: a tx.send(msg) that races with rx.drop(). This is consistent with the SIGSEGV that issue #3 reports in your own test suite.
Affected code (0.2.0, master at 23a9ce7)
// src/lib.rs:384-401
DISCONNECTED => {
self.inner.counter.store (DISCONNECTED, Ordering::SeqCst);
// We want to guarantee if a message was not received that we get it
// back; since spsc::{Producer,Consumer} have the same
// internal representation (as a singleton struct containing Arc
// <Buffer <T>>), we can safely transmute the producer in order to
// pop the message back if it was orphaned.
unsafe {
let consumer : spsc::Consumer <T>
= std::mem::transmute (self.producer.get()); // <-- POINTER, not value
let first = consumer.try_pop();
let second = consumer.try_pop();
assert!(second.is_none()); // <-- line 396; smoking-gun assert
if let Some(t) = first {
return Err (SendError (t))
}
}
},
self.producer is UnsafeCell<spsc::Producer<T>> (line 29). UnsafeCell::<X>::get(&self) returns *mut X, a raw pointer, 8 bytes on 64-bit. The signature of transmute is transmute::<Src, Dst>(src: Src) -> Dst, so the call expands to transmute::<*mut spsc::Producer<T>, spsc::Consumer<T>>(self.producer.get()). 8 bytes of pointer are reinterpreted as the bytes of a Consumer<T>.
In bounded-spsc-queue-0.4.0, both Producer<T> and Consumer<T> are newtypes around Arc<Buffer<T>>, one pointer wide. The destination value therefore has Arc::ptr == &mut Producer<T> as *const ArcInner<Buffer<T>>. To be a valid Arc<Buffer<T>>, that pointer must point to ArcInner { strong: AtomicUsize, weak: AtomicUsize, data: Buffer<T> }, but it actually points to the start of Sender<T> (the producer field). The first 8 bytes there hold the real Arc::ptr. The fake Arc reads those bytes as strong_count. The fake try_pop() then reads Buffer<T> head/tail/data slots starting at offset 16 inside the Sender<T>, that is, inside the send_new and inner fields.
The author's intent (per the comment at lines 386-390) was a value-level transmute:
let producer_val: spsc::Producer<T> = std::ptr::read(self.producer.get());
let consumer : spsc::Consumer<T> = std::mem::transmute(producer_val);
which is layout-sound iff Producer<T> and Consumer<T> have identical layouts (they do, both are single-Arc newtypes). The shipped code is one indirection off.
Reachability
The branch is not reachable single-threaded. Receiver::drop (line 332) stores connected = false before setting counter = DISCONNECTED; Sender::send (line 359) early-returns on connected == false. The trigger is a TOCTOU race:
Sender's self.inner.connected.load(SeqCst) reads true.
Receiver-drop runs: stores connected = false and counter.compare_exchange(_, DISCONNECTED, SeqCst, SeqCst).
Sender's…
- Record
GHSA-6m57-8r3p-pqx6- CVSS 3.1
- 5.8 Medium
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:H - Weakness
- CWE-125, CWE-415, CWE-704, CWE-787
- Updated
- 2026-11-02
NVD
5.8
unbounded_spsc is an "unbounded" extension of bounded_spsc_queue. In versions 0.2.0 and prior, sender::send pointer-as-value transmute causes OOB read and fake-Arc drop under TX/RX race. At time of publication, there are no publicly available patches.
- Record
CVE-2026-46690- CVSS 3.1
- 5.8 Medium
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:H - Weakness
- CWE-125, CWE-415, CWE-704, CWE-787
- Updated
- 2026-11-02
OSVGHSA-6m57-8r3p-pqx6
5.8
unbounded-spsc: Sender::send pointer-as-value transmute causes OOB read and fake-Arc drop under TX/RX race
Summary
Sender::send in src/lib.rs contains an unsafe block in the DISCONNECTED arm that transmutes a raw pointer (*mut Producer<T>) into the bytes of a value-level Consumer<T>. The author's intent, visible in the surrounding comment at lines 386-390, was a value transmute. The shipped code is one level of indirection off.
The resulting Consumer<T> has its internal Arc::ptr set to the address of the producer field on the Sender, not the real ArcInner<Buffer<T>>. Every subsequent consumer.try_pop() walks Buffer<T> fields at offsets that lie inside the Sender<T> struct (over send_new, inner) and adjacent memory, an out-of-bounds read. When the fake Consumer<T> is dropped at the end of the unsafe block, its Drop calls Arc::drop_in_place on a non-ArcInner address: it decrements bytes that the type system treats as strong_count: AtomicUsize but that are actually the real Arc::ptr value of the Sender, and at zero count it calls dealloc(Layout::for_value(...)) on an address the allocator never returned.
Reachable from 100% safe Rust through the canonical channel pattern: a tx.send(msg) that races with rx.drop(). This is consistent with the SIGSEGV that issue #3 reports in your own test suite.
Affected code (0.2.0, master at 23a9ce7)
// src/lib.rs:384-401
DISCONNECTED => {
self.inner.counter.store (DISCONNECTED, Ordering::SeqCst);
// We want to guarantee if a message was not received that we get it
// back; since spsc::{Producer,Consumer} have the same
// internal representation (as a singleton struct containing Arc
// <Buffer <T>>), we can safely transmute the producer in order to
// pop the message back if it was orphaned.
unsafe {
let consumer : spsc::Consumer <T>
= std::mem::transmute (self.producer.get()); // <-- POINTER, not value
let first = consumer.try_pop();
let second = consumer.try_pop();
assert!(second.is_none()); // <-- line 396; smoking-gun assert
if let Some(t) = first {
return Err (SendError (t))
}
}
},
self.producer is UnsafeCell<spsc::Producer<T>> (line 29). UnsafeCell::<X>::get(&self) returns *mut X, a raw pointer, 8 bytes on 64-bit. The signature of transmute is transmute::<Src, Dst>(src: Src) -> Dst, so the call expands to transmute::<*mut spsc::Producer<T>, spsc::Consumer<T>>(self.producer.get()). 8 bytes of pointer are reinterpreted as the bytes of a Consumer<T>.
In bounded-spsc-queue-0.4.0, both Producer<T> and Consumer<T> are newtypes around Arc<Buffer<T>>, one pointer wide. The destination value therefore has Arc::ptr == &mut Producer<T> as *const ArcInner<Buffer<T>>. To be a valid Arc<Buffer<T>>, that pointer must point to ArcInner { strong: AtomicUsize, weak: AtomicUsize, data: Buffer<T> }, but it actually points to the start of Sender<T> (the producer field). The first 8 bytes there hold the real Arc::ptr. The fake Arc reads those bytes as strong_count. The fake try_pop() then reads Buffer<T> head/tail/data slots starting at offset 16 inside the Sender<T>, that is, inside the send_new and inner fields.
The author's intent (per the comment at lines 386-390) was a value-level transmute:
let producer_val: spsc::Producer<T> = std::ptr::read(self.producer.get());
let consumer : spsc::Consumer<T> = std::mem::transmute(producer_val);
which is layout-sound iff Producer<T> and Consumer<T> have identical layouts (they do, both are single-Arc newtypes). The shipped code is one indirection off.
Reachability
The branch is not reachable single-threaded. Receiver::drop (line 332) stores connected = false before setting counter = DISCONNECTED; Sender::send (line 359) early-returns on connected == false. The trigger is a TOCTOU race:
Sender's self.inner.connected.load(SeqCst) reads true.
Receiver-drop runs: stores connected = false and counter.compare_exchange(_, DISCONNECTED, SeqCst, SeqCst).
Sender's…
- Record
GHSA-6m57-8r3p-pqx6- CVSS 3.1
- 5.8 Medium
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:H - Weakness
- CWE-125, CWE-415, CWE-704, CWE-787
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
unbounded-spsccrates.io |
through 0.2.0 | None known |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-46690 is a medium vulnerability (CVSS 5.8) in Spearman Unbounded-spsc, published May 29, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.13%, the 2nd percentile. No source names a fixed version.
References
Left out here: the rest of 2 source descriptions. The JSON record has them.