Fix SFTP client states dropping unsent bytes after partial channel send

pull/1011/head
Yosuke Shimizu 2026-06-08 15:10:32 +09:00 committed by John Safranek
parent d43dfd5b83
commit cb9bb7a2a7
5 changed files with 493 additions and 16 deletions

View File

@ -516,6 +516,7 @@ static int wolfSSH_SFTP_buffer_send(WOLFSSH* ssh, WS_SFTP_BUFFER* buffer)
{
int ret = WS_SUCCESS;
int err;
word32 sendSz;
if (buffer == NULL || ssh == NULL) {
return WS_BAD_ARGUMENT;
@ -564,8 +565,14 @@ static int wolfSSH_SFTP_buffer_send(WOLFSSH* ssh, WS_SFTP_BUFFER* buffer)
ret = WS_SUCCESS;
if (buffer->idx < buffer->sz) {
ret = wolfSSH_stream_send(ssh, buffer->data + buffer->idx,
buffer->sz - buffer->idx);
sendSz = buffer->sz - buffer->idx;
#ifdef WOLFSSH_TEST_INTERNAL
/* Test hook: force a partial send to exercise the resume path. */
if (ssh->testSftpSendCap > 0 && sendSz > ssh->testSftpSendCap) {
sendSz = ssh->testSftpSendCap;
}
#endif
ret = wolfSSH_stream_send(ssh, buffer->data + buffer->idx, sendSz);
if (ret > 0) {
buffer->idx += ret;
}
@ -577,6 +584,47 @@ static int wolfSSH_SFTP_buffer_send(WOLFSSH* ssh, WS_SFTP_BUFFER* buffer)
}
/* Decide whether an SFTP request buffer has been fully handed to the peer after
* a wolfSSH_SFTP_buffer_send call. Two conditions must both hold:
* 1. the buffer is fully consumed (idx == sz), i.e. SendChannelData did not
* clamp the send to a partial count, and
* 2. nothing is still queued in the SSH output buffer; SendChannelData can
* report the full dataSz while leaving the bytes buffered and returning
* WS_WANT_WRITE under socket back-pressure, so idx == sz alone is not
* enough.
* Returns WS_SUCCESS when the request is fully sent. Otherwise sets
* ssh->error = WS_WANT_WRITE and returns WS_WANT_WRITE so the caller can
* preserve its state/buffer and resume on the next call. */
static int wolfSSH_SFTP_buffer_send_finish(WOLFSSH* ssh,
WS_SFTP_BUFFER* buffer)
{
if (buffer == NULL || ssh == NULL) {
return WS_BAD_ARGUMENT;
}
if (wolfSSH_SFTP_buffer_idx(buffer) < wolfSSH_SFTP_buffer_size(buffer)) {
ssh->error = WS_WANT_WRITE;
return WS_WANT_WRITE;
}
#ifdef WOLFSSH_TEST_INTERNAL
/* Test hook: simulate the SSH output buffer still holding queued bytes
* (socket back-pressure) so the caller takes the flush-only resume path
* (idx == sz, buffer_send returns WS_SUCCESS) even over loopback, where
* real writes rarely block. */
if (ssh->testSftpStallPending > 0) {
ssh->testSftpStallPending--;
ssh->error = WS_WANT_WRITE;
return WS_WANT_WRITE;
}
#endif
if (wolfSSH_OutputPending(ssh)) {
ssh->error = WS_WANT_WRITE;
return WS_WANT_WRITE;
}
return WS_SUCCESS;
}
#ifdef WOLFSSH_TEST_INTERNAL
int wolfSSH_TestSftpBufferSend(WOLFSSH* ssh,
byte* data, word32 sz, word32 idx)
@ -588,6 +636,33 @@ int wolfSSH_TestSftpBufferSend(WOLFSSH* ssh,
buffer.idx = idx;
return wolfSSH_SFTP_buffer_send(ssh, &buffer);
}
/* Test hook: cap the number of bytes wolfSSH_SFTP_buffer_send writes per call
* so a single send returns a positive-but-partial count. A cap of 0 disables
* the limit. */
int wolfSSH_TestSftpSendCap(WOLFSSH* ssh, word32 cap)
{
if (ssh == NULL) {
return WS_BAD_ARGUMENT;
}
ssh->testSftpSendCap = cap;
return WS_SUCCESS;
}
/* Test hook: make the next `count` fully-sent SFTP buffers report as still
* pending in wolfSSH_SFTP_buffer_send_finish, simulating socket back-pressure
* so the flush-only resume path (idx == sz, buffer_send returns WS_SUCCESS)
* can be exercised deterministically. A count of 0 disables it. */
int wolfSSH_TestSftpStallPending(WOLFSSH* ssh, word32 count)
{
if (ssh == NULL) {
return WS_BAD_ARGUMENT;
}
ssh->testSftpStallPending = count;
return WS_SUCCESS;
}
#endif
@ -1681,16 +1756,10 @@ int wolfSSH_SFTP_read(WOLFSSH* ssh)
}
return WS_FATAL_ERROR;
}
if (wolfSSH_SFTP_buffer_idx(&state->buffer)
< wolfSSH_SFTP_buffer_size(&state->buffer)) {
ssh->error = WS_WANT_WRITE;
return WS_FATAL_ERROR;
}
/* Check if SSH layer still has pending data from WS_WANT_WRITE.
* Even if SFTP buffer is fully consumed, the data may still be
* sitting in the SSH output buffer waiting to be sent. */
if (wolfSSH_OutputPending(ssh)) {
ssh->error = WS_WANT_WRITE;
/* Resume if the request is only partially sent or is still
* queued in the SSH output buffer waiting to be flushed. */
if (wolfSSH_SFTP_buffer_send_finish(ssh, &state->buffer)
!= WS_SUCCESS) {
return WS_FATAL_ERROR;
}
ret = WS_SUCCESS;
@ -7467,6 +7536,13 @@ int wolfSSH_SFTP_SetSTAT(WOLFSSH* ssh, char* dir, WS_SFTP_FILEATRB* atr)
return WS_FATAL_ERROR;
}
/* resume if the request is only partially sent or still queued
* in the SSH output buffer */
if (wolfSSH_SFTP_buffer_send_finish(ssh, &state->buffer)
!= WS_SUCCESS) {
return WS_FATAL_ERROR;
}
/* free up the buffer used to send data so that a new fresh buffer
* can be created when next reading the attribute packet header */
wolfSSH_SFTP_buffer_free(ssh, &state->buffer);
@ -7641,6 +7717,12 @@ int wolfSSH_SFTP_Open(WOLFSSH* ssh, char* dir, word32 reason,
continue;
}
}
/* resume if the request is only partially sent or still queued
* in the SSH output buffer */
if (wolfSSH_SFTP_buffer_send_finish(ssh, &state->buffer)
!= WS_SUCCESS) {
return WS_FATAL_ERROR;
}
wolfSSH_SFTP_buffer_free(ssh, &state->buffer);
state->state = STATE_OPEN_GETHANDLE;
FALL_THROUGH;
@ -7773,6 +7855,12 @@ int wolfSSH_SFTP_SendWritePacket(WOLFSSH* ssh, byte* handle, word32 handleSz,
state->state = STATE_SEND_WRITE_CLEANUP;
continue;
}
/* keep state and buffer until the header is fully flushed so
* the body is not interleaved into a half-written header */
if (wolfSSH_SFTP_buffer_send_finish(ssh, &state->buffer)
!= WS_SUCCESS) {
return WS_FATAL_ERROR;
}
state->state = STATE_SEND_WRITE_SEND_BODY;
FALL_THROUGH;
@ -7994,6 +8082,12 @@ int wolfSSH_SFTP_SendReadPacket(WOLFSSH* ssh, byte* handle, word32 handleSz,
state->state = STATE_SEND_READ_CLEANUP;
continue;
}
/* resume if the request is only partially sent or still queued
* in the SSH output buffer */
if (wolfSSH_SFTP_buffer_send_finish(ssh, &state->buffer)
!= WS_SUCCESS) {
return WS_FATAL_ERROR;
}
wolfSSH_SFTP_buffer_free(ssh, &state->buffer);
state->state = STATE_SEND_READ_GET_HEADER;
FALL_THROUGH;
@ -8229,6 +8323,13 @@ int wolfSSH_SFTP_MKDIR(WOLFSSH* ssh, char* dir, WS_SFTP_FILEATRB* atr)
return ret;
}
/* resume if the request is only partially sent or still queued
* in the SSH output buffer */
if (wolfSSH_SFTP_buffer_send_finish(ssh, &state->buffer)
!= WS_SUCCESS) {
return WS_FATAL_ERROR;
}
/* free data pointer to reuse it later */
wolfSSH_SFTP_buffer_free(ssh, &state->buffer);
state->state = STATE_MKDIR_GET;
@ -8751,7 +8852,7 @@ int wolfSSH_SFTP_Rename(WOLFSSH* ssh, const char* old, const char* nw)
WLOG(WS_LOG_SFTP, "SFTP RENAME STATE: SEND");
/* send header and type specific data */
ret = wolfSSH_SFTP_buffer_send(ssh, &state->buffer);
if (ret <= 0) {
if (ret < 0) {
if (ssh->error == WS_WANT_READ ||
ssh->error == WS_WANT_WRITE) {
return WS_FATAL_ERROR;
@ -8759,6 +8860,12 @@ int wolfSSH_SFTP_Rename(WOLFSSH* ssh, const char* old, const char* nw)
state->state = STATE_RENAME_CLEANUP;
continue;
}
/* resume if the request is only partially sent or still queued
* in the SSH output buffer */
if (wolfSSH_SFTP_buffer_send_finish(ssh, &state->buffer)
!= WS_SUCCESS) {
return WS_FATAL_ERROR;
}
wolfSSH_SFTP_buffer_free(ssh, &state->buffer);
state->state = STATE_RENAME_GET_HEADER;
FALL_THROUGH;

View File

@ -1401,6 +1401,16 @@ static void test_wolfSSH_SFTP_SendReadPacket(void)
int outSz = 18;
int rxSz;
const word32 ofst[2] = {0};
#ifdef WOLFSSH_TEST_INTERNAL
int err;
int tries;
int sawPartial;
int wrRet;
byte whandle[WOLFSSH_MAX_HANDLE];
word32 whandleSz;
byte wrData[32];
char wrName[] = "wolfssh_5574_write.tmp";
#endif
current = wolfSSH_SFTP_LS(ssh, (char*)currentDir);
tmp = current;
@ -1458,10 +1468,91 @@ static void test_wolfSSH_SFTP_SendReadPacket(void)
AssertIntLE(rxSz, outSz);
}
#ifdef WOLFSSH_TEST_INTERNAL
/* Issue 5574: force partial positive sends of the read request and
* confirm STATE_SEND_READ_SEND_REQ preserves the buffer (returning
* WS_WANT_WRITE) instead of freeing it after the first chunk. With
* a 1-byte cap the request can only drain over many calls, so the
* read must still complete with uncorrupted data once consumed.
* Note: this covers the window/max-packet clamped-partial case
* (idx < sz). The complementary socket back-pressure case, where
* the buffer reports fully sent but bytes are still queued in the
* SSH output buffer, is handled by the same
* wolfSSH_SFTP_buffer_send_finish() OutputPending guard but is
* not exercised here because loopback writes rarely back-pressure. */
AssertIntEQ(wolfSSH_TestSftpSendCap(ssh, 1), WS_SUCCESS);
outSz = 18;
rxSz = WS_FATAL_ERROR;
sawPartial = 0;
for (tries = 0; tries < 1000; tries++) {
rxSz = wolfSSH_SFTP_SendReadPacket(ssh, handle, handleSz,
ofst, out, outSz);
if (rxSz > 0) {
break;
}
err = wolfSSH_get_error(ssh);
if (err == WS_WANT_WRITE) {
sawPartial = 1;
}
else if (err != WS_WANT_READ && err != WS_REKEYING) {
break; /* unexpected error */
}
}
AssertIntEQ(wolfSSH_TestSftpSendCap(ssh, 0), WS_SUCCESS);
AssertIntEQ(sawPartial, 1);
AssertIntLT(tries, 1000);
AssertIntGT(rxSz, 0);
AssertIntLE(rxSz, outSz);
#endif /* WOLFSSH_TEST_INTERNAL */
free(out);
wolfSSH_SFTP_Close(ssh, handle, handleSz);
}
wolfSSH_SFTPNAME_list_free(current);
#ifdef WOLFSSH_TEST_INTERNAL
/* Issue 5574: exercise the partial-send resume path for
* wolfSSH_SFTP_SendWritePacket. STATE_SEND_WRITE_SEND_HEADER must not
* advance to STATE_SEND_WRITE_SEND_BODY until the request header is
* fully sent; otherwise the body is interleaved into a half-written
* header and corrupts the stream. The 1-byte cap forces the header to
* drain over many WS_WANT_WRITE returns (the cap only clamps the header
* buffer_send; the body uses stream_send directly). The target file is
* created over SFTP so it is independent of the server's working
* directory, and is removed afterward. Skipped if create is denied. */
whandleSz = WOLFSSH_MAX_HANDLE;
WMEMSET(wrData, 'a', sizeof(wrData));
if (wolfSSH_SFTP_Open(ssh, wrName,
WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT | WOLFSSH_FXF_TRUNC,
NULL, whandle, &whandleSz) == WS_SUCCESS) {
AssertIntEQ(wolfSSH_TestSftpSendCap(ssh, 1), WS_SUCCESS);
wrRet = WS_FATAL_ERROR;
sawPartial = 0;
for (tries = 0; tries < 1000; tries++) {
wrRet = wolfSSH_SFTP_SendWritePacket(ssh, whandle, whandleSz,
ofst, wrData, (word32)sizeof(wrData));
if (wrRet > 0) {
break;
}
err = wolfSSH_get_error(ssh);
if (err == WS_WANT_WRITE) {
sawPartial = 1;
continue;
}
if (err == WS_WANT_READ || err == WS_REKEYING) {
continue;
}
break; /* unexpected error */
}
AssertIntEQ(wolfSSH_TestSftpSendCap(ssh, 0), WS_SUCCESS);
AssertIntEQ(sawPartial, 1); /* header drained over partial sends */
AssertIntLT(tries, 1000);
AssertIntEQ(wrRet, (int)sizeof(wrData));
wolfSSH_SFTP_Close(ssh, whandle, whandleSz);
wolfSSH_SFTP_Remove(ssh, wrName);
}
#endif /* WOLFSSH_TEST_INTERNAL */
}
/* take care of re-keying state before shutdown call */
@ -1497,6 +1588,276 @@ static void test_wolfSSH_SFTP_SendReadPacket(void)
ThreadJoin(serThread);
}
/* Issue 5574: drive SFTP client request states through the 1-byte send cap (and
* the stall hook) so the partial-send resume path (wolfSSH_SFTP_buffer_send_
* finish) is exercised. Each op is run in a bounded loop: with the cap set the
* request drains over many WS_WANT_WRITE returns and must still complete. Test
* objects are created and removed over SFTP so the test does not depend on the
* server's working directory. The whole body is test-only (needs the cap hook).
*
* Coverage of the seven sites that call wolfSSH_SFTP_buffer_send_finish:
* STATE_OPEN_SEND - covered here (Open under cap)
* STATE_MKDIR_SEND - covered here (MKDIR under cap)
* STATE_SET_ATR_SEND - covered here (SetSTAT under cap)
* STATE_RENAME_SEND - covered here (cap + stall sub-cases)
* STATE_SEND_READ_SEND_REQ - covered by test_wolfSSH_SFTP_SendReadPacket
* STATE_SEND_WRITE_SEND_HEADER - covered by test_wolfSSH_SFTP_SendReadPacket
* STATE_RECV_SEND - NOT directly driven (see below)
*
* STATE_RECV_SEND is the server side sending a read/dir/stat response from
* wolfSSH_SFTP_read(). The send cap is a per-WOLFSSH flag, but this test only
* holds the client ssh; the in-process echoserver's accepted server ssh
* (threadCtx->ssh in examples/echoserver) has no test hook to set the cap on,
* and capping the whole server SFTP session risks destabilizing the threaded
* exchange. STATE_RECV_SEND runs the identical wolfSSH_SFTP_buffer_send_finish
* logic already exercised by all six client-side cases above, so it is left as
* a documented coverage gap (residual risk is the call-site wiring only). */
static void test_wolfSSH_SFTP_PartialSend(void)
{
#ifdef WOLFSSH_TEST_INTERNAL
func_args ser;
tcp_ready ready;
int argsCount;
WS_SOCKET_T clientFd;
const char* args[10];
WOLFSSH_CTX* ctx = NULL;
WOLFSSH* ssh = NULL;
THREAD_TYPE serThread;
int err;
int tries;
int sawPartial;
int ret;
byte handle[WOLFSSH_MAX_HANDLE];
word32 handleSz;
WS_SFTP_FILEATRB atr;
char openName[] = "wolfssh_5574_open.tmp";
char mkName[] = "wolfssh_5574_mkdir.tmp";
char atrName[] = "wolfssh_5574_setatr.tmp";
char renA[] = "wolfssh_5574_rename_a.tmp";
char renB[] = "wolfssh_5574_rename_b.tmp";
char renBad[] = "wolfssh_5574_nodir/sub.tmp";
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);
sftp_client_connect(&ctx, &ssh, ready.port);
AssertNotNull(ctx);
AssertNotNull(ssh);
/* STATE_OPEN_SEND: create a file, then open it for read under the cap so
* the open request must drain over several partial sends. */
handleSz = WOLFSSH_MAX_HANDLE;
if (wolfSSH_SFTP_Open(ssh, openName,
WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT | WOLFSSH_FXF_TRUNC,
NULL, handle, &handleSz) == WS_SUCCESS) {
wolfSSH_SFTP_Close(ssh, handle, handleSz);
AssertIntEQ(wolfSSH_TestSftpSendCap(ssh, 1), WS_SUCCESS);
ret = WS_FATAL_ERROR;
sawPartial = 0;
handleSz = WOLFSSH_MAX_HANDLE;
for (tries = 0; tries < 1000; tries++) {
ret = wolfSSH_SFTP_Open(ssh, openName, WOLFSSH_FXF_READ,
NULL, handle, &handleSz);
if (ret == WS_SUCCESS) {
break;
}
err = wolfSSH_get_error(ssh);
if (err == WS_WANT_WRITE) {
sawPartial = 1;
continue;
}
if (err == WS_WANT_READ || err == WS_REKEYING) {
continue;
}
break; /* unexpected error */
}
AssertIntEQ(wolfSSH_TestSftpSendCap(ssh, 0), WS_SUCCESS);
AssertIntEQ(sawPartial, 1);
AssertIntLT(tries, 1000);
AssertIntEQ(ret, WS_SUCCESS);
wolfSSH_SFTP_Close(ssh, handle, handleSz);
wolfSSH_SFTP_Remove(ssh, openName);
}
/* STATE_MKDIR_SEND: make a directory under the cap. RMDIR any stale dir
* first so a leftover from a prior run does not fail the create. */
wolfSSH_SFTP_RMDIR(ssh, mkName);
AssertIntEQ(wolfSSH_TestSftpSendCap(ssh, 1), WS_SUCCESS);
ret = WS_FATAL_ERROR;
sawPartial = 0;
for (tries = 0; tries < 1000; tries++) {
ret = wolfSSH_SFTP_MKDIR(ssh, mkName, NULL);
if (ret == WS_SUCCESS) {
break;
}
err = wolfSSH_get_error(ssh);
if (err == WS_WANT_WRITE) {
sawPartial = 1;
continue;
}
if (err == WS_WANT_READ || err == WS_REKEYING) {
continue;
}
break; /* unexpected error */
}
AssertIntEQ(wolfSSH_TestSftpSendCap(ssh, 0), WS_SUCCESS);
AssertIntEQ(sawPartial, 1);
AssertIntLT(tries, 1000);
AssertIntEQ(ret, WS_SUCCESS);
wolfSSH_SFTP_RMDIR(ssh, mkName);
/* STATE_SET_ATR_SEND: SetSTAT needs an existing file and a non-NULL atr.
* Create the file, STAT it to populate the atr, then SetSTAT under cap. */
handleSz = WOLFSSH_MAX_HANDLE;
if (wolfSSH_SFTP_Open(ssh, atrName,
WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT | WOLFSSH_FXF_TRUNC,
NULL, handle, &handleSz) == WS_SUCCESS) {
wolfSSH_SFTP_Close(ssh, handle, handleSz);
if (wolfSSH_SFTP_STAT(ssh, atrName, &atr) == WS_SUCCESS) {
AssertIntEQ(wolfSSH_TestSftpSendCap(ssh, 1), WS_SUCCESS);
ret = WS_FATAL_ERROR;
sawPartial = 0;
for (tries = 0; tries < 1000; tries++) {
ret = wolfSSH_SFTP_SetSTAT(ssh, atrName, &atr);
if (ret == WS_SUCCESS) {
break;
}
err = wolfSSH_get_error(ssh);
if (err == WS_WANT_WRITE) {
sawPartial = 1;
continue;
}
if (err == WS_WANT_READ || err == WS_REKEYING) {
continue;
}
break; /* unexpected error */
}
AssertIntEQ(wolfSSH_TestSftpSendCap(ssh, 0), WS_SUCCESS);
AssertIntEQ(sawPartial, 1);
AssertIntLT(tries, 1000);
AssertIntEQ(ret, WS_SUCCESS);
}
wolfSSH_SFTP_Remove(ssh, atrName);
}
/* STATE_RENAME_SEND. Rename first STATs the source, so create it over SFTP.
* Two sub-cases exercise both branches of the state's resume handling. */
handleSz = WOLFSSH_MAX_HANDLE;
if (wolfSSH_SFTP_Open(ssh, renA,
WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT | WOLFSSH_FXF_TRUNC,
NULL, handle, &handleSz) == WS_SUCCESS) {
wolfSSH_SFTP_Close(ssh, handle, handleSz);
wolfSSH_SFTP_Remove(ssh, renB); /* clear any stale target */
/* (a) clamped-partial: the request drains over many WS_WANT_WRITE
* returns under the 1-byte cap; renA -> renB. */
AssertIntEQ(wolfSSH_TestSftpSendCap(ssh, 1), WS_SUCCESS);
ret = WS_FATAL_ERROR;
sawPartial = 0;
for (tries = 0; tries < 1000; tries++) {
ret = wolfSSH_SFTP_Rename(ssh, renA, renB);
if (ret == WS_SUCCESS) {
break;
}
err = wolfSSH_get_error(ssh);
if (err == WS_WANT_WRITE) {
sawPartial = 1;
continue;
}
if (err == WS_WANT_READ || err == WS_REKEYING) {
continue;
}
break; /* unexpected error */
}
AssertIntEQ(wolfSSH_TestSftpSendCap(ssh, 0), WS_SUCCESS);
AssertIntEQ(sawPartial, 1);
AssertIntLT(tries, 1000);
AssertIntEQ(ret, WS_SUCCESS);
AssertIntEQ(wolfSSH_SFTP_STAT(ssh, renB, &atr), WS_SUCCESS); /* renA->renB */
/* (b) flush-only resume (HIGH-1): the stall hook makes the fully-sent
* request report as still pending once, so the next buffer_send returns
* WS_SUCCESS (0) with idx == sz. STATE_RENAME_SEND's `ret < 0` guard
* must let that fall through to read the server's response, while the
* old `ret <= 0` guard treated the 0 as terminal and returned WS_SUCCESS
* without reading the status. Because the request bytes are already on
* the wire by the resume call, the server performs/answers the request
* either way, so the only client-visible difference is the return code:
* rename into a non-existent directory so the server *rejects* it -
* correct code surfaces the failure; the buggy guard swallows it and
* returns WS_SUCCESS. This also exercises the OutputPending branch. */
AssertIntEQ(wolfSSH_TestSftpStallPending(ssh, 1), WS_SUCCESS);
ret = WS_SUCCESS;
sawPartial = 0;
for (tries = 0; tries < 1000; tries++) {
ret = wolfSSH_SFTP_Rename(ssh, renB, renBad);
err = wolfSSH_get_error(ssh);
if (err == WS_WANT_WRITE) {
sawPartial = 1;
continue;
}
if (err == WS_WANT_READ || err == WS_REKEYING) {
continue;
}
break; /* terminal: success or a server-rejected failure */
}
AssertIntEQ(wolfSSH_TestSftpStallPending(ssh, 0), WS_SUCCESS);
AssertIntEQ(sawPartial, 1); /* flush-only resume path was taken */
AssertIntLT(tries, 1000); /* terminated; did not spin */
AssertIntNE(ret, WS_SUCCESS); /* server rejection must not be swallowed */
wolfSSH_SFTP_Remove(ssh, renA);
wolfSSH_SFTP_Remove(ssh, renB);
}
/* take care of re-keying state before shutdown call */
while (wolfSSH_get_error(ssh) == WS_REKEYING) {
wolfSSH_worker(ssh, NULL);
}
argsCount = wolfSSH_shutdown(ssh);
if (argsCount == WS_SOCKET_ERROR_E) {
argsCount = WS_SUCCESS;
}
#if DEFAULT_HIGHWATER_MARK < 8000
if (argsCount == WS_REKEYING) {
argsCount = WS_SUCCESS;
}
#endif
AssertIntEQ(argsCount, WS_SUCCESS);
clientFd = wolfSSH_get_fd(ssh);
WCLOSESOCKET(clientFd);
wolfSSH_free(ssh);
wolfSSH_CTX_free(ctx);
#ifdef WOLFSSH_ZEPHYR
k_sleep(Z_TIMEOUT_TICKS(100));
#endif
ThreadJoin(serThread);
#endif /* WOLFSSH_TEST_INTERNAL */
}
/* Upper bound on non-blocking retry iterations. A legitimate LS/shutdown across
* forced rekeys completes in well under this; the bound keeps a regression from
* hanging CI by tripping the AssertNotNull/AssertIntEQ below instead. */
@ -2085,6 +2446,7 @@ static void test_wolfSSH_SFTP_SetDefaultPath(void)
#else /* WOLFSSH_SFTP && !NO_WOLFSSH_CLIENT && !SINGLE_THREADED */
static void test_wolfSSH_SFTP_SendReadPacket(void) { ; }
static void test_wolfSSH_SFTP_PartialSend(void) { ; }
static void test_wolfSSH_SFTP_ReKey(void) { ; }
static void test_wolfSSH_SFTP_ReKey_NonBlock(void) { ; }
static void test_wolfSSH_SFTP_Confinement(void) { ; }
@ -2803,6 +3165,7 @@ int wolfSSH_ApiTest(int argc, char** argv)
/* SFTP tests */
test_wolfSSH_SFTP_SendReadPacket();
test_wolfSSH_SFTP_PartialSend();
test_wolfSSH_SFTP_ReKey();
test_wolfSSH_SFTP_ReKey_NonBlock();
test_wolfSSH_SFTP_Confinement();

View File

@ -13,9 +13,10 @@ tests_unit_test_DEPENDENCIES = src/libwolfssh_test.la
tests_api_test_SOURCES = tests/api.c tests/api.h \
examples/echoserver/echoserver.c
tests_api_test_CPPFLAGS = -DNO_MAIN_DRIVER $(AM_CPPFLAGS)
tests_api_test_LDADD = src/libwolfssh.la
tests_api_test_DEPENDENCIES = src/libwolfssh.la
tests_api_test_CPPFLAGS = -DNO_MAIN_DRIVER -DWOLFSSH_TEST_INTERNAL \
$(AM_CPPFLAGS)
tests_api_test_LDADD = src/libwolfssh_test.la
tests_api_test_DEPENDENCIES = src/libwolfssh_test.la
tests_testsuite_test_SOURCES = tests/testsuite.c tests/testsuite.h \
tests/sftp.c tests/sftp.h \

View File

@ -982,6 +982,10 @@ struct WOLFSSH {
WS_UserAuthData_Keyboard kbAuth;
byte kbAuthAttempts;
#endif
#ifdef WOLFSSH_TEST_INTERNAL
word32 testSftpSendCap; /* test hook: cap per-call SFTP buffer send */
word32 testSftpStallPending; /* test hook: force N flush-only resumes */
#endif
};

View File

@ -291,6 +291,8 @@ WOLFSSH_LOCAL void wolfSSH_SFTP_ShowSizes(void);
WOLFSSH_API int wolfSSH_TestSftpValidateFileHandle(WOLFSSH* ssh,
const byte* handle, word32 handleSz);
#endif
WOLFSSH_API int wolfSSH_TestSftpSendCap(WOLFSSH* ssh, word32 cap);
WOLFSSH_API int wolfSSH_TestSftpStallPending(WOLFSSH* ssh, word32 count);
#if defined(WOLFSSL_NUCLEUS) && !defined(NO_WOLFSSH_MKTIME)
WOLFSSH_API int wolfSSH_TestNucleusMonthFromDate(word16 d);
#endif