From 77c535a4f5acb99e55ad8312063f65cc85a3bcd9 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 16:23:11 -0300 Subject: pty/run takes one line a run, its answer read before the next Two lines -- in one write, or as bash writes them, a line at a time on one open -- each ran, and the first's answer was lost under the second's. A write with more than one line, or a next line before the last answer is read, is now refused, EINVAL. Co-Authored-By: Claude Opus 5.5 --- src/ninep/pty.zig | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'src') diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig index 41748bdc..8ba9f4df 100644 --- a/src/ninep/pty.zig +++ b/src/ninep/pty.zig @@ -208,6 +208,12 @@ fn answer(p: *Pardes, slot: *Run, comptime fmt: []const u8, args: anytype) void pub fn writeRun(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { const slot = &(tree.openOf(p, req) orelse return Reply.fail(req.tag, E.INVAL)).what.run; if (slot.phase == .sent or slot.phase == .waiting) return tree.failText(req.tag, E.BUSY, tree.e_in_use); + // One line a run, its answer read before the next: a second line (bash + // writes printf 'a\nb\n' a line at a time) would run and lose the + // first's answer. + const one = std.mem.trimEnd(u8, req.data, "\r\n"); + if (std.mem.indexOfScalar(u8, one, '\n') != null or (slot.phase == .done and slot.read == 0)) + return tree.failText(req.tag, E.INVAL, e_one_line); // A new line on this open: a new answer. p.gpa.free(slot.output); p.gpa.free(slot.line); @@ -425,6 +431,7 @@ fn waitingRun(p: *Pardes, pane: *Pane) ?*Run { } const e_bad_line = "bad command line"; +const e_one_line = "invalid run: one line a run, its answer read before the next"; const e_bad_pty_ctl = tree.e_bad_ctl ++ "; takes winsize COLS ROWS, sig INT|TERM|HUP|QUIT|KILL, or exec"; pub fn readData(p: *Pardes, req: Req, pf: *pane_files.State) Reply { @@ -1102,3 +1109,20 @@ test "exec on a command pane is refused: a command pane does not restart" { try testing.expectEqual(tree.E.INVAL, r.errno()); try testing.expectEqualStrings("invalid exec: a command pane does not restart", r.reply.ename); } + +test "pty/run takes one line a run: two in one write, or a second before the first's answer is read, are refused" { + const p = try th.withTerm(testing.allocator); + defer p.deinit(); + const serial = th.serialOf(p); + const run = tree.Node.of(serial, .pty_run); + const h = th.call(p, .{ .tag = 1, .op = .open, .node = run, .omode = 2 }).reply.handle; + const two = th.call(p, .{ .tag = 2, .op = .write, .node = run, .handle = h, .data = "true\nfalse\n" }); + try testing.expectEqual(tree.E.INVAL, two.errno()); + try testing.expectEqualStrings(e_one_line, two.reply.ename); + // A finished run whose answer nobody read takes no next line. + const slot = &tree.openOf(p, .{ .tag = 0, .op = .read, .node = run, .handle = h }).?.what.run; + slot.phase = .done; + slot.len = 7; + try testing.expectEqual(tree.E.INVAL, th.call(p, .{ .tag = 3, .op = .write, .node = run, .handle = h, .data = "true\n" }).errno()); + _ = th.call(p, .{ .tag = 4, .op = .release, .node = run, .handle = h, .opened = true }); +} -- cgit v1.3