╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1413 var nonce: [8]u8 = undefined; │ │1414 std.mem.writeInt(u64, &nonce, seq, .big); │ │1415 return nonce; │ │1416 + } │ │1417 + │ │1418 + fn bodyLength(self: *const Cipher, enc_length: *const [4]u8) !u32 { │ │1419 + var plain: [4]u8 = undefined; │ │1420 + ChaCha20.xor(&plain, enc_length, 0, self.header_key, makeNonce(self.seq)); │ │1421 + const len = std.mem.readInt(u32, &plain, .big); │ │1422 + if (len < 8 or len > max_packet_len or len % 8 != 0) return error.InvalidPacketLength;│ │1423 + return len; │ │1424 } │ │1425 │ │1426 + fn peekPacket(self: *const Cipher, reader: *std.Io.Reader) !void { │ │1427 + const len = 4 + (try self.bodyLength(try reader.peekArray(4))) + Poly1305.mac_length; │ │1428 + if (len > reader.buffer.len) return error.ReadBufferTooSmall; │ │1429 + _ = try reader.peek(len); │ │1430 + } │ │1431 + │ │1432 pub fn writePacket( │ │1433 self: *Cipher, │ │1434 io: std.Io, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1489 var enc_length: [4]u8 = undefined; │ │1490 try reader.readSliceAll(&enc_length); │ │1491 │ │1448 - var length_plain: [4]u8 = undefined; │ │1449 - ChaCha20.xor(&length_plain, &enc_length, 0, self.header_key, nonce); │ │1450 - const body_len = std.mem.readInt(u32, &length_plain, .big); │ │1451 - if (body_len < 8 or body_len > max_packet_len or body_len % 8 != 0) return error.InvalidPacketLength;│ │1492 + const body_len = try self.bodyLength(&enc_length); │ │1493 │ │1494 // the body allocation becomes the returned payload, so a packet only │ │1495 // ever costs one allocation │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │1747 pub const PtySize = struct { width_cells: u16, height_cells: u16 }; │ │1748 │ │1749 // take the uint32 width/height cell counts of a pty-req or window-change, │ │1709 -// clamped to u16 │ │1750 +// bounded before either initial rendering or a resize can allocate grids. │ │1751 +// keep zero dimensions intact (unspecified at startup, minimized on resize).│ │1752 fn takePtySize(r: *std.Io.Reader) !PtySize { │ │1711 - const width = try r.takeInt(u32, .big); │ │1712 - const height = try r.takeInt(u32, .big); │ │1753 + const width = @min(try r.takeInt(u32, .big), max_pty_width); │ │1754 + const height = @min(try r.takeInt(u32, .big), max_pty_height); │ │1755 return .{ │ │1714 - .width_cells = @intCast(@min(width, 0xFFFF)), │ │1715 - .height_cells = @intCast(@min(height, 0xFFFF)), │ │1756 + .width_cells = @intCast(width), │ │1757 + .height_cells = @intCast(height), │ │1758 }; │ │1759 } │ │1760 │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │1824 }, │ │1825 │ │1826 SSH_MSG_CHANNEL_CLOSE => { │ │1785 - if (channel) |*c| try parseChannelId(packet, c.local_id);│ │1827 + if (channel) |*c| { │ │1828 + try parseChannelId(packet, c.local_id); │ │1829 + try sendChannelClose(conn, c); │ │1830 + drainConnection(conn); │ │1831 + } │ │1832 return; │ │1833 }, │ │1834 │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────╮ │1989 try conn.sc_cipher.writePacket(conn.io, conn.writer, reply.items);│ │1990 } │ │1991 │ │1992 +fn sendChannelClose(conn: *Conn, ch: *Channel) !void { │ │1993 + var packet: [5]u8 = undefined; │ │1994 + packet[0] = SSH_MSG_CHANNEL_CLOSE; │ │1995 + std.mem.writeInt(u32, packet[1..5], ch.remote_id, .big); │ │1996 + try conn.sc_cipher.writePacket(conn.io, conn.writer, &packet); │ │1997 +} │ │1998 + │ │1999 fn handleWindowAdjust(ch: *Channel, packet: []const u8) !void { │ │2000 // byte SSH_MSG_CHANNEL_WINDOW_ADJUST │ │2001 // uint32 recipient_channel │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2065 var bytes: [4]u8 = undefined; │ │2066 std.mem.writeInt(u32, &bytes, value, .big); │ │2067 try buf.appendSlice(allocator, &bytes); │ │2068 +} │ │2069 + │ │2070 +test "pty dimensions have bounded axes and area, preserving normal and zero sizes" { │ │2071 + for ([_]struct { width: u32, height: u32, expected: PtySize }{ │ │2072 + .{ .width = 80, .height = 24, .expected = .{ .width_cells = 80, .height_cells = 24 } }, │ │2073 + .{ .width = 0, .height = 0, .expected = .{ .width_cells = 0, .height_cells = 0 } }, │ │2074 + .{ .width = 0xffffffff, .height = 0xffffffff, .expected = .{ .width_cells = 512, .height_cells = 128 } },│ │2075 + }) |case| { │ │2076 + var bytes: [8]u8 = undefined; │ │2077 + std.mem.writeInt(u32, bytes[0..4], case.width, .big); │ │2078 + std.mem.writeInt(u32, bytes[4..8], case.height, .big); │ │2079 + var reader = std.Io.Reader.fixed(&bytes); │ │2080 + try std.testing.expectEqual(case.expected, try takePtySize(&reader)); │ │2081 + } │ │2082 +} │ │2083 + │ │2084 +test "channel close before shell or exec is acknowledged" { │ │2085 + const io = std.testing.io; │ │2086 + const allocator = std.testing.allocator; │ │2087 + const key = [_]u8{0x5a} ** 64; │ │2088 + var encoded: [256]u8 = undefined; │ │2089 + var encoded_writer = std.Io.Writer.fixed(&encoded); │ │2090 + var sender = Cipher.init(&key, 0); │ │2091 + var open: std.ArrayList(u8) = .empty; │ │2092 + defer open.deinit(allocator); │ │2093 + try open.append(allocator, SSH_MSG_CHANNEL_OPEN); │ │2094 + try writeStringField(&open, allocator, "session"); │ │2095 + try writeU32(&open, allocator, 7); │ │2096 + try writeU32(&open, allocator, 32768); │ │2097 + try writeU32(&open, allocator, 32768); │ │2098 + try sender.writePacket(io, &encoded_writer, open.items); │ │2099 + try sender.writePacket(io, &encoded_writer, &.{ SSH_MSG_CHANNEL_CLOSE, 0, 0, 0, 0 }); │ │2100 + var reader = std.Io.Reader.fixed(encoded_writer.buffered()); │ │2101 + var output: [256]u8 = undefined; │ │2102 + var writer = std.Io.Writer.fixed(&output); │ │2103 + var conn = Conn{ │ │2104 + .io = io, │ │2105 + .allocator = allocator, │ │2106 + .reader = &reader, │ │2107 + .writer = &writer, │ │2108 + .cs_cipher = Cipher.init(&key, 0), │ │2109 + .sc_cipher = Cipher.init(&key, 0), │ │2110 + .rekey = undefined, │ │2111 + }; │ │2112 + const Handler = struct { │ │2113 + pub fn handleSession(_: *@This(), _: *SessionCtx, _: Request) !void { │ │2114 + return error.UnexpectedSession; │ │2115 + } │ │2116 + }; │ │2117 + var handler = Handler{}; │ │2118 + try runChannelLayer(&conn, &([_]u8{0} ** fingerprint_len), &handler); │ │2119 + var output_reader = std.Io.Reader.fixed(writer.buffered()); │ │2120 + var receiver = Cipher.init(&key, 0); │ │2121 + const confirmation = try receiver.readPacket(allocator, &output_reader); │ │2122 + defer allocator.free(confirmation); │ │2123 + try std.testing.expectEqual(SSH_MSG_CHANNEL_OPEN_CONFIRMATION, confirmation[0]); │ │2124 + const close = try receiver.readPacket(allocator, &output_reader); │ │2125 + defer allocator.free(close); │ │2126 + try std.testing.expectEqualSlices(u8, &.{ SSH_MSG_CHANNEL_CLOSE, 0, 0, 0, 7 }, close); │ │2127 + try std.testing.expectEqual(0, output_reader.buffered().len); │ │2128 } │ │2129 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯