summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-10-01 01:08:26 -0300
committerGabriel Schneider <[email protected]>2026-10-01 01:35:12 -0300
commit006fdc1d758fc2f9b956051e75f31a0122bd7883 (patch)
tree62e880140353b45c3ed6a7ba467bc1e941e1abe0
parentf453e3c3014ec578bbe6601a141134ce034185ff (diff)
downloadpardes-006fdc1d758fc2f9b956051e75f31a0122bd7883.tar.gz
pardes-006fdc1d758fc2f9b956051e75f31a0122bd7883.zip
With Pager pardes, a terminal's shell also gets SYSTEMD_PAGER and SYSTEMD_PAGERSECURE=0, and the pager's path is quoted only when it needs it
journalctl and systemctl read SYSTEMD_PAGER, not PAGER, and in their secure mode run only less, so a pty/run of either still waited in it; and systemd, splitting a pager on blanks and exec'ing it, never ran the quoted path. Where the user's environment sets neither, a terminal now gets SYSTEMD_PAGER as PAGER is set and secure mode off (this pager has no shell to escape to), and the path is single-quoted only when it holds a blank or shell character. Command panes keep cat. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--src/host_io.zig48
-rw-r--r--test/fs.py9
2 files changed, 50 insertions, 7 deletions
diff --git a/src/host_io.zig b/src/host_io.zig
index b138a840..f944e1cb 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -1287,7 +1287,7 @@ pub const ChildEnv = struct {
if (std.mem.eql(u8, name(entry.?), name(default))) break true;
} else false;
if (set) continue;
- if (n + own.len + 2 > slots.len) return null;
+ if (n + own.len + 4 > slots.len) return null;
slots[n] = default;
n += 1;
}
@@ -1305,23 +1305,51 @@ pub const ChildEnv = struct {
/// another; plain `pardes -` where the path cannot be read or quoted.
fn pardesPagers() []const [*:0]const u8 {
const S = struct {
- var pager: [std.fs.max_path_bytes + 16:0]u8 = undefined;
- var git_pager: [std.fs.max_path_bytes + 16:0]u8 = undefined;
- var entries: [2][*:0]const u8 = undefined;
+ var pager: [std.fs.max_path_bytes + 32:0]u8 = undefined;
+ var git_pager: [std.fs.max_path_bytes + 32:0]u8 = undefined;
+ var systemd_pager: [std.fs.max_path_bytes + 32:0]u8 = undefined;
+ var entries: [4][*:0]const u8 = undefined;
var ready = false;
};
if (!S.ready) {
var exe_buf: [std.fs.max_path_bytes]u8 = undefined;
+ var quoted_buf: [std.fs.max_path_bytes + 2]u8 = undefined;
const n = if (comptime builtin.os.tag == .linux) libc.readlink("/proc/self/exe", &exe_buf, exe_buf.len) else -1;
- const exe: []const u8 = if (n > 0 and @as(usize, @intCast(n)) < exe_buf.len and std.mem.indexOfScalar(u8, exe_buf[0..@intCast(n)], '\'') == null) exe_buf[0..@intCast(n)] else "pardes";
- S.entries[0] = if (std.fmt.bufPrintSentinel(&S.pager, "PAGER='{s}' -", .{exe}, 0)) |t| t.ptr else |_| "PAGER=pardes -";
- S.entries[1] = if (std.fmt.bufPrintSentinel(&S.git_pager, "GIT_PAGER='{s}' -", .{exe}, 0)) |t| t.ptr else |_| "GIT_PAGER=pardes -";
+ const exe: []const u8 = if (n > 0 and @as(usize, @intCast(n)) < exe_buf.len) exe_buf[0..@intCast(n)] else "pardes";
+ const command = pagerCommand(exe, &quoted_buf);
+ S.entries[0] = if (std.fmt.bufPrintSentinel(&S.pager, "PAGER={s} -", .{command}, 0)) |t| t.ptr else |_| "PAGER=pardes -";
+ S.entries[1] = if (std.fmt.bufPrintSentinel(&S.git_pager, "GIT_PAGER={s} -", .{command}, 0)) |t| t.ptr else |_| "GIT_PAGER=pardes -";
+ // systemd's tools (journalctl, systemctl) read their own, and
+ // in their secure mode run only less: this pager has no shell
+ // to escape to, so secure mode buys nothing.
+ S.entries[2] = if (std.fmt.bufPrintSentinel(&S.systemd_pager, "SYSTEMD_PAGER={s} -", .{command}, 0)) |t| t.ptr else |_| "SYSTEMD_PAGER=pardes -";
+ S.entries[3] = "SYSTEMD_PAGERSECURE=0";
S.ready = true;
}
return &S.entries;
}
+
+ /// The pager's path as a command line takes it: as it is when it needs
+ /// no quoting (systemd splits on blanks and execs, never through sh),
+ /// in single quotes when it holds a blank or a shell character, plain
+ /// `pardes` when even that cannot hold it.
+ fn pagerCommand(exe: []const u8, buf: []u8) []const u8 {
+ const plain = for (exe) |c| {
+ if (!(std.ascii.isAlphanumeric(c) or std.mem.indexOfScalar(u8, "/._+-,@%=:", c) != null)) break false;
+ } else true;
+ if (plain) return exe;
+ if (std.mem.indexOfScalar(u8, exe, '\'') != null) return "pardes";
+ return std.fmt.bufPrint(buf, "'{s}'", .{exe}) catch "pardes";
+ }
};
+test "the pager's path is quoted only when it needs it" {
+ var buf: [64]u8 = undefined;
+ try std.testing.expectEqualStrings("/usr/bin/pardes", ChildEnv.pagerCommand("/usr/bin/pardes", &buf));
+ try std.testing.expectEqualStrings("'/opt/my apps/pardes'", ChildEnv.pagerCommand("/opt/my apps/pardes", &buf));
+ try std.testing.expectEqualStrings("pardes", ChildEnv.pagerCommand("/odd'dir/pardes", &buf));
+}
+
test "the child environment replaces the launcher's terminal identity exactly once" {
const saved: ?[]const u8 = if (libc.getenv("TERM")) |t| std.mem.span(t) else null;
var saved_buf: [256]u8 = undefined;
@@ -1351,15 +1379,21 @@ test "the child environment replaces the launcher's terminal identity exactly on
var shell_slots: ChildEnv.Slots = undefined;
const shell_envp = ChildEnv.build(&shell_slots, paging) orelse return error.EnvironmentTooLarge;
var said: usize = 0;
+ var systemd: usize = 0;
var j: usize = 0;
while (shell_envp[j]) |entry| : (j += 1) {
const line = std.mem.span(entry);
+ if (std.c.getenv("SYSTEMD_PAGER") == null and std.mem.startsWith(u8, line, "SYSTEMD_PAGER=")) systemd += 1;
+ if (std.c.getenv("SYSTEMD_PAGERSECURE") == null and std.mem.eql(u8, line, "SYSTEMD_PAGERSECURE=0")) systemd += 1;
if (std.mem.startsWith(u8, line, "PAGER=") and std.c.getenv("PAGER") == null) {
try std.testing.expect(paging == .terminal and std.mem.endsWith(u8, line, " -"));
said += 1;
}
}
if (std.c.getenv("PAGER") == null) try std.testing.expectEqual(@as(usize, if (paging == .terminal) 1 else 0), said);
+ // systemd's pager and its secure mode off, with ours only.
+ if (std.c.getenv("SYSTEMD_PAGER") == null and std.c.getenv("SYSTEMD_PAGERSECURE") == null)
+ try std.testing.expectEqual(@as(usize, if (paging == .terminal) 2 else 0), systemd);
}
const envp = ChildEnv.build(&slots, .command) orelse return error.EnvironmentTooLarge;
var terms: usize = 0;
diff --git a/test/fs.py b/test/fs.py
index 282cca8b..fce315fc 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -495,6 +495,15 @@ def run_file(binary):
assert b'paged-commit-subject' in client.read(f'/pane/{pager[0]}/body')
assert b'\x1b' not in client.read(f'/pane/{pager[0]}/body')
client.remove(f'/pane/{pager[0]}')
+ # systemd's tools page through their own variable: set too, its
+ # secure mode off, the path unquoted where it needs none.
+ systemd_pager = run(client, term, b'printenv SYSTEMD_PAGER SYSTEMD_PAGERSECURE\n')
+ assert systemd_pager.startswith(b'exit 0\n') and systemd_pager.rstrip().endswith(b' -\n0'), systemd_pager
+ if shutil.which('journalctl'):
+ assert run(client, term, b'journalctl -n 3 --no-hostname\n').startswith(b'exit ')
+ for row in client.read('/index').splitlines():
+ if row.split()[3:4] == [f'{root}/+Pager'.encode()]:
+ client.remove(f'/pane/{row.split()[0].decode()}')
# A pty is not given one row (it loses the prompt's input mark,
# and the shell would read busy for ever): refused, and said.
try: