One Shot, One Kill: Race to UAF in Rust's 'oneshot' Crate
Jan 28, 2026·7 min read·5 visits
Executive Summary (TL;DR)
A race condition in `oneshot::Receiver::drop` allows a Use-After-Free. If a receiver is dropped (cancelled) while waiting, it might access the channel memory after the sender has already freed it. Fixed in v0.1.12.
The promise of Rust is memory safety without garbage collection, a contract signed in the blood of the borrow checker. But when developers step into the `unsafe` block to optimize concurrency primitives, that contract is suspended. A critical race condition in the popular `oneshot` crate—used for single-producer, single-consumer channels—exposes a Use-After-Free (UAF) vulnerability. This flaw arises during the destruction (`Drop`) of a `Receiver`. If a receiver is waiting for data (polled) and is suddenly cancelled (dropped, typically via a timeout), it attempts to clean up its `Waker`. However, in vulnerable versions, it signals 'disconnection' to the `Sender` *before* accessing the memory to clean up. If the `Sender` is quick enough to deallocate the channel in that nanosecond window, the `Receiver` reads from freed memory. Boom.
The Hook: Even Rust Bleeds
We love Rust. We love the borrow checker screaming at us because it stops us from doing stupid things. But there is a dirty little secret in the high-performance async ecosystem: to make things fast, library authors often have to bypass the safety rails using unsafe blocks. The oneshot crate is a staple in the Rust async diet. It does exactly what it says on the tin: sends a single value from A to B. It's the fundamental building block of request-response patterns in asynchronous code.
Here is the setup: You have a Sender and a Receiver. They share a heap-allocated memory slot (the channel) and coordinate via atomic state flags. It’s a classic synchronization dance. The Receiver registers a Waker so the runtime knows to wake it up when data arrives.
The vulnerability we are looking at today is a textbook race condition that leads to a Use-After-Free (UAF). It’s not a buffer overflow; it’s a logic error in the cleanup phase. It reminds us that even in Rust, if you screw up the order of operations in unsafe code, you are just writing C++ with better tooling.
The Flaw: The Premature Disconnection
Let's talk about the lifecycle of a oneshot channel. The shared state generally moves between EMPTY, SENDING, RECEIVING, SENT, and DISCONNECTED. When a Receiver is dropped (for example, if you wrap a database request in a timeout and the timeout fires), it needs to clean up. If it was waiting for data, it has a Waker sitting inside the channel structure that needs to be dropped properly.
In the vulnerable code, the Receiver::drop implementation made a fatal mistake in sequencing. It updated the atomic state to DISCONNECTED before it finished touching the memory.
Think of it like checking out of a hotel room. You are supposed to pack your bags (Waker) and then hang the 'Room Free' sign on the door. The oneshot developers essentially hung the 'Room Free' sign on the door while their luggage was still on the bed. The hotel cleaning crew (Sender) saw the sign, entered, and incinerated the room contents (deallocated the memory) while the guest was still trying to grab their toothbrush.
The Code: Anatomy of a Race
Let's look at the smoking gun in src/lib.rs. The critical failure happens in the Drop implementation for Receiver. This code runs when the Receiver goes out of scope.
The Vulnerable Code (Pre-0.1.12):
// The Receiver is being dropped.
// FATAL FLAW: We tell the world we are disconnected immediately.
let old_state = self.channel.state.swap(DISCONNECTED, Ordering::AcqRel);
// If we were in the RECEIVING state, we have a Waker inside the channel.
if old_state == RECEIVING {
// DANGER: We are accessing `self.channel` here.
// If the Sender saw the DISCONNECTED state above and freed the channel,
// this `unsafe` block triggers a Use-After-Free.
unsafe { self.channel.drop_waker() };
}The moment state.swap(DISCONNECTED) executes, the Sender (running on another thread) might see that state. If the Sender sees DISCONNECTED, it assumes it is the sole owner of the channel's memory and deallocates it. Meanwhile, the Receiver thread is just moving to the next line of code, attempting to call drop_waker() on a pointer that now points to garbage.
The Fixed Code (v0.1.12):
// The Fix: Check if we are RECEIVING and bail out of that state first.
// We try to swap RECEIVING -> EMPTY.
if self.channel.state.load(Ordering::Relaxed) == RECEIVING
&& self.channel.state.compare_exchange(
RECEIVING,
EMPTY,
Ordering::Relaxed,
Ordering::Relaxed
).is_ok()
{
// We successfully claimed ownership back from the 'RECEIVING' state.
// The Sender still thinks the channel is active (EMPTY).
// We can safely touch the memory.
unsafe { self.channel.drop_waker() };
}
// NOW we can signal disconnection.
let old_state = self.channel.state.swap(DISCONNECTED, Ordering::Release);The fix is subtle but crucial. It attempts to revert the state from RECEIVING to EMPTY before declaring the connection dead. This ensures the Receiver holds onto the memory rights while it cleans up its Waker.
The Exploit: Winning the Race
Exploiting this requires winning a tight race window, but in high-concurrency environments, 'tight' is just a matter of statistics. To trigger this, an attacker doesn't need to send malicious packets; they just need to induce a specific timing sequence in the application logic.
The Attack Recipe:
- Target: A service using
oneshotto handle async tasks (e.g., a web server handling requests). - Setup: The server must use a pattern where the
Receiveris polled but can be cancelled.tokio::time::timeoutis the perfect candidate. - Trigger: The attacker sends a request that causes the server to spawn a
oneshotchannel. The attacker then induces a condition where the server cancels the task (e.g., client disconnects or a timeout forces a drop) at the exact moment the task producer completes. - Heap Grooming: To turn this from a crash into RCE, the attacker needs to groom the heap. When the
Senderfrees the channel memory, the attacker wants to immediately reclaim that memory slot with a controlled object. When theReceiver(still executingdrop) tries to read theWakerfrom that memory, it will instead read from the attacker's fake object.
If the Waker is virtual-table based (which it is), hijacking the vtable pointer could lead to jumping to arbitrary code execution when drop() is called on the fake waker.
The Impact: Why Panic?
While this is a local race condition (AV:L), the context matters. In a networked Rust application (like a proxy or web server), remote events trigger local code paths. If a remote user can repeatedly trigger connection drops or timeouts, they can hammer this race condition.
Best Case: The application panics or segfaults. Denial of Service.
Worst Case: Remote Code Execution. If the allocator reuses the freed memory block immediately for something else (like a sensitive config structure or another object with function pointers), the dangling write/read from the Receiver can corrupt program state or hijack control flow. Given oneshot is a foundational library, the blast radius includes any application depending on it transitively.
The Fix: Patching the Hole
The remediation is straightforward: Update oneshot to version 0.1.12 or higher.
If you are auditing Rust codebases, check Cargo.lock immediately. Look for:
[[package]]
name = "oneshot"
version = "0.1.11" # VULNERABLEThere is no viable workaround other than patching. Attempting to wrap usage in mutexes or other synchronizers in your own code defeats the purpose of using a lightweight channel and likely won't catch the internal Drop race anyway. The fix must be internal to the crate's state machine logic.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:HAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
oneshot faern | < 0.1.12 | 0.1.12 |
| Attribute | Detail |
|---|---|
| Vulnerability Type | Use-After-Free (CWE-416) |
| Root Cause | Race Condition (CWE-362) |
| CVSS Score | 8.2 (High) |
| Vector | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Affected Component | oneshot::Receiver::drop |
| Exploit Status | Theoretical / Stability Issue |
MITRE ATT&CK Mapping
Use After Free occurs when memory is referenced after it has been freed, which can cause a program to crash, use unexpected values, or execute code.
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.