Attack ChainAIDerived
Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain
-
ReconObtain local access with low privileges
-
DeliverySpawn sender thread in high-frequency tx.send loop
-
ExploitDrop receiver concurrently to set DISCONNECTED counter
-
InstallWin TOCTOU race window in Sender::send
-
C2Pointer-as-value transmute creates fake Consumer<T>
-
Executefake try_pop reads OOB bytes from Sender struct
-
ImpactFake Arc::drop corrupts allocator metadata and calls dealloc on non-allocated address
-
Step 8SIGSEGV crash or heap corruption
Vulnerability Assessment
| Weakness |
CWE
ASVS 5.0
V15 - Secure Coding and Architecture Bounds checking, read limits
MITRE ATT&CK
1 technique across 1 tactic: Collection The technique list with ATT&CK links and the kill-chain view is part of the Basic plan and up. |
| Exploitation Conditions | The vulnerability requires local system access with at least low privileges (CVSS PR:L) and the ability to run a Rust binary that uses unbounded-spsc channels. The TOCTOU race requires two concurrent threads sharing a channel: one thread must be calling Sender::send while the other drops the Receiver. No special channel configuration is required - the standard channel::<T>() constructor is sufficient. The race window is narrow (CVSS AC:H), but under heavy send-loop contention it closes approximately 3 out of 10 trials in release mode with no sanitizers. Exploitation is limited by: (1) local access - remote exploitation is not possible given CVSS AV:L; (2) race timing - single-threaded usage or usage patterns without concurrent receiver teardown are not affected; (3) the Receiver must be dropped while the Sender is actively sending, which is the natural teardown pattern for callback-mailbox architectures. The gooey-rs use case (OpenGL/FMOD scene-graph teardown) represents a realistic natural trigger without any adversarial intent. |
| Impact | The CVSS 3.1 score of 5.8 (AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:H) accurately captures the primary risk profile: local access, high attack complexity due to the race window, and low privileges required, with availability impact rated High because SIGSEGV is the most consistent observed outcome. The confidentiality and integrity impacts rated Low reflect that OOB reads can leak process memory content (the Sender struct and adjacent heap bytes) and that the fake Arc drop can corrupt heap metadata, but controlled data exfiltration or arbitrary write primitives beyond allocator corruption are not straightforwardly demonstrated. No EPSS score was provided in the source data, and this CVE is not listed in the CISA KEV catalog, meaning exploitation is not confirmed at the population level. However, the advisory includes a working proof-of-concept that reliably produces SIGSEGV within a few hundred trials with no tooling beyond the standard Rust channel API. The 'reachable from 100% safe Rust' characteristic is significant: no misuse of unsafe APIs by callers is required, and the race is exercised by the canonical tx.send / rx.drop pattern that downstream crates (apis, gooey-rs) use at scene-graph teardown. Real-world priority is moderate for most users given the local-access and high-complexity prerequisites, but elevated for any process that uses unbounded-spsc channels across threads with frequent receiver teardown, such as the OpenGL/FMOD callback-mailbox pattern in gooey-rs. |
| Exploit ScenarioDerived | A process runs two threads sharing an unbounded-spsc channel: the sender thread loops calling tx.send(Box::new(value)) at high rate, while the main thread drops the receiver (drop(rx)). Under sufficient contention (reproducible in approximately 3 out of 10 trials in release mode per the advisory), the TOCTOU race fires: the sender reads connected == true, the receiver-drop stores DISCONNECTED, and the sender's fetch_add enters the unsafe transmute block. The fake Consumer::try_pop reads bytes from within the Sender struct and adjacent heap memory, returning garbage as a valid T. When the fake Consumer drops, Arc::drop_in_place corrupts the Sender's real Arc::ptr field and calls dealloc on a non-allocated address, producing SIGSEGV on standard allocators or an abort on hardened allocators. A proof-of-concept loop of 500 trials reliably triggers this within a few iterations. |
Get an e-mail when this CVE is exploited, patched or re-scored. Free account.
Alert meAnalysisAIv1 – May 29, 2026
Memory unsafety in unbounded-spsc 0.2.0 (Rust crate) allows a local attacker with low privileges to cause out-of-bounds reads, allocator corruption, and process crashes by winning a TOCTOU race between Sender::send and Receiver::drop. The root defect is a pointer-as-value transmute in the DISCONNECTED arm of Sender::send (src/lib.rs:384-401): the code transmutes an 8-byte raw pointer (*mut Producer<T>) directly into Consumer<T>, meaning the fake Consumer's internal Arc::ptr points at the Sender struct itself rather than the real ArcInner<Buffer<T>>. No public exploit identified at time of analysis, but a proof-of-concept reproducing SIGSEGV is included in the advisory and reproduces approximately 3 out of 10 trials under heavy contention in release mode.
RemediationAI
No vendor-released patch identified at time of analysis for unbounded-spsc. The advisory author provides a specific suggested fix: replace the pointer-level transmute with a value-level std::ptr::read of the Producer<T> followed by std::mem::transmute to Consumer<T>, and wrap the consumer in ManuallyDrop before it goes out of scope to suppress the aliased Producer::drop. This is layout-sound because both Producer<T> and Consumer<T> are newtypes around Arc<Buffer<T>> with identical single-pointer layouts. As a cleaner structural fix, the advisory recommends replacing the producer/consumer fields with a private enum Endpoint<T> to eliminate the transmute entirely. For the secondary defect (unwrap panic on send_new.send), replace the unwrap with an explicit Err(SendError(t)) return. Until an upstream patch is released, consumers of unbounded-spsc should pin the crate at its current version and apply the suggested patch locally via a Cargo patch override, or replace unbounded-spsc with an alternative SPSC channel crate such as crossbeam-channel or flume. For gooey-rs users, disabling the opengl and fmod feature flags eliminates the dependency. The regression test included in the advisory (race_disconnect_does_not_corrupt_sender_or_abort) should be added to the project test suite to prevent reintroduction. Advisory: https://github.com/spearman/unbounded-spsc/security/advisories/GHSA-6m57-8r3p-pqx6.
Affected VersionsENISA
Affected Packages
Source: OSV (Open Source Vulnerabilities)
Affects your stack
Affects 1 package in cargo
Checking this CVE against your own stacks and servers is part of the Basic plan and up.
Technical ContextAI · show details
The affected code is in the unbounded-spsc Rust crate (pkg:rust/unbounded-spsc), a single-producer single-consumer unbounded channel built on top of bounded-spsc-queue. The defect is a misuse of std::mem::transmute inside an unsafe block at src/lib.rs:384-401. self.producer is typed UnsafeCell<spsc::Producer<T>>; calling .get() on it returns *mut spsc::Producer<T>, an 8-byte raw pointer on 64-bit platforms. The transmute call is transmute::<*mut spsc::Producer<T>, spsc::Consumer<T>>, which reinterprets those 8 pointer bytes as the value bytes of Consumer<T>. Since Consumer<T> is a newtype around Arc<Buffer<T>>, the resulting value has its Arc::ptr field set to the address of the producer field within the Sender<T> struct - not to any real ArcInner<Buffer<T>>. Subsequent calls to consumer.try_pop() walk Buffer<T> field offsets starting inside the Sender<T> struct (crossing send_new and inner fields), constituting an out-of-bounds read (CWE-125). When the fake Consumer<T> is dropped, Arc::drop_in_place is called on a non-ArcInner address: it atomically decrements bytes it interprets as strong_count (which are actually the real Arc::ptr of the Sender), and at zero count calls dealloc(Layout::for_value(...)) on an address the allocator never issued. The race is triggered via a TOCTOU window: Sender reads connected == true, Receiver-drop stores connected = false and sets counter = DISCONNECTED, then Sender's fetch_add sees DISCONNECTED and enters the unsafe block. On glibc this produces double-free or arbitrary heap-bucket-poisoning primitives; on hardened allocators (jemalloc-secure, mimalloc-secure) it causes an immediate abort.
Know when Canonical gets its next vulnerability
- Watch products and get an email when something new is published
- Check this CVE against your own stack and servers
- Exploit and PoC references, threat intelligence from 22 sources
No credit card · 7-day full trial
Severity by source
AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:H
Primary rating from NVD · only source for this CVE.
CVSS VectorNVD
DescriptionNVD
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 self.inner.counter.fetch_add(1, SeqCst) (line 379) sees DISCONNECTED and enters the unsafe block.
Under heavy contention this reproduces ~3/10 trials in release mode.
Proof of concept (race shape)
// Cargo.toml: unbounded-spsc = "0.2"
use std::thread;
use unbounded_spsc::channel;
fn main() {
for trial in 0..500 {
let (tx, rx) = channel::<Box<u64>>();
let started = std::sync::Arc::new(
std::sync::atomic::AtomicBool::new(false));
let s = started.clone();
let h = thread::spawn(move || {
s.store(true, std::sync::atomic::Ordering::SeqCst);
for _ in 0..10_000 {
let _ = tx.send(Box::new(0xDEAD_BEEF));
}
});
while !started.load(std::sync::atomic::Ordering::SeqCst) {
std::hint::spin_loop();
}
drop(rx);
let _ = h.join();
eprintln!("trial {trial} ok");
}
}
Observed:
Release-mode (no sanitizer): Segmentation fault (core dumped) reliably within a few trials. The non-segfaulting trials are masked by the separate send_new.send(new_consumer).unwrap() panic, see Secondary defect below.
-Zsanitizer=address -Zbuild-std (nightly): ASan reports stack-buffer-overflow / stack-use-after-scope from the fake-Consumer's try_pop walking off the Sender frame.
This matches the SIGSEGV reported in your own issue #3.
Smoking-gun upstream evidence
src/lib.rs:975 in the project's test suite carries a TODO:
// TODO: failures
// - failed with assertion on line 394 in send fn
// assert!(second.is_none())
That is the assertion site of the transmute block (line 396 in 0.2.0 / master). You have observed try_pop() returning a non-None value where logically there should be none, which is exactly what reading random bytes from the Sender's send_new / inner fields produces, and the symptom has been marked as a flaky test rather than recognised as UB.
Impact
Reachable from 100% safe Rust. Concrete UB primitives:
OOB read of bytes adjacent to the Sender<T> struct via fake Consumer<T>::try_pop(). The popped T is returned through Err(SendError(t)) to safe-code, an allocator-layout-controlled leak of process memory.
OOB write via fake Arc::drop AtomicUsize::fetch_sub on bytes that are actually the real Arc::ptr value of the Sender.
Allocator corruption via fake Arc::drop calling dealloc(Layout::for_value(...)) on a non-allocated address. The Sender struct holds the real Arc<Inner> immediately after the producer field; the deallocator call therefore uses a layout the allocator never allocated, which on glibc is a confirmed double-free / arbitrary-bucket-poisoning primitive, and on hardened allocators (jemalloc-secure, mimalloc-secure) is an immediate abort.
Secondary defect (same call path, bonus)
Sender::send line 369:
self.send_new.send(new_consumer).unwrap();
When the Sender's message queue is full, a fresh bounded_spsc_queue::Channel is allocated and the new Consumer<T> is shipped over an std::sync::mpsc side-channel to the Receiver. If the Receiver has already been dropped, receive_new is gone and this unwrap() panics. The panic surfaces in your own test suite, issue #2 (tests::port_gone_concurrent panicked at src/lib.rs:369) and the in-source TODO at lines 365-368 already note the question "Are we sure that this is safe to unwrap or should we handle the result explicitly ?".
The fix is to return Err(SendError(t)) instead of unwrapping, same shape as the channel-closed result the function already returns on the connected-false path. This is not a memory-safety defect, only a panic, but it lives on the same TX/RX-race code path and a single coordinated patch can address both. Filing it here so we cover the full call site in one cycle.
Suggested patch (primary defect)
Replace the pointer-as-value transmute with a value-level read and a ManuallyDrop to suppress the alias's Producer::drop on subsequent exit:
unsafe {
use core::mem::ManuallyDrop;
// Sound value-level transmute: Producer<T> and Consumer<T> are both
// newtypes around Arc<Buffer<T>>, so the value layouts match.
// ptr::read takes ownership of the Producer's bytes without running
// Producer's Drop.
let producer_val: spsc::Producer<T> = std::ptr::read(self.producer.get());
let consumer : spsc::Consumer<T> = std::mem::transmute(producer_val);
let first = consumer.try_pop();
let second = consumer.try_pop();
assert!(second.is_none());
if let Some(t) = first {
return Err(SendError(t));
}
// consumer drops here; the same memory backs `producer`, so suppress
// the double Producer drop:
let _ = ManuallyDrop::new(consumer);
}
Cleaner: restructure Sender<T> to hold producer and consumer in a private enum Endpoint<T> so no transmute is required, or use the bounded_spsc_queue::Producer<T>::reclaim() escape hatch if available.
Suggested patch (secondary defect)
if let Err(std::sync::mpsc::SendError(_)) = self.send_new.send(new_consumer) {
// Receiver has been dropped: take the message back as the public
// SendError, the same way the connected==false early-return does.
return Err(SendError(t));
}
Regression test (release-mode, race shape)
#[test]
fn race_disconnect_does_not_corrupt_sender_or_abort() {
for _ in 0..200 {
let (tx, rx) = unbounded_spsc::channel::<Box<u64>>();
let h = std::thread::spawn(move || {
for _ in 0..10_000 {
let _ = tx.send(Box::new(0xDEAD_BEEF));
}
});
drop(rx);
h.join().unwrap();
}
}
Reverse dependencies
Two crates on crates.io depend on unbounded-spsc, both owned by you: apis (process-calculus framework) and gooey-rs (tile-UI library, unbounded-spsc gated behind opengl/fmod features). The OpenGL/FMOD callback-mailbox use is a natural rx-drop-during-tx-send scenario at scene-graph teardown. A single coordinated bump cycle is feasible.
Researcher
Berkant Koc me@berkoc.com
PGP: 0C588DFD76204987284213EA0AC529C41F8AA5D6Threat Intelligence Overview
How this decision was reached
The engine reads each signal below and applies a fixed list of rules from top to bottom. The first rule that matches sets the recommended action – like a statement, every line is shown.
What every action level means
SSVC AssessmentENISA
Priority Score
Attack Technique
A buffer overflow occurs when a program writes more data to a memory buffer than it was allocated to hold, causing the excess data to spill into adjacent memory regions.
Information disclosure occurs when an application unintentionally exposes sensitive data that aids attackers in reconnaissance or directly compromises security.
Oracle Java SE JDK/JRE 7 and 6 Update 27 and earlier allows remote code execution with complete system compromise throug
In PHP versions 7.1.x below 7.1.33, 7.2.x below 7.2.24 and 7.3.x below 7.3.11 in certain configurations of FPM setup it
GNU Bash through 4.3 processes trailing strings after function definitions in the values of environment variables, which
Multiple vulnerabilities in Oracle Java 7 before Update 11 allow remote attackers to execute arbitrary code by (1) using
The (1) EPHEMERAL, (2) HTTPS, (3) MVG, (4) MSL, (5) TEXT, (6) SHOW, (7) WIN, and (8) PLT coders in ImageMagick before 6.
When running Apache Tomcat versions 9.0.0.M1 to 9.0.0, 8.5.0 to 8.5.22, 8.0.0.RC1 to 8.0.46 and 7.0.0 to 7.0.81 with HTT
Remote code execution is possible with Apache Tomcat before 6.0.48, 7.x before 7.0.73, 8.x before 8.0.39, 8.5.x before 8
GNU Bash through 4.3 bash43-025 processes trailing strings after certain malformed function definitions in the values of
The (1) TLS and (2) DTLS implementations in OpenSSL 1.0.1 before 1.0.1g do not properly handle Heartbeat Extension packe
Race condition in mm/gup.c in the Linux kernel 2.x through 4.x before 4.8.3 allows local users to gain privileges by lev
Unspecified vulnerability in the Java Runtime Environment (JRE) component in Oracle Java SE 7 Update 17 and earlier, and
Local privilege escalation in the GNU C Library (glibc) dynamic loader ld.so allows unprivileged local users on affected
EUVD-2026-36469
GHSA-6m57-8r3p-pqx6
NVD
CVE.org
EPSS
Vulnrichment
VulDB
Exploit-DB