Skip to content

Stale waiter after send_timeout timeout: debug_assert_eq!(_prev_state, WAITING) fires in List::dequeue on drop #102

Description

@PeterKnego

A send_timeout that actually times out appears to leave a stale node in the sender wait queue. When the channel is later dropped, List::dequeue finds that node in state 0 rather than WAITING and the debug assertion at src/wait/queue.rs:582 fires.

Reproduction

use std::time::Duration;
use thingbuf::mpsc::blocking;

fn main() {
    // ch1: full, receiver alive -> send_timeout gives up after the timeout
    let (tx1, _rx1) = blocking::channel::<u64>(1);
    tx1.try_send(1).unwrap();
    let _ = tx1.send_timeout(2, Duration::from_millis(20));

    // ch2: receiver dropped -> send_timeout returns Closed
    let (tx2, rx2) = blocking::channel::<u64>(4);
    drop(rx2);
    let _ = tx2.send_timeout(1, Duration::from_millis(20));

    println!("reached end of main");
}

cargo run on 0.1.6:

reached end of main
thread 'main' panicked at .../thingbuf-0.1.6/src/wait/queue.rs:582:9:
assertion `left == right` failed
  left: 0
 right: 1

Note the panic happens after main's body completes, i.e. during the end-of-scope drops, not inside either send_timeout call. 5 runs out of 5 on my machine.

The assertion

fn dequeue(&mut self, new_state: usize) -> Option<T> {
    let mut last = self.tail?;
    let last = unsafe { last.as_mut() };
    let _prev_state = test_dbg!(last.state.swap(new_state, Release));
    debug_assert_eq!(_prev_state, WAITING);   // <-- fires, _prev_state == 0

Caveats, in the interest of not wasting your time

  • It is timing-sensitive. Inserting println! calls between the steps, or dropping the handles explicitly in a different order, makes it stop reproducing. Each half in isolation is clean — one channel alone does not trigger it, and neither send_timeout call panics on its own.
  • debug_assert only. cargo run --release on the same program prints reached end of main and exits 0, so release builds proceed past whatever state the assertion is objecting to. I have not tried to determine whether the resulting list state is merely inconsistent or actually unsafe, and I would not want to guess.
  • I have not minimised this further than the above, and I do not know the wait-queue design well enough to propose a fix.

Tested on 0.1.6, Linux x86_64, rustc 1.97.1. Found while benchmarking thingbuf against a ring buffer of my own; the reproduction above is reduced from that test program.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions