Skip to content

write: Interrupted system call — FdOutputStream::append() does not retry write() on EINTR from non-cancel signals (e.g. SIGWINCH) #12496

Description

@afreakk

write: Interrupted system call — FdOutputStream::append() does not retry write() on EINTR from non-cancel signals (e.g. SIGWINCH)

Description

FdOutputStream::append() in src/io.rs permanently kills the output stream when write() is interrupted by a benign signal like SIGWINCH (terminal resize). Only EINTR from SIGINT/SIGHUP is handled gracefully — all other EINTR causes are treated as fatal errors.

This is the write() equivalent of #10250, which was fixed for open_cloexec() in PR #10251.

Steps to reproduce

In tmux, with a large fish history (thousands of entries):

builtin history -z --show-time="%s%t" | fzf-tmux -d 35% -- --read0

Or simply press Ctrl+R to use fzf's built-in fzf-history-widget, which pipes builtin history to fzf.

Observed behavior

  • write: Interrupted system call is printed
  • fzf receives only ~660 entries instead of the full history (52,000+ in my case)
  • The count is consistent across invocations — it's however many entries get written before fzf-tmux creates a split pane and triggers SIGWINCH

Expected behavior

All history entries should be piped to fzf. The write should be retried on EINTR from SIGWINCH.

Root cause

In src/io.rs, FdOutputStream::append() (lines ~757-778):

if unescape_bytes_and_write_to_fd(s, self.fd).is_none() {
    if errno::errno().0 == EINTR && self.sigcheck.check() {
        // Only handles EINTR + SIGINT/SIGHUP (SigHupInt topic)
        // SIGWINCH falls through
    } else if errno::errno().0 != EPIPE {
        perror("write");  // prints "write: Interrupted system call"
    }
    self.errored = true;  // permanently kills the output stream
}

self.sigcheck is SigChecker::new(Topic::SigHupInt) — it only detects SIGHUP and SIGINT. When SIGWINCH (or any other benign signal) causes EINTR:

  1. self.sigcheck.check() returns false
  2. Code falls through to perror("write")
  3. self.errored = true — all future append() calls bail immediately
  4. The pipe closes, downstream consumer gets truncated data

What triggers SIGWINCH

  • fzf-tmux creating a tmux split pane (resizes the original pane)
  • fzf --height adjusting terminal layout
  • fzf --tmux creating a tmux popup
  • User manually resizing the terminal during a long-running pipe

Suggested fix

Similar to PR #10251 for open_cloexec(): retry write() on EINTR when it's not from a cancel signal.

Ideally, make unescape_bytes_and_write_to_fd retry on EINTR unconditionally (EINTR-transparent), and check for user cancellation separately in append() via sigcheck:

fn append(&mut self, s: impl IntoCharIter) -> bool {
    if self.errored {
        return false;
    }
    if self.sigcheck.check() {
        self.errored = true;
        return false;
    }
    if unescape_bytes_and_write_to_fd(s, self.fd).is_none() {
        if errno::errno().0 != EPIPE {
            perror("write");
        }
        self.errored = true;
    }
    !self.errored
}

With the write helper retrying internally:

loop {
    match write(fd, &buf[bytes_written..]) {
        Ok(n) => { bytes_written += n; if bytes_written >= buf.len() { break; } }
        Err(e) if e == EINTR => continue,  // always retry
        Err(_) => return None,
    }
}

This also handles partial writes correctly (resumes from offset rather than re-sending the full buffer).

Other potentially affected sites

The same pattern may exist elsewhere — a grep -rn "EINTR" src/ audit of all write-side EINTR handling would be worthwhile.

Workaround

Override fzf-history-widget to not pipe builtin history to fzf. Instead, let fzf execute FZF_DEFAULT_COMMAND in a child process (which won't receive SIGWINCH):

# In ~/.config/fish/functions/fzf-history-widget.fish
# Change:
#   eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --query=$fzf_query
# To:
#   eval (__fzfcmd) --query=$fzf_query

Environment

  • fish 4.5.0 (Rust port)
  • fzf 0.68.0
  • tmux 3.6a
  • Linux 6.19.3

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions