From cf49c329cb401a5c530ef2533066702fe1545360 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 03:10:29 -0300 Subject: An exec line over 1024 bytes fails the write, naming the limit A command line longer than the 1024 bytes a command pane takes was said on the message row after the write had already succeeded. An exec write (the root's, a pane's, tagexec, a column's) now refuses it before anything runs, EINVAL with "a command line is at most 1024 bytes" and an err record; a builtin's line, a long Msg or an Edit block, is not limited. Co-Authored-By: Claude Opus 5.5 --- src/fs-help.txt | 2 +- src/ninep/cols.zig | 1 + src/ninep/ctl.zig | 26 ++++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/fs-help.txt b/src/fs-help.txt index c3578af9..ca4d3d77 100644 --- a/src/fs-help.txt +++ b/src/fs-help.txt @@ -39,7 +39,7 @@ Pitfalls, one each: data: > replaces the addr range, : > deletes it, a 2nd > inserts (addr moved); body: > empties all. tag reads the path, then its own text; > replaces that text (default words too), >> appends. A terminal's body is a history snapshot frozen per open, wrapped rows joined into lines; pty/data is the live stream. - exec: a non-builtin line runs as a command pane (ctl's Shell -c), a shell's typed in; a line runs once whole. + exec: a non-builtin line (at most 1024 bytes) runs as a command pane (Shell -c), a shell's typed in; once whole. Through a mount a malformed write is EINVAL, a failed one EIO or an errno that fits (ENOENT): log's err says why. lock/unlock need a held ctl fd (exec 3>ctl); a held lock fails: retry. fs.md has the rest. Repl python on a terminal's ctl: a .py body's clicks go to it (Repl - unbinds, Repl says it); tags stay commands. diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig index 8a3474b3..ad7d30f8 100644 --- a/src/ninep/cols.zig +++ b/src/ninep/cols.zig @@ -158,6 +158,7 @@ pub fn writeExec(p: *Pardes, req: Req, serial: ?u32) Reply { const line = std.mem.trim(u8, raw, " \t\r"); if (line.len == 0) continue; for (line) |c| if (c < ' ' and c != '\t') return Reply.fail(req.tag, E.INVAL); + if (pardes.ctlfs.ctl.tooLong(req, line)) |refusal| return refusal; const col = if (serial) |s| layout.columnBySerial(p, s) orelse return Reply.fail(req.tag, E.NOENT) else null; if (p.panes[p.active] == null) return Reply.fail(req.tag, E.NOENT); p.exec_column = col; diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index d1bb5907..25abd9b5 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -158,6 +158,7 @@ pub fn command(p: *Pardes, req: Req, serial: ?u32, exec: bool) Reply { if (text.len == 0) continue; // Only an Edit block holds newlines (Messages). for (text) |c| if (c < ' ' and c != '\t' and c != '\n') return Reply.fail(req.tag, E.INVAL); + 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; if (p.panes[id] == null) return Reply.fail(req.tag, E.NOENT); @@ -174,6 +175,20 @@ pub fn command(p: *Pardes, req: Req, serial: ?u32, exec: bool) Reply { return .{ .tag = req.tag, .written = @intCast(req.data.len) }; } +pub const e_too_long = std.fmt.comptimePrint("a command line is at most {d} bytes", .{pardes.exec.command_max}); + +/// A line an exec would run as a command, over the most a command line may +/// be: refused whole, before anything runs, rather than said on the message +/// row after the write succeeded. A builtin's line (Msg, an Edit block) may +/// be longer. +pub fn tooLong(req: Req, line: []const u8) ?Reply { + if (line.len <= pardes.exec.command_max) return null; + const cmd = exec_line.commandText(line); + const word = cmd[0 .. std.mem.indexOfAny(u8, cmd, " \t+") orelse cmd.len]; + if (std.meta.stringToEnum(Builtin, word) != null) return null; + return tree.failText(req.tag, E.INVAL, e_too_long); +} + pub fn resultsLen(p: *Pardes) u64 { var n: u64 = 0; for (p.fs.results[0..p.fs.results_len]) |serial| { @@ -1421,3 +1436,14 @@ test "a builtin that fails a ctl write logs only its err, so the same failure ag try testing.expect(std.mem.indexOf(u8, text, "msg - Kill") == null); _ = call(p, .{ .tag = 3, .op = .release, .node = log, .handle = h }); } + +test "an exec line over the command limit fails the write and says the limit; a builtin's may be longer" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + const long = "echo " ++ "y" ** pardes.exec.command_max ++ "\n"; + const refused = wr(p, root_exec, long); + try testing.expectEqual(E.INVAL, refused.errno()); + try testing.expectEqualStrings(e_too_long, refused.reply.ename); + try testing.expect(th.logHas(p, "a command line is at most 1024 bytes")); + try testing.expectEqual(Status.ok, wr(p, root_exec, "Msg " ++ "z" ** 1100 ++ "\n").reply.status); +} -- cgit v1.3