#1126

Unix Mio source loses remaining readiness tokens after returning Resize (single-key stall)

Openfemboy2112opened 1mo ago

On Linux with Crossterm 0.29.0's default Mio backend, a single input byte can remain unread after a resize when both readiness tokens were returned in one signal-first batch. Repeated event::poll calls time out until new input supplies another edge.

This is a one-byte, standalone reproducer, distinct from the >1024-byte incomplete-read case in #1057.

Reproduce without an application framework

Small public sources, pinned to the tested commit:

Download both into one directory, then run:

python3 crossterm_resize_repro.py --repeat 2 --strace --out-dir /tmp/crossterm-evidence

--strace is optional. The parent builds an isolated temporary Cargo project with no LibGibson dependency. Exit 1 means failed delivery; 2 means instrument/build failure. The command was executed from outside the LibGibson checkout.

  1. Enable raw mode, initialize the event source, signal READY over a separate file.
  2. Wait at a separate gate while allowing signal handlers to execute.
  3. Parent applies TIOCSWINSZ, then writes exactly r, then releases the gate.
  4. Child repeatedly polls/reads events and samples FIONREAD for three seconds.
  5. No second key is sent. Parent bounds lifetime, reaps, and verifies termios restoration.

The same parent runs raw poll/read and reversed key-before-resize controls.

Observed results

Gate-based control: raw 4/4 delivered; Crossterm resize-first 0/2; Crossterm key-first 2/2. All eight children exited zero and restored termios.

An earlier fixed-suspension version reproduced resize-first failure 10/10. A scratch-only intervention retaining the unprocessed readiness-token queue delivered Resize then exactly one r in 10/10 paired trials. This intervention is a causal control, not a production patch: it does not establish large-read handling, EOF/error behavior or fairness.

Selected actual syscall records (FD/PID decorations normalized):

ioctl(0, FIONREAD, [1]) = 0
epoll_wait(epfd, [{EPOLLIN, token=1}, {EPOLLIN, token=0}], 3, 20) = 2
# application receives Resize(80,24)
ioctl(0, FIONREAD, [1]) = 0
epoll_wait(epfd, [], 3, 20) = 0
# repeats through deadline; no read(0,...) occurs

With only the scratch token-retention intervention, the same batch is followed by read(0,"r",1024)=1 and a key event. Reversed ordering exposes the symmetric case: key delivered, resize stranded. In a separate diagnostic-only trial, a later z produces read(0,"rz",1024)=2.

Compact witness, hashes, paired results and exact experimental diff

Source path / expected behavior

The 0.29 event source returns immediately while iterating an Events batch, without retaining the remaining tokens. The next Mio poll clears the Events buffer. With edge-triggered TTY readiness, already-readable data need not generate a fresh edge.

Expected: returning Resize must preserve the unprocessed TTY readiness so the next poll/read can deliver the key without additional input. Conversely, returning a key should not discard pending signal readiness.

Environment: Crossterm0.29.0, Mio1.2.3, signal-hook-mio0.2.5, libc0.2.189, Rust1.98.1, Linux x86_64 kernel7.0.0-28-generic (Ubuntu build). No graphical renderer, terminal emulator, vt100 parser, event-stream consumer, or competing input reader is needed. No Windows/macOS or cross-kernel claim.

Activity

2
jeiea23d ago

It seems reproducible on mac.

yuann312d ago

Also reproduced with crossterm 0.29.0 (mio 1.2.3), on macOS (kqueue) and Linux (epoll), from a PTY test.

The test sends 50 TIOCSWINSZ + SIGWINCH pairs back to back, then writes a few keys. On macOS about 40% of single storms leave the keys unread: the input thread sits in kevent with the bytes pending, and the next keystroke delivers them all at once. Repeating the storm 5 times fails almost every run on macOS and 2 of 8 runs on Linux. The same keys written after the storm settles are always read.