8cc75997
refactor: one owner for "this shell is standing in that session"
a73x 2026-08-29 10:01
Commit message
src/cli/mux_main.zig
| Old | New | ||
|---|---|---|---|
| @@ -165,32 +165,6 @@ fn agentReachable(path: []const u8) bool { | |||
| 165 | const self_attach_refusal = | 165 | const self_attach_refusal = |
| 166 | "mux: this shell is inside that session (unset " ++ proto.session_env ++ " to override)\n"; | 166 | "mux: this shell is inside that session (unset " ++ proto.session_env ++ " to override)\n"; |
| 167 | 167 | ||
| 168 | /// An inner client repaints its own grid forever, and no chord steers | ||
| 169 | /// back out. Refused before it starts. | ||
| 170 | fn insideThisSession( | ||
| 171 | env_sock: ?[]const u8, | ||
| 172 | env_session: ?[]const u8, | ||
| 173 | sock: ?[]const u8, | ||
| 174 | session: []const u8, | ||
| 175 | ) bool { | ||
| 176 | const es = env_sock orelse return false; | ||
| 177 | const en = env_session orelse return false; | ||
| 178 | // A unix socket path only: a host or quic:// target is a different | ||
| 179 | // daemon whatever its sessions are called. | ||
| 180 | const target = sock orelse return false; | ||
| 181 | // Emptied counts as unset: `MUX_SESSION=` is how a shell overrides an | ||
| 182 | // exported variable it cannot unset, and the refusal names unsetting as | ||
| 183 | // the way out — both spellings of that have to work. | ||
| 184 | if (es.len == 0 or en.len == 0) return false; | ||
| 185 | // Both halves, so session 0 attaching to session 1 of the same daemon | ||
| 186 | // keeps working. String equality on the path: a symlinked or relatively | ||
| 187 | // spelled `--sock` for the same socket evades this, accepted, because | ||
| 188 | // this guards the mistake people make (typing `mux` in a mux shell) and | ||
| 189 | // is not a security boundary. | ||
| 190 | return std.mem.eql(u8, es, target) and | ||
| 191 | std.mem.eql(u8, en, proto.resolveName(session)); | ||
| 192 | } | ||
| 193 | |||
| 194 | /// The attach line, read off the struct: the field's type is the flag's | 168 | /// The attach line, read off the struct: the field's type is the flag's |
| 195 | /// arity and its name is the flag's spelling. What a flag MEANS stays here, | 169 | /// arity and its name is the flag's spelling. What a flag MEANS stays here, |
| 196 | /// in the post-checks below. | 170 | /// in the post-checks below. |
| @@ -447,11 +421,11 @@ fn attachLocal( | |||
| 447 | // the wall and never come back through here, so focusing from session 0 | 421 | // the wall and never come back through here, so focusing from session 0 |
| 448 | // to session 1 keeps working. The picker's `a` is the one chord that | 422 | // to session 1 keeps working. The picker's `a` is the one chord that |
| 449 | // takes a spelling, and it runs `wallview.showsSelf` itself. | 423 | // takes a spelling, and it runs `wallview.showsSelf` itself. |
| 450 | if (insideThisSession( | 424 | if (wallview.showsSelf( |
| 425 | .{ .sock = sock_path }, | ||
| 426 | session, | ||
| 451 | std.posix.getenv(proto.sock_env), | 427 | std.posix.getenv(proto.sock_env), |
| 452 | std.posix.getenv(proto.session_env), | 428 | std.posix.getenv(proto.session_env), |
| 453 | sock_path, | ||
| 454 | session, | ||
| 455 | )) { | 429 | )) { |
| 456 | std.debug.print("{s}", .{self_attach_refusal}); | 430 | std.debug.print("{s}", .{self_attach_refusal}); |
| 457 | return 2; | 431 | return 2; |
| @@ -1181,36 +1155,6 @@ test "parseArgs: no --session means the empty wire name (older-daemon compat)" { | |||
| 1181 | try std.testing.expectEqualStrings("", s.attach.session); | 1155 | try std.testing.expectEqualStrings("", s.attach.session); |
| 1182 | } | 1156 | } |
| 1183 | 1157 | ||
| 1184 | test "insideThisSession: only the exact socket-and-session pair is the loop" { | ||
| 1185 | const sock = "/run/user/1000/muxd.sock"; | ||
| 1186 | |||
| 1187 | // The incident, both spellings of the default session: `mux` typed in a | ||
| 1188 | // shell of session 0, and `mux --session 0` typed in the same shell. | ||
| 1189 | try std.testing.expect(insideThisSession(sock, "0", sock, "")); | ||
| 1190 | try std.testing.expect(insideThisSession(sock, "0", sock, "0")); | ||
| 1191 | try std.testing.expect(insideThisSession(sock, "work", sock, "work")); | ||
| 1192 | |||
| 1193 | // A different session of the SAME daemon is the useful case and must | ||
| 1194 | // stay allowed — only the self-pair feeds its own paint back. | ||
| 1195 | try std.testing.expect(!insideThisSession(sock, "0", sock, "1")); | ||
| 1196 | try std.testing.expect(!insideThisSession(sock, "work", sock, "")); | ||
| 1197 | |||
| 1198 | // Same session name, different daemon: names are per-daemon, so this is | ||
| 1199 | // two unrelated sessions that happen to agree on a word. | ||
| 1200 | try std.testing.expect(!insideThisSession(sock, "0", "/run/user/1000/other.sock", "0")); | ||
| 1201 | |||
| 1202 | // Nothing planted, or emptied to override: no refusal either way. | ||
| 1203 | try std.testing.expect(!insideThisSession(null, "0", sock, "0")); | ||
| 1204 | try std.testing.expect(!insideThisSession(sock, null, sock, "0")); | ||
| 1205 | try std.testing.expect(!insideThisSession("", "0", sock, "0")); | ||
| 1206 | try std.testing.expect(!insideThisSession(sock, "", sock, "0")); | ||
| 1207 | |||
| 1208 | // A host or quic:// target has no unix socket to compare, and a remote | ||
| 1209 | // daemon is a different daemon whatever its sessions are called — the | ||
| 1210 | // call sites pass null for those, so null must never refuse. | ||
| 1211 | try std.testing.expect(!insideThisSession(sock, "0", null, "0")); | ||
| 1212 | } | ||
| 1213 | |||
| 1214 | // Forces semantic analysis of every pub decl under `zig build test`, so an | 1158 | // Forces semantic analysis of every pub decl under `zig build test`, so an |
| 1215 | // unreferenced decl must at least compile (the silent-module-loss hazard, | 1159 | // unreferenced decl must at least compile (the silent-module-loss hazard, |
| 1216 | // decisions.md). Pub decls only: std.meta.declarations sees nothing private. | 1160 | // decisions.md). Pub decls only: std.meta.declarations sees nothing private. |
src/server/server.zig
| Old | New | ||
|---|---|---|---|
| @@ -900,7 +900,7 @@ pub const Server = struct { | |||
| 900 | 900 | ||
| 901 | // After extra_env, not before: this is the daemon's identity, not an | 901 | // After extra_env, not before: this is the daemon's identity, not an |
| 902 | // option, and a client reads it back to refuse attaching to the very | 902 | // option, and a client reads it back to refuse attaching to the very |
| 903 | // session it is running inside (mux_main.insideThisSession). A caller | 903 | // session it is running inside (`wallview.showsSelf`). A caller |
| 904 | // that could overwrite it could hand a shell a lie about where it is. | 904 | // that could overwrite it could hand a shell a lie about where it is. |
| 905 | // One plan serves every session, so only the socket belongs here — | 905 | // One plan serves every session, so only the socket belongs here — |
| 906 | // the session name differs per session and createSession appends it. | 906 | // the session name differs per session and createSession appends it. |
src/tui/wall_test_wall.zig
| Old | New | ||
|---|---|---|---|
| @@ -475,7 +475,7 @@ test "labelText: the state word survives truncation at every width" { | |||
| 475 | if (widest.len > buf.len) return error.TwoDigitWidestBarOverranItsBuffer; | 475 | if (widest.len > buf.len) return error.TwoDigitWidestBarOverranItsBuffer; |
| 476 | } | 476 | } |
| 477 | 477 | ||
| 478 | test "showsSelf: the wall never tiles the session the walling shell is standing in" { | 478 | test "showsSelf: neither the wall nor the CLI attach opens the session the shell is standing in" { |
| 479 | const sock = "/run/user/1000/muxd.sock"; | 479 | const sock = "/run/user/1000/muxd.sock"; |
| 480 | const target: client.Target = .{ .sock = sock }; | 480 | const target: client.Target = .{ .sock = sock }; |
| 481 | 481 | ||
src/tui/wallview.zig
| Old | New | ||
|---|---|---|---|
| @@ -809,20 +809,29 @@ pub fn vanishTile(tiles: []Tile, present: []bool, shared: *Shared, i: usize, to: | |||
| 809 | } | 809 | } |
| 810 | 810 | ||
| 811 | /// A stripe of the shell's own session paints into the grid it reads. | 811 | /// A stripe of the shell's own session paints into the grid it reads. |
| 812 | /// Mirrors `mux_main.insideThisSession`. | ||
| 813 | pub fn showsSelf( | 812 | pub fn showsSelf( |
| 814 | target: client.Target, | 813 | target: client.Target, |
| 815 | name: []const u8, | 814 | name: []const u8, |
| 816 | env_sock: ?[]const u8, | 815 | env_sock: ?[]const u8, |
| 817 | env_session: ?[]const u8, | 816 | env_session: ?[]const u8, |
| 818 | ) bool { | 817 | ) bool { |
| 818 | // A unix socket path only: a host or quic:// target is a different | ||
| 819 | // daemon whatever its sessions are called. | ||
| 819 | const sock = switch (target) { | 820 | const sock = switch (target) { |
| 820 | .sock => |p| p, | 821 | .sock => |p| p, |
| 821 | else => return false, | 822 | else => return false, |
| 822 | }; | 823 | }; |
| 823 | const es = env_sock orelse return false; | 824 | const es = env_sock orelse return false; |
| 824 | const en = env_session orelse return false; | 825 | const en = env_session orelse return false; |
| 826 | // Emptied counts as unset: `MUX_SESSION=` is how a shell overrides an | ||
| 827 | // exported variable it cannot unset, and the refusal names unsetting as | ||
| 828 | // the way out — both spellings of that have to work. | ||
| 825 | if (es.len == 0 or en.len == 0) return false; | 829 | if (es.len == 0 or en.len == 0) return false; |
| 830 | // Both halves, so session 0 attaching to session 1 of the same daemon | ||
| 831 | // keeps working. String equality on the path: a symlinked or relatively | ||
| 832 | // spelled `--sock` for the same socket evades this, accepted, because | ||
| 833 | // this guards the mistake people make (typing `mux` in a mux shell) and | ||
| 834 | // is not a security boundary. | ||
| 826 | return std.mem.eql(u8, es, sock) and std.mem.eql(u8, en, proto.resolveName(name)); | 835 | return std.mem.eql(u8, es, sock) and std.mem.eql(u8, en, proto.resolveName(name)); |
| 827 | } | 836 | } |
| 828 | 837 | ||