╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │187 fingerprint: [fingerprint_len]u8, │ │188 closed: bool = false, │ │189 │ │190 - // shared state for the blocking I/O mode (SessionReader + writeBytes │ │191 - // window-stall pump). incoming CHANNEL_DATA bytes accumulate here │ │192 - // until the consumer drains them via SessionReader. │ │190 + // shared state for the packet pump. incoming CHANNEL_DATA bytes │ │191 + // accumulate in incoming_buffer (already-consumed prefix tracked by │ │192 + // incoming_start) until drained via SessionReader or nextEvent. │ │193 incoming_buffer: std.ArrayList(u8) = .empty, │ │194 + incoming_start: usize = 0, │ │195 incoming_eof: bool = false, │ │196 // resize seen by the background pump, returned by the next nextEvent call│ │197 pending_resize: ?PtySize = null, │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────╮ │198 + // reused per-chunk message buffer for writeChannel│ │199 + write_scratch: std.ArrayList(u8) = .empty, │ │200 │ │201 pub fn deinit(self: *SessionCtx) void { │ │202 self.incoming_buffer.deinit(self.allocator); │ ╰────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │203 + self.write_scratch.deinit(self.allocator); │ │204 + } │ │205 + │ │206 + fn incomingBytes(self: *const SessionCtx) []const u8 { │ │207 + return self.incoming_buffer.items[self.incoming_start..]; │ │208 + } │ │209 + │ │210 + // mark n incoming bytes as consumed; the buffer is recycled once empty │ │211 + fn consumeIncoming(self: *SessionCtx, n: usize) void { │ │212 + self.incoming_start += n; │ │213 + if (self.incoming_start == self.incoming_buffer.items.len) { │ │214 + self.incoming_buffer.clearRetainingCapacity(); │ │215 + self.incoming_start = 0; │ │216 + } │ │217 + } │ │218 + │ │219 + // append to the incoming buffer, first compacting away the consumed │ │220 + // prefix so the buffer only ever holds unread bytes │ │221 + fn appendIncoming(self: *SessionCtx, payload: []const u8) !void { │ │222 + if (self.incoming_start > 0) { │ │223 + const remaining = self.incoming_buffer.items.len - self.incoming_start; │ │224 + std.mem.copyForwards(u8, self.incoming_buffer.items[0..remaining], self.incoming_buffer.items[self.incoming_start..]);│ │225 + self.incoming_buffer.shrinkRetainingCapacity(remaining); │ │226 + self.incoming_start = 0; │ │227 + } │ │228 + if (payload.len > max_incoming_buffered - self.incoming_buffer.items.len) { │ │229 + return error.IncomingBufferExceeded; │ │230 + } │ │231 + try self.incoming_buffer.appendSlice(self.allocator, payload); │ │232 } │ │233 │ │234 /// interactive sessions idle legitimately; tell the host's idle │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │239 │ │240 /// pump SSH packets until something interesting (data / resize / close) │ │241 /// arrives. background traffic (window adjusts, env requests, etc.) is │ │210 - /// handled silently. used by pumped-event consumers (e.g. the TUI path); │ │211 - /// do not mix with SessionReader, which has its own packet pump. │ │242 + /// handled silently. drains the same incoming state as SessionReader, │ │243 + /// so a session should consume through one or the other, not both. │ │244 pub fn nextEvent(self: *SessionCtx) !Event { │ │213 - if (self.pending_resize) |sz| { │ │214 - self.pending_resize = null; │ │215 - return .{ .resize = sz }; │ │216 - } │ │245 while (true) { │ │218 - const packet = readSessionPacket(self.io, self.allocator, self.reader, self.writer, self.cs_cipher, self.sc_cipher, self.rekey) catch |err| switch (err) {│ │219 - error.EndOfStream => { │ │220 - self.closed = true; │ │221 - return .close; │ │222 - }, │ │223 - else => return err, │ │224 - }; │ │225 - defer self.allocator.free(packet); │ │226 - if (packet.len < 1) return error.EmptyPacket; │ │227 - │ │228 - switch (packet[0]) { │ │229 - SSH_MSG_CHANNEL_DATA => { │ │230 - const payload = try parseChannelData(packet, false, self.channel.local_id); │ │231 - const data_len: u32 = @intCast(payload.len); │ │232 - if (data_len > self.channel.local_window) return error.WindowExceeded; │ │233 - self.channel.local_window -= data_len; │ │234 - try maybeRefillRecvWindow(self.io, self.allocator, self.sc_cipher, self.writer, self.channel); │ │235 - // hand the payload bytes to the caller (caller owns) │ │236 - return .{ .data = try self.allocator.dupe(u8, payload) }; │ │237 - }, │ │238 - SSH_MSG_CHANNEL_EXTENDED_DATA => { │ │239 - // stderr from the client is unusual; discard, but adjust window │ │240 - try discardChannelData(self.io, self.allocator, self.sc_cipher, self.writer, self.channel, packet, true); │ │241 - }, │ │242 - SSH_MSG_CHANNEL_WINDOW_ADJUST => try handleWindowAdjust(self.channel, packet), │ │243 - SSH_MSG_CHANNEL_REQUEST => { │ │244 - var r = std.Io.Reader.fixed(packet[1..]); │ │245 - const recipient_channel = try r.takeInt(u32, .big); │ │246 - if (recipient_channel != self.channel.local_id) return error.UnknownChannel; │ │247 - const req_type = try takeStringField(self.allocator, &r, 64); │ │248 - defer self.allocator.free(req_type); │ │249 - const want_reply = (try r.takeByte()) != 0; │ │250 - if (std.mem.eql(u8, req_type, "window-change")) { │ │251 - const sz = try takePtySize(&r); │ │252 - if (self.channel.pty != null) self.channel.pty = sz; │ │253 - try replyChannelRequest(self.io, self.allocator, self.sc_cipher, self.writer, self.channel, want_reply, true); │ │254 - return .{ .resize = sz }; │ │255 - } │ │256 - // unknown / not interesting — fail it and keep going │ │257 - try replyChannelRequest(self.io, self.allocator, self.sc_cipher, self.writer, self.channel, want_reply, false); │ │258 - }, │ │259 - SSH_MSG_CHANNEL_EOF, SSH_MSG_CHANNEL_CLOSE => { │ │260 - try parseChannelId(packet, self.channel.local_id); │ │261 - self.closed = true; │ │262 - return .close; │ │263 - }, │ │264 - SSH_MSG_GLOBAL_REQUEST => try handleGlobalRequest(self.io, self.allocator, self.sc_cipher, self.writer, packet), │ │265 - else => {}, │ │246 + if (self.pending_resize) |sz| { │ │247 + self.pending_resize = null; │ │248 + return .{ .resize = sz }; │ │249 + } │ │250 + if (self.incomingBytes().len > 0) { │ │251 + // hand the buffered bytes to the caller (caller owns) │ │252 + const payload = try self.allocator.dupe(u8, self.incomingBytes()); │ │253 + self.consumeIncoming(payload.len); │ │254 + return .{ .data = payload }; │ │255 + } │ │256 + if (self.incoming_eof) { │ │257 + self.closed = true; │ │258 + return .close; │ │259 } │ │260 + try self.processOneBackgroundPacket(); │ │261 } │ │262 } │ │263 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │286 if (self.incoming_eof) return error.RemoteClosed; │ │287 try self.processOneBackgroundPacket(); │ │288 } │ │295 - const cap = @min(@min(rest.len, self.channel.max_packet), self.channel.remote_window); │ │289 + // also clamp to our own max so the chunk fits writePacket's buffer │ │290 + const cap = @min(rest.len, self.channel.max_packet, self.channel.remote_window, max_packet_size); │ │291 const chunk_len: u32 = @intCast(cap); │ │292 │ │298 - var msg: std.ArrayList(u8) = .empty; │ │299 - defer msg.deinit(self.allocator); │ │300 - try msg.append(self.allocator, if (extended) SSH_MSG_CHANNEL_EXTENDED_DATA else SSH_MSG_CHANNEL_DATA); │ │301 - try writeU32(&msg, self.allocator, self.channel.remote_id); │ │302 - if (extended) try writeU32(&msg, self.allocator, SSH_EXTENDED_DATA_STDERR); │ │303 - try writeStringField(&msg, self.allocator, rest[0..chunk_len]); │ │304 - try self.sc_cipher.writePacket(self.io, self.allocator, self.writer, msg.items); │ │293 + self.write_scratch.clearRetainingCapacity(); │ │294 + try self.write_scratch.append(self.allocator, if (extended) SSH_MSG_CHANNEL_EXTENDED_DATA else SSH_MSG_CHANNEL_DATA);│ │295 + try writeU32(&self.write_scratch, self.allocator, self.channel.remote_id); │ │296 + if (extended) try writeU32(&self.write_scratch, self.allocator, SSH_EXTENDED_DATA_STDERR); │ │297 + try writeStringField(&self.write_scratch, self.allocator, rest[0..chunk_len]); │ │298 + try self.sc_cipher.writePacket(self.io, self.writer, self.write_scratch.items); │ │299 │ │300 self.channel.remote_window -= chunk_len; │ │301 rest = rest[chunk_len..]; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │302 } │ │303 } │ │304 │ │311 - /// process exactly one SSH packet for side effects only. CHANNEL_DATA │ │312 - /// payloads accumulate in incoming_buffer; CHANNEL_EOF/CLOSE flips │ │313 - /// incoming_eof; WINDOW_ADJUST updates the channel state; window-change │ │314 - /// is stashed in pending_resize for the next nextEvent call; other │ │315 - /// channel/global requests get a polite failure. used both by │ │316 - /// SessionReader (waiting for inbound bytes) and writeBytes (waiting │ │317 - /// for send-window). │ │305 + /// the session's packet pump: process exactly one SSH packet for side │ │306 + /// effects only. CHANNEL_DATA payloads accumulate in incoming_buffer; │ │307 + /// CHANNEL_EOF/CLOSE flips incoming_eof; WINDOW_ADJUST updates the │ │308 + /// channel state; window-change is stashed in pending_resize; other │ │309 + /// channel/global requests get a polite failure. driven by nextEvent, │ │310 + /// SessionReader, and writeBytes (waiting for send-window). │ │311 fn processOneBackgroundPacket(self: *SessionCtx) !void { │ │312 const packet = readSessionPacket(self.io, self.allocator, self.reader, self.writer, self.cs_cipher, self.sc_cipher, self.rekey) catch |err| switch (err) {│ │313 error.EndOfStream => { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │325 const data_len: u32 = @intCast(payload.len); │ │326 if (data_len > self.channel.local_window) return error.WindowExceeded; │ │327 self.channel.local_window -= data_len; │ │335 - if (payload.len > max_incoming_buffered - self.incoming_buffer.items.len) {│ │336 - return error.IncomingBufferExceeded; │ │337 - } │ │338 - try self.incoming_buffer.appendSlice(self.allocator, payload); │ │328 + try self.appendIncoming(payload); │ │329 try self.maybeRefillRecvWindowForBufferedInput(); │ │330 }, │ │331 SSH_MSG_CHANNEL_EXTENDED_DATA => { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │359 } │ │360 │ │361 fn maybeRefillRecvWindowForBufferedInput(self: *SessionCtx) !void { │ │372 - if (self.incoming_buffer.items.len >= incoming_refill_threshold) return; │ │362 + if (self.incomingBytes().len >= incoming_refill_threshold) return; │ │363 try maybeRefillRecvWindow(self.io, self.allocator, self.sc_cipher, self.writer, self.channel);│ │364 } │ │365 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │378 try writeStringField(&req, self.allocator, "exit-status"); │ │379 try req.append(self.allocator, 0); // want_reply MUST be false │ │380 try writeU32(&req, self.allocator, status); │ │391 - try self.sc_cipher.writePacket(self.io, self.allocator, self.writer, req.items);│ │381 + try self.sc_cipher.writePacket(self.io, self.writer, req.items); │ │382 } │ │383 │ │384 // EOF then CLOSE │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │386 defer eof.deinit(self.allocator); │ │387 try eof.append(self.allocator, SSH_MSG_CHANNEL_EOF); │ │388 try writeU32(&eof, self.allocator, self.channel.remote_id); │ │399 - try self.sc_cipher.writePacket(self.io, self.allocator, self.writer, eof.items);│ │389 + try self.sc_cipher.writePacket(self.io, self.writer, eof.items); │ │390 │ │391 var close: std.ArrayList(u8) = .empty; │ │392 defer close.deinit(self.allocator); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯