GHSA-RVR2-R3PV-5M4P

One Shot, One Kill: Race to UAF in Rust's 'oneshot' Crate

Amit Schendel
Amit Schendel
Senior Security Researcher

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:

  1. Target: A service using oneshot to handle async tasks (e.g., a web server handling requests).
  2. Setup: The server must use a pattern where the Receiver is polled but can be cancelled. tokio::time::timeout is the perfect candidate.
  3. Trigger: The attacker sends a request that causes the server to spawn a oneshot channel. 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.
  4. Heap Grooming: To turn this from a crash into RCE, the attacker needs to groom the heap. When the Sender frees the channel memory, the attacker wants to immediately reclaim that memory slot with a controlled object. When the Receiver (still executing drop) tries to read the Waker from 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" # VULNERABLE

There 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.

Fix Analysis (1)

Technical Appendix

CVSS Score
8.2/ 10
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H
EPSS Probability
0.04%
Top 100% most exploited

Affected Systems

Rust applications using the `oneshot` crate < 0.1.12Async runtimes relying on `oneshot` for signaling

Affected Versions Detail

Product
Affected Versions
Fixed Version
oneshot
faern
< 0.1.120.1.12
AttributeDetail
Vulnerability TypeUse-After-Free (CWE-416)
Root CauseRace Condition (CWE-362)
CVSS Score8.2 (High)
VectorCVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H
Affected Componentoneshot::Receiver::drop
Exploit StatusTheoretical / Stability Issue
CWE-416
Use After Free

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

Fix Committed
2024-03-18
Advisory Published
2024-03-19

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.