diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 09:46:55 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:16 -0300 |
| commit | 7a18df8aab1638622ab93fd3203930399c680544 (patch) | |
| tree | ea8616aa0a67a09c0f31dd67f507a88e2fc4f87f | |
| parent | 157df8cb547ba4cf900b1f9eb75df15384985708 (diff) | |
| download | pardes-7a18df8aab1638622ab93fd3203930399c680544.tar.gz pardes-7a18df8aab1638622ab93fd3203930399c680544.zip | |
A tag write refuses NUL and the other control characters; a Dump that fails says so
A NUL written into a tag went into the dump, which the host then could
not write, and Dump said nothing: a success that wrote nothing. Tag
writes, the workspace's, a column's and a pane's, refuse control
characters but tab and newline, DEL, C1 and bytes not UTF-8, EINVAL; and
Dump reports any failure of its own instead of swallowing it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | docs/fs.md | 4 | ||||
| -rw-r--r-- | src/builtins.zig | 3 | ||||
| -rw-r--r-- | src/ninep/cols.zig | 1 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 11 | ||||
| -rw-r--r-- | src/ninep/pane.zig | 13 |
5 files changed, 30 insertions, 2 deletions
@@ -130,7 +130,9 @@ Existing Plan9port/v9fs clients need a userspace bridge for QUIC. <serial>: acme's activecol, which the keyboard leaving for another column's tag does not move, so the two can differ -- the active column, where pane/new and a look place a pane next (- when there is none) -/tag the workspace tag; > replaces it, >> appends, one line +/tag the workspace tag; > replaces it, >> appends, one line; a control character + but a tab (a NUL too), DEL, a C1 control or bytes not UTF-8 are refused, `invalid + tag text: ...` (EINVAL), in any tag, a pane's or a column's too /tagexec write a word: a middle click on it in the workspace tag; read as /exec. A pane's word (Undo, Msg, Save, Edit too) is refused there and at a column's exec, `not a session control message "Undo": write it to pane/<n>/ctl` (EINVAL), diff --git a/src/builtins.zig b/src/builtins.zig index b97f68f8..7fdf2176 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -595,7 +595,8 @@ pub const Kill = struct { pub const Dump = struct { pub const scope: Scope = .session; pub fn run(c: Ctx) void { - dump.dumpState(c.p) catch {}; + // Never a Dump said done that wrote nothing: its failure is said. + dump.dumpState(c.p) catch |err| c.p.reportError(c.id, "Dump", err); } }; diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig index 3eeadfdc..aae6d364 100644 --- a/src/ninep/cols.zig +++ b/src/ninep/cols.zig @@ -78,6 +78,7 @@ pub fn read(p: *Pardes, req: Req, serial: ?u32) Reply { pub fn write(p: *Pardes, req: Req, serial: ?u32) Reply { const t = (header(p, serial) orelse return Reply.fail(req.tag, E.NOENT)).text; if (req.data.len == 0) return .{ .tag = req.tag, .written = 0 }; + if (pardes.ctlfs.pane.tagFault(req.data)) |why| return tree.failText(req.tag, E.INVAL, why); const was = headerText(p, serial).?; var data = req.data; const rewriting = p.fs.header_rewrite == rewriteKey(serial); diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index d9a479fe..2958fc38 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -2026,6 +2026,17 @@ test "a served-tree pane spoils no Grep, and Look . from a gone directory is ENO try testing.expectStringStartsWith(looked.reply.ename, "look: /tmp/pardes-no-such-dir-zz: no such directory"); } +test "a tag write refuses NUL and the other control characters" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + for ([_]u64{ @intFromEnum(tree.TopFile.tag), tree.Node.ofCol(pardes.layout.columnSerial(p, 0), .tag), Node.of(serialOf(p), .tag) }) |node| { + const refused = wr(p, node, " a\x00b"); + try testing.expectEqual(E.INVAL, refused.errno()); + try testing.expectStringStartsWith(refused.reply.ename, "invalid tag text"); + try testing.expectEqual(E.INVAL, wr(p, node, " \x7f").errno()); + } +} + 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(); diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index d29c8b49..1148041a 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -458,8 +458,21 @@ fn writeBody(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { return .{ .tag = req.tag, .written = @intCast(take) }; } +/// Why `data` is no text a tag takes: a control character but a tab or a +/// newline, DEL, a C1 control, or bytes that are not UTF-8 (a NUL in a tag +/// made a Dump that wrote nothing). +pub fn tagFault(data: []const u8) ?[]const u8 { + for (data) |c| if ((c < ' ' and c != '\t' and c != '\n') or c == 0x7f) return "invalid tag text: a control character"; + if (!std.unicode.utf8ValidateSlice(data)) return "invalid tag text: not UTF-8"; + var i: usize = 0; + while (std.mem.indexOfScalarPos(u8, data, i, 0xC2)) |at| : (i = at + 1) + if (at + 1 < data.len and data[at + 1] <= 0x9F) return "invalid tag text: a control character"; + return null; +} + fn writeTag(req: Req, pane: *Pane) Reply { if (req.data.len == 0) return .{ .tag = req.tag, .written = 0 }; + if (tagFault(req.data)) |why| return tree.failText(req.tag, E.INVAL, why); // The tag's own text grows by what is written, newlines and all: a tag // is a text like any other (acme's tag file appends the same way). const pf = &pane.fs; |
