repos / zmx

session persistence for terminal processes
git clone https://github.com/neurosnap/zmx.git

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
M src/main.zig
+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.