repos / zmx

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

commit
1fc6306
parent
df38b13
author
Ian Tay
date
2026-03-08 12:58:59 -0400 EDT
fix(ipc): validate wire data before arithmetic and indexing

- expectedLength: header.len is u32 off the wire; adding sizeof(Header)
  at u32 width could wrap (panic in safe mode, UB slice bounds in
  release). Widen to usize first.
- get_session_entries: cmd_len/cwd_len are u16 (max 65535) but index
  into [256]u8 arrays. Clamp before slicing.
3 files changed,  +17, -7
M src/ipc.zig
+7, -1
 1@@ -15,6 +15,10 @@ pub const Tag = enum(u8) {
 2     History = 8,
 3     Run = 9,
 4     Ack = 10,
 5+    // Non-exhaustive: this enum comes off the wire via bytesToValue and
 6+    // @enumFromInt, so out-of-range values (11-255) are representable
 7+    // rather than UB. Switches must handle `_` (unknown tag).
 8+    _,
 9 };
10 
11 pub const Header = packed struct {
12@@ -53,7 +57,9 @@ pub const Info = extern struct {
13 pub fn expectedLength(data: []const u8) ?usize {
14     if (data.len < @sizeOf(Header)) return null;
15     const header = std.mem.bytesToValue(Header, data[0..@sizeOf(Header)]);
16-    return @sizeOf(Header) + header.len;
17+    // header.len comes off the wire; widen to usize before adding so a
18+    // near-u32-max value can't wrap (panic in safe mode, UB in release).
19+    return @as(usize, @sizeOf(Header)) + @as(usize, header.len);
20 }
21 
22 pub fn send(fd: i32, tag: Tag, data: []const u8) !void {
M src/main.zig
+2, -1
 1@@ -624,7 +624,7 @@ const Daemon = struct {
 2 
 3     pub fn handleHistory(self: *Daemon, client: *Client, term: *ghostty_vt.Terminal, payload: []const u8) !void {
 4         const format: util.HistoryFormat = if (payload.len > 0)
 5-            @enumFromInt(payload[0])
 6+            std.meta.intToEnum(util.HistoryFormat, payload[0]) catch .plain
 7         else
 8             .plain;
 9         if (util.serializeTerminal(self.alloc, term, format)) |output| {
10@@ -1489,6 +1489,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
11                         .History => try daemon.handleHistory(client, &term, msg.payload),
12                         .Run => try daemon.handleRun(client, pty_fd, msg.payload),
13                         .Output, .Ack => {},
14+                        _ => std.log.warn("ignoring unknown IPC tag={d}", .{@intFromEnum(msg.header.tag)}),
15                     }
16                 }
17             }
M src/util.zig
+8, -5
 1@@ -64,13 +64,16 @@ pub fn get_session_entries(alloc: std.mem.Allocator, socket_dir: []const u8) !st
 2             };
 3             posix.close(result.fd);
 4 
 5-            // Extract cmd and cwd from the fixed-size arrays
 6-            const cmd: ?[]const u8 = if (result.info.cmd_len > 0)
 7-                alloc.dupe(u8, result.info.cmd[0..result.info.cmd_len]) catch null
 8+            // Extract cmd and cwd from the fixed-size arrays. Lengths come
 9+            // off the wire (u16 range), so clamp to the actual array size.
10+            const cmd_len = @min(result.info.cmd_len, ipc.MAX_CMD_LEN);
11+            const cwd_len = @min(result.info.cwd_len, ipc.MAX_CWD_LEN);
12+            const cmd: ?[]const u8 = if (cmd_len > 0)
13+                alloc.dupe(u8, result.info.cmd[0..cmd_len]) catch null
14             else
15                 null;
16-            const cwd: ?[]const u8 = if (result.info.cwd_len > 0)
17-                alloc.dupe(u8, result.info.cwd[0..result.info.cwd_len]) catch null
18+            const cwd: ?[]const u8 = if (cwd_len > 0)
19+                alloc.dupe(u8, result.info.cwd[0..cwd_len]) catch null
20             else
21                 null;
22