diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 11:23:19 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:16 -0300 |
| commit | 1ac8fbeedaf7cba881a7423b92ead1f739b8ac21 (patch) | |
| tree | c3916f9918fb98252987331195cd1b70712c890d /src/host_io.zig | |
| parent | 2c98513a23d060093d690dbd7743dda5ad86c117 (diff) | |
| download | pardes-1ac8fbeedaf7cba881a7423b92ead1f739b8ac21.tar.gz pardes-1ac8fbeedaf7cba881a7423b92ead1f739b8ac21.zip | |
A script whose interpreter is not there is told apart from a missing shell, and Tty refuses it up front
Tty's up-front check found the script and let it through, so the pane was made, its exec failed ENOENT, and the log read new, msg shell: shell not found, del, then the err. The check now reads a script's #! line and refuses it, interpreter /no/such/interp not found, and an exec that fails ENOENT on a file that is there says the same.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/host_io.zig')
| -rw-r--r-- | src/host_io.zig | 49 |
1 files changed, 47 insertions, 2 deletions
diff --git a/src/host_io.zig b/src/host_io.zig index 913930ac..85ba94f4 100644 --- a/src/host_io.zig +++ b/src/host_io.zig @@ -861,7 +861,13 @@ pub const Shell = struct { /// null when it is one. pub fn refusal(bin: []const u8, said: []u8) ?[]const u8 { var buf: [std.fs.max_path_bytes]u8 = undefined; - if (find(bin, &buf) != null) return null; + if (find(bin, &buf)) |found| { + // A script whose `#!` names a program that is not there: its + // exec fails ENOENT as a missing shell's does, so told apart. + var interp: [256]u8 = undefined; + const missing = missingInterpreter(found, &interp) orelse return null; + return std.fmt.bufPrint(said, "interpreter {s} not found", .{missing}) catch "interpreter not found"; + } if (std.mem.indexOfScalar(u8, bin, '/') != null and bin.len < buf.len) { @memcpy(buf[0..bin.len], bin); buf[bin.len] = 0; @@ -871,6 +877,43 @@ pub const Shell = struct { return std.fmt.bufPrint(said, "no shell \"{s}\", not found (a name on the usual paths, or a path to one)", .{bin}) catch "no such shell"; } + /// The program a script's `#!` line names, when it is not there to + /// run; null for a script whose interpreter is there, or no script. + pub fn missingInterpreter(path: [*:0]const u8, out: []u8) ?[]const u8 { + pardes.turn.yield(); + defer pardes.turn.back(); + const fd = libc.open(path, .{ .ACCMODE = .RDONLY }, @as(libc.mode_t, 0)); + if (fd < 0) return null; + defer _ = libc.close(fd); + var head: [256]u8 = undefined; + const n = libc.read(fd, &head, head.len); + if (n < 2) return null; + const got = head[0..@intCast(n)]; + if (!std.mem.startsWith(u8, got, "#!")) return null; + const line = got[2 .. std.mem.indexOfScalar(u8, got, '\n') orelse got.len]; + var words = std.mem.tokenizeAny(u8, line, " \t\r"); + const interp = words.next() orelse return null; + if (interp.len + 1 > out.len) return null; + @memcpy(out[0..interp.len], interp); + out[interp.len] = 0; + if (libc.access(@ptrCast(out.ptr), X_OK) == 0) return null; + return out[0..interp.len]; + } + + test "a script whose #! names a program not there is told apart from a missing shell" { + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "bad", .data = "#!/no/such/interp -x\n", .flags = .{ .permissions = .executable_file } }); + try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "good", .data = "#!/bin/sh\n", .flags = .{ .permissions = .executable_file } }); + var dir_buf: [4096]u8 = undefined; + const dir = dir_buf[0..try tmp.dir.realPath(std.testing.io, &dir_buf)]; + var path: [4200]u8 = undefined; + var said: [320]u8 = undefined; + try std.testing.expectEqualStrings("interpreter /no/such/interp not found", refusal(try std.fmt.bufPrint(&path, "{s}/bad", .{dir}), &said).?); + try std.testing.expect(refusal(try std.fmt.bufPrint(&path, "{s}/good", .{dir}), &said) == null); + try std.testing.expect(std.mem.startsWith(u8, refusal(try std.fmt.bufPrint(&path, "{s}/none", .{dir}), &said).?, "no shell ")); + } + fn fallback(buf: *[std.fs.max_path_bytes]u8) [*:0]const u8 { for (fallbacks) |f| { @memcpy(buf[0..f.len], f); @@ -1203,7 +1246,9 @@ pub fn forkShell( const e: libc.E = @enumFromInt(why[1]); if (why[0] == 'c') return if (e == .NOENT or e == .NOTDIR) error.FileNotFound else error.AccessDenied; return switch (e) { - .NOENT => error.ShellNotFound, // itself, or its script's interpreter + // Itself, or its script's interpreter: told apart by whether it + // is there. + .NOENT => if (libc.access(spawn.path, 0) == 0) error.InterpreterNotFound else error.ShellNotFound, .ACCES, .PERM => error.ShellNotExecutable, .NOEXEC => error.ShellNotExecutable, else => error.ShellDidNotStart, |
