1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
|
//! A pardes launched inside a pardes hands its file to the outer one.
//!
//! Every top-level instance listens on `<dir>/pardes-<pid>.sock`, where `<dir>`
//! is `$XDG_RUNTIME_DIR` or, when the session has none, `~/.local/state/pardes`
//! created 0700. NOT /tmp: this socket takes a command line and runs it, and a
//! world-writable directory means both that somebody else can plant a listener
//! at a pid we are about to guess and that a file they planted under the sticky
//! bit cannot be unlinked, so bind fails and the feature goes quietly off.
//!
//! An instance that finds an ancestor process running the same executable
//! resolves its positional argument, writes ONE line — `Look /abs/path` — to
//! that ancestor's socket and exits silently; the outer pardes runs the line
//! through executeBuiltinLine and opens a pane for it. The wire format is a
//! builtin command line because that is a language pardes already speaks: no
//! serialization, nothing to version. The receive side still filters it down
//! to `Look `, because executeBuiltinLine dispatches ANY builtin and this
//! socket sits at a path anyone can derive from a pid — `Exec …` arriving here
//! is not something this protocol is allowed to say.
//!
//! Linux only. ponytail: darwin has no /proc, no SOCK_CLOEXEC and no accept4,
//! and its `sockaddr.un.path` is 104 bytes rather than the 108 every buffer
//! and unguarded memcpy below assumes. None of that is testable from here, so
//! detection is simply off: a pardes inside a pardes on macOS opens a second
//! session the way it always did.
const std = @import("std");
const builtin = @import("builtin");
const libc = std.c;
const linux = std.os.linux; // statx; referenced only on linux
// std.c has getenv but neither setter; the tests below need both
extern "c" fn setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: c_int) c_int;
extern "c" fn unsetenv(name: [*:0]const u8) c_int;
/// The longest command line this protocol carries or accepts. `Look ` plus a
/// PATH_MAX path fits with room over; anything longer cannot have come from
/// the client and is dropped rather than truncated into a different command.
pub const max_line = 4200;
/// Where the sockets live. `$XDG_RUNTIME_DIR` first — a per-user 0700 tmpfs
/// the login session already cleans up — else `~/.local/state/pardes`, which
/// is per-user for the same reason a home directory is. Asked by the client
/// (to derive the path), by the listener (to create and vet it) and by the
/// sweeper (to scan it), so it is written once.
fn socketDir(buf: *[108:0]u8) ?[:0]const u8 {
if (libc.getenv("XDG_RUNTIME_DIR")) |x|
return std.fmt.bufPrintSentinel(buf, "{s}", .{std.mem.span(x)}, 0) catch null;
const home = libc.getenv("HOME") orelse return null;
return std.fmt.bufPrintSentinel(buf, "{s}/.local/state/pardes", .{std.mem.span(home)}, 0) catch null;
}
/// `<dir>/pardes-<pid>.sock`. `<pid>` is the LISTENING instance's own pid, so
/// two pardes never collide and a nested child derives the exact path from the
/// ancestor pid its tree walk found. The buffer is sun_path-sized: a longer
/// path is not a socket address at all.
pub fn socketPath(buf: *[108]u8, pid: libc.pid_t) ?[:0]const u8 {
var dir_buf: [108:0]u8 = undefined;
const dir = socketDir(&dir_buf) orelse return null;
// unsigned: {d} prints a leading '+' for a positive SIGNED int
return std.fmt.bufPrintSentinel(buf, "{s}/pardes-{d}.sock", .{ dir, @as(u32, @intCast(pid)) }, 0) catch null;
}
/// A `/proc/<pid>/exe` readlink with the kernel's `" (deleted)"` suffix taken
/// off. `zig build` replaces the binary under a running pardes — that is the
/// daily loop in this repo — and from that moment the OUTER instance's exe
/// link reads `/path/to/pardes (deleted)` while the freshly built child's
/// reads `/path/to/pardes`. Comparing them raw made every nested launch after
/// a rebuild open a second full-screen UI inside the pane.
fn stripDeleted(link: []const u8) []const u8 {
const suffix = " (deleted)";
return if (std.mem.endsWith(u8, link, suffix)) link[0 .. link.len - suffix.len] else link;
}
/// The `PPid:` field of a /proc/<pid>/status blob. Deliberately NOT field 4 of
/// /proc/<pid>/stat: that field is positional after `comm`, and a comm may
/// contain spaces and parentheses — a process named `sh (a b)` shifts every
/// field after it and the parse silently reads the wrong number.
fn parsePPid(status: []const u8) ?libc.pid_t {
var lines = std.mem.splitScalar(u8, status, '\n');
while (lines.next()) |line| {
if (!std.mem.startsWith(u8, line, "PPid:")) continue;
return std.fmt.parseInt(libc.pid_t, std.mem.trim(u8, line["PPid:".len..], " \t\r"), 10) catch null;
}
return null;
}
/// The pid in a `pardes-<pid>.sock` filename, for the startup sweep. Strictly
/// digits: parseInt alone would take `pardes-+7.sock` and `pardes--7.sock`,
/// and the sweep unlinks what this answers about.
fn sweepPid(name: []const u8) ?libc.pid_t {
if (!std.mem.startsWith(u8, name, "pardes-") or !std.mem.endsWith(u8, name, ".sock")) return null;
const digits = name["pardes-".len .. name.len - ".sock".len];
if (digits.len == 0) return null;
for (digits) |ch| if (!std.ascii.isDigit(ch)) return null;
return std.fmt.parseInt(libc.pid_t, digits, 10) catch null;
}
/// The pid of the nearest ancestor running THIS executable, or null. Identity
/// is `readlink("/proc/<pid>/exe")` against our own, not a name: a name match
/// would call every `vim pardes.zig` an outer pardes. The hop cap is not for
/// /proc, which cannot loop, but because the walk is driven by numbers read
/// out of files and should not be able to spin on a surprising one.
pub fn outer() ?libc.pid_t {
if (comptime builtin.os.tag != .linux) return null;
var self_buf: [4096]u8 = undefined;
const self_n = libc.readlink("/proc/self/exe", &self_buf, self_buf.len);
if (self_n <= 0) return null;
const self_exe = stripDeleted(self_buf[0..@intCast(self_n)]);
var pid = libc.getppid();
var hops: usize = 0;
while (pid > 1 and hops < 64) : (hops += 1) {
var name: [64:0]u8 = undefined;
var buf: [4096]u8 = undefined;
const exe = std.fmt.bufPrintSentinel(&name, "/proc/{d}/exe", .{@as(u32, @intCast(pid))}, 0) catch return null;
const n = libc.readlink(exe, &buf, buf.len);
if (n > 0 and std.mem.eql(u8, stripDeleted(buf[0..@intCast(n)]), self_exe)) return pid;
const status = std.fmt.bufPrintSentinel(&name, "/proc/{d}/status", .{@as(u32, @intCast(pid))}, 0) catch return null;
const fd = libc.open(status, .{ .ACCMODE = .RDONLY });
if (fd < 0) return null;
const got = libc.read(fd, &buf, buf.len);
_ = libc.close(fd);
if (got <= 0) return null;
pid = parsePPid(buf[0..@intCast(got)]) orelse return null;
}
return null;
}
/// Hand `Look <path>[:<line>]` to the pardes listening as `pid` and say
/// whether it landed. False for every failure — no socket file, nobody
/// accepting, a path that does not fit — because an outer instance that
/// cannot be reached (an older build, a stale path) must never cost the
/// caller its own launch. Writes and returns: the answer is a pane appearing
/// on someone else's screen, and there is nothing to wait for.
pub fn sendLook(pid: libc.pid_t, path: []const u8, line: usize) bool {
if (comptime builtin.os.tag != .linux) return false;
// The protocol is one line, so a path with a line break IN it says
// something else entirely: `we\nird.txt` arrived as `Look .../we` and the
// outer instance opened a different file that happened to exist. \r goes
// too — the receive side trims a trailing one. Unsendable, not escaped:
// the caller falls through and opens the file in its own session.
if (std.mem.indexOfAny(u8, path, "\r\n") != null) return false;
var cmd_buf: [max_line]u8 = undefined;
const cmd = (if (line > 0)
std.fmt.bufPrint(&cmd_buf, "Look {s}:{d}\n", .{ path, line })
else
std.fmt.bufPrint(&cmd_buf, "Look {s}\n", .{path})) catch return false;
var path_buf: [108]u8 = undefined;
const sock = socketPath(&path_buf, pid) orelse return false;
var addr: libc.sockaddr.un = .{ .path = @splat(0) };
@memcpy(addr.path[0 .. sock.len + 1], sock[0 .. sock.len + 1]);
const fd = libc.socket(libc.AF.UNIX, libc.SOCK.STREAM | libc.SOCK.CLOEXEC, 0);
if (fd < 0) return false;
defer _ = libc.close(fd);
if (libc.connect(fd, @ptrCast(&addr), @sizeOf(@TypeOf(addr))) != 0) return false;
var off: usize = 0;
while (off < cmd.len) {
const n = libc.write(fd, cmd.ptr + off, cmd.len - off);
if (n < 0) {
if (libc.errno(n) == .INTR) continue;
return false;
}
if (n == 0) return false;
off += @intCast(n);
}
return true;
}
/// Create the socket directory if it is missing and refuse it unless it is a
/// directory WE own with nothing granted to group or other. A planted path is
/// the whole attack on a socket that runs commands, and $XDG_RUNTIME_DIR
/// passes this untouched (the login session already makes it 0700).
fn ensureSocketDir(dir: [:0]const u8) bool {
// mkdir -p, because the HOME branch is three levels deep and a machine
// without ~/.local/state would otherwise switch the feature off in
// silence. Under $XDG_RUNTIME_DIR every prefix already exists and simply
// EEXISTs, which is the ordinary case for the leaf too.
var partial: [108:0]u8 = undefined;
@memcpy(partial[0 .. dir.len + 1], dir[0 .. dir.len + 1]);
for (1..dir.len) |i| {
if (dir[i] != '/') continue;
partial[i] = 0;
_ = libc.mkdir(partial[0..i :0], 0o700);
partial[i] = '/';
}
_ = libc.mkdir(dir, 0o700);
var stx: linux.Statx = undefined;
const want: linux.STATX = .{ .TYPE = true, .MODE = true, .UID = true };
// NOFOLLOW: a symlink where the directory should be is exactly the plant
if (libc.statx(linux.AT.FDCWD, dir, linux.AT.SYMLINK_NOFOLLOW, want, &stx) != 0) return false;
if (!linux.S.ISDIR(stx.mode)) return false;
if (stx.uid != libc.getuid()) return false;
return stx.mode & 0o077 == 0;
}
/// Unlink the socket files of pardes processes that are gone. A pardes killed
/// rather than quit runs no defer, so its file outlives it; harmless by
/// construction (bind unlinks first, a client's connect is refused) but it is
/// our own litter and the snapshot suite alone leaves ~90 behind per run.
/// Bounded: one readdir of a directory only we write to, one kill(0) each.
fn sweep(dir: [:0]const u8) void {
const d = libc.opendir(dir) orelse return;
defer _ = libc.closedir(d);
const me = libc.getpid();
while (libc.readdir(d)) |ent| {
const pid = sweepPid(std.mem.sliceTo(&ent.name, 0)) orelse continue;
if (pid == me) continue;
// 0 = alive; EPERM = alive and someone else's. Only ESRCH is a corpse.
const rc = libc.kill(pid, @enumFromInt(0));
if (rc == 0 or libc.errno(rc) != .SRCH) continue;
var pbuf: [108]u8 = undefined;
_ = libc.unlink(socketPath(&pbuf, pid) orelse continue);
}
}
/// Bind and listen so nested instances can find us; -1 if anything fails, and
/// a pardes without a socket is simply one whose children open their own UI.
/// The path is always this process's own, so nobody outside holds a buffer of
/// it — the shells each kept one and passed it back to be unlinked, which is a
/// way for the two spellings to go out of step and for no other reason.
///
/// CLOEXEC matters more here than on any other fd in the program: pane shells
/// are forked with forkpty and inherit everything open, and an orphaned bash
/// holding this one would keep the socket bound long after we exit — the same
/// shape as the inherited lock fd that once held a flock forever.
pub fn listen() c_int {
if (comptime builtin.os.tag != .linux) return -1;
var dir_buf: [108:0]u8 = undefined;
const dir = socketDir(&dir_buf) orelse return -1;
if (!ensureSocketDir(dir)) return -1;
sweep(dir);
var path_buf: [108]u8 = undefined;
const path = socketPath(&path_buf, libc.getpid()) orelse return -1;
var addr: libc.sockaddr.un = .{ .path = @splat(0) };
if (path.len + 1 > addr.path.len) return -1;
@memcpy(addr.path[0 .. path.len + 1], path[0 .. path.len + 1]);
const fd = libc.socket(libc.AF.UNIX, libc.SOCK.STREAM | libc.SOCK.CLOEXEC, 0);
if (fd < 0) return -1;
_ = libc.unlink(path); // pid reuse: a dead pardes' file would EADDRINUSE forever
if (libc.bind(fd, @ptrCast(&addr), @sizeOf(@TypeOf(addr))) != 0) {
_ = libc.close(fd);
return -1;
}
// Owner-only, and BEFORE listen(2), which is the moment anyone could
// connect: the directory is already private, this is the second wall.
_ = libc.chmod(path, 0o600);
if (libc.listen(fd, 8) != 0) {
_ = libc.close(fd);
return -1;
}
return fd;
}
/// Close the listener and take its file away. Guarded on the fd rather than on
/// the path, so a bind that FAILED cannot unlink a path this process never
/// created; anything else is a no-op, which is what --nested and every
/// non-linux build hand it.
pub fn unlisten(fd: c_int) void {
if (fd < 0) return;
_ = libc.close(fd);
var path_buf: [108]u8 = undefined;
if (socketPath(&path_buf, libc.getpid())) |path| _ = libc.unlink(path);
}
/// Block until a nested instance sends a `Look` line, and return it inside
/// `buf`. Null only when the listening fd itself is gone — teardown closed it,
/// or it was never a socket — because anything else (EMFILE, ECONNABORTED)
/// would otherwise kill the listener thread for the life of the process while
/// the socket stayed bound, and every later launch would exit 0 having done
/// nothing. Every accepted connection is CLOEXEC for the reason the listener
/// is.
pub fn acceptLine(fd: c_int, buf: []u8) ?[]const u8 {
if (comptime builtin.os.tag != .linux) return null;
while (true) {
const conn = libc.accept4(fd, null, null, libc.SOCK.CLOEXEC);
if (conn < 0) {
switch (libc.errno(conn)) {
.INTR => continue,
// the fd went away or never was one: nothing will ever arrive
.BADF, .INVAL, .NOTSOCK => return null,
// transient. Sleep first: EMFILE persists until some other fd
// is freed, and a bare `continue` would spin a core on it.
else => {
var ts: libc.timespec = .{ .sec = 0, .nsec = 100 * std.time.ns_per_ms };
_ = libc.nanosleep(&ts, null);
continue;
},
}
}
defer _ = libc.close(conn);
// A peer that connects and says nothing must not hold the listener:
// this is a serial accept loop, and one silent connection used to
// block every later launch until it let go. The client writes its one
// short line immediately, so a second is already generous.
const tv: libc.timeval = .{ .sec = 1, .usec = 0 };
_ = libc.setsockopt(conn, libc.SOL.SOCKET, libc.SO.RCVTIMEO, &tv, @sizeOf(libc.timeval));
var len: usize = 0;
while (len < buf.len) {
const n = libc.read(conn, buf.ptr + len, buf.len - len);
if (n < 0 and libc.errno(n) == .INTR) continue;
if (n <= 0) break; // EOF, or the receive timeout expired
len += @intCast(n);
if (std.mem.indexOfScalar(u8, buf[0..len], '\n') != null) break;
}
const end = std.mem.indexOfScalar(u8, buf[0..len], '\n') orelse len;
// a full buffer with no newline is an overlong line: drop it whole
// rather than run its truncation as some other command
if (end == buf.len) continue;
const line = std.mem.trimEnd(u8, buf[0..end], "\r");
// one verb (see the file header): this socket may open things, and
// that is all it may do
if (!std.mem.startsWith(u8, line, "Look ")) continue;
return line;
}
}
test "socket path: XDG first, then a private dir under HOME, never /tmp" {
var buf: [108]u8 = undefined;
// The environment is process-wide and every test in this binary shares it.
// The last case below reaches the "no directory at all" branch by blanking
// both variables, and without this every later test ran without a HOME.
var xdg_buf: [4096:0]u8 = undefined;
var home_buf: [4096:0]u8 = undefined;
const xdg0 = if (libc.getenv("XDG_RUNTIME_DIR")) |v| std.fmt.bufPrintSentinel(&xdg_buf, "{s}", .{std.mem.span(v)}, 0) catch null else null;
const home0 = if (libc.getenv("HOME")) |v| std.fmt.bufPrintSentinel(&home_buf, "{s}", .{std.mem.span(v)}, 0) catch null else null;
defer {
if (xdg0) |v| {
_ = setenv("XDG_RUNTIME_DIR", v, 1);
} else _ = unsetenv("XDG_RUNTIME_DIR");
if (home0) |v| {
_ = setenv("HOME", v, 1);
} else _ = unsetenv("HOME");
}
_ = setenv("XDG_RUNTIME_DIR", "/run/user/1000", 1);
try std.testing.expectEqualStrings("/run/user/1000/pardes-4242.sock", socketPath(&buf, 4242).?);
_ = unsetenv("XDG_RUNTIME_DIR");
_ = setenv("HOME", "/home/who", 1);
try std.testing.expectEqualStrings("/home/who/.local/state/pardes/pardes-4242.sock", socketPath(&buf, 4242).?);
// sun_path is 108 bytes including the NUL, so a directory that long has no
// socket address at all — say so instead of binding a truncated one
_ = setenv("XDG_RUNTIME_DIR", "/" ++ ("x" ** 100), 1);
try std.testing.expect(socketPath(&buf, 4242) == null);
_ = unsetenv("XDG_RUNTIME_DIR");
_ = unsetenv("HOME");
try std.testing.expect(socketPath(&buf, 4242) == null);
}
test "a rebuilt binary still matches its own running instance" {
// `zig build` under a live pardes: the outer's exe link gains the suffix,
// the new process's does not, and before this the two stopped comparing
// equal — every nested launch opened a second UI.
try std.testing.expectEqualStrings("/usr/bin/pardes", stripDeleted("/usr/bin/pardes (deleted)"));
try std.testing.expectEqualStrings("/usr/bin/pardes", stripDeleted("/usr/bin/pardes"));
try std.testing.expectEqualStrings("", stripDeleted(" (deleted)"));
// only a SUFFIX, and only the whole one
try std.testing.expectEqualStrings("/x (deleted) y", stripDeleted("/x (deleted) y"));
try std.testing.expectEqualStrings("/x (delete)", stripDeleted("/x (delete)"));
}
test "the sweep only recognises its own socket names" {
try std.testing.expectEqual(@as(libc.pid_t, 7), sweepPid("pardes-7.sock").?);
try std.testing.expectEqual(@as(libc.pid_t, 4194304), sweepPid("pardes-4194304.sock").?);
try std.testing.expect(sweepPid("pardes-.sock") == null);
try std.testing.expect(sweepPid("pardes-7.sockx") == null);
try std.testing.expect(sweepPid("pardes-7") == null);
try std.testing.expect(sweepPid("bus") == null);
try std.testing.expect(sweepPid("pardes-osc133.bash") == null);
// parseInt alone would take these, and the sweep UNLINKS what it answers
try std.testing.expect(sweepPid("pardes-+7.sock") == null);
try std.testing.expect(sweepPid("pardes--7.sock") == null);
try std.testing.expect(sweepPid("pardes- 7.sock") == null);
}
test "PPid comes off the status field, not a comm-shifted stat line" {
// the comm here contains a space AND parentheses — the exact shape that
// breaks `field 4 of /proc/<pid>/stat`
const status = "Name:\tsh (a b)\nUmask:\t0022\nState:\tS (sleeping)\n" ++
"Tgid:\t1234\nNgid:\t0\nPid:\t1234\nPPid:\t991\nTracerPid:\t0\n";
try std.testing.expectEqual(@as(libc.pid_t, 991), parsePPid(status).?);
try std.testing.expectEqual(@as(libc.pid_t, 0), parsePPid("PPid:\t0\n").?);
try std.testing.expect(parsePPid("Name:\tinit\nTracerPid:\t0\n") == null);
try std.testing.expect(parsePPid("PPid:\tnotanumber\n") == null);
// a truncated read must not answer from a half line
try std.testing.expect(parsePPid("Name:\tsh\nPPi") == null);
}
|