a73x

2a47b9f5

fix: audit refinements — client length check, nothing-listening, actionable stop hint

a73x   2026-08-10 18:28

Commit message
fix: audit refinements — client length check, nothing-listening, actionable stop hint

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

src/client.zig
Old New
@@ -46,10 +46,6 @@ const Incoming = union(enum) {
46 /// left behind, short enough that a genuinely full session still reports it. 46 /// left behind, short enough that a genuinely full session still reports it.
47 const reconnect_grace_ms: i64 = 5000; 47 const reconnect_grace_ms: i64 = 5000;
48 48
49 /// One live connection to a muxd, however it was reached. The point of the
50 /// struct is that it can be closed and opened again from the same recipe
51 /// (`sock_path` or `via`), which is what lets a session outlive its
52 /// transport instead of exiting with it.
53 /// What to say when the link dies. A `--via` transport that died before a 49 /// What to say when the link dies. A `--via` transport that died before a
54 /// single frame arrived never carried a connection at all — ssh refused, the 50 /// single frame arrived never carried a connection at all — ssh refused, the
55 /// host is unreachable, or `muxd` is not on its PATH — and "connection to 51 /// host is unreachable, or `muxd` is not on its PATH — and "connection to
@@ -85,6 +81,10 @@ pub const QuicTarget = struct {
85 idle_ms: u32 = quic_idle_ms_default, 81 idle_ms: u32 = quic_idle_ms_default,
86 }; 82 };
87 83
84 /// One live connection to a muxd, however it was reached. The point of the
85 /// struct is that it can be closed and opened again from the same recipe
86 /// (`sock_path` or `via`), which is what lets a session outlive its
87 /// transport instead of exiting with it.
88 const Transport = struct { 88 const Transport = struct {
89 conn: Conn, 89 conn: Conn,
90 /// Only set for `--via`: the command whose stdio *is* the transport. 90 /// Only set for `--via`: the command whose stdio *is* the transport.
src/main.zig
Old New
@@ -248,8 +248,9 @@ pub fn main() !u8 {
248 } 248 }
249 249
250 switch (o.cmd) { 250 switch (o.cmd) {
251 // The socket path resolved above is unused here and harmless: asking 251 // The socket path resolved above is unused here and unchecked (see
252 // a binary its version must work with no daemon and no runtime dir. 252 // the length guard above): asking a binary its version must work
253 // with no daemon and no runtime dir.
253 .version => { 254 .version => {
254 var vbuf: [64]u8 = undefined; 255 var vbuf: [64]u8 = undefined;
255 const s = std.fmt.bufPrint(&vbuf, "muxd {s}\n", .{build_options.version}) catch unreachable; 256 const s = std.fmt.bufPrint(&vbuf, "muxd {s}\n", .{build_options.version}) catch unreachable;
@@ -389,7 +390,10 @@ fn run(alloc: std.mem.Allocator, o: Opts, sock_path: []const u8) !u8 {
389 return 1; 390 return 1;
390 }, 391 },
391 error.SockPathNotASocket => { 392 error.SockPathNotASocket => {
392 std.debug.print("muxd: {s} exists and is not a socket (move it, or name another with --sock)\n", .{sock_path}); 393 std.debug.print(
394 "muxd: {s} exists and is not a socket (move it, or name another with --sock)\n",
395 .{sock_path},
396 );
393 return 1; 397 return 1;
394 }, 398 },
395 else => return err, 399 else => return err,
@@ -415,7 +419,10 @@ pub fn defaultSockPath(alloc: std.mem.Allocator) ![]const u8 {
415 419
416 fn dump(alloc: std.mem.Allocator, sock_path: []const u8, vt_mode: bool) !u8 { 420 fn dump(alloc: std.mem.Allocator, sock_path: []const u8, vt_mode: bool) !u8 {
417 const stream = std.net.connectUnixSocket(sock_path) catch { 421 const stream = std.net.connectUnixSocket(sock_path) catch {
418 std.debug.print("muxd dump: cannot connect to {s} (no daemon; `muxd start` starts one)\n", .{sock_path}); 422 std.debug.print(
423 "muxd dump: nothing listening on {s} (`muxd start` starts a daemon)\n",
424 .{sock_path},
425 );
419 return 1; 426 return 1;
420 }; 427 };
421 defer stream.close(); 428 defer stream.close();
@@ -433,7 +440,10 @@ fn dump(alloc: std.mem.Allocator, sock_path: []const u8, vt_mode: bool) !u8 {
433 440
434 fn stats(alloc: std.mem.Allocator, sock_path: []const u8) !u8 { 441 fn stats(alloc: std.mem.Allocator, sock_path: []const u8) !u8 {
435 const stream = std.net.connectUnixSocket(sock_path) catch { 442 const stream = std.net.connectUnixSocket(sock_path) catch {
436 std.debug.print("muxd stats: cannot connect to {s} (no daemon; `muxd start` starts one)\n", .{sock_path}); 443 std.debug.print(
444 "muxd stats: nothing listening on {s} (`muxd start` starts a daemon)\n",
445 .{sock_path},
446 );
437 return 1; 447 return 1;
438 }; 448 };
439 defer stream.close(); 449 defer stream.close();
@@ -551,8 +561,8 @@ fn startCmd(alloc: std.mem.Allocator, sock_path: []const u8, forwarded: []const
551 }; 561 };
552 if (r == .already_running) { 562 if (r == .already_running) {
553 std.debug.print( 563 std.debug.print(
554 "muxd: already running on {s} (`muxd stop` it first if you meant different flags)\n", 564 "muxd: already running on {s} (stop it first with `muxd stop --sock {s}` if you meant different flags)\n",
555 .{sock_path}, 565 .{ sock_path, sock_path },
556 ); 566 );
557 } 567 }
558 return 0; 568 return 0;
src/mux_main.zig
Old New
@@ -188,6 +188,21 @@ pub fn main() !u8 {
188 try std.fmt.allocPrint(alloc, "/tmp/muxd-{d}.sock", .{std.os.linux.getuid()}); 188 try std.fmt.allocPrint(alloc, "/tmp/muxd-{d}.sock", .{std.os.linux.getuid()});
189 defer alloc.free(sock_path); 189 defer alloc.free(sock_path);
190 190
191 // The same 107-byte `sun_path` guard muxd applies (main.zig), for
192 // the same reason and with the same constant. It sits before the
193 // PATH search rather than at the connect because auto-start would
194 // otherwise reach it first: mux finds muxd, spawns a child that
195 // refuses the path instantly, and polls the full 2s into "daemon
196 // did not answer" — a timeout story about a path that was doomed
197 // at parse. Refusing here costs nothing and says the real thing.
198 if (sock_path.len > 107) {
199 std.debug.print(
200 "mux: socket path too long ({d} bytes, max 107): {s}\n",
201 .{ sock_path.len, sock_path },
202 );
203 return 1;
204 }
205
191 // Attach auto-start (M13): give the attach a daemon to land on. 206 // Attach auto-start (M13): give the attach a daemon to land on.
192 // Unix-socket transport only — quic:// has nothing local to 207 // Unix-socket transport only — quic:// has nothing local to
193 // spawn, and --via's auto-starter is the remote proxy. 208 // spawn, and --via's auto-starter is the remote proxy.
test/e2e.sh
Old New
@@ -489,7 +489,7 @@ grep -q "transport command failed before a session started" "$OUT.dead" || {
489 # a cause the transport's own stderr had already named. 489 # a cause the transport's own stderr had already named.
490 grep -q "is muxd installed on the host" "$OUT.dead" && { 490 grep -q "is muxd installed on the host" "$OUT.dead" && {
491 echo "e2e FAIL: old lostMsg wording still emitted alongside the new pin" 491 echo "e2e FAIL: old lostMsg wording still emitted alongside the new pin"
492 cat "$OUT.dead"; exit 1; } || true 492 cat "$OUT.dead"; exit 1; }
493 rm -f "$OUT.dead" 493 rm -f "$OUT.dead"
494 494
495 # --- M7: aborting a reconnect exits cleanly. The client establishes a real 495 # --- M7: aborting a reconnect exits cleanly. The client establishes a real