Fix 5MB SFTP download hang: batch process SFTP packets + WINDOW_ADJUST chaining
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

Root cause: handle_channel_data processed only ONE SFTP packet per call,
leaving remaining batched packets stuck in the buffer. Client waited for
READ responses while server waited for more data — deadlock after ~3.1MB.

Fix:
- sftp_handler.rs: fix SSH_FXP_VERSION format (remove uint32 extension_count)
- sftp_handler.rs: fix handle_open error mapping (.ok() → build_status_from_io_error)
- channel.rs: batch-process ALL complete SFTP packets from buffer in loop
- channel.rs: add pending_packets VecDeque for multi-response queuing
- channel.rs: chain WINDOW_ADJUST + SFTP response when window is low
- channel.rs: add adjust_remote_window() for client WINDOW_ADJUST
- server.rs: drain pending_packets after each CHANNEL_DATA handler

Verified: 5MB upload + download with matching MD5
This commit is contained in:
Warren
2026-06-18 17:15:00 +08:00
parent 1d81db3af5
commit 83fb0de78a
3 changed files with 155 additions and 68 deletions

View File

@@ -341,21 +341,6 @@ impl SftpHandler {
let version = cursor.read_u32::<BigEndian>()?;
info!("Client SFTP version: {}", version);
// Read any extension data client sent (SSH_FXP_INIT may contain extensions)
let pos = cursor.position() as usize;
let inner = cursor.get_ref();
if inner.len() > pos && (inner.len() - pos) >= 4 {
let ext_count = match cursor.read_u32::<BigEndian>() {
Ok(n) => n,
Err(_) => 0,
};
for i in 0..ext_count {
let ext_name = read_sftp_string(&mut cursor).unwrap_or_default();
let ext_data = read_sftp_string(&mut cursor).unwrap_or_default();
debug!("Client extension[{}]: {} = {}", i, ext_name, ext_data);
}
}
let response = self.build_version_response(3)?;
Ok(response)
}
@@ -376,8 +361,8 @@ impl SftpHandler {
let full_path = self.resolve_path(&path)?;
let file = if pflags & SftpFileFlags::SSH_FXF_READ != 0 {
OpenOptions::new().read(true).open(&full_path).ok()
let file_result = if pflags & SftpFileFlags::SSH_FXF_READ != 0 {
OpenOptions::new().read(true).open(&full_path)
} else if pflags & SftpFileFlags::SSH_FXF_WRITE != 0 {
let mut opts = OpenOptions::new();
opts.write(true);
@@ -393,13 +378,13 @@ impl SftpHandler {
if pflags & SftpFileFlags::SSH_FXF_EXCL != 0 {
opts.create_new(true);
}
opts.open(&full_path).ok()
opts.open(&full_path)
} else {
None
return self.build_status_response(id, SftpStatus::SSH_FX_OP_UNSUPPORTED, "Unsupported open flags");
};
match file {
Some(file) => {
match file_result {
Ok(file) => {
if self.handles.len() >= Self::MAX_HANDLES {
warn!("SSH_FXP_OPEN: handle limit reached ({})", Self::MAX_HANDLES);
return self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Handle limit reached");
@@ -419,8 +404,8 @@ impl SftpHandler {
self.build_handle_response(id, &handle_id.to_be_bytes())
}
None => {
self.build_status_response(id, SftpStatus::SSH_FX_FAILURE, "Failed to open file")
Err(e) => {
self.build_status_from_io_error(id, &e)
}
}
}
@@ -1478,14 +1463,25 @@ impl SftpHandler {
}
}
/// 构建SSH_FXP_VERSION响应包含扩展声明参考OpenSSH sftp-server.c
/// 构建SSH_FXP_VERSION响应包含扩展声明参考OpenSSH sftp-server.c: process_init()
///
/// SFTP协议格式draft-ietf-secsh-filexfer-02
/// uint32 length
/// uint8 type (SSH_FXP_VERSION = 2)
/// uint32 version
/// // extensions: NO count field, simply paired strings until buffer empty
/// string extension_name (= uint32(len_with_nul) + data + \0)
/// string extension_data (= uint32(len_with_nul) + data + \0)
///
/// OpenSSH uses sshbuf_put_cstring() which includes NUL terminator.
/// Client reads with sshbuf_get_cstring() which expects \0 at end.
fn build_version_response(&self, version: u32) -> Result<Vec<u8>> {
let mut buffer = Vec::new();
buffer.write_u8(SftpPacketType::SSH_FXP_VERSION as u8)?;
buffer.write_u32::<BigEndian>(version)?;
// 扩展声明OpenSSH sftp-server.c: process_init() 中声明支持的扩展)
// 扩展声明OpenSSH sftp-server.c: process_init() style, NO count field
let extensions: &[(&str, &str)] = &[
("posix-rename@openssh.com", "1"),
("hardlink@openssh.com", "1"),
@@ -1498,13 +1494,14 @@ impl SftpHandler {
("sha384-hash@openssh.com", "1"),
("sha512-hash@openssh.com", "1"),
];
buffer.write_u32::<BigEndian>(extensions.len() as u32)?;
for (name, data) in extensions {
buffer.write_u32::<BigEndian>(name.len() as u32)?;
// sshbuf_put_cstring(buf, s) → sshbuf_put_string(buf, s, strlen(s)+1)
buffer.write_u32::<BigEndian>((name.len() + 1) as u32)?;
buffer.write_all(name.as_bytes())?;
buffer.write_u32::<BigEndian>(data.len() as u32)?;
buffer.write_u8(0)?;
buffer.write_u32::<BigEndian>((data.len() + 1) as u32)?;
buffer.write_all(data.as_bytes())?;
buffer.write_u8(0)?;
}
self.wrap_sftp_packet(&buffer)