- commit
- fb0e621
- parent
- f520c4c
- author
- Michael Sakaluk
- date
- 2026-03-10 18:09:01 -0400 EDT
fix: validate session name length against Unix socket path limit Session names that cause the socket path to exceed the OS sun_path limit (104 bytes on macOS, 108 on Linux) now produce a clear error message instead of failing silently with exit code 1. - Add max_socket_path_len constant derived from platform sockaddr_un - Add validation in getSocketPath returning error.NameTooLong - Add maxSessionNameLen helper for dynamic limit computation - Add printSessionNameTooLong for user-facing error on stderr - Handle NameTooLong in all command paths (run, attach, kill, history, detach, list) - Move validation before sessionExists in kill/history so the error message is shown even when the socket file doesn't exist - Add 6 unit tests covering boundary conditions and platform constants Fixes: https://github.com/neurosnap/zmx/issues/84
3 files changed,
+135,
-12
+41,
-9
1@@ -128,7 +128,10 @@ pub fn main() !void {
2 .cwd = cwd,
3 .created_at = @intCast(std.time.timestamp()),
4 };
5- daemon.socket_path = try socket.getSocketPath(alloc, cfg.socket_dir, sesh);
6+ daemon.socket_path = socket.getSocketPath(alloc, cfg.socket_dir, sesh) catch |err| switch (err) {
7+ error.NameTooLong => return printSessionNameTooLong(sesh, &cfg),
8+ error.OutOfMemory => return err,
9+ };
10 std.log.info("socket path={s}", .{daemon.socket_path});
11 return attach(&daemon);
12 } else if (std.mem.eql(u8, cmd, "run") or std.mem.eql(u8, cmd, "r")) {
13@@ -160,7 +163,10 @@ pub fn main() !void {
14 .is_task_mode = true,
15 .task_command = cmd_args_raw.items,
16 };
17- daemon.socket_path = try socket.getSocketPath(alloc, cfg.socket_dir, sesh);
18+ daemon.socket_path = socket.getSocketPath(alloc, cfg.socket_dir, sesh) catch |err| switch (err) {
19+ error.NameTooLong => return printSessionNameTooLong(sesh, &cfg),
20+ error.OutOfMemory => return err,
21+ };
22 std.log.info("socket path={s}", .{daemon.socket_path});
23 return run(&daemon, cmd_args_raw.items);
24 } else if (std.mem.eql(u8, cmd, "wait") or std.mem.eql(u8, cmd, "w")) {
25@@ -711,6 +717,23 @@ fn help() !void {
26 try w.interface.flush();
27 }
28
29+fn printSessionNameTooLong(session_name: []const u8, cfg: *Cfg) void {
30+ var buf: [4096]u8 = undefined;
31+ var w = std.fs.File.stderr().writer(&buf);
32+ if (socket.maxSessionNameLen(cfg.socket_dir)) |max_len| {
33+ w.interface.print(
34+ "error: session name is too long ({d} bytes, max {d} for socket directory \"{s}\")\n",
35+ .{ session_name.len, max_len, cfg.socket_dir },
36+ ) catch {};
37+ } else {
38+ w.interface.print(
39+ "error: socket directory path is too long (\"{s}\")\n",
40+ .{cfg.socket_dir},
41+ ) catch {};
42+ }
43+ w.interface.flush() catch {};
44+}
45+
46 fn wait(cfg: *Cfg, session_names: std.ArrayList([]const u8)) !void {
47 var gpa = std.heap.GeneralPurposeAllocator(.{}){};
48 defer _ = gpa.deinit();
49@@ -860,7 +883,10 @@ fn detachAll(cfg: *Cfg) !void {
50 var dir = try std.fs.openDirAbsolute(cfg.socket_dir, .{});
51 defer dir.close();
52
53- const socket_path = try socket.getSocketPath(alloc, cfg.socket_dir, session_name);
54+ const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
55+ error.NameTooLong => return printSessionNameTooLong(session_name, cfg),
56+ error.OutOfMemory => return err,
57+ };
58 defer alloc.free(socket_path);
59 const result = ipc.probeSession(alloc, socket_path) catch |err| {
60 std.log.err("session unresponsive: {s}", .{@errorName(err)});
61@@ -879,6 +905,12 @@ fn kill(cfg: *Cfg, session_name: []const u8) !void {
62 defer _ = gpa.deinit();
63 const alloc = gpa.allocator();
64
65+ const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
66+ error.NameTooLong => return printSessionNameTooLong(session_name, cfg),
67+ error.OutOfMemory => return err,
68+ };
69+ defer alloc.free(socket_path);
70+
71 var dir = try std.fs.openDirAbsolute(cfg.socket_dir, .{});
72 defer dir.close();
73
74@@ -890,9 +922,6 @@ fn kill(cfg: *Cfg, session_name: []const u8) !void {
75 w.interface.flush() catch {};
76 return error.SessionNotFound;
77 }
78-
79- const socket_path = try socket.getSocketPath(alloc, cfg.socket_dir, session_name);
80- defer alloc.free(socket_path);
81 const result = ipc.probeSession(alloc, socket_path) catch |err| {
82 std.log.err("session unresponsive: {s}", .{@errorName(err)});
83 var buf: [4096]u8 = undefined;
84@@ -923,6 +952,12 @@ fn history(cfg: *Cfg, session_name: []const u8, format: util.HistoryFormat) !voi
85 defer _ = gpa.deinit();
86 const alloc = gpa.allocator();
87
88+ const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
89+ error.NameTooLong => return printSessionNameTooLong(session_name, cfg),
90+ error.OutOfMemory => return err,
91+ };
92+ defer alloc.free(socket_path);
93+
94 var dir = try std.fs.openDirAbsolute(cfg.socket_dir, .{});
95 defer dir.close();
96
97@@ -934,9 +969,6 @@ fn history(cfg: *Cfg, session_name: []const u8, format: util.HistoryFormat) !voi
98 w.interface.flush() catch {};
99 return error.SessionNotFound;
100 }
101-
102- const socket_path = try socket.getSocketPath(alloc, cfg.socket_dir, session_name);
103- defer alloc.free(socket_path);
104 const result = ipc.probeSession(alloc, socket_path) catch |err| {
105 std.log.err("session unresponsive: {s}", .{@errorName(err)});
106 if (err == error.ConnectionRefused) socket.cleanupStaleSocket(dir, session_name);
+90,
-2
1@@ -67,11 +67,99 @@ pub fn createSocket(fname: []const u8) !i32 {
2 return fd;
3 }
4
5-pub fn getSocketPath(alloc: std.mem.Allocator, socket_dir: []const u8, session_name: []const u8) ![]const u8 {
6+/// Maximum number of usable bytes in a Unix domain socket path.
7+/// Derived from the platform's sockaddr_un.path field, minus 1 for the
8+/// required null terminator.
9+pub const max_socket_path_len: usize = @typeInfo(
10+ @TypeOf(@as(posix.sockaddr.un, undefined).path),
11+).array.len - 1;
12+
13+pub fn getSocketPath(alloc: std.mem.Allocator, socket_dir: []const u8, session_name: []const u8) error{ NameTooLong, OutOfMemory }![]const u8 {
14 const dir = socket_dir;
15- const fname = try alloc.alloc(u8, dir.len + session_name.len + 1);
16+ const path_len = dir.len + 1 + session_name.len;
17+ if (path_len > max_socket_path_len) return error.NameTooLong;
18+ const fname = try alloc.alloc(u8, path_len);
19 @memcpy(fname[0..dir.len], dir);
20 @memcpy(fname[dir.len .. dir.len + 1], "/");
21 @memcpy(fname[dir.len + 1 ..], session_name);
22 return fname;
23 }
24+
25+/// Returns the maximum session name length for a given socket directory,
26+/// or null if the socket directory itself is already too long.
27+pub fn maxSessionNameLen(socket_dir: []const u8) ?usize {
28+ // path = socket_dir + "/" + session_name
29+ const overhead = socket_dir.len + 1;
30+ if (overhead >= max_socket_path_len) return null;
31+ return max_socket_path_len - overhead;
32+}
33+
34+test "max_socket_path_len matches platform sockaddr_un" {
35+ const path_field_len = @typeInfo(
36+ @TypeOf(@as(posix.sockaddr.un, undefined).path),
37+ ).array.len;
38+ try std.testing.expectEqual(path_field_len - 1, max_socket_path_len);
39+ try std.testing.expect(max_socket_path_len > 0);
40+}
41+
42+test "getSocketPath succeeds for paths within limit" {
43+ const alloc = std.testing.allocator;
44+ const result = try getSocketPath(alloc, "/tmp/zmx", "mysession");
45+ defer alloc.free(result);
46+ try std.testing.expectEqualStrings("/tmp/zmx/mysession", result);
47+}
48+
49+test "getSocketPath returns NameTooLong when path exceeds limit" {
50+ const alloc = std.testing.allocator;
51+ const dir = [_]u8{'d'} ** (max_socket_path_len - 2);
52+ const dir_slice: []const u8 = &dir;
53+
54+ const ok = try getSocketPath(alloc, dir_slice, "x");
55+ defer alloc.free(ok);
56+ try std.testing.expectEqual(max_socket_path_len, ok.len);
57+
58+ const err = getSocketPath(alloc, dir_slice, "xx");
59+ try std.testing.expectError(error.NameTooLong, err);
60+}
61+
62+test "getSocketPath returns NameTooLong for empty dir with oversized name" {
63+ const alloc = std.testing.allocator;
64+ const name = [_]u8{'n'} ** (max_socket_path_len);
65+ const name_slice: []const u8 = &name;
66+ const err = getSocketPath(alloc, "", name_slice);
67+ try std.testing.expectError(error.NameTooLong, err);
68+}
69+
70+test "maxSessionNameLen computes correct dynamic limit" {
71+ const short_dir = "/tmp/zmx";
72+ const short_max = maxSessionNameLen(short_dir).?;
73+ try std.testing.expectEqual(max_socket_path_len - short_dir.len - 1, short_max);
74+
75+ const full_dir = [_]u8{'f'} ** max_socket_path_len;
76+ const full_dir_slice: []const u8 = &full_dir;
77+ try std.testing.expectEqual(@as(?usize, null), maxSessionNameLen(full_dir_slice));
78+
79+ const tight_dir = [_]u8{'t'} ** (max_socket_path_len - 2);
80+ const tight_dir_slice: []const u8 = &tight_dir;
81+ try std.testing.expectEqual(@as(?usize, 1), maxSessionNameLen(tight_dir_slice));
82+}
83+
84+test "getSocketPath boundary: name fills exactly to limit" {
85+ const alloc = std.testing.allocator;
86+ const dir = "/tmp/zmx";
87+ const max_name_len = maxSessionNameLen(dir).?;
88+
89+ const name_at_limit = try alloc.alloc(u8, max_name_len);
90+ defer alloc.free(name_at_limit);
91+ @memset(name_at_limit, 'a');
92+
93+ const path = try getSocketPath(alloc, dir, name_at_limit);
94+ defer alloc.free(path);
95+ try std.testing.expectEqual(max_socket_path_len, path.len);
96+
97+ const name_over_limit = try alloc.alloc(u8, max_name_len + 1);
98+ defer alloc.free(name_over_limit);
99+ @memset(name_over_limit, 'b');
100+
101+ try std.testing.expectError(error.NameTooLong, getSocketPath(alloc, dir, name_over_limit));
102+}
+4,
-1
1@@ -40,7 +40,10 @@ pub fn get_session_entries(alloc: std.mem.Allocator, socket_dir: []const u8) !st
2 const name = try alloc.dupe(u8, entry.name);
3 errdefer alloc.free(name);
4
5- const socket_path = try socket.getSocketPath(alloc, socket_dir, entry.name);
6+ const socket_path = socket.getSocketPath(alloc, socket_dir, entry.name) catch |err| switch (err) {
7+ error.NameTooLong => continue,
8+ error.OutOfMemory => return err,
9+ };
10 defer alloc.free(socket_path);
11
12 const result = ipc.probeSession(alloc, socket_path) catch |err| {