Skip to content

Commit 4206815

Browse files
authored
Fix pty-req terminal modes: deliver them unpadded, encode the right l… (#755)
1 parent 99732d3 commit 4206815

3 files changed

Lines changed: 114 additions & 2 deletions

File tree

russh/src/client/session.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,16 @@ impl Session {
126126
pix_width.encode(&mut enc.write)?;
127127
pix_height.encode(&mut enc.write)?;
128128

129-
((1 + 5 * terminal_modes.len()) as u32).encode(&mut enc.write)?;
129+
// TTY_OP_END entries are skipped below and the single
130+
// terminator is written afterwards, so the length must
131+
// count only the modes that are actually encoded --
132+
// otherwise the declared string length overruns the
133+
// bytes written and the rest of the packet is garbage.
134+
let encoded_modes = terminal_modes
135+
.iter()
136+
.filter(|&&(code, _)| code != Pty::TTY_OP_END)
137+
.count();
138+
((1 + 5 * encoded_modes) as u32).encode(&mut enc.write)?;
130139
for &(code, value) in terminal_modes {
131140
if code == Pty::TTY_OP_END {
132141
continue;

russh/src/server/encrypted.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1389,6 +1389,11 @@ impl Session {
13891389
map_err!(ensure_end(r))?;
13901390

13911391
if let Some(chan) = self.channels.get(&channel_num) {
1392+
// Only the first `i` entries were decoded from the
1393+
// request; the rest of `modes` is padding and must
1394+
// not be passed on as if the client had sent it.
1395+
#[allow(clippy::indexing_slicing)] // `i <= modes.len()` checked above
1396+
let terminal_modes = modes[..i].to_vec();
13921397
let _ = chan
13931398
.send(ChannelMsg::RequestPty {
13941399
want_reply: true,
@@ -1397,7 +1402,7 @@ impl Session {
13971402
row_height,
13981403
pix_width,
13991404
pix_height,
1400-
terminal_modes: modes.into(),
1405+
terminal_modes,
14011406
})
14021407
.await;
14031408
}

russh/src/tests.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,104 @@ mod channels {
683683
)
684684
.await;
685685
}
686+
687+
/// What a client sends in a pty-req is what the application on the server
688+
/// side must see: exactly those modes, with no padding behind them.
689+
#[tokio::test]
690+
async fn test_pty_req_modes_are_delivered_unpadded() {
691+
let modes = vec![(Pty::VINTR, 3), (Pty::VQUIT, 28)];
692+
assert_eq!(send_pty_req(modes.clone()).await, modes);
693+
}
694+
695+
/// A mode list padded with TTY_OP_END entries -- the shape an application
696+
/// proxying a pty-req ends up holding -- must still produce a well formed
697+
/// request. The encoder skips those entries, so the length it declares for
698+
/// the modes string must not count them, or the receiver reads the rest of
699+
/// the packet as terminal modes and rejects it.
700+
#[tokio::test]
701+
async fn test_pty_req_with_padded_modes_is_well_formed() {
702+
let mut padded = vec![(Pty::VINTR, 3)];
703+
padded.resize(130, (Pty::TTY_OP_END, 0));
704+
assert_eq!(send_pty_req(padded).await, vec![(Pty::VINTR, 3)]);
705+
}
706+
707+
/// Send one pty-req and return the modes the server side received.
708+
async fn send_pty_req(modes: Vec<(Pty, u32)>) -> Vec<(Pty, u32)> {
709+
struct Client {}
710+
711+
impl client::Handler for Client {
712+
type Error = crate::Error;
713+
714+
async fn check_server_key(
715+
&mut self,
716+
_server_public_key: &PublicKeyOrCertificate,
717+
) -> Result<bool, Self::Error> {
718+
Ok(true)
719+
}
720+
}
721+
722+
struct ServerHandle {
723+
seen: tokio::sync::mpsc::UnboundedSender<Vec<(Pty, u32)>>,
724+
}
725+
726+
impl server::Handler for ServerHandle {
727+
type Error = crate::Error;
728+
729+
async fn auth_publickey(
730+
&mut self,
731+
_: &str,
732+
_: &crate::keys::ssh_key::PublicKey,
733+
) -> Result<server::Auth, Self::Error> {
734+
Ok(server::Auth::Accept)
735+
}
736+
737+
async fn channel_open_session(
738+
&mut self,
739+
mut channel: Channel<server::Msg>,
740+
reply: server::ChannelOpenHandle,
741+
session: &mut Session,
742+
) -> Result<(), Self::Error> {
743+
reply.accept().await;
744+
745+
let seen = self.seen.clone();
746+
let handle = session.handle();
747+
let id = channel.id();
748+
tokio::spawn(async move {
749+
while let Some(msg) = channel.wait().await {
750+
if let ChannelMsg::RequestPty { terminal_modes, .. } = msg {
751+
let _ = seen.send(terminal_modes);
752+
// Answer, so the client knows we are done looking.
753+
let _ = handle.channel_success(id).await;
754+
break;
755+
}
756+
}
757+
});
758+
Ok(())
759+
}
760+
}
761+
762+
let (seen, mut received) = tokio::sync::mpsc::unbounded_channel();
763+
test_session(
764+
Client {},
765+
ServerHandle { seen },
766+
move |client| async move {
767+
let mut channel = client.channel_open_session().await.unwrap();
768+
channel
769+
.request_pty(true, "xterm", 80, 24, 0, 0, &modes)
770+
.await
771+
.unwrap();
772+
match channel.wait().await {
773+
Some(ChannelMsg::Success) => (),
774+
other => panic!("pty request was not accepted: {other:?}"),
775+
}
776+
client
777+
},
778+
|server| async move { server },
779+
)
780+
.await;
781+
782+
received.try_recv().expect("server never saw the pty request")
783+
}
686784
}
687785

688786
mod gex {

0 commit comments

Comments
 (0)