- commit
- df38b13
- parent
- 4a6604b
- author
- Ian Tay
- date
- 2026-03-08 12:58:30 -0400 EDT
fix(attach): skip termios setup when stdin is not a TTY tcgetattr's return value was discarded, so when stdin was piped (e.g. `zmx attach foo < /dev/null`), orig_termios remained Zig `undefined` stack bytes. These garbage bytes were then copied, modified by cfmakeraw, and applied via tcsetattr — UB in safe builds.
1 files changed,
+27,
-19
+27,
-19
1@@ -981,13 +981,19 @@ fn attach(daemon: *Daemon) !void {
2 // - modify the desired attributes in the termios structure
3 // - then apply the changes with tcsetattr().
4 // This prevents unintended side effects by preserving other settings.
5- var orig_termios: cross.c.termios = undefined;
6- _ = cross.c.tcgetattr(posix.STDIN_FILENO, &orig_termios);
7-
8 // restore stdin fd to its original state after exiting.
9 // Use TCSAFLUSH to discard any unread input, preventing stale input after detach.
10+ //
11+ // tcgetattr fails when stdin is not a TTY (e.g. piped). In that case,
12+ // skip terminal setup entirely rather than applying undefined stack bytes
13+ // via tcsetattr.
14+ var orig_termios: cross.c.termios = undefined;
15+ const stdin_is_tty = cross.c.tcgetattr(posix.STDIN_FILENO, &orig_termios) == 0;
16+
17 defer {
18- _ = cross.c.tcsetattr(posix.STDIN_FILENO, cross.c.TCSAFLUSH, &orig_termios);
19+ if (stdin_is_tty) {
20+ _ = cross.c.tcsetattr(posix.STDIN_FILENO, cross.c.TCSAFLUSH, &orig_termios);
21+ }
22 // Reset terminal modes on detach:
23 // - Mouse: 1000=basic, 1002=button-event, 1003=any-event, 1006=SGR extended
24 // - 2004=bracketed paste, 1004=focus events, 1049=alt screen
25@@ -1003,21 +1009,23 @@ fn attach(daemon: *Daemon) !void {
26 _ = posix.write(posix.STDOUT_FILENO, restore_seq) catch {};
27 }
28
29- var raw_termios = orig_termios;
30- // set raw mode after successful connection.
31- // disables canonical mode (line buffering), input echoing, signal generation from
32- // control characters (like Ctrl+C), and flow control.
33- cross.c.cfmakeraw(&raw_termios);
34-
35- // Additional granular raw mode settings for precise control
36- // (matches what abduco and shpool do)
37- raw_termios.c_cc[cross.c.VLNEXT] = cross.c._POSIX_VDISABLE; // Disable literal-next (Ctrl-V)
38- // We want to intercept Ctrl+\ (SIGQUIT) so we can use it as a detach key
39- raw_termios.c_cc[cross.c.VQUIT] = cross.c._POSIX_VDISABLE; // Disable SIGQUIT (Ctrl+\)
40- raw_termios.c_cc[cross.c.VMIN] = 1; // Minimum chars to read: return after 1 byte
41- raw_termios.c_cc[cross.c.VTIME] = 0; // Read timeout: no timeout, return immediately
42-
43- _ = cross.c.tcsetattr(posix.STDIN_FILENO, cross.c.TCSANOW, &raw_termios);
44+ if (stdin_is_tty) {
45+ var raw_termios = orig_termios;
46+ // set raw mode after successful connection.
47+ // disables canonical mode (line buffering), input echoing, signal generation from
48+ // control characters (like Ctrl+C), and flow control.
49+ cross.c.cfmakeraw(&raw_termios);
50+
51+ // Additional granular raw mode settings for precise control
52+ // (matches what abduco and shpool do)
53+ raw_termios.c_cc[cross.c.VLNEXT] = cross.c._POSIX_VDISABLE; // Disable literal-next (Ctrl-V)
54+ // We want to intercept Ctrl+\ (SIGQUIT) so we can use it as a detach key
55+ raw_termios.c_cc[cross.c.VQUIT] = cross.c._POSIX_VDISABLE; // Disable SIGQUIT (Ctrl+\)
56+ raw_termios.c_cc[cross.c.VMIN] = 1; // Minimum chars to read: return after 1 byte
57+ raw_termios.c_cc[cross.c.VTIME] = 0; // Read timeout: no timeout, return immediately
58+
59+ _ = cross.c.tcsetattr(posix.STDIN_FILENO, cross.c.TCSANOW, &raw_termios);
60+ }
61
62 // Clear screen before attaching. This provides a clean slate before
63 // the session restore.