summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 03:10:29 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commitcf49c329cb401a5c530ef2533066702fe1545360 (patch)
treecde7ce577a16ba09ddc9453886433144091d403d /src
parentcf2415cc6527cb92898694aae88fd84d54aac28c (diff)
downloadpardes-cf49c329cb401a5c530ef2533066702fe1545360.tar.gz
pardes-cf49c329cb401a5c530ef2533066702fe1545360.zip
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 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/fs-help.txt2
-rw-r--r--src/ninep/cols.zig1
-rw-r--r--src/ninep/ctl.zig26
3 files changed, 28 insertions, 1 deletions
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);
+}