Commit df38b13

Ian Tay  ·  2026-03-08 12:58:30 -0400 EDT
parent 4a6604b
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
......@@ -981,13 +981,19 @@ fn attach(daemon: *Daemon) !void {
981981 // - modify the desired attributes in the termios structure
982982 // - then apply the changes with tcsetattr().
983983 // This prevents unintended side effects by preserving other settings.
984- var orig_termios: cross.c.termios = undefined;
985- _ = cross.c.tcgetattr(posix.STDIN_FILENO, &orig_termios);
986-
987984 // restore stdin fd to its original state after exiting.
988985 // Use TCSAFLUSH to discard any unread input, preventing stale input after detach.
986+ //
987+ // tcgetattr fails when stdin is not a TTY (e.g. piped). In that case,
988+ // skip terminal setup entirely rather than applying undefined stack bytes
989+ // via tcsetattr.
990+ var orig_termios: cross.c.termios = undefined;
991+ const stdin_is_tty = cross.c.tcgetattr(posix.STDIN_FILENO, &orig_termios) == 0;
992+
989993 defer {
990- _ = cross.c.tcsetattr(posix.STDIN_FILENO, cross.c.TCSAFLUSH, &orig_termios);
994+ if (stdin_is_tty) {
995+ _ = cross.c.tcsetattr(posix.STDIN_FILENO, cross.c.TCSAFLUSH, &orig_termios);
996+ }
991997 // Reset terminal modes on detach:
992998 // - Mouse: 1000=basic, 1002=button-event, 1003=any-event, 1006=SGR extended
993999 // - 2004=bracketed paste, 1004=focus events, 1049=alt screen
......@@ -1003,21 +1009,23 @@ fn attach(daemon: *Daemon) !void {
10031009 _ = posix.write(posix.STDOUT_FILENO, restore_seq) catch {};
10041010 }
10051011
1006- var raw_termios = orig_termios;
1007- // set raw mode after successful connection.
1008- // disables canonical mode (line buffering), input echoing, signal generation from
1009- // control characters (like Ctrl+C), and flow control.
1010- cross.c.cfmakeraw(&raw_termios);
1011-
1012- // Additional granular raw mode settings for precise control
1013- // (matches what abduco and shpool do)
1014- raw_termios.c_cc[cross.c.VLNEXT] = cross.c._POSIX_VDISABLE; // Disable literal-next (Ctrl-V)
1015- // We want to intercept Ctrl+\ (SIGQUIT) so we can use it as a detach key
1016- raw_termios.c_cc[cross.c.VQUIT] = cross.c._POSIX_VDISABLE; // Disable SIGQUIT (Ctrl+\)
1017- raw_termios.c_cc[cross.c.VMIN] = 1; // Minimum chars to read: return after 1 byte
1018- raw_termios.c_cc[cross.c.VTIME] = 0; // Read timeout: no timeout, return immediately
1019-
1020- _ = cross.c.tcsetattr(posix.STDIN_FILENO, cross.c.TCSANOW, &raw_termios);
1012+ if (stdin_is_tty) {
1013+ var raw_termios = orig_termios;
1014+ // set raw mode after successful connection.
1015+ // disables canonical mode (line buffering), input echoing, signal generation from
1016+ // control characters (like Ctrl+C), and flow control.
1017+ cross.c.cfmakeraw(&raw_termios);
1018+
1019+ // Additional granular raw mode settings for precise control
1020+ // (matches what abduco and shpool do)
1021+ raw_termios.c_cc[cross.c.VLNEXT] = cross.c._POSIX_VDISABLE; // Disable literal-next (Ctrl-V)
1022+ // We want to intercept Ctrl+\ (SIGQUIT) so we can use it as a detach key
1023+ raw_termios.c_cc[cross.c.VQUIT] = cross.c._POSIX_VDISABLE; // Disable SIGQUIT (Ctrl+\)
1024+ raw_termios.c_cc[cross.c.VMIN] = 1; // Minimum chars to read: return after 1 byte
1025+ raw_termios.c_cc[cross.c.VTIME] = 0; // Read timeout: no timeout, return immediately
1026+
1027+ _ = cross.c.tcsetattr(posix.STDIN_FILENO, cross.c.TCSANOW, &raw_termios);
1028+ }
10211029
10221030 // Clear screen before attaching. This provides a clean slate before
10231031 // the session restore.