summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 13:52:14 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commita33fc226c32d97a053bafdb13d6ec3bf8b537f35 (patch)
tree21e2df58d94bebe3effde8cb447ae7f64e9c5b9a
parent0fb5881520884691c395ef1ce4b9d1d902434bd6 (diff)
downloadpardes-a33fc226c32d97a053bafdb13d6ec3bf8b537f35.tar.gz
pardes-a33fc226c32d97a053bafdb13d6ec3bf8b537f35.zip
winsize refuses fewer than 2 rows, saying so, and its range reads 1x2 to 4096x4096 as the docs say
`winsize 20 1` was taken and quietly raised to 2 rows, so the write succeeded with a size other than the one asked for. The range refusal also said "1x1 to 4096x4096", although fs.md says at least 2 rows. One row is now refused with "invalid winsize: at least 2 rows", and the range text starts at 1x2. size already refused anything under 20x6 with a text that names it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/fs.md2
-rw-r--r--src/ninep/pty.zig27
-rw-r--r--test/fs.py11
3 files changed, 32 insertions, 8 deletions
diff --git a/docs/fs.md b/docs/fs.md
index 28c4fef9..efbcf21a 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -574,7 +574,7 @@ Terminal panes also have `pty/`:
replay).
- `pty/status`: one line, `cols rows busy`; busy is 1 while a command runs or
text is typed at the prompt.
-- `pty/ctl`: `winsize C R` (at least 2 rows), `sig INT|TERM|HUP|QUIT|KILL`,
+- `pty/ctl`: `winsize C R` (at least 2 rows, fewer refused `invalid winsize: at least 2 rows`; at most 4096 a side), `sig INT|TERM|HUP|QUIT|KILL`,
`exec` (restart the shell in its directory: refused on a command pane, `a
command pane does not restart`; `exec: <dir>: no such directory` if it is
gone; a shell that cannot start fails and leaves the old one running).
diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig
index 390ae58d..2245255b 100644
--- a/src/ninep/pty.zig
+++ b/src/ninep/pty.zig
@@ -33,7 +33,17 @@ pub const pty_rows_min = 2;
/// As the root ctl's `size` (4096 a side).
const winsize_max = 4096;
-const e_winsize_range = std.fmt.comptimePrint("invalid winsize: out of range, 1x1 to {d}x{d}", .{ winsize_max, winsize_max });
+const e_winsize_range = std.fmt.comptimePrint("invalid winsize: out of range, 1x{d} to {d}x{d}", .{ pty_rows_min, winsize_max, winsize_max });
+const e_winsize_rows = std.fmt.comptimePrint("invalid winsize: at least {d} rows", .{pty_rows_min});
+
+/// `winsize C R` whose R is a size but fewer rows than a pty takes.
+fn tooFewRows(line: []const u8) bool {
+ var words = std.mem.tokenizeAny(u8, line, " \t");
+ if (!std.mem.eql(u8, words.next() orelse return false, "winsize")) return false;
+ _ = dimension(words.next() orelse return false) orelse return false;
+ const rows = dimension(words.next() orelse return false) orelse return false;
+ return words.next() == null and rows < pty_rows_min;
+}
/// A winsize whose numbers are numbers but not sizes: said as such.
fn outOfRange(line: []const u8) bool {
@@ -79,7 +89,7 @@ pub fn writeCtl(p: *Pardes, req: Req, id: usize) Reply {
if (@import("../host_io.zig").Shell.refusal(bin[0..@min(bin.len, 200)], &why)) |refused|
return tree.failText(req.tag, E.NOENT, std.fmt.bufPrint(&p.fs.ename, "exec: {s}", .{refused}) catch "exec: no such shell");
};
- if (!verb(p, id, line, apply)) return tree.failText(req.tag, E.INVAL, if (outOfRange(line)) e_winsize_range else e_bad_pty_ctl);
+ if (!verb(p, id, line, apply)) return tree.failText(req.tag, E.INVAL, if (tooFewRows(line)) e_winsize_rows else if (outOfRange(line)) e_winsize_range else e_bad_pty_ctl);
}
}
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
@@ -93,8 +103,10 @@ fn verb(p: *Pardes, id: usize, line: []const u8, apply: bool) bool {
.winsize => {
const cols = dimension(words.next() orelse return false) orelse return false;
// Two rows at least, as a pane is laid out: a one-row pty
- // loses its prompt's input mark and reads busy for ever.
- const rows = @max(pty_rows_min, dimension(words.next() orelse return false) orelse return false);
+ // loses its prompt's input mark and reads busy for ever. Fewer
+ // is refused, never quietly raised.
+ const rows = dimension(words.next() orelse return false) orelse return false;
+ if (rows < pty_rows_min) return false;
if (words.next() != null) return false;
if (!apply) return true;
p.emit(.{ .resize_pty = .{ .pane = @intCast(id), .cols = cols, .rows = rows } });
@@ -619,6 +631,13 @@ test "every pty/ctl verb, and every refusal" {
try testing.expect(a.winsize == null);
}
+ // One row is a size, but fewer than a pty takes: refused, and said so.
+ const one = wr(p, ctl, "winsize 80 1");
+ try testing.expectEqual(E.INVAL, one.errno());
+ try testing.expectEqualStrings("invalid winsize: at least 2 rows", one.reply.ename);
+ try testing.expect(one.winsize == null);
+ try testing.expectEqualStrings("invalid winsize: out of range, 1x2 to 4096x4096", e_winsize_range);
+
const spaced = wr(p, ctl, "\n winsize 90 20 \n\n");
try testing.expectEqual(Status.ok, spaced.reply.status);
try testing.expectEqual(@as(u16, 90), spaced.winsize.?.cols);
diff --git a/test/fs.py b/test/fs.py
index 10802e19..a1e2ae79 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -436,9 +436,14 @@ def run_file(binary):
assert run(client, term, b'fi\n') == b'error not run\n' # a syntax error runs nothing
assert run(client, term, b"echo 'open\n") == b'error not run\n' # nor does an open quote
assert run(client, term, b'true\n') == b'exit 0\n' # ...and the shell is clear after
- # A pty asked for one row gets two: one loses the prompt's input
- # mark, and the shell would read busy for ever.
- client.write(f'/pane/{term}/pty/ctl', b'winsize 20 1\n')
+ # A pty is not given one row (it loses the prompt's input mark,
+ # and the shell would read busy for ever): refused, and said.
+ try:
+ client.write(f'/pane/{term}/pty/ctl', b'winsize 20 1\n')
+ raise AssertionError('winsize 20 1 was taken')
+ except OSError as refused:
+ assert 'at least 2 rows' in str(refused), refused
+ client.write(f'/pane/{term}/pty/ctl', b'winsize 20 2\n')
time.sleep(.5)
assert run(client, term, b'true\n') == b'exit 0\n'
assert client.read(f'/pane/{term}/pty/ctl').split()[2] == b'2', client.read(f'/pane/{term}/pty/ctl')