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 { | |
| 981 | 981 | // - modify the desired attributes in the termios structure | |
| 982 | 982 | // - then apply the changes with tcsetattr(). | |
| 983 | 983 | // 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 | - | ||
| 987 | 984 | // restore stdin fd to its original state after exiting. | |
| 988 | 985 | // 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 | + | ||
| 989 | 993 | 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 | + | } | |
| 991 | 997 | // Reset terminal modes on detach: | |
| 992 | 998 | // - Mouse: 1000=basic, 1002=button-event, 1003=any-event, 1006=SGR extended | |
| 993 | 999 | // - 2004=bracketed paste, 1004=focus events, 1049=alt screen |
| ... | ... | @@ -1003,21 +1009,23 @@ fn attach(daemon: *Daemon) !void { | |
| 1003 | 1009 | _ = posix.write(posix.STDOUT_FILENO, restore_seq) catch {}; | |
| 1004 | 1010 | } | |
| 1005 | 1011 | ||
| 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 | + | } | |
| 1021 | 1029 | ||
| 1022 | 1030 | // Clear screen before attaching. This provides a clean slate before | |
| 1023 | 1031 | // the session restore. |