diff options
| -rw-r--r-- | src/host_io.zig | 48 | ||||
| -rw-r--r-- | test/fs.py | 9 |
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, "ed_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; @@ -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: |
