╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │362 return ssh.formatFingerprint(blob_buf[0..blob_len]); │ │363 } │ │364 │ │365 -// write an error to the client and exit non-zero │ │365 +// write an error to the client's stderr and exit non-zero. stderr (not stdout)│ │366 +// because the git exec sessions that call this carry the pack protocol on │ │367 +// stdout, where stray text breaks the client's pkt-line parser. │ │368 fn writeError(sess: *ssh.SessionCtx, msg: []const u8) !void { │ │369 var buf: [256]u8 = undefined; │ │370 const text = try std.fmt.bufPrint(&buf, "haxy ssh: {s}\n", .{msg}); │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────╮ │369 - try sess.writeBytes(text); │ │371 + try sess.writeStderr(text);│ │372 try sess.exit(1); │ │373 } │ │374 │ ╰────────────────────────────────────╯ ╭ changed ─────────────────╮ │src/serve_ssh_protocol.zig│ ╰──────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │271 } │ │272 } │ │273 │ │274 - /// send bytes to the client as one or more CHANNEL_DATA packets, │ │275 - /// respecting the peer's max-packet size. if the remote window is │ │276 - /// exhausted, pump incoming SSH packets until a WINDOW_ADJUST arrives │ │277 - /// (background packets are processed for their side effects — incoming │ │278 - /// CHANNEL_DATA goes into incoming_buffer for a future SessionReader). │ │274 + /// send bytes to the client's stdout (CHANNEL_DATA). │ │275 pub fn writeBytes(self: *SessionCtx, bytes: []const u8) !void { │ │276 + try self.writeChannel(bytes, false); │ │277 + } │ │278 + │ │279 + /// send bytes to the client's stderr (CHANNEL_EXTENDED_DATA). use this for │ │280 + /// server messages on a git exec session, where stdout carries the pack │ │281 + /// protocol and stray text breaks the client's pkt-line parser. │ │282 + pub fn writeStderr(self: *SessionCtx, bytes: []const u8) !void { │ │283 + try self.writeChannel(bytes, true); │ │284 + } │ │285 + │ │286 + /// ship bytes as one or more stdout CHANNEL_DATA (or stderr │ │287 + /// CHANNEL_EXTENDED_DATA) packets, respecting the peer's max-packet size. │ │288 + /// if the remote window is exhausted, pump incoming SSH packets until a │ │289 + /// WINDOW_ADJUST arrives (background packets are processed for their side │ │290 + /// effects — incoming CHANNEL_DATA goes into incoming_buffer for a future │ │291 + /// SessionReader). │ │292 + fn writeChannel(self: *SessionCtx, bytes: []const u8, extended: bool) !void {│ │293 var rest = bytes; │ │294 while (rest.len > 0) { │ │295 while (self.channel.remote_window == 0) { │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │301 │ │302 var msg: std.ArrayList(u8) = .empty; │ │303 defer msg.deinit(self.allocator); │ │291 - try msg.append(self.allocator, SSH_MSG_CHANNEL_DATA); │ │304 + try msg.append(self.allocator, if (extended) SSH_MSG_CHANNEL_EXTENDED_DATA else SSH_MSG_CHANNEL_DATA);│ │305 try writeU32(&msg, self.allocator, self.channel.remote_id); │ │306 + if (extended) try writeU32(&msg, self.allocator, SSH_EXTENDED_DATA_STDERR); │ │307 try writeStringField(&msg, self.allocator, rest[0..chunk_len]); │ │308 try self.sc_cipher.writePacket(self.io, self.allocator, self.writer, msg.items); │ │309 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯