diff --git a/src/wolfscp.c b/src/wolfscp.c index 62591596e..7c184adba 100644 --- a/src/wolfscp.c +++ b/src/wolfscp.c @@ -63,6 +63,17 @@ const char scpError[] = "scp error: %s, %d"; const char scpState[] = "scp state: %s"; +/* Logs an SCP state error, except a non-blocking WS_WANT_READ/WS_WANT_WRITE, + * which is normal back-pressure the caller retries (matching the quiet + * SCP_SEND_FILE handling) rather than a transfer failure. */ +static void LogScpStateError(const char* state, int ret) +{ + (void)state; + if (ret != WS_WANT_READ && ret != WS_WANT_WRITE) + WLOG(WS_LOG_ERROR, scpError, state, ret); +} + + static int _DumpExtendedData(WOLFSSH* ssh) { byte msg[WOLFSSH_DEFAULT_EXTDATA_SZ]; @@ -79,6 +90,122 @@ static int _DumpExtendedData(WOLFSSH* ssh) } +/* Sends sz bytes from data, completing any rekey or full window that blocks + * the send. + * + * Attempts the send; on a WS_WINDOW_FULL (peer window reached zero) or + * WS_REKEYING result, drives wolfSSH_worker once to pick up the peer's window + * adjust or to advance the rekey, then retries the send. The retry is what + * clears the status: wolfSSH_worker does not reset ssh->error, so a loop that + * keyed on ssh->error staying WS_WINDOW_FULL would keep driving the worker + * after the window already reopened and stall the transfer. This mirrors the + * original SCP_SEND_FILE handling, extended to the control-message senders so a + * rekey or full window during a timestamp/header/confirmation send is no longer + * reported as fatal. Termination needs no retry count: in blocking mode + * wolfSSH_worker waits on the socket for the peer's packet; in non-blocking + * mode it returns WS_WANT_READ/WS_WANT_WRITE, which is returned to the caller + * so a stalled rekey cannot spin forever. Returns the byte count from + * wolfSSH_stream_send (>= 0) or a negative error code, matching the wrapped + * call so callers keep their existing return handling. + */ +static int ScpStreamSend(WOLFSSH* ssh, byte* data, word32 sz) +{ + int ret = WS_SUCCESS; + int err; + int done = 0; + + if (ssh == NULL || data == NULL) + return WS_BAD_ARGUMENT; + + /* Flush queued output before sending. Otherwise a KEXINIT enqueued by a + * rekey triggered on the prior send sits unsent while wolfSSH_worker runs + * DoReceive before its own flush, blocking on the socket with the peer + * waiting for our KEXINIT. */ + if (wolfSSH_OutputPending(ssh)) { + ret = wolfSSH_SendPacket(ssh); + if (ret < 0) + return ret; + } + + while (!done) { + ret = wolfSSH_stream_send(ssh, data, sz); + if (ret >= 0) { + /* sent (full or partial byte count); exits via while (!done) */ + done = 1; + } + else { + err = wolfSSH_get_error(ssh); + if (err == WS_WINDOW_FULL || err == WS_REKEYING) { + ret = wolfSSH_worker(ssh, NULL); + err = wolfSSH_get_error(ssh); + /* A non-blocking want surfaces as a generic worker error with + * the want recorded in ssh->error (see GetInputData). Return it + * so the caller retries instead of tearing down the send. */ + if (err == WS_WANT_READ || err == WS_WANT_WRITE) + return err; + /* Only a rekey/window/channel-data status means "keep driving". + * Any other negative status is fatal and returned. */ + if (ret < 0 && ret != WS_REKEYING && ret != WS_WINDOW_FULL + && ret != WS_CHAN_RXD) + return ret; + /* otherwise loop and retry the send, which clears the status */ + } + else { + /* Fatal or other final status; return it unchanged. */ + done = 1; + } + } + } + + return ret; +} + + +/* Reads up to sz bytes into data, completing any rekey that fires mid-read. + * + * Flushes queued output before reading so a KEXINIT enqueued by a receive-side + * highwater rekey is actually sent, otherwise the peer can wait for our KEXINIT + * while we block on the read. On a read that fails with WS_REKEYING the worker + * is driven to finish the rekey and the read is retried. The helper is + * error-code transparent: every other status (WS_EOF, WS_EXTDATA, + * WS_CHANNEL_CLOSED, WS_SOCKET_ERROR_E, WS_WANT_READ/WS_WANT_WRITE, byte count) + * is returned unchanged so each caller keeps its existing branch handling. + */ +static int ScpStreamRead(WOLFSSH* ssh, byte* data, word32 sz) +{ + int ret = WS_SUCCESS; + int done = 0; + + if (ssh == NULL || data == NULL) + return WS_BAD_ARGUMENT; + + do { + if (wolfSSH_OutputPending(ssh)) { + ret = wolfSSH_SendPacket(ssh); + if (ret < 0) + return ret; + } + + ret = wolfSSH_stream_read(ssh, data, sz); + if (ret < 0 && wolfSSH_get_error(ssh) == WS_REKEYING) { + /* Drive the rekey to completion, then retry the read. A worker + * status that is not rekey or channel data means the rekey stalled + * or a non-blocking want occurred, so return it rather than + * spin. */ + ret = wolfSSH_worker(ssh, NULL); + if (ret < 0 && ret != WS_CHAN_RXD + && wolfSSH_get_error(ssh) != WS_REKEYING) + return ret; + } + else { + done = 1; + } + } while (!done); + + return ret; +} + + int DoScpSink(WOLFSSH* ssh) { int ret = WS_SUCCESS; @@ -112,7 +239,7 @@ int DoScpSink(WOLFSSH* ssh) break; } - WLOG(WS_LOG_ERROR, scpError, "RECEIVE_MESSAGE", ret); + LogScpStateError("RECEIVE_MESSAGE", ret); break; } @@ -153,7 +280,7 @@ int DoScpSink(WOLFSSH* ssh) WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_CONFIRMATION"); if ( (ret = SendScpConfirmation(ssh)) < WS_SUCCESS) { - WLOG(WS_LOG_ERROR, scpError, "SEND_CONFIRMATION", ret); + LogScpStateError("SEND_CONFIRMATION", ret); break; } @@ -164,7 +291,7 @@ int DoScpSink(WOLFSSH* ssh) WLOG(WS_LOG_DEBUG, scpState, "SCP_RECEIVE_CONFIRMATION"); if ( (ret = ReceiveScpConfirmation(ssh)) < WS_SUCCESS) { - WLOG(WS_LOG_ERROR, scpError, "RECEIVE_CONFIRMATION", ret); + LogScpStateError("RECEIVE_CONFIRMATION", ret); break; } @@ -175,7 +302,7 @@ int DoScpSink(WOLFSSH* ssh) WLOG(WS_LOG_DEBUG, scpState, "SCP_RECEIVE_FILE"); if ( (ret = ReceiveScpFile(ssh)) < WS_SUCCESS) { - WLOG(WS_LOG_ERROR, scpError, "RECEIVE_FILE", ret); + LogScpStateError("RECEIVE_FILE", ret); break; } @@ -283,13 +410,17 @@ static int SendScpTimestamp(WOLFSSH* ssh) #endif bufSz = (int)WSTRLEN(buf); - ret = wolfSSH_stream_send(ssh, (byte*)buf, bufSz); - if (ret != bufSz) { - ret = WS_FATAL_ERROR; - } else { + ret = ScpStreamSend(ssh, (byte*)buf, bufSz); + if (ret == bufSz) { WLOG(WS_LOG_DEBUG, "scp: sent timestamp: %s", buf); ret = WS_SUCCESS; } + /* A non-blocking want is left as WS_WANT_READ/WS_WANT_WRITE for the caller + * to retry (nothing is queued yet, so the resend is clean), consistent with + * the SCP_SEND_FILE data path; only a real short send is fatal. */ + else if (ret != WS_WANT_READ && ret != WS_WANT_WRITE) { + ret = WS_FATAL_ERROR; + } return ret; } @@ -324,13 +455,16 @@ static int SendScpFileHeader(WOLFSSH* ssh) return WS_BAD_ARGUMENT; #endif bufSz = (int)WSTRLEN(filehdr); - ret = wolfSSH_stream_send(ssh, (byte*)filehdr, bufSz); - if (ret != bufSz) { - ret = WS_FATAL_ERROR; - } else { + ret = ScpStreamSend(ssh, (byte*)filehdr, bufSz); + if (ret == bufSz) { WLOG(WS_LOG_DEBUG, "scp: sent file header: %s", filehdr); ret = WS_SUCCESS; } + /* leave a non-blocking want for the caller to retry; only a real short + * send is fatal (see SendScpTimestamp) */ + else if (ret != WS_WANT_READ && ret != WS_WANT_WRITE) { + ret = WS_FATAL_ERROR; + } return ret; } @@ -357,13 +491,16 @@ static int SendScpEnterDirectory(WOLFSSH* ssh) bufSz = (int)WSTRLEN(buf); - ret = wolfSSH_stream_send(ssh, (byte*)buf, bufSz); - if (ret != bufSz) { - ret = WS_FATAL_ERROR; - } else { + ret = ScpStreamSend(ssh, (byte*)buf, bufSz); + if (ret == bufSz) { WLOG(WS_LOG_DEBUG, "scp: sent directory msg: %s", buf); ret = WS_SUCCESS; } + /* leave a non-blocking want for the caller to retry; only a real short + * send is fatal (see SendScpTimestamp) */ + else if (ret != WS_WANT_READ && ret != WS_WANT_WRITE) { + ret = WS_FATAL_ERROR; + } return ret; } @@ -383,13 +520,16 @@ static int SendScpExitDirectory(WOLFSSH* ssh) buf[0] = 'E'; buf[1] = '\n'; - ret = wolfSSH_stream_send(ssh, (byte*)buf, sizeof(buf)); - if (ret != sizeof(buf)) { - ret = WS_FATAL_ERROR; - } else { + ret = ScpStreamSend(ssh, (byte*)buf, sizeof(buf)); + if (ret == sizeof(buf)) { WLOG(WS_LOG_DEBUG, "scp: sent end directory msg: E"); ret = WS_SUCCESS; } + /* leave a non-blocking want for the caller to retry; only a real short + * send is fatal (see SendScpTimestamp) */ + else if (ret != WS_WANT_READ && ret != WS_WANT_WRITE) { + ret = WS_FATAL_ERROR; + } return ret; } @@ -436,7 +576,7 @@ int DoScpSource(WOLFSSH* ssh) WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_CONFIRMATION"); if ( (ret = SendScpConfirmation(ssh)) < WS_SUCCESS) { - WLOG(WS_LOG_ERROR, scpError, "SEND_CONFIRMATION", ret); + LogScpStateError("SEND_CONFIRMATION", ret); break; } @@ -447,7 +587,7 @@ int DoScpSource(WOLFSSH* ssh) WLOG(WS_LOG_DEBUG, scpState, "SCP_CONFIRMATION_WITH_RECEIPT"); if ( (ret = SendScpConfirmation(ssh)) < WS_SUCCESS) { - WLOG(WS_LOG_ERROR, scpError, "SEND_CONFIRMATION", ret); + LogScpStateError("SEND_CONFIRMATION", ret); break; } @@ -459,8 +599,7 @@ int DoScpSource(WOLFSSH* ssh) "SCP_RECEIVE_CONFIRMATION_WITH_RECEIPT"); if ( (ret = ReceiveScpConfirmation(ssh)) < WS_SUCCESS) { - WLOG(WS_LOG_ERROR, scpError, - "RECEIVE_CONFIRMATION_WITH_RECEIPT", ret); + LogScpStateError("RECEIVE_CONFIRMATION_WITH_RECEIPT", ret); break; } @@ -471,7 +610,7 @@ int DoScpSource(WOLFSSH* ssh) WLOG(WS_LOG_DEBUG, scpState, "SCP_RECEIVE_CONFIRMATION"); if ( (ret = ReceiveScpConfirmation(ssh)) < WS_SUCCESS) { - WLOG(WS_LOG_ERROR, scpError, "RECEIVE_CONFIRMATION", ret); + LogScpStateError("RECEIVE_CONFIRMATION", ret); break; } @@ -540,7 +679,7 @@ int DoScpSource(WOLFSSH* ssh) WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_TIMESTAMP"); if ( (ret = SendScpTimestamp(ssh)) < WS_SUCCESS) { - WLOG(WS_LOG_ERROR, scpError, "SEND_TIMESTAMP", ret); + LogScpStateError("SEND_TIMESTAMP", ret); break; } @@ -552,7 +691,7 @@ int DoScpSource(WOLFSSH* ssh) WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_ENTER_DIRECTORY"); if ( (ret = SendScpEnterDirectory(ssh)) < WS_SUCCESS) { - WLOG(WS_LOG_ERROR, scpError, "SEND_ENTER_DIRECTORY", ret); + LogScpStateError("SEND_ENTER_DIRECTORY", ret); break; } @@ -564,7 +703,7 @@ int DoScpSource(WOLFSSH* ssh) WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_EXIT_DIRECTORY"); if ( (ret = SendScpExitDirectory(ssh)) < WS_SUCCESS) { - WLOG(WS_LOG_ERROR, scpError, "SEND_EXIT_DIRECTORY", ret); + LogScpStateError("SEND_EXIT_DIRECTORY", ret); break; } @@ -576,7 +715,7 @@ int DoScpSource(WOLFSSH* ssh) WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_EXIT_DIRECTORY_FINAL"); if ( (ret = SendScpExitDirectory(ssh)) < WS_SUCCESS) { - WLOG(WS_LOG_ERROR, scpError, "SEND_EXIT_DIRECTORY", ret); + LogScpStateError("SEND_EXIT_DIRECTORY", ret); break; } @@ -587,7 +726,7 @@ int DoScpSource(WOLFSSH* ssh) WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_FILE_HEADER"); if ( (ret = SendScpFileHeader(ssh)) < WS_SUCCESS) { - WLOG(WS_LOG_ERROR, scpError, "SEND_FILE_HEADER", ret); + LogScpStateError("SEND_FILE_HEADER", ret); break; } @@ -598,12 +737,15 @@ int DoScpSource(WOLFSSH* ssh) case SCP_SEND_FILE: WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_FILE"); - ret = wolfSSH_stream_send(ssh, ssh->scpFileBuffer, - ssh->scpBufferedSz); - if (ret == WS_WINDOW_FULL || ret == WS_REKEYING) { - ret = wolfSSH_worker(ssh, NULL); - if (ret == WS_SUCCESS || ssh->error == WS_WANT_READ) - continue; + ret = ScpStreamSend(ssh, ssh->scpFileBuffer, + ssh->scpBufferedSz); + if (ret == WS_WANT_READ || ret == WS_WANT_WRITE) { + /* ScpStreamSend already drove the worker through any rekey + * or full window; a non-blocking want means the socket is + * not ready. Surface it for the caller to retry without + * closing the file mid-transfer. scpBufferedSz and + * scpFileOffset are preserved for the next call. */ + break; } if (ret == WS_EXTDATA) { _DumpExtendedData(ssh); @@ -718,14 +860,14 @@ int DoScpRequest(WOLFSSH* ssh) case SCP_SINK: WLOG(WS_LOG_DEBUG, scpState, "SCP_SINK"); if ( (ret = DoScpSink(ssh)) < WS_SUCCESS) { - WLOG(WS_LOG_ERROR, scpError, "SCP_SINK", ret); + LogScpStateError("SCP_SINK", ret); } break; case SCP_SOURCE: WLOG(WS_LOG_DEBUG, scpState, "SCP_SOURCE"); if ( (ret = DoScpSource(ssh)) < WS_SUCCESS) { - WLOG(WS_LOG_ERROR, scpError, "SCP_SOURCE", ret); + LogScpStateError("SCP_SOURCE", ret); } break; } @@ -736,7 +878,7 @@ int DoScpRequest(WOLFSSH* ssh) /* Peer MUST send back a SSH_MSG_CHANNEL_CLOSE unless already sent*/ - ret = wolfSSH_stream_read(ssh, buf, 1); + ret = ScpStreamRead(ssh, buf, 1); if (ret == WS_SOCKET_ERROR_E || ret == WS_CHANNEL_CLOSED) { WLOG(WS_LOG_DEBUG, scpState, "Peer hung up, but SCP is done"); ret = WS_SUCCESS; @@ -1395,6 +1537,33 @@ int ReceiveScpMessage(WOLFSSH* ssh) return WS_BUFFER_E; } + /* Flush queued output before polling. A KEXINIT enqueued by a rekey + * would otherwise sit unsent while wolfSSH_worker runs DoReceive before + * its own flush, deadlocking against a peer that waits for our + * KEXINIT. */ + if (wolfSSH_OutputPending(ssh)) { + ret = wolfSSH_SendPacket(ssh); + if (ret < 0) + return ret; + } + + /* If channel data is already buffered, read it directly rather than + * polling the socket. A control message delivered into the channel + * buffer while a rekey was completing leaves wolfSSH_worker returning + * the rekey status (not WS_CHAN_RXD), so without this the buffered + * message is never read and the next worker blocks on the socket. */ + if (wolfSSH_stream_peek(ssh, NULL, 1) > 0) { + sz = wolfSSH_stream_read(ssh, buf + ssh->scpRecvMsgSz, + DEFAULT_SCP_MSG_SZ - ssh->scpRecvMsgSz); + /* match the WS_CHAN_RXD branch below: return on a non-positive + * read so a hypothetical zero cannot re-loop this peek path */ + if (sz <= 0) + return sz; + ssh->scpRecvMsgSz += sz; + sz = ssh->scpRecvMsgSz; + continue; + } + err = wolfSSH_worker(ssh, &lastChannel); if (err < 0) { int rc; @@ -1517,7 +1686,7 @@ int ReceiveScpFile(WOLFSSH* ssh) } if (ret == WS_SUCCESS) { - ret = wolfSSH_stream_read(ssh, ssh->scpFileBuffer, partSz); + ret = ScpStreamRead(ssh, ssh->scpFileBuffer, partSz); if (ret > 0) { ssh->scpFileBufferSz = ret; } @@ -1559,11 +1728,8 @@ int SendScpConfirmation(WOLFSSH* ssh) /* skip first byte for accurate strlen, may be 0 */ msgSz = (int)XSTRLEN(msg + 1) + 1; - ret = wolfSSH_stream_send(ssh, (byte*)msg, msgSz); - if (ret != msgSz || ssh->scpConfirm == WS_SCP_ABORT) { - ret = WS_FATAL_ERROR; - - } else { + ret = ScpStreamSend(ssh, (byte*)msg, msgSz); + if (ret == msgSz && ssh->scpConfirm != WS_SCP_ABORT) { ret = WS_SUCCESS; WLOG(WS_LOG_DEBUG, "scp: sent confirmation (code: %d)", msg[0]); @@ -1573,6 +1739,11 @@ int SendScpConfirmation(WOLFSSH* ssh) ssh->scpConfirmMsgSz = 0; } } + /* leave a non-blocking want for the caller to retry; a real short send or a + * peer abort is fatal (see SendScpTimestamp) */ + else if (ret != WS_WANT_READ && ret != WS_WANT_WRITE) { + ret = WS_FATAL_ERROR; + } return ret; } @@ -1587,7 +1758,7 @@ int ReceiveScpConfirmation(WOLFSSH* ssh) return WS_BAD_ARGUMENT; WMEMSET(msg, 0, sizeof(msg)); - msgSz = wolfSSH_stream_read(ssh, msg, DEFAULT_SCP_MSG_SZ); + msgSz = ScpStreamRead(ssh, msg, DEFAULT_SCP_MSG_SZ); if (msgSz < 0) { if (wolfSSH_get_error(ssh) == WS_EXTDATA) diff --git a/tests/api.c b/tests/api.c index 5131a7721..57a124b96 100644 --- a/tests/api.c +++ b/tests/api.c @@ -49,7 +49,7 @@ #include #endif -#ifdef WOLFSSH_SFTP +#if defined(WOLFSSH_SFTP) || defined(WOLFSSH_SCP) #define WOLFSSH_TEST_LOCKING #ifndef SINGLE_THREADED #define WOLFSSH_TEST_THREADING @@ -62,6 +62,9 @@ #endif #include #include "tests/api.h" +#ifdef WOLFSSH_TEST_ECHOSERVER + #include "examples/echoserver/echoserver.h" +#endif /* for echoserver test cases */ int myoptind = 0; @@ -1249,8 +1252,6 @@ static void test_wolfSSH_agent_signrequest_success(void) #if defined(WOLFSSH_SFTP) && !defined(NO_WOLFSSH_CLIENT) && \ !defined(SINGLE_THREADED) -#include "examples/echoserver/echoserver.h" - byte userPassword[256]; static int sftpUserAuth(byte authType, WS_UserAuthData* authData, void* ctx) @@ -2092,6 +2093,342 @@ static void test_wolfSSH_SFTP_SetDefaultPath(void) { ; } #endif /* WOLFSSH_SFTP && !NO_WOLFSSH_CLIENT && !SINGLE_THREADED */ +#if defined(WOLFSSH_SCP) && !defined(NO_WOLFSSH_CLIENT) && \ + !defined(SINGLE_THREADED) && !defined(NO_FILESYSTEM) && \ + !defined(WOLFSSH_SCP_USER_CALLBACKS) && !defined(WOLFSSH_ZEPHYR) + +/* Upper bound on non-blocking retry iterations. A legitimate transfer across a + * forced rekey completes in well under this; the bound keeps a regression from + * hanging CI by tripping the AssertIntLE below instead. */ +#define SCP_REKEY_MAX_TRIES 100 + +/* Payload larger than the forced highwater so the transfer straddles it. */ +#define SCP_REKEY_FILE_SZ 2048 + +static byte scpUserPassword[256]; + +static int scpUserAuth(byte authType, WS_UserAuthData* authData, void* ctx) +{ + int ret = WOLFSSH_USERAUTH_INVALID_AUTHTYPE; + + if (authType == WOLFSSH_USERAUTH_PASSWORD) { + const char* password = (const char*)ctx; + word32 passwordSz; + + if (password != NULL) { + passwordSz = (word32)WSTRLEN(password); + if (passwordSz > (word32)sizeof(scpUserPassword)) + passwordSz = (word32)sizeof(scpUserPassword); + WMEMCPY(scpUserPassword, password, passwordSz); + authData->sf.password.password = scpUserPassword; + authData->sf.password.passwordSz = passwordSz; + ret = WOLFSSH_USERAUTH_SUCCESS; + } + } + + return ret; +} + +static int scpAcceptAnyServerHostKey(const byte* pubKey, word32 pubKeySz, + void* ctx) +{ + (void)pubKey; + (void)pubKeySz; + (void)ctx; + return 0; +} + +/* Writes sz bytes of buf to name. Returns 0 on success. */ +static int scpWriteTestFile(const char* name, const byte* buf, word32 sz) +{ + WFILE* fp = NULL; + int ret = 0; + + if (WFOPEN(NULL, &fp, name, "wb") != 0 || fp == NULL) + return -1; + + if (WFWRITE(NULL, buf, 1, sz, fp) != sz) + ret = -1; + + WFCLOSE(NULL, fp); + return ret; +} + +/* Returns 0 if the first sz bytes of name match expect. */ +static int scpFilesMatch(const char* name, const byte* expect, word32 sz) +{ + WFILE* fp = NULL; + byte got[SCP_REKEY_FILE_SZ]; + int ret = 0; + + if (sz > sizeof(got)) + return -1; + + if (WFOPEN(NULL, &fp, name, "rb") != 0 || fp == NULL) + return -1; + + if (WFREAD(NULL, got, 1, sz, fp) != sz) + ret = -1; + + if (ret == 0 && XMEMCMP(got, expect, sz) != 0) + ret = -1; + + WFCLOSE(NULL, fp); + return ret; +} + +/* Connects an SCP client to port, completes the SSH handshake and opens the + * exec channel carrying cmd, leaving ssh ready for wolfSSH_SCP_to/from. Doing + * the handshake here (rather than inside the transfer call) lets the caller set + * a low highwater before the data phase so a rekey fires mid-transfer. + */ +static void scp_client_connect(WOLFSSH_CTX** ctx, WOLFSSH** ssh, int port, + const char* cmd) +{ + WS_SOCKET_T sockFd = WOLFSSH_SOCKET_INVALID; + SOCKADDR_IN_T clientAddr; + socklen_t clientAddrSz = sizeof(clientAddr); + int ret; + char* host = (char*)wolfSshIp; + const char* username = "jill"; + const char* password = "upthehill"; + + if (ctx == NULL || ssh == NULL) + return; + + *ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + if (*ctx == NULL) + return; + + wolfSSH_CTX_SetPublicKeyCheck(*ctx, scpAcceptAnyServerHostKey); + wolfSSH_SetUserAuth(*ctx, scpUserAuth); + *ssh = wolfSSH_new(*ctx); + if (*ssh == NULL) { + wolfSSH_CTX_free(*ctx); + *ctx = NULL; + return; + } + + build_addr(&clientAddr, host, port); + tcp_socket(&sockFd, ((struct sockaddr_in *)&clientAddr)->sin_family); + if (sockFd < 0) { + wolfSSH_free(*ssh); + wolfSSH_CTX_free(*ctx); + *ctx = NULL; + *ssh = NULL; + return; + } + + ret = connect(sockFd, (const struct sockaddr *)&clientAddr, clientAddrSz); + if (ret != 0) { + WCLOSESOCKET(sockFd); + wolfSSH_free(*ssh); + wolfSSH_CTX_free(*ctx); + *ctx = NULL; + *ssh = NULL; + return; + } + + wolfSSH_SetUserAuthCtx(*ssh, (void*)password); + ret = wolfSSH_SetUsername(*ssh, username); + if (ret == WS_SUCCESS) + ret = wolfSSH_SetChannelType(*ssh, WOLFSSH_SESSION_EXEC, (byte*)cmd, + (word32)WSTRLEN(cmd)); + if (ret == WS_SUCCESS) + ret = wolfSSH_set_fd(*ssh, (int)sockFd); + if (ret == WS_SUCCESS) + ret = wolfSSH_connect(*ssh); + + if (ret != WS_SUCCESS) { + WCLOSESOCKET(sockFd); + wolfSSH_free(*ssh); + wolfSSH_CTX_free(*ctx); + *ctx = NULL; + *ssh = NULL; + return; + } +} + +/* Drives an SCP transfer with a forced mid-transfer rekey. + * + * toServer == 0: client SINK (wolfSSH_SCP_from), exercises ScpStreamRead, the + * confirmed hang path. toServer == 1: client SOURCE + * (wolfSSH_SCP_to), exercises the ScpStreamSend rekey/window + * drain loop. nonBlock drives the non-blocking retry path. + */ +static void scp_rekey_test(int nonBlock, int toServer) +{ + func_args ser; + tcp_ready ready; + int argsCount; + int ret; + int err; + int tries; + word32 i; + WS_SOCKET_T clientFd; +#ifdef USE_WINDOWS_API + DWORD rcvTimeout = 20000; +#else + struct timeval rcvTimeout; +#endif + byte fileData[SCP_REKEY_FILE_SZ]; + char cmd[64]; + const char* args[10]; + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + /* Fixed names used for filesystem create/verify/cleanup. The *Buf copies + * are what get passed to the SCP API, which rewrites the path in place + * (rename/clean), so they cannot be reused to name the file afterward. The + * leading "./" keeps a directory component so the base-dir open succeeds, + * as the real scpclient passes $PWD-prefixed paths. */ + const char* srcName = "./scp_rekey_src.txt"; + const char* fromName = "./scp_rekey_from.txt"; + const char* toName = "./scp_rekey_to.txt"; + char srcBuf[32]; + char fromBuf[32]; + char toBuf[32]; + const char* verifyName; + + THREAD_TYPE serThread; + + /* mutable copies for the SCP API (rewritten in place during the transfer) */ + WSTRNCPY(srcBuf, srcName, sizeof(srcBuf)); + WSTRNCPY(fromBuf, fromName, sizeof(fromBuf)); + WSTRNCPY(toBuf, toName, sizeof(toBuf)); + + /* deterministic source content */ + for (i = 0; i < SCP_REKEY_FILE_SZ; i++) + fileData[i] = (byte)(i & 0xff); + AssertIntEQ(scpWriteTestFile(srcName, fileData, SCP_REKEY_FILE_SZ), 0); + + WMEMSET(&ser, 0, sizeof(func_args)); + argsCount = 0; + args[argsCount++] = "."; + args[argsCount++] = "-1"; +#ifndef USE_WINDOWS_API + args[argsCount++] = "-p"; + args[argsCount++] = "0"; +#endif + ser.argv = (char**)args; + ser.argc = argsCount; + ser.signal = &ready; + InitTcpReady(ser.signal); + ThreadStart(echoserver_test, (void*)&ser, &serThread); + WaitTcpReady(&ready); + + /* -f: server is source (client SINK); -t: server is sink (client SOURCE) */ + if (toServer) { + WSNPRINTF(cmd, sizeof(cmd), "scp -t %s", toName); + verifyName = toName; + } + else { + WSNPRINTF(cmd, sizeof(cmd), "scp -f %s", srcName); + verifyName = fromName; + } + + scp_client_connect(&ctx, &ssh, ready.port, cmd); + AssertNotNull(ctx); + AssertNotNull(ssh); + + /* handshake done in blocking mode; switch to non-blocking for the data + * phase so the WS_WANT_READ/WS_WANT_WRITE retry path is exercised */ + clientFd = wolfSSH_get_fd(ssh); + if (nonBlock) + tcp_set_nonblocking(&clientFd); + + /* Bound the blocking-mode recv so a KEXINIT/rekey deadlock regression fails + * the AssertIntEQ below instead of hanging CI forever. The + * SCP_REKEY_MAX_TRIES bound only covers the non-blocking retry loop; a + * non-blocking socket never blocks in recv, so this is a no-op there. */ +#ifdef USE_WINDOWS_API + (void)setsockopt(clientFd, SOL_SOCKET, SO_RCVTIMEO, + (const char*)&rcvTimeout, sizeof(rcvTimeout)); +#else + rcvTimeout.tv_sec = 20; + rcvTimeout.tv_usec = 0; + (void)setsockopt(clientFd, SOL_SOCKET, SO_RCVTIMEO, + &rcvTimeout, sizeof(rcvTimeout)); +#endif + + /* 256 is well below the 2 KB payload, so the highwater check fires partway + * through and the ScpStreamRead/ScpStreamSend rekey handling must carry the + * transfer to completion. */ + AssertIntEQ(wolfSSH_SetHighwater(ssh, 256), WS_SUCCESS); + + /* The retry loop only applies to non-blocking. In blocking mode the + * ScpStreamRead/ScpStreamSend fixes must carry the rekey transparently, so + * a single call completes the transfer; gating on nonBlock keeps the + * blocking path from masking a regression that leaves WS_REKEYING set. */ + tries = 0; + do { + if (toServer) + ret = wolfSSH_SCP_to(ssh, srcBuf, toBuf); + else + ret = wolfSSH_SCP_from(ssh, srcBuf, fromBuf); + err = wolfSSH_get_error(ssh); + /* tcp_select() waits for receive-readiness; on WS_WANT_WRITE it has no + * write event to wait on, so its 1s timeout is the intended (rare) + * fallback that yields the CPU instead of busy-spinning. */ + if (nonBlock && ret != WS_SUCCESS && (err == WS_WANT_READ + || err == WS_WANT_WRITE || err == WS_REKEYING + || err == WS_CHAN_RXD)) + tcp_select(clientFd, 1); + tries++; + } while (nonBlock && ret != WS_SUCCESS && (err == WS_WANT_READ + || err == WS_WANT_WRITE || err == WS_REKEYING + || err == WS_CHAN_RXD) + && tries <= SCP_REKEY_MAX_TRIES); + /* Fails fast (instead of hanging CI) if a regression keeps the transfer + * stuck in a want/rekey state past the retry bound. */ + AssertIntLE(tries, SCP_REKEY_MAX_TRIES); + AssertIntEQ(ret, WS_SUCCESS); + + /* best-effort shutdown; the completed transfer above is the real assertion */ + ret = wolfSSH_shutdown(ssh); + (void)ret; + + clientFd = wolfSSH_get_fd(ssh); + WCLOSESOCKET(clientFd); + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + ThreadJoin(serThread); + + /* verify the transferred file matches the source once the server is done */ + AssertIntEQ(scpFilesMatch(verifyName, fileData, SCP_REKEY_FILE_SZ), 0); + + WREMOVE(NULL, srcName); + WREMOVE(NULL, verifyName); +} + +static void test_wolfSSH_SCP_ReKey(void) +{ + scp_rekey_test(0, 0); +} + +static void test_wolfSSH_SCP_ReKey_NonBlock(void) +{ + scp_rekey_test(1, 0); +} + +static void test_wolfSSH_SCP_ReKey_ToServer(void) +{ + scp_rekey_test(0, 1); +} + +static void test_wolfSSH_SCP_ReKey_ToServer_NonBlock(void) +{ + scp_rekey_test(1, 1); +} + +#else /* WOLFSSH_SCP && !NO_WOLFSSH_CLIENT && !SINGLE_THREADED && + * !NO_FILESYSTEM && !WOLFSSH_SCP_USER_CALLBACKS && !WOLFSSH_ZEPHYR */ +static void test_wolfSSH_SCP_ReKey(void) { ; } +static void test_wolfSSH_SCP_ReKey_NonBlock(void) { ; } +static void test_wolfSSH_SCP_ReKey_ToServer(void) { ; } +static void test_wolfSSH_SCP_ReKey_ToServer_NonBlock(void) { ; } +#endif + + #ifdef USE_WINDOWS_API static byte color_test[] = { 0x1B, 0x5B, 0x34, 0x6D, 0x75, 0x6E, 0x64, 0x65, @@ -2800,6 +3137,10 @@ int wolfSSH_ApiTest(int argc, char** argv) /* SCP tests */ test_wolfSSH_SCP_CB(); + test_wolfSSH_SCP_ReKey(); + test_wolfSSH_SCP_ReKey_NonBlock(); + test_wolfSSH_SCP_ReKey_ToServer(); + test_wolfSSH_SCP_ReKey_ToServer_NonBlock(); /* SFTP tests */ test_wolfSSH_SFTP_SendReadPacket();