From 73b165651c80a20047d913d32b0ffac47cb99ef6 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Mon, 17 Nov 2025 09:50:48 -0800 Subject: [PATCH 1/7] Client Out Of Order Messaging Checking 1. Add macro for logging an expected message. 2. Add an expected message ID to the HandshakeInfo. 3. Add a message ID for "none (0)". 4. Add a check in IsMessageAllowedClient() for the expected message ID. Clear it if successful. 5. The KEXDH messages sent to the server have expected responses. Set them if sending the message is successful. 6. Add the set of message ID ranges and macros for testing if a message ID is in a specific range. 7. Add flags for having sent the kexinit message and received it. Tweak the checks for isKeying and these flags. 8. IsMessageAllowedClient() to check for appropriate messages at the appropriate time during the connect. --- src/internal.c | 158 +++++++++++++++++++++++++++++++++++---------- wolfssh/internal.h | 64 ++++++++++++++---- wolfssh/log.h | 2 + 3 files changed, 178 insertions(+), 46 deletions(-) diff --git a/src/internal.c b/src/internal.c index 5c553c6b7..3b4fe5f30 100644 --- a/src/internal.c +++ b/src/internal.c @@ -612,8 +612,8 @@ INLINE static int IsMessageAllowedKeying(WOLFSSH *ssh, byte msg) return 0; } - /* case of resending SSH_MSG_KEXINIT */ - if (msg == MSGID_KEXINIT) { + /* case of peer resending SSH_MSG_KEXINIT */ + if ((ssh->isKeying & WOLFSSH_PEER_IS_KEYING) && msg == MSGID_KEXINIT) { WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by during rekeying", msg); ssh->error = WS_REKEYING; return 0; @@ -632,34 +632,50 @@ INLINE static int IsMessageAllowedKeying(WOLFSSH *ssh, byte msg) #ifndef NO_WOLFSSH_SERVER INLINE static int IsMessageAllowedServer(WOLFSSH *ssh, byte msg) { + /* Only the server should send these messages, never receive. */ + if (msg == MSGID_SERVICE_ACCEPT) { + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", + msg, "client", "ever"); + return 0; + } + + /* Transport Layer Generic messages are always allowed. */ + if (MSGIDLIMIT_TRANS_GEN(msg)) { + return 1; + } + /* Has client userauth started? */ + /* Allows the server to receive up to KEXDH GEX Request during KEX. */ if (ssh->acceptState < ACCEPT_KEYED) { - if (msg > MSGID_KEXDH_LIMIT) { + if (msg > MSGID_KEXDH_GEX_REQUEST) { return 0; } } /* Is server userauth complete? */ if (ssh->acceptState < ACCEPT_SERVER_USERAUTH_SENT) { + /* The server should only receive the user auth request message, + * it should not accept the other user auth messages, it sends + * them. (>50) */ /* Explicitly check for messages not allowed before user * authentication has comleted. */ - if (msg >= MSGID_USERAUTH_LIMIT) { - WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by server " - "before user authentication is complete", msg); + if (MSGIDLIMIT_POST_USERAUTH(msg)) { + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", + msg, "server", "before user authentication is complete"); return 0; } /* Explicitly check for the user authentication messages that * only the server sends, it shouldn't receive them. */ - if ((msg > MSGID_USERAUTH_RESTRICT) && + if ((msg > MSGID_USERAUTH_REQUEST) && (msg != MSGID_USERAUTH_INFO_RESPONSE)) { - WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by server " - "during user authentication", msg); + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", + msg, "server", "during user authentication"); return 0; } } else { - if (msg >= MSGID_USERAUTH_RESTRICT && msg < MSGID_USERAUTH_LIMIT) { - WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by server " - "after user authentication", msg); + if (msg >= MSGID_USERAUTH_REQUEST && msg < MSGID_GLOBAL_REQUEST) { + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", + msg, "server", "after user authentication"); return 0; } } @@ -672,37 +688,95 @@ INLINE static int IsMessageAllowedServer(WOLFSSH *ssh, byte msg) #ifndef NO_WOLFSSH_CLIENT INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) { - /* Has client userauth started? */ - if (ssh->connectState < CONNECT_CLIENT_KEXDH_INIT_SENT) { - if (msg >= MSGID_KEXDH_LIMIT) { + /* Only the client should send these messages, never receive. */ + if (msg == MSGID_SERVICE_REQUEST || msg == MSGID_USERAUTH_REQUEST) { + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", + msg, "client", "ever"); + return 0; + } + + if (msg == MSGID_SERVICE_ACCEPT) { + if (ssh->connectState == CONNECT_CLIENT_USERAUTH_REQUEST_SENT) { + return 1; + } + else { + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", + msg, "client", "after starting user auth"); return 0; } } - /* Is client userauth complete? */ - if (ssh->connectState < CONNECT_SERVER_USERAUTH_ACCEPT_DONE) { - /* Explicitly check for messages not allowed before user - * authentication has comleted. */ - if (msg >= MSGID_USERAUTH_LIMIT) { - WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by client " - "before user authentication is complete", msg); + + /* Transport Layer Generic messages are always allowed. */ + if (MSGIDLIMIT_TRANS_GEN(msg)) { + return 1; + } + + /* Is KEX complete? */ + if (MSGIDLIMIT_TRANS(msg)) { + if (ssh->isKeying & WOLFSSH_PEER_IS_KEYING) { + /* MSGID_KEXINIT not allowed when keying. */ + if (msg == MSGID_KEXINIT) { + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", + msg, "client", "when keying"); + return 0; + } + + /* Error if expecting a specific message and didn't receive. */ + if (ssh->handshake && ssh->handshake->expectMsgId != MSGID_NONE) { + if (msg != ssh->handshake->expectMsgId) { + WLOG(WS_LOG_DEBUG, + "Message ID %u not the expected message %u", + msg, ssh->handshake->expectMsgId); + return 0; + } + else { + /* Got the expected message, clear expectation. */ + ssh->handshake->expectMsgId = MSGID_NONE; + return 1; + } + } + } + else { + /* MSGID_KEXINIT only allowed when not keying. */ + if (msg == MSGID_KEXINIT) { + return 1; + } + + /* All other transport KEX and ALGO messages are not allowed + * when not keying. */ + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", + msg, "client", "when not keying"); return 0; } - /* Explicitly check for the user authentication message that - * only the client sends, it shouldn't receive it. */ - if (msg == MSGID_USERAUTH_RESTRICT) { - WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by client " - "during user authentication", msg); + } + + /* Is client userauth complete? */ + if (ssh->connectState >= CONNECT_KEYED + && ssh->connectState < CONNECT_SERVER_USERAUTH_ACCEPT_DONE) { + /* The endpoints should not allow message IDs greater than or + * equal to msgid 80 before user authentication is complete. + * Per RFC 4252 section 6. */ + if (MSGIDLIMIT_POST_USERAUTH(msg)) { + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", + msg, "client", "before user authentication is complete"); return 0; } + else if (MSGIDLIMIT_AUTH(msg)) { + return 1; + } } else { - if (msg >= MSGID_USERAUTH_RESTRICT && msg < MSGID_USERAUTH_LIMIT) { - WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by client " - "after user authentication", msg); + if (MSGIDLIMIT_POST_USERAUTH(msg)) { + return 1; + } + else if (MSGIDLIMIT_AUTH(msg)) { + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", + msg, "client", "after user authentication"); return 0; } } - return 1; + + return 0; } #endif /* NO_WOLFSSH_CLIENT */ @@ -711,7 +785,7 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) * Returns 1 if allowed 0 if not allowed. */ INLINE static int IsMessageAllowed(WOLFSSH *ssh, byte msg, byte state) { - if (state == WS_MSG_SEND && !IsMessageAllowedKeying(ssh, msg)) { + if (!IsMessageAllowedKeying(ssh, msg)) { return 0; } @@ -725,6 +799,7 @@ INLINE static int IsMessageAllowed(WOLFSSH *ssh, byte msg, byte state) return IsMessageAllowedClient(ssh, msg); } #endif /* NO_WOLFSSH_CLIENT */ + (void)state; return 0; } @@ -5872,8 +5947,10 @@ static int DoKexDhReply(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) ret = GenerateKeys(ssh, hashId, !ssh->handshake->useEccMlKem); } - if (ret == WS_SUCCESS) + if (ret == WS_SUCCESS) { ret = SendNewKeys(ssh); + ssh->handshake->expectMsgId = MSGID_NEWKEYS; + } if (sigKeyBlock_ptr) WFREE(sigKeyBlock_ptr, ssh->ctx->heap, DYNTYPE_PRIVKEY); @@ -10648,8 +10725,9 @@ int SendKexInit(WOLFSSH* ssh) ret = BundlePacket(ssh); } - if (ret == WS_SUCCESS) + if (ret == WS_SUCCESS) { ret = wolfSSH_SendPacket(ssh); + } if (ret != WS_WANT_WRITE && ret != WS_SUCCESS) PurgePacket(ssh); @@ -12613,6 +12691,11 @@ int SendKexDhGexRequest(WOLFSSH* ssh) if (ret == WS_SUCCESS) ret = wolfSSH_SendPacket(ssh); + if (ret == WS_SUCCESS) { + WLOG_EXPECT_MSGID(MSGID_KEXDH_GEX_GROUP); + ssh->handshake->expectMsgId = MSGID_KEXDH_GEX_GROUP; + } + WLOG(WS_LOG_DEBUG, "Leaving SendKexDhGexRequest(), ret = %d", ret); return ret; } @@ -12701,6 +12784,7 @@ int SendKexDhInit(WOLFSSH* ssh) #endif int ret = WS_SUCCESS; byte msgId = MSGID_KEXDH_INIT; + byte expectMsgId = MSGID_KEXDH_REPLY; byte e[MAX_KEX_KEY_SZ+1]; /* plus 1 in case of padding. */ word32 eSz = (word32)sizeof(e); byte ePad = 0; @@ -12752,6 +12836,7 @@ int SendKexDhInit(WOLFSSH* ssh) generator = ssh->handshake->generator; generatorSz = ssh->handshake->generatorSz; msgId = MSGID_KEXDH_GEX_INIT; + expectMsgId = MSGID_KEXDH_GEX_REPLY; break; #endif #ifndef WOLFSSH_NO_ECDH_SHA2_NISTP256 @@ -12963,6 +13048,11 @@ int SendKexDhInit(WOLFSSH* ssh) if (ret == WS_SUCCESS) ret = wolfSSH_SendPacket(ssh); + if (ret == WS_SUCCESS) { + WLOG_EXPECT_MSGID(expectMsgId); + ssh->handshake->expectMsgId = expectMsgId; + } + WLOG(WS_LOG_DEBUG, "Leaving SendKexDhInit(), ret = %d", ret); return ret; } diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 6df5f1147..9bba76367 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -607,6 +607,7 @@ typedef struct Keys { typedef struct HandshakeInfo { + byte expectMsgId; byte kexId; byte kexIdGuess; byte kexHashId; @@ -1183,6 +1184,8 @@ enum ProcessReplyStates { enum WS_MessageIds { + MSGID_NONE = 0, + MSGID_DISCONNECT = 1, MSGID_IGNORE = 2, MSGID_UNIMPLEMENTED = 3, @@ -1238,19 +1241,56 @@ enum WS_MessageIds { }; -/* Allows the server to receive up to KEXDH GEX Request during KEX. */ -#define MSGID_KEXDH_LIMIT MSGID_KEXDH_GEX_REQUEST - -/* The endpoints should not allow message IDs greater than or - * equal to msgid 80 before user authentication is complete. - * Per RFC 4252 section 6. */ -#define MSGID_USERAUTH_LIMIT 80 +/* The following message ID ranges are described in RFC 5251, section 7. */ +enum WS_MessageIdLimits { +/* Transport Layer Protocol: */ + MSGIDLIMIT_TRANS_MIN = 1, + MSGIDLIMIT_TRANS_GEN_MIN = 1, + MSGIDLIMIT_TRANS_GEN_MAX = 19, + MSGIDLIMIT_TRANS_ALGO_MIN = 20, + MSGIDLIMIT_TRANS_ALGO_MAX = 29, + MSGIDLIMIT_TRANS_KEX_MIN = 30, + MSGIDLIMIT_TRANS_KEX_MAX = 49, + MSGIDLIMIT_TRANS_MAX = 49, +/* User Authentication Protocol: */ + MSGIDLIMIT_AUTH_MIN = 50, + MSGIDLIMIT_AUTH_GEN_MIN = 50, + MSGIDLIMIT_AUTH_GEN_MAX = 59, + MSGIDLIMIT_AUTH_METH_MIN = 60, + MSGIDLIMIT_AUTH_METH_MAX = 79, + MSGIDLIMIT_AUTH_MAX = 79, +/* Connection Protocol: */ + MSGIDLIMIT_CONN_MIN = 80, + MSGIDLIMIT_CONN_GEN_MIN = 80, + MSGIDLIMIT_CONN_GEN_MAX = 89, + MSGIDLIMIT_CONN_CHAN_MIN = 90, + MSGIDLIMIT_CONN_CHAN_MAX = 127, + MSGIDLIMIT_CONN_MAX = 127, +/* Reserved For Client Protocols: */ + MSGIDLIMIT_RESERVED_MIN = 128, + MSGIDLIMIT_RESERVED_MAX = 191, +/* Local Extensions: */ + MSGIDLIMIT_EXTENDED_MIN = 192, + MSGIDLIMIT_EXTENDED_MAX = 255, +}; -/* The client should only send the user auth request message - * (50), it should not accept it. The server should only receive - * the user auth request message, it should not accept the other - * user auth messages, it sends them. (>50) */ -#define MSGID_USERAUTH_RESTRICT 50 +/* Message ID bounds checking. */ +#define MSGIDLIMIT_BOUND(x,y,z) ((x) >= (y) && (x) <= (z)) +#define MSGIDLIMIT_COMP(x,name) \ + MSGIDLIMIT_BOUND((x),MSGIDLIMIT_##name##_MIN,MSGIDLIMIT_##name##_MAX) +#define MSGIDLIMIT_TRANS(x) MSGIDLIMIT_COMP((x),TRANS) +#define MSGIDLIMIT_TRANS_GEN(x) MSGIDLIMIT_COMP((x),TRANS_GEN) +#define MSGIDLIMIT_TRANS_ALGO(x) MSGIDLIMIT_COMP((x),TRANS_ALGO) +#define MSGIDLIMIT_TRANS_KEX(x) MSGIDLIMIT_COMP((x),TRANS_KEX) +#define MSGIDLIMIT_AUTH(x) MSGIDLIMIT_COMP((x),AUTH) +#define MSGIDLIMIT_AUTH_GEN(x) MSGIDLIMIT_COMP((x),AUTH_GEN) +#define MSGIDLIMIT_AUTH_METH(x) MSGIDLIMIT_COMP((x),AUTH_METH) +#define MSGIDLIMIT_CONN(x) MSGIDLIMIT_COMP((x),CONN) +#define MSGIDLIMIT_CONN_GEN(x) MSGIDLIMIT_COMP((x),CONN_GEN) +#define MSGIDLIMIT_CONN_CHAN(x) MSGIDLIMIT_COMP((x),CONN_CHAN) +#define MSGIDLIMIT_RESERVED(x) MSGIDLIMIT_COMP((x),RESERVED) +#define MSGIDLIMIT_EXTENDED(x) MSGIDLIMIT_COMP((x),EXTENDED) +#define MSGIDLIMIT_POST_USERAUTH(x) ((x) >= MSGIDLIMIT_CONN_MIN) #define CHANNEL_EXTENDED_DATA_STDERR WOLFSSH_EXT_DATA_STDERR diff --git a/wolfssh/log.h b/wolfssh/log.h index 8758e7d43..89bd93827 100644 --- a/wolfssh/log.h +++ b/wolfssh/log.h @@ -81,6 +81,8 @@ WOLFSSH_API void wolfSSH_Log(enum wolfSSH_LogLevel, #define WLOG(...) WC_DO_NOTHING #endif +#define WLOG_EXPECT_MSGID(x) WLOG(WS_LOG_DEBUG, "Expecting message %d", (x)) + #ifdef __cplusplus } #endif From a87ab400b3900d1e7fdda33c898094d6e3ada21d Mon Sep 17 00:00:00 2001 From: John Safranek Date: Mon, 24 Nov 2025 09:15:29 -0800 Subject: [PATCH 2/7] Guard out IsKeyingAllowed() as it is stepping on the wrong messages. --- src/internal.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/internal.c b/src/internal.c index 3b4fe5f30..0a7a2ad00 100644 --- a/src/internal.c +++ b/src/internal.c @@ -595,6 +595,7 @@ static void HandshakeInfoFree(HandshakeInfo* hs, void* heap) } +#if 0 /* RFC 4253 section 7.1, Once having sent SSH_MSG_KEXINIT the only messages * that can be sent are 1-19 (except SSH_MSG_SERVICE_REQUEST and * SSH_MSG_SERVICE_ACCEPT), 20-29 (except SSH_MSG_KEXINIT again), and 30-49 @@ -627,6 +628,7 @@ INLINE static int IsMessageAllowedKeying(WOLFSSH *ssh, byte msg) } return 1; } +#endif #ifndef NO_WOLFSSH_SERVER @@ -785,9 +787,11 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) * Returns 1 if allowed 0 if not allowed. */ INLINE static int IsMessageAllowed(WOLFSSH *ssh, byte msg, byte state) { +#if 0 if (!IsMessageAllowedKeying(ssh, msg)) { return 0; } +#endif #ifndef NO_WOLFSSH_SERVER if (ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER) { From 5ae5c250e2d9b3bfdaea4f9c73cdf4b9f2592daa Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Mon, 1 Dec 2025 15:03:53 +0000 Subject: [PATCH 3/7] Add tests and fix issues --- Makefile.am | 1 + src/include.am | 12 ++ src/internal.c | 62 +++---- tests/include.am | 8 +- tests/regress.c | 401 +++++++++++++++++++++++++++++++++++++++++++++ wolfssh/internal.h | 5 + 6 files changed, 446 insertions(+), 43 deletions(-) create mode 100644 tests/regress.c diff --git a/Makefile.am b/Makefile.am index e729d5150..d5db195d5 100644 --- a/Makefile.am +++ b/Makefile.am @@ -2,6 +2,7 @@ bin_PROGRAMS = noinst_HEADERS = lib_LTLIBRARIES = +noinst_LTLIBRARIES = noinst_PROGRAMS = nobase_include_HEADERS = check_PROGRAMS = diff --git a/src/include.am b/src/include.am index 8b70bdccd..88e535610 100644 --- a/src/include.am +++ b/src/include.am @@ -11,30 +11,42 @@ src_libwolfssh_la_SOURCES = src/ssh.c \ src_libwolfssh_la_CPPFLAGS = -DBUILDING_WOLFSSH ${AM_CPPFLAGS} src_libwolfssh_la_LDFLAGS = -no-undefined -version-info ${WOLFSSH_LIBRARY_VERSION} +noinst_LTLIBRARIES += src/libwolfssh_test.la +src_libwolfssh_test_la_SOURCES = $(src_libwolfssh_la_SOURCES) +src_libwolfssh_test_la_CPPFLAGS = -DBUILDING_WOLFSSH -DWOLFSSH_TEST_INTERNAL ${AM_CPPFLAGS} +src_libwolfssh_test_la_LDFLAGS = -no-undefined + if !BUILD_INLINE src_libwolfssh_la_SOURCES += src/misc.c +src_libwolfssh_test_la_SOURCES += src/misc.c endif if BUILD_KEYGEN src_libwolfssh_la_SOURCES += src/keygen.c +src_libwolfssh_test_la_SOURCES += src/keygen.c endif if BUILD_SCP src_libwolfssh_la_SOURCES += src/wolfscp.c +src_libwolfssh_test_la_SOURCES += src/wolfscp.c endif if BUILD_SFTP src_libwolfssh_la_SOURCES += src/wolfsftp.c +src_libwolfssh_test_la_SOURCES += src/wolfsftp.c endif if BUILD_TERM src_libwolfssh_la_SOURCES += src/wolfterm.c +src_libwolfssh_test_la_SOURCES += src/wolfterm.c endif if BUILD_AGENT src_libwolfssh_la_SOURCES += src/agent.c +src_libwolfssh_test_la_SOURCES += src/agent.c endif if BUILD_CERTS src_libwolfssh_la_SOURCES += src/certman.c +src_libwolfssh_test_la_SOURCES += src/certman.c endif diff --git a/src/internal.c b/src/internal.c index 0a7a2ad00..dfadcedea 100644 --- a/src/internal.c +++ b/src/internal.c @@ -595,42 +595,6 @@ static void HandshakeInfoFree(HandshakeInfo* hs, void* heap) } -#if 0 -/* RFC 4253 section 7.1, Once having sent SSH_MSG_KEXINIT the only messages -* that can be sent are 1-19 (except SSH_MSG_SERVICE_REQUEST and -* SSH_MSG_SERVICE_ACCEPT), 20-29 (except SSH_MSG_KEXINIT again), and 30-49 -*/ -INLINE static int IsMessageAllowedKeying(WOLFSSH *ssh, byte msg) -{ - if (ssh->isKeying == 0) { - return 1; - } - - /* case of service request or accept in 1-19 */ - if (msg == MSGID_SERVICE_REQUEST || msg == MSGID_SERVICE_ACCEPT) { - WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by during rekeying", msg); - ssh->error = WS_REKEYING; - return 0; - } - - /* case of peer resending SSH_MSG_KEXINIT */ - if ((ssh->isKeying & WOLFSSH_PEER_IS_KEYING) && msg == MSGID_KEXINIT) { - WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by during rekeying", msg); - ssh->error = WS_REKEYING; - return 0; - } - - /* case where message id greater than 49 */ - if (msg >= MSGID_USERAUTH_REQUEST) { - WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by during rekeying", msg); - ssh->error = WS_REKEYING; - return 0; - } - return 1; -} -#endif - - #ifndef NO_WOLFSSH_SERVER INLINE static int IsMessageAllowedServer(WOLFSSH *ssh, byte msg) { @@ -694,6 +658,7 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) if (msg == MSGID_SERVICE_REQUEST || msg == MSGID_USERAUTH_REQUEST) { WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", msg, "client", "ever"); + ssh->error = WS_MSGID_NOT_ALLOWED_E; return 0; } @@ -720,6 +685,7 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) if (msg == MSGID_KEXINIT) { WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", msg, "client", "when keying"); + ssh->error = WS_REKEYING; return 0; } @@ -729,6 +695,7 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) WLOG(WS_LOG_DEBUG, "Message ID %u not the expected message %u", msg, ssh->handshake->expectMsgId); + ssh->error = WS_REKEYING; return 0; } else { @@ -748,6 +715,7 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) * when not keying. */ WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", msg, "client", "when not keying"); + ssh->error = WS_MSGID_NOT_ALLOWED_E; return 0; } } @@ -761,9 +729,17 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) if (MSGIDLIMIT_POST_USERAUTH(msg)) { WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", msg, "client", "before user authentication is complete"); + ssh->error = WS_MSGID_NOT_ALLOWED_E; return 0; } else if (MSGIDLIMIT_AUTH(msg)) { + /* Do not accept any userauth messages until we've asked for auth. */ + if (ssh->connectState < CONNECT_CLIENT_USERAUTH_REQUEST_SENT) { + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", + msg, "client", "before sending userauth request"); + ssh->error = WS_MSGID_NOT_ALLOWED_E; + return 0; + } return 1; } } @@ -774,6 +750,7 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) else if (MSGIDLIMIT_AUTH(msg)) { WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", msg, "client", "after user authentication"); + ssh->error = WS_MSGID_NOT_ALLOWED_E; return 0; } } @@ -787,12 +764,6 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) * Returns 1 if allowed 0 if not allowed. */ INLINE static int IsMessageAllowed(WOLFSSH *ssh, byte msg, byte state) { -#if 0 - if (!IsMessageAllowedKeying(ssh, msg)) { - return 0; - } -#endif - #ifndef NO_WOLFSSH_SERVER if (ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER) { return IsMessageAllowedServer(ssh, msg); @@ -807,6 +778,13 @@ INLINE static int IsMessageAllowed(WOLFSSH *ssh, byte msg, byte state) return 0; } +#ifdef WOLFSSH_TEST_INTERNAL +int wolfSSH_TestIsMessageAllowed(WOLFSSH* ssh, byte msg, byte state) +{ + return IsMessageAllowed(ssh, msg, state); +} +#endif + static const char cannedKexAlgoNames[] = #if !defined(WOLFSSH_NO_NISTP256_MLKEM768_SHA256) diff --git a/tests/include.am b/tests/include.am index f14b3007a..a9aed41c0 100644 --- a/tests/include.am +++ b/tests/include.am @@ -3,7 +3,8 @@ # All paths should be given relative to the root check_PROGRAMS += tests/unit.test tests/api.test \ - tests/testsuite.test tests/kex.test + tests/testsuite.test tests/kex.test \ + tests/regress.test tests_unit_test_SOURCES = tests/unit.c tests/unit.h tests_unit_test_CPPFLAGS = -DNO_MAIN_DRIVER $(AM_CPPFLAGS) @@ -43,3 +44,8 @@ tests_kex_test_SOURCES = tests/kex.c tests/kex.h \ tests_kex_test_CPPFLAGS = -DNO_MAIN_DRIVER $(AM_CPPFLAGS) tests_kex_test_LDADD = src/libwolfssh.la tests_kex_test_DEPENDENCIES = src/libwolfssh.la + +tests_regress_test_SOURCES = tests/regress.c +tests_regress_test_CPPFLAGS = -DNO_MAIN_DRIVER -DWOLFSSH_TEST_INTERNAL $(AM_CPPFLAGS) +tests_regress_test_LDADD = src/libwolfssh_test.la +tests_regress_test_DEPENDENCIES = src/libwolfssh_test.la diff --git a/tests/regress.c b/tests/regress.c new file mode 100644 index 000000000..fbd6032e5 --- /dev/null +++ b/tests/regress.c @@ -0,0 +1,401 @@ +/* regress.c + * + * Regression coverage for message ordering / keying state handling. + * + * Copyright (C) 2025 wolfSSL Inc. + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#ifdef WOLFSSL_USER_SETTINGS + #include +#else + #include +#endif + +#include +#include +#include +#include + +#include +#include +#include + +#ifndef WOLFSSH_NO_ABORT + #define WABORT() abort() +#else + #define WABORT() +#endif + +#define PrintError(description, result) do { \ + printf("\nERROR - %s line %d failed with:", __FILE__, __LINE__); \ + printf("\n expected: "); printf description; \ + printf("\n result: "); printf result; printf("\n\n"); \ +} while(0) + +#define Fail(description, result) do { \ + PrintError(description, result); \ + WABORT(); \ +} while(0) + +#define Assert(test, description, result) if (!(test)) Fail(description, result) + +#define AssertTrue(x) Assert((x), ("%s is true", #x), (#x " => FALSE")) +#define AssertFalse(x) Assert(!(x), ("%s is false", #x), (#x " => TRUE")) +#define AssertNotNull(x) Assert((x), ("%s is not null", #x), (#x " => NULL")) +#define AssertIntEQ(x, y) do { int _x = (int)(x); int _y = (int)(y); \ + Assert(_x == _y, ("%s == %s", #x, #y), ("%d != %d", _x, _y)); } while (0) + + +static void ResetSession(WOLFSSH* ssh) +{ + if (ssh->handshake != NULL) { + WFREE(ssh->handshake, ssh->ctx->heap, DYNTYPE_HS); + ssh->handshake = NULL; + } + ssh->isKeying = 0; + ssh->connectState = CONNECT_BEGIN; + ssh->error = 0; +} + + +static HandshakeInfo* AllocHandshake(WOLFSSH* ssh) +{ + HandshakeInfo* hs; + + hs = (HandshakeInfo*)WMALLOC(sizeof(HandshakeInfo), ssh->ctx->heap, + DYNTYPE_HS); + AssertNotNull(hs); + WMEMSET(hs, 0, sizeof(HandshakeInfo)); + hs->blockSz = MIN_BLOCK_SZ; + hs->eSz = (word32)sizeof(hs->e); + hs->xSz = (word32)sizeof(hs->x); + + return hs; +} + +/* Build a minimal SSH binary packet carrying only a message ID. + * Layout: uint32 packetLen, byte padLen, payload[msgId], pad[padLen]. + * Choose padLen so total is 8-byte aligned for the clear transport case. */ +static word32 BuildPacket(byte msgId, byte* out, word32 outSz) +{ + byte padLen = 6; /* 1 (msgId) +1 (padLen) +6 = 8 */ + word32 packetLen = 1 + 1 + padLen; /* payload + padLen field + pad */ + word32 need = 4 + packetLen; + + AssertTrue(outSz >= need); + out[0] = (byte)(packetLen >> 24); + out[1] = (byte)(packetLen >> 16); + out[2] = (byte)(packetLen >> 8); + out[3] = (byte)(packetLen); + out[4] = padLen; + out[5] = msgId; + WMEMSET(out + 6, 0, padLen); + return need; +} + +static byte ParseMsgId(const byte* pkt, word32 sz) +{ + AssertTrue(sz >= 6); + return pkt[5]; +} + +/* Simple in-memory transport harness */ +typedef struct { + byte* in; /* data to feed into client */ + word32 inSz; + word32 inOff; + byte* out; /* data written by client */ + word32 outSz; + word32 outCap; +} MemIo; + +/* Minimal send/recv helpers for future transport-level tests; keep them static + * and unused for now to avoid warnings when Werror is on. */ +#ifdef WOLFSSH_TEST_MEMIO +static int MemRecv(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) +{ + (void)ssh; + MemIo* io = (MemIo*)ctx; + word32 remain = io->inSz - io->inOff; + if (remain == 0) + return WS_CBIO_ERR_WANT_READ; + if (sz > remain) + sz = remain; + WMEMCPY(buf, io->in + io->inOff, sz); + io->inOff += sz; + return (int)sz; +} + +static int MemSend(WOLFSSH* ssh, void* buf, word32 sz, void* ctx) +{ + (void)ssh; + MemIo* io = (MemIo*)ctx; + if (io->outSz + sz > io->outCap) { + return WS_CBIO_ERR_GENERAL; + } + WMEMCPY(io->out + io->outSz, buf, sz); + io->outSz += sz; + return (int)sz; +} + +static void MemIoInit(MemIo* io, byte* in, word32 inSz, byte* out, word32 outCap) +{ + io->in = in; + io->inSz = inSz; + io->inOff = 0; + io->out = out; + io->outSz = 0; + io->outCap = outCap; +} +#endif + + +/* Reject auth messages while the peer is still keying and the client + * expects the KEX reply. */ +static void TestAuthMessageBlockedDuringKeying(WOLFSSH* ssh) +{ + int allowed; + + ResetSession(ssh); + ssh->isKeying = WOLFSSH_PEER_IS_KEYING; + ssh->connectState = CONNECT_CLIENT_KEXDH_INIT_SENT; + ssh->handshake = AllocHandshake(ssh); + ssh->handshake->expectMsgId = MSGID_KEXDH_REPLY; + + allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_USERAUTH_FAILURE, + WS_MSG_RECV); + AssertFalse(allowed); + + /* The expected message must be allowed and clear the expectation. */ + allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_KEXDH_REPLY, + WS_MSG_RECV); + AssertTrue(allowed); + AssertIntEQ(ssh->handshake->expectMsgId, MSGID_NONE); +} + +/* Reject USERAUTH_FAILURE with password list during keying (password-leak PoC). */ +static void TestUserauthFailureDuringKeying(WOLFSSH* ssh) +{ + byte buf[32]; + word32 sz; + int allowed; + + ResetSession(ssh); + ssh->isKeying = WOLFSSH_PEER_IS_KEYING; + ssh->connectState = CONNECT_CLIENT_KEXDH_INIT_SENT; + ssh->handshake = AllocHandshake(ssh); + ssh->handshake->expectMsgId = MSGID_KEXDH_REPLY; + + sz = BuildPacket(MSGID_USERAUTH_FAILURE, buf, sizeof(buf)); + allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), + WS_MSG_RECV); + AssertFalse(allowed); +} + + +/* Expect an abort/error to be set when password-leak sequence hits during keying. */ +static void TestPasswordLeakAborts(WOLFSSH* ssh) +{ + byte buf[32]; + word32 sz; + int allowed; + + ResetSession(ssh); + ssh->isKeying = WOLFSSH_PEER_IS_KEYING; + ssh->connectState = CONNECT_CLIENT_KEXDH_INIT_SENT; + ssh->handshake = AllocHandshake(ssh); + ssh->handshake->expectMsgId = MSGID_KEXDH_REPLY; + + sz = BuildPacket(MSGID_USERAUTH_FAILURE, buf, sizeof(buf)); + allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), + WS_MSG_RECV); + AssertFalse(allowed); + AssertTrue(ssh->error != 0); /* should set an error / abort path */ +} + + +/* Reject USERAUTH_SUCCESS before the client has even sent a userauth request. */ +static void TestPrematureUserauthSuccess(WOLFSSH* ssh) +{ + int allowed; + + ResetSession(ssh); + ssh->connectState = CONNECT_KEYED; + + allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_USERAUTH_SUCCESS, + WS_MSG_RECV); + AssertFalse(allowed); +} + + +/* Reject a spoofed sequence: bogus USERAUTH_SUCCESS followed by channel msgs. */ +static void TestChannelSpoofSequence(WOLFSSH* ssh) +{ + byte buf[32]; + word32 sz; + int allowed; + + ResetSession(ssh); + ssh->connectState = CONNECT_KEYED; + + sz = BuildPacket(MSGID_USERAUTH_SUCCESS, buf, sizeof(buf)); + allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), + WS_MSG_RECV); + AssertFalse(allowed); + + sz = BuildPacket(MSGID_CHANNEL_OPEN_CONF, buf, sizeof(buf)); + allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), + WS_MSG_RECV); + AssertFalse(allowed); + + sz = BuildPacket(MSGID_CHANNEL_SUCCESS, buf, sizeof(buf)); + allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), + WS_MSG_RECV); + AssertFalse(allowed); + + sz = BuildPacket(MSGID_CHANNEL_DATA, buf, sizeof(buf)); + allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), + WS_MSG_RECV); + AssertFalse(allowed); +} + +/* Expect abort/error on spoofed auth+channel sequence. */ +static void TestChannelSpoofAborts(WOLFSSH* ssh) +{ + byte buf[32]; + word32 sz; + int allowed; + + ResetSession(ssh); + ssh->connectState = CONNECT_KEYED; + + sz = BuildPacket(MSGID_USERAUTH_SUCCESS, buf, sizeof(buf)); + allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), + WS_MSG_RECV); + AssertFalse(allowed); + + sz = BuildPacket(MSGID_CHANNEL_OPEN_CONF, buf, sizeof(buf)); + allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), + WS_MSG_RECV); + AssertFalse(allowed); + + AssertTrue(ssh->error != 0); +} + + +/* Reject USERAUTH_FAILURE(publickey) before any auth request (static-signature PoC). */ +static void TestPublicKeyFailureBeforeRequest(WOLFSSH* ssh) +{ + byte buf[32]; + word32 sz; + int allowed; + + ResetSession(ssh); + ssh->connectState = CONNECT_KEYED; + + sz = BuildPacket(MSGID_USERAUTH_FAILURE, buf, sizeof(buf)); + allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), + WS_MSG_RECV); + AssertFalse(allowed); +} + +/* Expect abort/error when publickey failure arrives before any request. */ +static void TestPublicKeyFailureAborts(WOLFSSH* ssh) +{ + byte buf[32]; + word32 sz; + int allowed; + + ResetSession(ssh); + ssh->connectState = CONNECT_KEYED; + + sz = BuildPacket(MSGID_USERAUTH_FAILURE, buf, sizeof(buf)); + allowed = wolfSSH_TestIsMessageAllowed(ssh, ParseMsgId(buf, sz), + WS_MSG_RECV); + AssertFalse(allowed); + AssertTrue(ssh->error != 0); +} + + +/* Reject channel messages before user authentication completes. */ +static void TestChannelBlockedBeforeAuth(WOLFSSH* ssh) +{ + int allowed; + + ResetSession(ssh); + ssh->connectState = CONNECT_KEYED; + + allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_CHANNEL_OPEN, + WS_MSG_RECV); + AssertFalse(allowed); +} + + +/* Allow channel messages after user authentication completes. */ +static void TestChannelAllowedAfterAuth(WOLFSSH* ssh) +{ + int allowed; + + ResetSession(ssh); + ssh->connectState = CONNECT_SERVER_USERAUTH_ACCEPT_DONE; + + allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_CHANNEL_OPEN, + WS_MSG_RECV); + AssertTrue(allowed); +} + + +/* Reject a peer KEXINIT once keying is in progress. */ +static void TestKexInitRejectedWhenKeying(WOLFSSH* ssh) +{ + int allowed; + + ResetSession(ssh); + ssh->isKeying = WOLFSSH_PEER_IS_KEYING; + ssh->connectState = CONNECT_SERVER_KEXINIT_DONE; + + allowed = wolfSSH_TestIsMessageAllowed(ssh, MSGID_KEXINIT, WS_MSG_RECV); + AssertFalse(allowed); +} + + +int main(int argc, char** argv) +{ + WOLFSSH_CTX* ctx; + WOLFSSH* ssh; + + (void)argc; + (void)argv; + + wolfSSH_Init(); + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + AssertNotNull(ctx); + + ssh = wolfSSH_new(ctx); + AssertNotNull(ssh); + + TestAuthMessageBlockedDuringKeying(ssh); + TestUserauthFailureDuringKeying(ssh); + TestPasswordLeakAborts(ssh); + TestPrematureUserauthSuccess(ssh); + TestChannelSpoofSequence(ssh); + TestChannelSpoofAborts(ssh); + TestPublicKeyFailureBeforeRequest(ssh); + TestPublicKeyFailureAborts(ssh); + TestChannelBlockedBeforeAuth(ssh); + TestChannelAllowedAfterAuth(ssh); + TestKexInitRejectedWhenKeying(ssh); + + ResetSession(ssh); + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + wolfSSH_Cleanup(); + + printf("regress: PASS\n"); + return 0; +} diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 9bba76367..fa3a2d590 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -1300,6 +1300,11 @@ enum WS_MessageIdLimits { #define WS_MSG_SEND 1 #define WS_MSG_RECV 2 +#ifdef WOLFSSH_TEST_INTERNAL + WOLFSSH_API int wolfSSH_TestIsMessageAllowed(WOLFSSH* ssh, byte msg, + byte state); +#endif + /* dynamic memory types */ enum WS_DynamicTypes { DYNTYPE_STRING = 500, From dee1c59f263220ecd05a88a72da104c22bef0598 Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Mon, 1 Dec 2025 15:41:13 +0000 Subject: [PATCH 4/7] Fix double-free crash and socket-close spin --- apps/wolfssh/common.c | 35 +++++++++++++++++-- apps/wolfssh/wolfssh.c | 79 ++++++++++++++++++++++++++++-------------- src/internal.c | 2 ++ tests/include.am | 3 +- tests/regress.c | 47 +++++++++++++++++++++++++ 5 files changed, 137 insertions(+), 29 deletions(-) diff --git a/apps/wolfssh/common.c b/apps/wolfssh/common.c index 283708cc1..e8e347a40 100644 --- a/apps/wolfssh/common.c +++ b/apps/wolfssh/common.c @@ -45,10 +45,12 @@ static byte* userPublicKey = userPublicKeyBuf; static const byte* userPublicKeyType = NULL; static byte userPassword[256]; static const byte* userPrivateKeyType = NULL; +static byte userPublicKeyAlloc = 0; static word32 userPublicKeySz = 0; static byte pubKeyLoaded = 0; /* was a public key loaded */ static byte userPrivateKeyBuf[1191]; static byte* userPrivateKey = userPrivateKeyBuf; +static byte userPrivateKeyAlloc = 0; static word32 userPublicKeyTypeSz = 0; static word32 userPrivateKeySz = sizeof(userPrivateKeyBuf); static word32 userPrivateKeyTypeSz = 0; @@ -670,6 +672,13 @@ int ClientUseCert(const char* certName) userPublicKeyType = publicKeyType; userPublicKeyTypeSz = (word32)WSTRLEN((const char*)publicKeyType); pubKeyLoaded = 1; + userPublicKeyAlloc = 1; + } + else { + userPublicKey = userPublicKeyBuf; + userPublicKeySz = 0; + userPublicKeyType = NULL; + userPublicKeyAlloc = 0; } #else fprintf(stderr, "Certificate support not compiled in"); @@ -687,12 +696,22 @@ int ClientSetPrivateKey(const char* privKeyName) { int ret; + userPrivateKeyAlloc = 0; userPrivateKey = NULL; /* create new buffer based on parsed input */ ret = wolfSSH_ReadKey_file(privKeyName, (byte**)&userPrivateKey, &userPrivateKeySz, (const byte**)&userPrivateKeyType, &userPrivateKeyTypeSz, &isPrivate, NULL); + if (ret == 0) { + userPrivateKeyAlloc = 1; + } + else { + userPrivateKey = userPrivateKeyBuf; + userPrivateKeySz = sizeof(userPrivateKeyBuf); + userPrivateKeyType = NULL; + } + return ret; } @@ -703,6 +722,7 @@ int ClientUsePubKey(const char* pubKeyName) { int ret; + userPublicKeyAlloc = 0; userPublicKey = NULL; /* create new buffer based on parsed input */ ret = wolfSSH_ReadKey_file(pubKeyName, &userPublicKey, &userPublicKeySz, @@ -711,6 +731,11 @@ int ClientUsePubKey(const char* pubKeyName) if (ret == 0) { pubKeyLoaded = 1; + userPublicKeyAlloc = 1; + } + else { + userPublicKey = userPublicKeyBuf; + userPublicKeySz = 0; } return ret; @@ -747,11 +772,17 @@ int ClientLoadCA(WOLFSSH_CTX* ctx, const char* caCert) void ClientFreeBuffers(void) { - if (userPublicKey != userPublicKeyBuf) { + if (userPublicKeyAlloc && userPublicKey != NULL) { WFREE(userPublicKey, NULL, DYNTYPE_PRIVKEY); + userPublicKey = userPublicKeyBuf; + userPublicKeySz = 0; + userPublicKeyAlloc = 0; } - if (userPrivateKey != userPrivateKeyBuf) { + if (userPrivateKeyAlloc && userPrivateKey != NULL) { WFREE(userPrivateKey, NULL, DYNTYPE_PRIVKEY); + userPrivateKey = userPrivateKeyBuf; + userPrivateKeySz = sizeof(userPrivateKeyBuf); + userPrivateKeyAlloc = 0; } } diff --git a/apps/wolfssh/wolfssh.c b/apps/wolfssh/wolfssh.c index dc2d83bc9..ca405d41b 100644 --- a/apps/wolfssh/wolfssh.c +++ b/apps/wolfssh/wolfssh.c @@ -220,6 +220,7 @@ typedef struct thread_args { wolfSSL_Mutex lock; byte rawMode; byte quit; + int readError; } thread_args; #ifdef _POSIX_THREADS @@ -390,6 +391,7 @@ static THREAD_RET readPeer(void* in) int bufSz = sizeof(buf); thread_args* args = (thread_args*)in; int ret = 0; + int stop = 0; int fd = wolfSSH_get_fd(args->ssh); word32 bytes; #ifdef USE_WINDOWS_API @@ -398,11 +400,6 @@ static THREAD_RET readPeer(void* in) fd_set readSet; fd_set errSet; - FD_ZERO(&readSet); - FD_ZERO(&errSet); - FD_SET(fd, &readSet); - FD_SET(fd, &errSet); - #ifdef USE_WINDOWS_API if (args->rawMode == 0) { DWORD wrd; @@ -431,9 +428,13 @@ static THREAD_RET readPeer(void* in) #endif while (ret >= 0) { - #if defined(WOLFSSH_TERM) && defined(USE_WINDOWS_API) +#if defined(WOLFSSH_TERM) && defined(USE_WINDOWS_API) (void)windowMonitor(args); - #endif +#endif + FD_ZERO(&readSet); + FD_ZERO(&errSet); + FD_SET(fd, &readSet); + FD_SET(fd, &errSet); bytes = select(fd + 1, &readSet, NULL, &errSet, NULL); wc_LockMutex(&args->lock); @@ -458,18 +459,18 @@ static THREAD_RET readPeer(void* in) } while (ret > 0); } else if (ret <= 0) { - if (ret == WS_FATAL_ERROR) { - ret = wolfSSH_get_error(args->ssh); - if (ret == WS_WANT_READ) { - continue; - } - #ifdef WOLFSSH_AGENT - else if (ret == WS_CHAN_RXD) { - byte agentBuf[512]; - int rxd, txd; - word32 channel = 0; + int err = (ret == WS_FATAL_ERROR) ? + wolfSSH_get_error(args->ssh) : ret; + if (err == WS_WANT_READ) { + bytes = 0; + } +#ifdef WOLFSSH_AGENT + else if (err == WS_CHAN_RXD) { + byte agentBuf[512]; + int rxd, txd; + word32 channel = 0; - wolfSSH_GetLastRxId(args->ssh, &channel); + wolfSSH_GetLastRxId(args->ssh, &channel); rxd = wolfSSH_ChannelIdRead(args->ssh, channel, agentBuf, sizeof(agentBuf)); if (rxd > 4) { @@ -495,9 +496,17 @@ static THREAD_RET readPeer(void* in) WMEMSET(agentBuf, 0, sizeof(agentBuf)); continue; } - #endif /* WOLFSSH_AGENT */ +#endif /* WOLFSSH_AGENT */ + else if (err == WS_CBIO_ERR_CONN_CLOSE || + err == WS_SOCKET_ERROR_E || + err == WS_MSGID_NOT_ALLOWED_E) { + args->readError = err; + ret = err; + stop = 1; + bytes = 0; } - else if (ret != WS_EOF) { + else if (err != WS_EOF) { + wc_UnLockMutex(&args->lock); err_sys("Stream read failed."); } } @@ -517,12 +526,16 @@ static THREAD_RET readPeer(void* in) } #endif } - ret = wolfSSH_stream_peek(args->ssh, buf, bufSz); - if (ret <= 0) { - bytes = 0; /* read it all */ + if (!stop) { + ret = wolfSSH_stream_peek(args->ssh, buf, bufSz); + if (ret <= 0) { + bytes = 0; /* read it all */ + } } } wc_UnLockMutex(&args->lock); + if (stop) + break; } #if !defined(WOLFSSH_NO_ECC) && defined(FP_ECC) && defined(HAVE_THREAD_LS) wc_ecc_fp_free(); /* free per thread cache */ @@ -791,7 +804,8 @@ static int config_parse_command_line(struct config* config, if (found != NULL) { *found = '\0'; if (config->user) { - free(config->user); + WFREE(config->user, NULL, 0); + config->user = NULL; } sz = WSTRLEN(cursor); config->user = (char*)WMALLOC(sz + 1, NULL, 0); @@ -818,7 +832,7 @@ static int config_parse_command_line(struct config* config, strcpy(config->hostname, cursor); } - free(dest); + WFREE(dest, NULL, 0); myoptind++; } @@ -874,18 +888,23 @@ static int config_cleanup(struct config* config) { if (config->user) { WFREE(config->user, NULL, 0); + config->user = NULL; } if (config->hostname) { WFREE(config->hostname, NULL, 0); + config->hostname = NULL; } if (config->keyFile) { WFREE(config->keyFile, NULL, 0); + config->keyFile = NULL; } if (config->pubKeyFile) { WFREE(config->pubKeyFile, NULL, 0); + config->pubKeyFile = NULL; } if (config->command) { WFREE(config->command, NULL, 0); + config->command = NULL; } return 0; @@ -900,6 +919,7 @@ static THREAD_RETURN WOLFSSH_THREAD wolfSSH_Client(void* args) SOCKADDR_IN_T clientAddr; socklen_t clientAddrSz = sizeof(clientAddr); int ret = 0; + int ioErr = 0; byte keepOpen = 1; #ifdef USE_WINDOWS_API byte rawMode = 0; @@ -1037,6 +1057,7 @@ static THREAD_RETURN WOLFSSH_THREAD wolfSSH_Client(void* args) wc_InitMutex(&arg.lock); arg.ssh = ssh; + arg.readError = 0; #ifdef WOLFSSH_TERM arg.quit = 0; #if (defined(__OSX__) || defined(__APPLE__)) @@ -1082,12 +1103,14 @@ static THREAD_RETURN WOLFSSH_THREAD wolfSSH_Client(void* args) sem_destroy(&windowSem); #endif #endif /* WOLFSSH_TERM */ + ioErr = arg.readError; #elif defined(_MSC_VER) thread_args arg; HANDLE thread[2]; arg.ssh = ssh; arg.rawMode = rawMode; + arg.readError = 0; wc_InitMutex(&arg.lock); if (config.command) { @@ -1107,6 +1130,7 @@ static THREAD_RETURN WOLFSSH_THREAD wolfSSH_Client(void* args) WaitForSingleObject(thread[1], INFINITE); CloseHandle(thread[0]); CloseHandle(thread[1]); + ioErr = arg.readError; #else err_sys("No threading to use"); #endif @@ -1139,10 +1163,13 @@ static THREAD_RETURN WOLFSSH_THREAD wolfSSH_Client(void* args) #if defined(WOLFSSH_TERM) || defined(WOLFSSH_SHELL) ((func_args*)args)->return_code = wolfSSH_GetExitStatus(ssh); #endif + if (ioErr != 0 && ((func_args*)args)->return_code == 0) { + ((func_args*)args)->return_code = 1; + } wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); - if (ret != WS_SUCCESS && ret != WS_SOCKET_ERROR_E) { + if ((ret != WS_SUCCESS && ret != WS_SOCKET_ERROR_E) || ioErr != 0) { WLOG(WS_LOG_DEBUG, "Closing client stream failed"); #if defined(WOLFSSH_TERM) || defined(WOLFSSH_SHELL) /* override return value, do not want to return success if connection diff --git a/src/internal.c b/src/internal.c index dfadcedea..5a162f3ce 100644 --- a/src/internal.c +++ b/src/internal.c @@ -691,6 +691,8 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg) /* Error if expecting a specific message and didn't receive. */ if (ssh->handshake && ssh->handshake->expectMsgId != MSGID_NONE) { + /* The explicit expectMsgId check supersedes the old + * IsMessageAllowedKeying() stub for rekey filtering. */ if (msg != ssh->handshake->expectMsgId) { WLOG(WS_LOG_DEBUG, "Message ID %u not the expected message %u", diff --git a/tests/include.am b/tests/include.am index a9aed41c0..644b4af59 100644 --- a/tests/include.am +++ b/tests/include.am @@ -45,7 +45,8 @@ tests_kex_test_CPPFLAGS = -DNO_MAIN_DRIVER $(AM_CPPFLAGS) tests_kex_test_LDADD = src/libwolfssh.la tests_kex_test_DEPENDENCIES = src/libwolfssh.la -tests_regress_test_SOURCES = tests/regress.c +tests_regress_test_SOURCES = tests/regress.c apps/wolfssh/common.c \ + apps/wolfssh/common.h tests_regress_test_CPPFLAGS = -DNO_MAIN_DRIVER -DWOLFSSH_TEST_INTERNAL $(AM_CPPFLAGS) tests_regress_test_LDADD = src/libwolfssh_test.la tests_regress_test_DEPENDENCIES = src/libwolfssh_test.la diff --git a/tests/regress.c b/tests/regress.c index fbd6032e5..0fe4218f2 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -19,10 +19,13 @@ #include #include #include +#include +#include #include #include #include +#include "apps/wolfssh/common.h" #ifndef WOLFSSH_NO_ABORT #define WABORT() abort() @@ -362,6 +365,44 @@ static void TestKexInitRejectedWhenKeying(WOLFSSH* ssh) AssertFalse(allowed); } +/* Ensure client buffer cleanup tolerates multiple invocations after allocs. */ +static void TestClientBuffersIdempotent(void) +{ + int ret; + + ret = ClientUsePubKey("keys/gretel-key-rsa.pub"); + AssertIntEQ(ret, 0); + ret = ClientSetPrivateKey("keys/gretel-key-rsa.pem"); + AssertIntEQ(ret, 0); + + ClientFreeBuffers(); + /* Should be safe to call again without double free. */ + ClientFreeBuffers(); +} + +/* Simulate Ctrl+D (stdin EOF) during password prompt; expect failure but no crash. */ +static void TestPasswordEofNoCrash(void) +{ + WS_UserAuthData auth; + int savedStdin, devNull, ret; + + WMEMSET(&auth, 0, sizeof(auth)); + + savedStdin = dup(STDIN_FILENO); + devNull = open("/dev/null", O_RDONLY); + AssertTrue(devNull >= 0); + AssertTrue(dup2(devNull, STDIN_FILENO) >= 0); + + ret = ClientUserAuth(WOLFSSH_USERAUTH_PASSWORD, &auth, NULL); + AssertIntEQ(ret, WOLFSSH_USERAUTH_FAILURE); + + close(devNull); + dup2(savedStdin, STDIN_FILENO); + close(savedStdin); + + ClientFreeBuffers(); +} + int main(int argc, char** argv) { @@ -390,6 +431,12 @@ int main(int argc, char** argv) TestChannelBlockedBeforeAuth(ssh); TestChannelAllowedAfterAuth(ssh); TestKexInitRejectedWhenKeying(ssh); + TestClientBuffersIdempotent(); + TestPasswordEofNoCrash(); + + /* TODO: add app-level regressions that simulate stdin EOF/password + * prompts and mid-session socket closes once the test harness can + * drive the wolfssh client without real sockets/tty. */ ResetSession(ssh); wolfSSH_free(ssh); From 2e5484f36f279484ca06e02ef34ddd420fc0cf87 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Thu, 4 Dec 2025 15:52:39 -0800 Subject: [PATCH 5/7] Server Out of Order Message Handling 1. Updated the checking for the server to be more like the client's checking. --- src/internal.c | 59 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/src/internal.c b/src/internal.c index 5a162f3ce..c3e6c41e3 100644 --- a/src/internal.c +++ b/src/internal.c @@ -601,15 +601,71 @@ INLINE static int IsMessageAllowedServer(WOLFSSH *ssh, byte msg) /* Only the server should send these messages, never receive. */ if (msg == MSGID_SERVICE_ACCEPT) { WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", - msg, "client", "ever"); + msg, "server", "ever"); + ssh->error = WS_MSGID_NOT_ALLOWED_E; return 0; } + if (msg == MSGID_SERVICE_REQUEST) { + if (ssh->acceptState == ACCEPT_KEYED) { + return 1; + } + else { + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", + msg, "server", "after starting user auth"); + return 0; + } + } + /* Transport Layer Generic messages are always allowed. */ if (MSGIDLIMIT_TRANS_GEN(msg)) { return 1; } + /* Is KEX complete? */ + if (MSGIDLIMIT_TRANS(msg)) { + if (ssh->isKeying & WOLFSSH_PEER_IS_KEYING) { + /* MSGID_KEXINIT not allowed when keying. */ + if (msg == MSGID_KEXINIT) { + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", + msg, "server", "when keying"); + ssh->error = WS_REKEYING; + return 0; + } + + /* Error if expecting a specific message and didn't receive. */ + if (ssh->handshake && ssh->handshake->expectMsgId != MSGID_NONE) { + /* The explicit expectMsgId check supersedes the old + * IsMessageAllowedKeying() stub for rekey filtering. */ + if (msg != ssh->handshake->expectMsgId) { + WLOG(WS_LOG_DEBUG, + "Message ID %u not the expected message %u", + msg, ssh->handshake->expectMsgId); + ssh->error = WS_REKEYING; + return 0; + } + else { + /* Got the expected message, clear expectation. */ + ssh->handshake->expectMsgId = MSGID_NONE; + return 1; + } + } + } + else { + /* MSGID_KEXINIT only allowed when not keying. */ + if (msg == MSGID_KEXINIT) { + return 1; + } + + /* All other transport KEX and ALGO messages are not allowed + * when not keying. */ + WLOG(WS_LOG_DEBUG, "Message ID %u not allowed by %s %s", + msg, "server", "when not keying"); + ssh->error = WS_MSGID_NOT_ALLOWED_E; + return 0; + } + } + /* Has client userauth started? */ /* Allows the server to receive up to KEXDH GEX Request during KEX. */ if (ssh->acceptState < ACCEPT_KEYED) { @@ -617,6 +673,7 @@ INLINE static int IsMessageAllowedServer(WOLFSSH *ssh, byte msg) return 0; } } + /* Is server userauth complete? */ if (ssh->acceptState < ACCEPT_SERVER_USERAUTH_SENT) { /* The server should only receive the user auth request message, From 03ca9221a36a2a7160a99aeee7bc7ea8368013b5 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Tue, 9 Dec 2025 09:17:47 -0800 Subject: [PATCH 6/7] Zephyr 1. Exclude the file regress.c from the Zephyr testing sample. The test is covered in many other environments already. The test needs some retooling to fit in with the Zephyr build, as it is a standalone application with a main() function and it depends on a testing build of libwolfssh. 2. Whitespace. --- zephyr/samples/tests/CMakeLists.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/zephyr/samples/tests/CMakeLists.txt b/zephyr/samples/tests/CMakeLists.txt index 4f563b495..3de00b745 100644 --- a/zephyr/samples/tests/CMakeLists.txt +++ b/zephyr/samples/tests/CMakeLists.txt @@ -4,8 +4,12 @@ find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(wolfssl_tests) FILE(GLOB app_sources ../../../tests/*.c ../../../examples/client/client.c - ../../../examples/client/common.c ../../../examples/echoserver/echoserver.c - ../../../examples/sftpclient/sftpclient.c tests.c) + ../../../examples/client/common.c ../../../examples/echoserver/echoserver.c + ../../../examples/sftpclient/sftpclient.c tests.c) +# Remove the file regress.c from the list of test app sources. The regression +# test is covered in many other environments. +list(REMOVE_ITEM app_sources + "${CMAKE_CURRENT_SOURCE_DIR}/../../../tests/regress.c") target_sources(app PRIVATE ${app_sources}) add_definitions(-DWOLFSSL_ZEPHYR) add_definitions(-DWOLFSSL_USER_SETTINGS) From 2086f34ff37911a1fa388f843ee9d336e740bf44 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Thu, 11 Dec 2025 10:43:35 -0800 Subject: [PATCH 7/7] Out of Order Message Handling 1. Always set the expected message right before sending. If the send fails, it is either because the socket is closing, or it is wanting to block. If it is wanting to block, we still want to check the next message as expected. --- src/internal.c | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/internal.c b/src/internal.c index c3e6c41e3..fb67a53c8 100644 --- a/src/internal.c +++ b/src/internal.c @@ -5989,8 +5989,9 @@ static int DoKexDhReply(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) } if (ret == WS_SUCCESS) { - ret = SendNewKeys(ssh); ssh->handshake->expectMsgId = MSGID_NEWKEYS; + WLOG_EXPECT_MSGID(ssh->handshake->expectMsgId); + ret = SendNewKeys(ssh); } if (sigKeyBlock_ptr) @@ -12729,12 +12730,10 @@ int SendKexDhGexRequest(WOLFSSH* ssh) ret = BundlePacket(ssh); } - if (ret == WS_SUCCESS) - ret = wolfSSH_SendPacket(ssh); - if (ret == WS_SUCCESS) { WLOG_EXPECT_MSGID(MSGID_KEXDH_GEX_GROUP); ssh->handshake->expectMsgId = MSGID_KEXDH_GEX_GROUP; + ret = wolfSSH_SendPacket(ssh); } WLOG(WS_LOG_DEBUG, "Leaving SendKexDhGexRequest(), ret = %d", ret); @@ -13086,12 +13085,10 @@ int SendKexDhInit(WOLFSSH* ssh) ret = BundlePacket(ssh); } - if (ret == WS_SUCCESS) - ret = wolfSSH_SendPacket(ssh); - if (ret == WS_SUCCESS) { WLOG_EXPECT_MSGID(expectMsgId); ssh->handshake->expectMsgId = expectMsgId; + ret = wolfSSH_SendPacket(ssh); } WLOG(WS_LOG_DEBUG, "Leaving SendKexDhInit(), ret = %d", ret);