summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-10-01 08:35:03 -0300
committerGabriel Schneider <[email protected]>2026-10-01 08:53:11 -0300
commit642bcfd9e37c1a3437c2757434c1d6d9c941d9c4 (patch)
treedad1fac568ffa9583dab5bd41c287cbb3fb17ab2 /src
parentc4bc85b8da17ae2a191d0c73424e17fde8b6391e (diff)
downloadpardes-642bcfd9e37c1a3437c2757434c1d6d9c941d9c4.tar.gz
pardes-642bcfd9e37c1a3437c2757434c1d6d9c941d9c4.zip
A ctl, exec or look line over 1 MiB is refused once, EINVAL in words a mount maps, and the rest of it is dropped through its newline, not run as a second line
Past the 1 MiB a line may hold, the write that crossed it was refused and the pending text dropped, but what followed of the same line was taken as a new line and ran when its newline came: a second refusal, `unknown control message "zzz…"`, and a second err. The open now drops the rest through the newline, and the refusal says `invalid write: a line or Edit block over 1 MiB`. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/ninep/ctl.zig24
-rw-r--r--src/ninep/tree.zig17
2 files changed, 39 insertions, 2 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 2b366de0..89c2e5dd 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -4333,3 +4333,27 @@ test "a look of ./x that is not there is a miss, as x and /abs/x are: answered,
}
try testing.expect(th.logHas(p, "look: ./zzq-nosuch.txt: no such file"));
}
+
+test "a ctl line over 1 MiB is refused once, EINVAL, and its tail is not run as a line of its own" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const node = Node.of(serialOf(p), .ctl);
+ const h = call(p, .{ .tag = 1, .op = .open, .node = node, .omode = 1 }).reply.handle;
+ const chunk: [4096]u8 = @splat('z');
+ var refused: usize = 0;
+ var off: u64 = 0;
+ for (0..270) |_| {
+ const r = call(p, .{ .tag = 2, .op = .write, .node = node, .handle = h, .off = off, .data = &chunk });
+ if (r.reply.status == .err) {
+ refused += 1;
+ try testing.expectEqual(E.INVAL, r.errno());
+ }
+ off += chunk.len;
+ }
+ try testing.expectEqual(Status.ok, call(p, .{ .tag = 3, .op = .write, .node = node, .handle = h, .off = off, .data = "zzz\nEdit ,d\n" }).reply.status);
+ try testing.expectEqual(@as(usize, 1), refused);
+ try testing.expect(!th.logHas(p, "unknown control message"));
+ // What came after the long line's newline ran.
+ try testing.expectEqualStrings("", pane_files.fileOf(p.panes[0].?).?.content);
+ _ = call(p, .{ .tag = 4, .op = .release, .node = node, .handle = h });
+}
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index e566482c..95e46ab3 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -165,6 +165,9 @@ pub const Open = struct {
/// A `name` open has named its pane: one name an open, so a second line
/// on it (bash writes `printf 'a\nb\n'` a line at a time) is refused.
named: bool = false,
+ /// A line over 1 MiB was refused: what follows of it, through its
+ /// newline, is dropped rather than run as a line of its own.
+ discarding: bool = false,
pub const Replay = struct { off: u64, bytes: []u8 };
@@ -1176,7 +1179,16 @@ fn write(p: *Pardes, req: Req, target: Target) Reply {
if (resultsFile(target) or ctlFile(target)) for (req.data) |c| if ((c < ' ' and c != '\t' and c != '\n' and c != '\r') or c == 0x7f) return failText(req.tag, E.INVAL, ctl.e_control);
const o = (if (linesFile(target)) openOf(p, req) else null) orelse return writeNow(p, req, target);
if (o.what != .lines and o.what != .ctl) return writeNow(p, req, target);
- o.pending.appendSlice(p.gpa, req.data) catch return Reply.fail(req.tag, E.NOMEM);
+ // The rest of a line refused for its length goes through its newline
+ // unread: its one refusal said it, and its tail is no command of its own.
+ var data = req.data;
+ if (o.discarding) {
+ const nl = std.mem.indexOfScalar(u8, data, '\n') orelse return .{ .tag = req.tag, .written = @intCast(req.data.len) };
+ o.discarding = false;
+ data = data[nl + 1 ..];
+ if (data.len == 0) return .{ .tag = req.tag, .written = @intCast(req.data.len) };
+ }
+ o.pending.appendSlice(p.gpa, data) catch return Reply.fail(req.tag, E.NOMEM);
var end = ctl.completeEnd(p, o.pending.items);
// A tail with no newline is a whole line when the write is the whole of
// what its client wrote (shorter than a Twrite can carry): it runs now,
@@ -1197,7 +1209,8 @@ fn write(p: *Pardes, req: Req, target: Target) Reply {
if (end == 0) {
if (o.pending.items.len <= pending_cap) return .{ .tag = req.tag, .written = @intCast(req.data.len) };
o.pending.clearRetainingCapacity();
- return failText(req.tag, E.INVAL, "a line or Edit block over 1 MiB");
+ o.discarding = true;
+ return failText(req.tag, E.INVAL, "invalid write: a line or Edit block over 1 MiB");
}
if (target == .pane and target.pane.file == .name) {
if (o.named or std.mem.count(u8, o.pending.items[0..end], "\n") > 1) {