Skip to content

fix: SFTP server subsystem sends exit-status and EOF before channel close#1492

Open
Luwdo wants to merge 3 commits intomscdex:masterfrom
ordinlabs:fix-sftp-subsystem-exit-status
Open

fix: SFTP server subsystem sends exit-status and EOF before channel close#1492
Luwdo wants to merge 3 commits intomscdex:masterfrom
ordinlabs:fix-sftp-subsystem-exit-status

Conversation

@Luwdo
Copy link
Copy Markdown

@Luwdo Luwdo commented Apr 7, 2026

Problem

When using ssh2 as an SSH server, the server-side SFTP class's destroy() method sends channelClose without first sending exit-status or channelEOF. This causes SCP clients to report exit code -1 (or 0xffffffff), even when the SFTP transfer completes successfully.

Modern OpenSSH's scp uses the SFTP subsystem by default (since OpenSSH 9.0), so this affects any SCP operation through an ssh2-based server.

Root Cause

The SFTP class extends EventEmitter, not Channel. The Channel class handles exit-status properly via its onFinish handler (which calls eof() then close()), but SFTP.destroy() skips straight to channelClose:

// Before
destroy() {
    if (this.outgoing.state === 'open' || this.outgoing.state === 'eof') {
      this.outgoing.state = 'closing';
      this._protocol.channelClose(this.outgoing.id);
    }
}

In contrast, OpenSSH's sftp-server process exits with code 0 when done, which causes the server to send exit-status(0) → SSH_MSG_CHANNEL_EOF → SSH_MSG_CHANNEL_CLOSE on the channel.

Fix

When running in server mode, send exitStatus(0) and channelEOF before channelClose in destroy():

destroy() {
    if (this.outgoing.state === 'open' || this.outgoing.state === 'eof') {
      if (this.server && this.outgoing.state === 'open') {
        this._protocol.exitStatus(this.outgoing.id, 0);
        this._protocol.channelEOF(this.outgoing.id);
      }
      this.outgoing.state = 'closing';
      this._protocol.channelClose(this.outgoing.id);
    }
}

Testing

Verified with:

  • scp (SFTP mode, default) — now returns exit code 0 instead of -1
  • scp -O (legacy mode, uses exec) — unaffected, still works
  • Mutagen file sync agent auto-install via SCP — now succeeds without manual agent installation
  • Normal SFTP file operations (read, write, mkdir, stat, etc.) — unaffected

Luwdo added 2 commits April 7, 2026 12:01
…P destroy

SFTP.destroy() was sending channelClose without first sending exit-status
or channelEOF. This caused SCP clients (which use the SFTP subsystem) to
report exit code -1, breaking tools like Mutagen that check exit codes
during agent installation. Now matches OpenSSH behavior by sending
exitStatus(0) + channelEOF before channelClose when running as a server.
@Luwdo Luwdo force-pushed the fix-sftp-subsystem-exit-status branch from 44d1097 to 2eef389 Compare April 7, 2026 17:32
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