╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │330 const conn = self.conn; │ │331 var rest = bytes; │ │332 while (rest.len > 0) { │ │333 + if (self.closed or self.remote_closed) return error.RemoteClosed;│ │334 while (self.channel.remote_window == 0) { │ │335 if (self.remote_closed) return error.RemoteClosed; │ │336 try self.processOneBackgroundPacket(); │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │412 try parseChannelId(packet, self.channel.local_id); │ │413 self.incoming_eof = true; │ │414 self.remote_closed = true; │ │415 + try sendChannelClose(conn, self.channel); │ │416 }, │ │417 SSH_MSG_GLOBAL_REQUEST => try handleGlobalRequest(conn, packet),│ │418 else => try sendUnimplemented(conn), │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │420 } │ │421 │ │422 fn maybeRefillRecvWindowForBufferedInput(self: *SessionCtx) !void { │ │423 + if (self.remote_closed or self.closed) return; │ │424 // credit only what the incoming buffer can still take │ │425 const buffered: u32 = @intCast(self.incomingBytes().len); │ │426 try maybeRefillRecvWindow(self.conn, self.channel, max_incoming_buffered - buffered);│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │433 const conn = self.conn; │ │434 if (conn.idle) |idle| idle.closing.store(true, .release); │ │435 │ │404 - // once the peer has sent CHANNEL_CLOSE the channel is gone on its │ │405 - // side, and these would be addressed to an id it no longer knows. │ │436 + // a peer CLOSE is acknowledged by the packet pump. don't send data,│ │437 + // exit-status, or EOF after that acknowledgement. │ │438 if (!self.remote_closed) { │ │439 // exit-status request (informational; client uses it as the │ │440 // command's exit code) │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │457 try writeU32(&eof, conn.allocator, self.channel.remote_id); │ │458 try conn.sc_cipher.writePacket(conn.io, conn.writer, eof.items); │ │459 } │ │428 - { │ │429 - var close: std.ArrayList(u8) = .empty; │ │430 - defer close.deinit(conn.allocator); │ │431 - try close.append(conn.allocator, SSH_MSG_CHANNEL_CLOSE); │ │432 - try writeU32(&close, conn.allocator, self.channel.remote_id); │ │433 - try conn.sc_cipher.writePacket(conn.io, conn.writer, close.items); │ │434 - } │ │460 + try sendChannelClose(conn, self.channel); │ │461 } │ │462 + drainConnection(conn); │ │463 + } │ │464 +}; │ │465 │ │437 - // read and discard until the peer closes (TCP FIN) before letting the │ │438 - // caller close the socket. a git client, after our CHANNEL_CLOSE, still │ │439 - // sends its own CHANNEL_CLOSE and SSH_MSG_DISCONNECT before closing; if │ │440 - // we close with those unread, Linux sends RST instead of FIN, which the │ │441 - // client reports as "Connection reset by peer" and treats as a failed │ │442 - // push even though the ref updated. draining to EOF leaves nothing │ │443 - // unread, so we send a clean FIN. we're tearing down, so the packet │ │444 - // contents don't matter — just get them off the socket. │ │445 - var drain_packets: u32 = 0; │ │446 - while (drain_packets < max_exit_drain_packets) : (drain_packets += 1) { │ │447 - const packet = conn.cs_cipher.readPacket(conn.allocator, conn.reader) catch break;│ │448 - conn.allocator.free(packet); │ │449 - } │ │466 +fn drainConnection(conn: *Conn) void { │ │467 + if (conn.idle) |idle| idle.closing.store(true, .release); │ │468 + // read and discard until the peer closes (TCP FIN) before letting the │ │469 + // caller close the socket. a git client, after our CHANNEL_CLOSE, still │ │470 + // sends its own CHANNEL_CLOSE and SSH_MSG_DISCONNECT before closing; if │ │471 + // we close with those unread, Linux sends RST instead of FIN, which the │ │472 + // client reports as "Connection reset by peer" and treats as a failed │ │473 + // push even though the ref updated. draining to EOF leaves nothing │ │474 + // unread, so we send a clean FIN. we're tearing down, so the packet │ │475 + // contents don't matter — just get them off the socket. │ │476 + var drain_packets: u32 = 0; │ │477 + while (drain_packets < max_exit_drain_packets) : (drain_packets += 1) { │ │478 + const packet = conn.cs_cipher.readPacket(conn.allocator, conn.reader) catch break; │ │479 + conn.allocator.free(packet); │ │480 } │ │451 -}; │ │481 +} │ │482 │ │483 /// std.Io.Writer adapter that ships bytes to a SessionCtx as CHANNEL_DATA. │ │484 /// useful for plugging the session into anything that wants a *std.Io.Writer │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │792 // skipping ignorable messages and servicing client-initiated rekeys. │ │793 pub fn readSessionPacket(conn: *Conn) ![]u8 { │ │794 while (true) { │ │765 - const packet = try conn.cs_cipher.readPacket(conn.allocator, conn.reader);│ │795 + const packet = try conn.readPacket(); │ │796 if (packet.len >= 1 and isIgnorableMsg(packet[0])) { │ │797 conn.allocator.free(packet); │ │798 continue; │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯