Skip to content

Fix pty-req terminal modes: deliver them unpadded, encode the right l… - #755

Open
tluyben wants to merge 1 commit into
Eugeny:mainfrom
tluyben:main
Open

Fix pty-req terminal modes: deliver them unpadded, encode the right l…#755
tluyben wants to merge 1 commit into
Eugeny:mainfrom
tluyben:main

Conversation

@tluyben

@tluyben tluyben commented Aug 18, 2026

Copy link
Copy Markdown

Description

Forwarding a pty-req from a server-side channel to a client-side channel — what any SSH proxy or jump host does — produces a malformed request that the receiving end rejects, dropping the session as soon as the user asks for a pty. Two separate defects combine to cause it; each is worth fixing on its own.

  1. The server delivers terminal modes that the client never sent.

Session::server_read_encrypted decodes pty modes into a fixed [(Pty, u32); 130] array and passes &modes[0..i] to handler.pty_request() — but sends the entire padded array to the channel stream:

terminal_modes: modes.into(), // 130 entries, 128 of them TTY_OP_END padding

So the same event, delivered two ways, disagrees: a handler callback sees the two modes the client sent, while an application reading ChannelMsg::RequestPty off the channel sees 130.

  1. The client encoder declares a length it does not write.

client::Session::request_pty writes the modes string as:

((1 + 5 * terminal_modes.len()) as u32).encode(&mut enc.write)?;
for &(code, value) in terminal_modes {
if code == Pty::TTY_OP_END { continue; } // <-- skipped, but counted above
...
}

Any TTY_OP_END entry in the input is counted in the length and then skipped when writing. Passing the padded array from (1) declares 651 bytes and writes 1, so the receiver reads far past the end of the modes string and fails the packet. This is reachable without a proxy: it fires for any caller that includes a terminator in its mode
list, which is a natural thing to do given the wire format.

Fix

  • Server: send modes[..i], matching what the handler callback already receives.
  • Client: count only the entries actually encoded.

Reproducing

An application holding the server-side channel:

async fn channel_open_session(&mut self, mut channel: Channel, reply: ChannelOpenHandle, _: &mut Session) -> Result<(), Self::Error> {
reply.accept().await;
tokio::spawn(async move {
while let Some(msg) = channel.wait().await {
if let ChannelMsg::RequestPty { terminal_modes, .. } = msg {
// 130 entries, regardless of what the client sent;
// handing these to Channel::request_pty emits a corrupt packet
}
}
});
Ok(())
}

AI Usage

  • AI-designed, AI-coded, manually checked

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant