Commit 050a330

Kai Fronsdal  ·  2026-06-10 19:47:51 -0400 EDT
parent fbbc6be
fix(kill): release socket before grace sleep so name is reusable

`zmx kill X; zmx run X` raced: the daemon's shutdown defer ran
handleKill() first and only then closed the listen socket and unlinked the
socket file.

Two changes:

- Reorder the shutdown defer so the listen socket is closed and the
  socket file unlinked before handleKill()'s 500ms grace period.
  
- Make `zmx kill` synchronous: after sending .Kill, drain-read the
  connection until EOF. The daemon closes client fds after unlinking
  the socket file, so when `kill` returns the name is guaranteed
  free. Avoids a client-side deleteFile, which would race with a
  fresh session created in between.

Adds test/kill_run_race.bats which fails on main and passes here.
2 files changed,  +54, -4
+19, -4
 1@@ -873,15 +873,19 @@ const Daemon = struct {
 2                 };
 3 
 4                 defer {
 5-                    self.handleKill();
 6-                    self.deinit();
 7-                    posix.close(pty_fd);
 8-                    _ = posix.waitpid(self.pid, 0);
 9+                    // Close and unlink the listen socket BEFORE handleKill()'s
10+                    // 500ms SIGHUP->SIGKILL grace sleep. Otherwise a `zmx run`
11+                    // for the same name issued in that window will hang waiting
12+                    // for a connect.
13                     posix.close(server_sock_fd);
14                     std.log.info("deleting socket file session={s}", .{self.session_name});
15                     dir.deleteFile(self.session_name) catch |err| {
16                         std.log.warn("failed to delete socket file err={s}", .{@errorName(err)});
17                     };
18+                    self.handleKill();
19+                    self.deinit();
20+                    posix.close(pty_fd);
21+                    _ = posix.waitpid(self.pid, 0);
22                 }
23 
24                 try daemonLoop(self, server_sock_fd, pty_fd);
25@@ -1817,6 +1821,17 @@ fn kill(cfg: *Cfg, session_name: []const u8, force: bool) !void {
26         else => return err,
27     };
28 
29+    // Block until the daemon hangs up. The daemon's shutdown defer closes
30+    // and unlinks the listen socket before it closes client connections,
31+    // so by the time we read EOF here the session name is free for reuse
32+    // and a subsequent `zmx run <name>` can't land in the dying daemon's
33+    // accept backlog.
34+    var drain: [256]u8 = undefined;
35+    while (true) {
36+        const n = posix.read(fd, &drain) catch break;
37+        if (n == 0) break;
38+    }
39+
40     var buf: [100]u8 = undefined;
41     var w = std.fs.File.stdout().writer(&buf);
42     try w.interface.print("killed session {s}\n", .{session_name});
+35, -0
 1@@ -0,0 +1,35 @@
 2+#!/usr/bin/env bats
 3+# Regression test for the `zmx kill X; zmx run X` race.
 4+#
 5+# Previously `zmx kill` returned immediately after sending the IPC .Kill,
 6+# while the daemon's shutdown defer ran handleKill() -- SIGHUP, 500ms sleep,
 7+# SIGKILL -- BEFORE closing/unlinking the listen socket. A `zmx run X`
 8+# issued in that window would connect() into the kernel backlog of a
 9+# socket the daemon would never accept() on again, then get RST'd
10+# (ConnectionResetByPeer) when the daemon finally closed the listen fd,
11+# exiting 1 with no output and no session created.
12+
13+load test_helper
14+
15+@test "kill then immediate run with same name succeeds" {
16+  for i in 1 2 3; do
17+    "$ZMX" run race-x -d echo first
18+    wait_for_session race-x
19+
20+    "$ZMX" kill race-x
21+
22+    # Immediately reuse the same session name. Must not land in the
23+    # dying daemon's listen backlog.
24+    run "$ZMX" run race-x -d echo second
25+    echo "iteration $i: status=$status output=$output"
26+    [ "$status" -eq 0 ]
27+    [[ "$output" == *"session \"race-x\" created"* ]]
28+
29+    # New session must be live and serving requests.
30+    wait_for_session race-x
31+    run "$ZMX" history race-x
32+    [ "$status" -eq 0 ]
33+
34+    "$ZMX" kill race-x
35+  done
36+}