summaryrefslogtreecommitdiff
path: root/src/host_io.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 11:23:19 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commit1ac8fbeedaf7cba881a7423b92ead1f739b8ac21 (patch)
treec3916f9918fb98252987331195cd1b70712c890d /src/host_io.zig
parent2c98513a23d060093d690dbd7743dda5ad86c117 (diff)
downloadpardes-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.zig49
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,