From a550753fb60be3cdc040c521ddaab9022f5d3248 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 09:11:37 -0300 Subject: Every EINVAL a write gets says why; DEL in an exec or look line is a control character An empty name, a word written to log other than follow, a data, sel, dot or dirty write to a pane with no text, a truncate other than to zero, and a control character in a look line written through a held open were refused with no words, logged as `Invalid argument`. Each says its reason now; and DEL is refused in exec and look lines as in names. Co-Authored-By: Claude Opus 5.5 --- src/ninep/ctl.zig | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) (limited to 'src/ninep/ctl.zig') diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 2f94dd3c..ccba342e 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -179,7 +179,7 @@ pub fn command(p: *Pardes, req: Req, serial: ?u32, exec: bool, in_tag: bool) Rep while (it.next()) |text| { if (text.len == 0) continue; // Only an Edit block holds newlines (Messages). - for (text) |c| if (c < ' ' and c != '\t' and c != '\n') return tree.failText(req.tag, E.INVAL, e_control); + for (text) |c| if ((c < ' ' and c != '\t' and c != '\n') or c == 0x7f) return tree.failText(req.tag, E.INVAL, e_control); if (exec) if (tooLong(req, text)) |refusal| return refusal; if (!apply) continue; const id = if (serial) |s| p.paneBySerial(s) orelse break else p.active; @@ -214,7 +214,7 @@ pub fn tooLong(req: Req, line: []const u8) ?Reply { return tree.failText(req.tag, E.INVAL, e_too_long); } -pub const e_control = "invalid command line: it holds a control character other than a tab"; +pub const e_control = "invalid command line: it holds a control character (or DEL) other than a tab"; /// Runs one click (`run`) as a 9P write's: a builtin that fails there fails /// the write, with its words and an err record, and no msg, as a ctl line @@ -1968,6 +1968,28 @@ test "pty/ctl exec in a directory that is gone fails ENOENT; a shell that cannot try testing.expectEqualStrings("shell: access denied", p.fs.late_failure[0..p.fs.late_failure_len]); } +test "every EINVAL a write gets says why, in its err record too; DEL is a control character in a line" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + const serial = serialOf(p); + const log = @intFromEnum(tree.TopFile.log); + const h = call(p, .{ .tag = 1, .op = .open, .node = log, .omode = 2 }).reply.handle; + const Case = struct { node: u64, data: []const u8, words: []const u8, handle: u32 = 0 }; + for ([_]Case{ + .{ .node = root_exec, .data = "echo a\x7fb\n", .words = "control character" }, + .{ .node = root_look, .data = "a\x7fb\n", .words = "control character" }, + .{ .node = Node.of(serial, .name), .data = "\n", .words = "an empty name" }, + .{ .node = log, .data = "bogus\n", .words = "takes `follow`", .handle = h }, + }) |c| { + const refused = call(p, .{ .tag = 2, .op = .write, .node = c.node, .handle = c.handle, .data = c.data }); + try testing.expectEqual(E.INVAL, refused.errno()); + try testing.expect(std.mem.indexOf(u8, refused.reply.ename, c.words) != null); + } + _ = call(p, .{ .tag = 3, .op = .release, .node = log, .handle = h }); + try testing.expect(!th.logHas(p, "Invalid argument")); + try testing.expect(th.logHas(p, "an empty name")); +} + test "size is monotonic: growing is never refused, and a size once taken is taken again" { const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 60 }); defer p.deinit(); -- cgit v1.3