Skip to content

Commit e0d32bb

Browse files
committed
scp: fix duplicate file header on send
- Gate header on new scpFileHeaderSent flag, not scpFileOffset==0 - A send callback returning 0 bytes first no longer re-sends header - Skip zero-length SCP_SEND_FILE send to avoid empty CHANNEL_DATA - Reset offset/bufferedSz/flag in ScpSourceInit for connection reuse - Add test_wolfSSH_SCP_SendZeroFirst regression (func_args scp_send hook) Issue: ZD-22176
1 parent 216c1a0 commit e0d32bb

6 files changed

Lines changed: 204 additions & 37 deletions

File tree

examples/echoserver/echoserver.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3184,6 +3184,11 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args)
31843184

31853185
wolfSSH_SetUserAuthResult(ctx, wsUserAuthResult);
31863186
wolfSSH_CTX_SetBanner(ctx, echoserverBanner);
3187+
#ifdef WOLFSSH_SCP
3188+
/* let a test inject a custom scp send callback in place of the default */
3189+
if (serverArgs->scp_send != NULL)
3190+
wolfSSH_SetScpSend(ctx, serverArgs->scp_send);
3191+
#endif
31873192
#ifdef WOLFSSH_AGENT
31883193
wolfSSH_CTX_set_agent_cb(ctx, wolfSSH_AGENT_DefaultActions, NULL);
31893194
#endif

src/internal.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1514,6 +1514,7 @@ WOLFSSH* SshInit(WOLFSSH* ssh, WOLFSSH_CTX* ctx)
15141514
ssh->scpATime = 0;
15151515
ssh->scpMTime = 0;
15161516
ssh->scpRequestType = WOLFSSH_SCP_SINGLE_FILE_REQUEST;
1517+
ssh->scpFileHeaderSent = 0;
15171518
ssh->scpIsRecursive = 0;
15181519
ssh->scpDirection = WOLFSSH_SCP_DIR_NONE;
15191520
ssh->scpDirDepth = 0;

src/wolfscp.c

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,11 @@ static int ScpSourceInit(WOLFSSH* ssh)
383383
ssh->scpFileBufferSz = DEFAULT_SCP_BUFFER_SZ;
384384
WMEMSET(ssh->scpFileBuffer, 0, DEFAULT_SCP_BUFFER_SZ);
385385

386+
/* reset per-file state so a reused connection starts a fresh transfer */
387+
ssh->scpFileOffset = 0;
388+
ssh->scpBufferedSz = 0;
389+
ssh->scpFileHeaderSent = 0;
390+
386391
return WS_SUCCESS;
387392
}
388393

@@ -669,8 +674,10 @@ int DoScpSource(WOLFSSH* ssh)
669674
ssh->scpBufferedSz += ssh->scpConfirm;
670675
ssh->scpConfirm = WS_SCP_CONTINUE;
671676

672-
/* only send timestamp and file header first time */
673-
if (ssh->scpFileOffset == 0) {
677+
/* send timestamp and file header once per file. A send
678+
* callback may return 0 bytes on its first call (metadata
679+
* now, data next), so key on the flag not scpFileOffset. */
680+
if (!ssh->scpFileHeaderSent) {
674681
if (ssh->scpTimestamp == 1) {
675682
ssh->scpState = SCP_SEND_TIMESTAMP;
676683
} else {
@@ -743,51 +750,58 @@ int DoScpSource(WOLFSSH* ssh)
743750
break;
744751
}
745752

753+
ssh->scpFileHeaderSent = 1;
746754
ssh->scpState = SCP_RECEIVE_CONFIRMATION;
747755
ssh->scpNextState = SCP_SEND_FILE;
748756
continue;
749757

750758
case SCP_SEND_FILE:
751759
WLOG(WS_LOG_DEBUG, scpState, "SCP_SEND_FILE");
752760

753-
ret = ScpStreamSend(ssh, ssh->scpFileBuffer,
754-
ssh->scpBufferedSz);
755-
if (ret == WS_WANT_READ || ret == WS_WANT_WRITE) {
756-
/* ScpStreamSend already drove the worker through any rekey
757-
* or full window; a non-blocking want means the socket is
758-
* not ready. Surface it for the caller to retry without
759-
* closing the file mid-transfer. scpBufferedSz and
760-
* scpFileOffset are preserved for the next call. */
761-
break;
762-
}
763-
if (ret == WS_EXTDATA) {
764-
_DumpExtendedData(ssh);
765-
continue;
766-
}
767-
if (ret < 0) {
768-
#if !defined(NO_FILESYSTEM) && \
769-
!defined(WOLFSSH_SCP_USER_CALLBACKS)
770-
/* if the socket send had a fatal error, try to close any
771-
* open file descriptor before exit */
772-
ScpSendCtx* sendCtx = NULL;
773-
sendCtx = (ScpSendCtx*)wolfSSH_GetScpSendCtx(ssh);
774-
if (sendCtx != NULL) {
775-
WFCLOSE(ssh->fs, sendCtx->fp);
776-
sendCtx->fp = NULL;
761+
/* nothing buffered (send callback returned 0 bytes): skip the
762+
* send so no zero-length CHANNEL_DATA goes on the wire; routing
763+
* below handles the empty buffer */
764+
if (ssh->scpBufferedSz > 0) {
765+
ret = ScpStreamSend(ssh, ssh->scpFileBuffer,
766+
ssh->scpBufferedSz);
767+
if (ret == WS_WANT_READ || ret == WS_WANT_WRITE) {
768+
/* ScpStreamSend already drove the worker through any
769+
* rekey or full window; a non-blocking want means the
770+
* socket is not ready. Surface it for the caller to
771+
* retry without closing the file mid-transfer.
772+
* scpBufferedSz and scpFileOffset are preserved for the
773+
* next call. */
774+
break;
775+
}
776+
if (ret == WS_EXTDATA) {
777+
_DumpExtendedData(ssh);
778+
continue;
779+
}
780+
if (ret < 0) {
781+
#if !defined(NO_FILESYSTEM) && \
782+
!defined(WOLFSSH_SCP_USER_CALLBACKS)
783+
/* if the socket send had a fatal error, try to close any
784+
* open file descriptor before exit */
785+
ScpSendCtx* sendCtx = NULL;
786+
sendCtx = (ScpSendCtx*)wolfSSH_GetScpSendCtx(ssh);
787+
if (sendCtx != NULL) {
788+
WFCLOSE(ssh->fs, sendCtx->fp);
789+
sendCtx->fp = NULL;
790+
}
791+
#endif
792+
WLOG(WS_LOG_ERROR, scpError, "failed to send file", ret);
793+
break;
777794
}
778-
#endif
779-
WLOG(WS_LOG_ERROR, scpError, "failed to send file", ret);
780-
break;
781-
}
782795

783-
ssh->scpFileOffset += ret;
784-
if (ret != (int)ssh->scpBufferedSz) {
785-
/* case where not all of buffer was sent */
786-
WMEMMOVE(ssh->scpFileBuffer, ssh->scpFileBuffer + ret,
787-
ssh->scpBufferedSz - ret);
796+
ssh->scpFileOffset += ret;
797+
if (ret != (int)ssh->scpBufferedSz) {
798+
/* case where not all of buffer was sent */
799+
WMEMMOVE(ssh->scpFileBuffer, ssh->scpFileBuffer + ret,
800+
ssh->scpBufferedSz - ret);
801+
}
802+
ssh->scpBufferedSz -= ret;
803+
ret = WS_SUCCESS;
788804
}
789-
ssh->scpBufferedSz -= ret;
790-
ret = WS_SUCCESS;
791805

792806
if (ssh->scpBufferedSz > 0) {
793807
/* There is still file data in the buffer to send,
@@ -804,6 +818,7 @@ int DoScpSource(WOLFSSH* ssh)
804818
if (ssh->scpIsRecursive) {
805819
ssh->scpFileOffset = 0;
806820
ssh->scpBufferedSz = 0;
821+
ssh->scpFileHeaderSent = 0;
807822
ssh->scpATime = 0;
808823
ssh->scpMTime = 0;
809824
ssh->scpNextState = SCP_TRANSFER;

tests/api.c

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3307,12 +3307,146 @@ static void test_wolfSSH_SCP_ReKey_ToServer_NonBlock(void)
33073307
scp_rekey_test(1, 1);
33083308
}
33093309

3310+
/* A send callback that returns 0 bytes on its first
3311+
* WOLFSSH_SCP_SINGLE_FILE_REQUEST (metadata now, data on the following call)
3312+
* must not make the server send the file header twice. The offset is static
3313+
* because the one-shot echoserver runs a single transfer. */
3314+
static byte scpZeroFirstData[SCP_REKEY_FILE_SZ];
3315+
static word32 scpZeroFirstOffset;
3316+
3317+
static int scpSendZeroFirst(WOLFSSH* ssh, int state, const char* peerRequest,
3318+
char* fileName, word32 fileNameSz, word64* mTime, word64* aTime,
3319+
int* fileMode, word32 fileOffset, word32* totalFileSz,
3320+
byte* buf, word32 bufSz, void* ctx)
3321+
{
3322+
word32 remain, n;
3323+
3324+
(void)ssh;
3325+
(void)peerRequest;
3326+
(void)fileOffset;
3327+
(void)ctx;
3328+
3329+
switch (state) {
3330+
case WOLFSSH_SCP_NEW_REQUEST:
3331+
return WS_SUCCESS;
3332+
3333+
case WOLFSSH_SCP_SINGLE_FILE_REQUEST:
3334+
/* fill metadata, but hand back zero data bytes on this first call */
3335+
WSTRNCPY(fileName, "scp_hdr_zero.txt", fileNameSz);
3336+
if (totalFileSz != NULL) *totalFileSz = SCP_REKEY_FILE_SZ;
3337+
if (mTime != NULL) *mTime = 0;
3338+
if (aTime != NULL) *aTime = 0;
3339+
if (fileMode != NULL) *fileMode = 0644;
3340+
scpZeroFirstOffset = 0;
3341+
return 0;
3342+
3343+
case WOLFSSH_SCP_CONTINUE_FILE_TRANSFER:
3344+
remain = SCP_REKEY_FILE_SZ - scpZeroFirstOffset;
3345+
if (remain == 0)
3346+
return WS_SCP_COMPLETE;
3347+
n = (remain < bufSz) ? remain : bufSz;
3348+
WMEMCPY(buf, scpZeroFirstData + scpZeroFirstOffset, n);
3349+
scpZeroFirstOffset += n;
3350+
return (int)n;
3351+
3352+
default:
3353+
return WS_SCP_ABORT;
3354+
}
3355+
}
3356+
3357+
static void test_wolfSSH_SCP_SendZeroFirst(void)
3358+
{
3359+
func_args ser;
3360+
tcp_ready ready;
3361+
int argsCount;
3362+
int ret;
3363+
word32 i;
3364+
WS_SOCKET_T clientFd;
3365+
#ifdef USE_WINDOWS_API
3366+
DWORD rcvTimeout = 20000;
3367+
#else
3368+
struct timeval rcvTimeout;
3369+
#endif
3370+
const char* args[6];
3371+
WOLFSSH_CTX* ctx = NULL;
3372+
WOLFSSH* ssh = NULL;
3373+
const char* srcName = "./scp_hdr_src.txt";
3374+
const char* fromName = "./scp_hdr_from.txt";
3375+
char srcBuf[32];
3376+
char fromBuf[32];
3377+
char cmd[64];
3378+
THREAD_TYPE serThread;
3379+
3380+
WSTRNCPY(srcBuf, srcName, sizeof(srcBuf));
3381+
WSTRNCPY(fromBuf, fromName, sizeof(fromBuf));
3382+
3383+
for (i = 0; i < SCP_REKEY_FILE_SZ; i++)
3384+
scpZeroFirstData[i] = (byte)((i * 7 + 1) & 0xff);
3385+
/* The on-disk file only satisfies the server's base-path parsing; the
3386+
* custom callback supplies the actual bytes, so a duplicated header shows
3387+
* up as a content mismatch below rather than a missing file. */
3388+
AssertIntEQ(scpWriteTestFile(srcName, scpZeroFirstData, SCP_REKEY_FILE_SZ),
3389+
0);
3390+
3391+
WMEMSET(&ser, 0, sizeof(func_args));
3392+
argsCount = 0;
3393+
args[argsCount++] = ".";
3394+
args[argsCount++] = "-1";
3395+
args[argsCount++] = "-p";
3396+
args[argsCount++] = "0";
3397+
ser.argv = (char**)args;
3398+
ser.argc = argsCount;
3399+
ser.signal = &ready;
3400+
ser.scp_send = scpSendZeroFirst;
3401+
InitTcpReady(ser.signal);
3402+
ThreadStart(echoserver_test, (void*)&ser, &serThread);
3403+
WaitTcpReady(&ready);
3404+
3405+
WSNPRINTF(cmd, sizeof(cmd), "scp -f %s", srcName);
3406+
scp_client_connect(&ctx, &ssh, ready.port, cmd);
3407+
AssertNotNull(ctx);
3408+
AssertNotNull(ssh);
3409+
3410+
/* bound the recv so a regression fails the match assert below, not CI */
3411+
clientFd = wolfSSH_get_fd(ssh);
3412+
#ifdef USE_WINDOWS_API
3413+
(void)setsockopt(clientFd, SOL_SOCKET, SO_RCVTIMEO,
3414+
(const char*)&rcvTimeout, sizeof(rcvTimeout));
3415+
#else
3416+
rcvTimeout.tv_sec = 20;
3417+
rcvTimeout.tv_usec = 0;
3418+
(void)setsockopt(clientFd, SOL_SOCKET, SO_RCVTIMEO,
3419+
&rcvTimeout, sizeof(rcvTimeout));
3420+
#endif
3421+
3422+
ret = wolfSSH_SCP_from(ssh, srcBuf, fromBuf);
3423+
AssertIntEQ(ret, WS_SUCCESS);
3424+
3425+
ret = wolfSSH_shutdown(ssh);
3426+
(void)ret;
3427+
3428+
clientFd = wolfSSH_get_fd(ssh);
3429+
WCLOSESOCKET(clientFd);
3430+
wolfSSH_free(ssh);
3431+
wolfSSH_CTX_free(ctx);
3432+
ThreadJoin(serThread);
3433+
FreeTcpReady(&ready);
3434+
3435+
/* a duplicate header would corrupt the stream; an exact match proves the
3436+
* header was sent once */
3437+
AssertIntEQ(scpFilesMatch(fromName, scpZeroFirstData, SCP_REKEY_FILE_SZ), 0);
3438+
3439+
WREMOVE(NULL, srcName);
3440+
WREMOVE(NULL, fromName);
3441+
}
3442+
33103443
#else /* WOLFSSH_SCP && !NO_WOLFSSH_CLIENT && !SINGLE_THREADED &&
33113444
* !NO_FILESYSTEM && !WOLFSSH_SCP_USER_CALLBACKS && !WOLFSSH_ZEPHYR */
33123445
static void test_wolfSSH_SCP_ReKey(void) { ; }
33133446
static void test_wolfSSH_SCP_ReKey_NonBlock(void) { ; }
33143447
static void test_wolfSSH_SCP_ReKey_ToServer(void) { ; }
33153448
static void test_wolfSSH_SCP_ReKey_ToServer_NonBlock(void) { ; }
3449+
static void test_wolfSSH_SCP_SendZeroFirst(void) { ; }
33163450
#endif
33173451

33183452

@@ -4346,6 +4480,7 @@ int wolfSSH_ApiTest(int argc, char** argv)
43464480
test_wolfSSH_SCP_ReKey_NonBlock();
43474481
test_wolfSSH_SCP_ReKey_ToServer();
43484482
test_wolfSSH_SCP_ReKey_ToServer_NonBlock();
4483+
test_wolfSSH_SCP_SendZeroFirst();
43494484

43504485
/* SFTP tests */
43514486
test_wolfSSH_SFTP_SendReadPacket();

wolfssh/internal.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,7 @@ struct WOLFSSH {
989989
word32 scpFileBufferSz; /* size of transfer buffer, octets */
990990
word32 scpFileOffset; /* current offset into file transfer */
991991
word32 scpBufferedSz; /* bytes buffered to send to peer */
992+
byte scpFileHeaderSent; /* file header already sent for current file */
992993
word32 scpDirDepth; /* SCP nested NEW_DIR depth below base path */
993994
#ifdef WOLFSSL_NUCLEUS
994995
int scpFd; /* SCP receive callback context handle */

wolfssh/test.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@
4141
/*#include <wolfssh/error.h>*/
4242
#endif
4343

44+
#ifdef WOLFSSH_SCP
45+
/* for WS_CallbackScpSend used in func_args; test.h is included by sources
46+
* (e.g. testsuite.c) that do not otherwise pull in wolfscp.h */
47+
#include <wolfssh/wolfscp.h>
48+
#endif
49+
4450
#ifdef USE_WINDOWS_API
4551
#ifndef _WIN32_WCE
4652
#include <process.h>
@@ -857,6 +863,10 @@ typedef struct func_args {
857863
/* callback for example sftp client commands instead of WFGETS */
858864
WS_CallbackSftpCommand sftp_cb;
859865
#endif
866+
#ifdef WOLFSSH_SCP
867+
/* override the server's default scp send callback (test injection) */
868+
WS_CallbackScpSend scp_send;
869+
#endif
860870
} func_args;
861871

862872

0 commit comments

Comments
 (0)