Fix exit-status: send SSH_MSG_CHANNEL_REQUEST exit-status per RFC 4254 §6.10
This commit is contained in:
37
AGENTS.md
37
AGENTS.md
@@ -2850,5 +2850,38 @@ chacha20-poly1305@openssh.com
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-06-20 13:45
|
||||
**版本**:1.30(Phase 8.3 Docker 测试完成)
|
||||
---
|
||||
|
||||
## exit-status 修复完成(2026-06-20)⭐⭐⭐⭐⭐
|
||||
|
||||
**背景**:SSH `ssh -p 2024 demo@127.0.0.1 "echo hello"` 返回 `exit status -1`
|
||||
|
||||
**根本原因**(RFC 4254 §6.10):
|
||||
- OpenSSH client 要求 server 在通道关闭前发送 `SSH_MSG_CHANNEL_REQUEST "exit-status"`
|
||||
- `handle_child_exited()` 之前只发送 EOF + CLOSE,从未发送 `exit-status` 请求
|
||||
- 因此 OpenSSH client 不知道子进程的退出码,报告 `exit status -1`
|
||||
|
||||
**修复内容**(`channel.rs`):
|
||||
|
||||
| 修改 | 位置 | 说明 |
|
||||
|------|------|------|
|
||||
| `exit_status: Option<u32>` | `Channel` struct | 存储子进程退出码 |
|
||||
| `channel.exit_status = Some(exit_code)` | `poll_exec_stdout_and_client()` | 在 `child.try_wait()` 返回 status 时保存退出码 |
|
||||
| `build_channel_exit_status()` | 新函数 | 构建 `SSH_MSG_CHANNEL_REQUEST "exit-status" FALSE <code>` |
|
||||
| 发送 exit-status → EOF → CLOSE | `handle_child_exited()` | 按正确顺序发送三个消息 |
|
||||
|
||||
**验证结果**:
|
||||
```bash
|
||||
$ ssh -p 2024 demo@127.0.0.1 "echo hello"
|
||||
hello # ✅ exit status 0 (默认)
|
||||
|
||||
$ ssh -p 2024 demo@127.0.0.1 "exit 42"
|
||||
# 无输出(exit 42 不产生 stdout)
|
||||
$ echo $?
|
||||
42 # ✅ exit status 42 正确传递
|
||||
```
|
||||
|
||||
**累计代码**:5061 行(新增 31 行)
|
||||
|
||||
**最后更新**:2026-06-20 14:15
|
||||
**版本**:1.31(exit-status 修复完成)
|
||||
|
||||
@@ -176,6 +176,7 @@ impl ChannelManager {
|
||||
scp_handler: None,
|
||||
rsync_handler: None,
|
||||
exec_process: None, // Phase 14: 交互式exec
|
||||
exit_status: None, // ⭐⭐⭐⭐⭐ exit status from child process
|
||||
sftp_input_buffer: Vec::new(), // ⭐⭐⭐⭐⭐ Phase 14.2修复:SFTP packet累积
|
||||
scp_input_buffer: Vec::new(), // ⭐⭐⭐⭐⭐ Phase 14.4修复:SCP packet累积
|
||||
scp_state: ScpState::Idle, // ⭐⭐⭐⭐⭐ Phase 8.3: SCP state machine
|
||||
@@ -254,6 +255,7 @@ impl ChannelManager {
|
||||
scp_handler: None,
|
||||
rsync_handler: None,
|
||||
exec_process: None,
|
||||
exit_status: None, // ⭐⭐⭐⭐⭐ exit status from child process
|
||||
sftp_input_buffer: Vec::new(),
|
||||
scp_input_buffer: Vec::new(),
|
||||
scp_state: ScpState::Idle, // ⭐⭐⭐⭐⭐ Phase 8.3: SCP state machine
|
||||
@@ -329,6 +331,7 @@ impl ChannelManager {
|
||||
scp_handler: None,
|
||||
rsync_handler: None,
|
||||
exec_process: None, // Phase 14: 交互式exec
|
||||
exit_status: None, // ⭐⭐⭐⭐⭐ exit status from child process
|
||||
sftp_input_buffer: Vec::new(), // ⭐⭐⭐⭐⭐ Phase 14.2修复
|
||||
scp_input_buffer: Vec::new(), // ⭐⭐⭐⭐⭐ Phase 14.4修复
|
||||
scp_state: ScpState::Idle, // ⭐⭐⭐⭐⭐ Phase 8.3: SCP state machine
|
||||
@@ -1303,6 +1306,19 @@ impl ChannelManager {
|
||||
/// ⭐⭐⭐⭐⭐ 关键:非阻塞读取数据,不等待子进程完成
|
||||
/// ⭐⭐⭐⭐⭐ Phase 14.2: 处理child exited(发送EOF + CLOSE)
|
||||
/// 参考:OpenSSH session.c: do_exec_no_pty()
|
||||
/// Build SSH_MSG_CHANNEL_REQUEST "exit-status" (RFC 4254 §6.10)
|
||||
pub fn build_channel_exit_status(&self, channel: u32, exit_code: u32) -> Result<SshPacket> {
|
||||
let mut payload = Vec::new();
|
||||
payload.write_u8(PacketType::SSH_MSG_CHANNEL_REQUEST as u8)?;
|
||||
payload.write_u32::<BigEndian>(channel)?;
|
||||
let name = "exit-status";
|
||||
payload.write_u32::<BigEndian>(name.len() as u32)?;
|
||||
payload.write_all(name.as_bytes())?;
|
||||
payload.write_u8(0)?; // FALSE (want_reply)
|
||||
payload.write_u32::<BigEndian>(exit_code)?;
|
||||
Ok(SshPacket::new(payload))
|
||||
}
|
||||
|
||||
pub fn handle_child_exited(&mut self) -> Result<Vec<SshPacket>> {
|
||||
// 1. 收集需要处理的channel IDs (exec_process OR rsync_handler)
|
||||
let channel_ids: Vec<u32> = self
|
||||
@@ -1320,6 +1336,14 @@ impl ChannelManager {
|
||||
// 2. 构建packets(避免borrow冲突)
|
||||
let mut packets = Vec::new();
|
||||
for channel_id in &channel_ids {
|
||||
// Send exit-status first (RFC 4254 §6.10)
|
||||
let exit_code = self.channels.get(channel_id)
|
||||
.and_then(|c| c.exit_status)
|
||||
.unwrap_or(255);
|
||||
let exit_packet = self.build_channel_exit_status(*channel_id, exit_code)?;
|
||||
packets.push(exit_packet);
|
||||
|
||||
// Then EOF + CLOSE
|
||||
let eof_packet = self.build_channel_eof(*channel_id)?;
|
||||
packets.push(eof_packet);
|
||||
|
||||
@@ -1332,12 +1356,13 @@ impl ChannelManager {
|
||||
if let Some(channel) = self.channels.get_mut(channel_id) {
|
||||
channel.exec_process = None;
|
||||
channel.rsync_handler = None;
|
||||
channel.exit_status = None;
|
||||
}
|
||||
}
|
||||
|
||||
if !channel_ids.is_empty() {
|
||||
info!(
|
||||
"Child/rsync exited, sent EOF + CLOSE for {} channels",
|
||||
"Child/rsync exited, sent exit-status + EOF + CLOSE for {} channels",
|
||||
channel_ids.len()
|
||||
);
|
||||
}
|
||||
@@ -1498,11 +1523,13 @@ impl ChannelManager {
|
||||
if let Some(exec_process) = &mut channel.exec_process {
|
||||
match exec_process.child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
let exit_code = status.code().unwrap_or(-1) as u32;
|
||||
info!(
|
||||
"Child process exited (channel {}, status: {:?})",
|
||||
channel_id, status
|
||||
"Child process exited (channel {}, exit_status: {}, status: {:?})",
|
||||
channel_id, exit_code, status
|
||||
);
|
||||
child_exited = true;
|
||||
channel.exit_status = Some(exit_code);
|
||||
|
||||
let command_str = exec_process.command.clone();
|
||||
let should_trigger_hook = status.success()
|
||||
@@ -2005,6 +2032,7 @@ struct Channel {
|
||||
scp_handler: Option<ScpHandler>, // Phase 8: SCP处理器
|
||||
rsync_handler: Option<RsyncHandler>, // Phase 8: rsync处理器
|
||||
exec_process: Option<ExecProcess>, // Phase 14: 交互式exec进程
|
||||
exit_status: Option<u32>, // ⭐⭐⭐⭐⭐ exit status from child process
|
||||
// ⭐⭐⭐⭐⭐ Critical修复:SFTP packet累积buffer
|
||||
sftp_input_buffer: Vec<u8>, // Phase 14.2修复:累积不完整的SFTP packets
|
||||
// ⭐⭐⭐⭐⭐ Phase 14.4:SCP packet累积buffer
|
||||
|
||||
Reference in New Issue
Block a user