Every CVE Search/ Get API key

CVE-2026-46690

unbounded-spsc: Sender::send pointer-as-value transmute causes OOB read and fake-Arc drop under TX/RX race

5.8 Medium

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

0.13%Up 0.1 points since Jun 12, 2026
90%50%10%1%

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

Affected packages
PackageAffectedFixed
unbounded-spsc
crates.io
through 0.2.0 None known

History

History
RecordedChangedSources
first indexedCVE 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

This page doesn't list everything

Left out here: the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open