fix: send SSH_MSG_DISCONNECT on KEX failure#1091
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because no GitHub Actions runner was available. Make sure your repository has a runner available to run Copilot's review, or add a copilot-setup-steps.yml file specifying one with the runs-on attribute. See the docs for more details.
Ensures the library sends a best-effort SSH_MSG_DISCONNECT when KEX negotiation fails due to no mutually supported algorithms, aligning behavior with RFC 4253 guidance and existing disconnect-on-error patterns in the codebase.
Changes:
- Adds a best-effort
(void)SendDisconnect(...)call inDoKexInit()when KEX/hostkey/cipher/MAC matching fails. - Uses
WOLFSSH_DISCONNECT_KEY_EXCHANGE_FAILEDas the disconnect reason for these negotiation failures.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /* RFC 4253 11.1: best-effort SSH_MSG_DISCONNECT on negotiation failure. */ | ||
| if (ret == WS_MATCH_KEX_ALGO_E || ret == WS_MATCH_KEY_ALGO_E || | ||
| ret == WS_MATCH_ENC_ALGO_E || ret == WS_MATCH_MAC_ALGO_E) { | ||
| (void)SendDisconnect(ssh, WOLFSSH_DISCONNECT_KEY_EXCHANGE_FAILED); |
aidangarske
left a comment
There was a problem hiding this comment.
🐺 Skoll Code Review
Overall recommendation: REQUEST_CHANGES
Findings: 2 total — 2 posted, 0 skipped
Posted findings
- [High] Disconnect can be followed by another KEX packet during rekey failure —
src/internal.c:5335-5338 - [Medium] Missing regression coverage for the new disconnect side effect —
src/internal.c:5335-5338
Review generated by Skoll.
| ret = ssh->error; | ||
| } | ||
| } | ||
| /* RFC 4253 11.1: best-effort SSH_MSG_DISCONNECT on negotiation failure. */ |
There was a problem hiding this comment.
🟠 [High] Disconnect can be followed by another KEX packet during rekey failure
🚫 BLOCK bug
The new SendDisconnect() call is placed inside DoKexInit(), but the production caller still continues its post-DoKexInit() rekey send path based only on ssh->isKeying. DoKexInit() sets WOLFSSH_PEER_IS_KEYING before negotiation completes, so on a post-auth rekey (connectState == CONNECT_SERVER_CHANNEL_REQUEST_DONE) a host-key, cipher, or MAC mismatch can send SSH_MSG_DISCONNECT here and then return to DoPacket(), which can immediately call SendKexDhInit() or SendKexDhGexRequest() because ret is not checked. That means the PR can emit a key-exchange packet after the disconnect it just added, which breaks the intended terminate-on-negotiation-failure behavior.
Recommendation: Gate the rekey response in DoPacket() on successful DoKexInit() completion (e.g. if (ret == WS_SUCCESS && ssh->isKeying && ssh->connectState == CONNECT_SERVER_CHANNEL_REQUEST_DONE)) so no follow-on KEX response is emitted after DoKexInit() returns a negotiation-failure error.
| ret = ssh->error; | ||
| } | ||
| } | ||
| /* RFC 4253 11.1: best-effort SSH_MSG_DISCONNECT on negotiation failure. */ |
There was a problem hiding this comment.
🟡 [Medium] Missing regression coverage for the new disconnect side effect
💡 SUGGEST test
The PR adds observable behavior: negotiation match failures should now send SSH_MSG_DISCONNECT with reason WOLFSSH_DISCONNECT_KEY_EXCHANGE_FAILED. Existing regression tests assert the returned match errors, such as WS_MATCH_KEX_ALGO_E, WS_MATCH_ENC_ALGO_E, and WS_MATCH_MAC_ALGO_E, but they do not capture the outbound packet or reason code. A future change could preserve the return code while dropping the new disconnect behavior.
Recommendation: Add a regression test using a send-capture IO callback for at least one negotiation mismatch path, and assert that the emitted packet is MSGID_DISCONNECT with reason WOLFSSH_DISCONNECT_KEY_EXCHANGE_FAILED.
Bug
DoKexInit()insrc/internal.csetsretto one of the negotiation-failure codesWS_MATCH_KEX_ALGO_E/WS_MATCH_KEY_ALGO_E/WS_MATCH_ENC_ALGO_E/WS_MATCH_MAC_ALGO_Ewhen no common KEX / host-key / cipher / MAC algorithm can be negotiated, then returns. That code propagates unchanged throughDoPacket()andDoReceive(), which setssh->errorand returnWS_FATAL_ERROR. NoSSH_MSG_DISCONNECTis ever sent on these paths, so the peer is dropped without notification.RFC 4253 s11.1 says an implementation SHOULD send
SSH_MSG_DISCONNECTon a protocol error. The library already follows this best-effort pattern with(void)SendDisconnect(...)for service-request/accept errors (internal.c:7691, 7723); the KEX negotiation paths were inconsistent with that pattern.Fix
At the single
DoKexInit()exit point, send a best-effortSSH_MSG_DISCONNECTwith reasonWOLFSSH_DISCONNECT_KEY_EXCHANGE_FAILED(3) whenretis any of the four match-failure codes. Uses the same(void)SendDisconnectbest-effort form as existing call sites. Safe:retis not overwritten for match errors (theSendKexInit/ssh->errorpropagation block is gated onret == WS_SUCCESS).Build verification
Compiled the flagged translation unit against a full wolfSSL build (
make src/libwolfssh_la-internal.lo, exit 0) on wolfssh master 974f4fd.Reported by static analysis (Fenrir finding 608). Severity Low (RFC uses SHOULD, not MUST).