Skip to content

Commit cb9bb7a

Browse files
yosuke-wolfsslejohnstown
authored andcommitted
Fix SFTP client states dropping unsent bytes after partial channel send
1 parent d43dfd5 commit cb9bb7a

5 files changed

Lines changed: 493 additions & 16 deletions

File tree

src/wolfsftp.c

Lines changed: 120 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,7 @@ static int wolfSSH_SFTP_buffer_send(WOLFSSH* ssh, WS_SFTP_BUFFER* buffer)
516516
{
517517
int ret = WS_SUCCESS;
518518
int err;
519+
word32 sendSz;
519520

520521
if (buffer == NULL || ssh == NULL) {
521522
return WS_BAD_ARGUMENT;
@@ -564,8 +565,14 @@ static int wolfSSH_SFTP_buffer_send(WOLFSSH* ssh, WS_SFTP_BUFFER* buffer)
564565
ret = WS_SUCCESS;
565566

566567
if (buffer->idx < buffer->sz) {
567-
ret = wolfSSH_stream_send(ssh, buffer->data + buffer->idx,
568-
buffer->sz - buffer->idx);
568+
sendSz = buffer->sz - buffer->idx;
569+
#ifdef WOLFSSH_TEST_INTERNAL
570+
/* Test hook: force a partial send to exercise the resume path. */
571+
if (ssh->testSftpSendCap > 0 && sendSz > ssh->testSftpSendCap) {
572+
sendSz = ssh->testSftpSendCap;
573+
}
574+
#endif
575+
ret = wolfSSH_stream_send(ssh, buffer->data + buffer->idx, sendSz);
569576
if (ret > 0) {
570577
buffer->idx += ret;
571578
}
@@ -577,6 +584,47 @@ static int wolfSSH_SFTP_buffer_send(WOLFSSH* ssh, WS_SFTP_BUFFER* buffer)
577584
}
578585

579586

587+
/* Decide whether an SFTP request buffer has been fully handed to the peer after
588+
* a wolfSSH_SFTP_buffer_send call. Two conditions must both hold:
589+
* 1. the buffer is fully consumed (idx == sz), i.e. SendChannelData did not
590+
* clamp the send to a partial count, and
591+
* 2. nothing is still queued in the SSH output buffer; SendChannelData can
592+
* report the full dataSz while leaving the bytes buffered and returning
593+
* WS_WANT_WRITE under socket back-pressure, so idx == sz alone is not
594+
* enough.
595+
* Returns WS_SUCCESS when the request is fully sent. Otherwise sets
596+
* ssh->error = WS_WANT_WRITE and returns WS_WANT_WRITE so the caller can
597+
* preserve its state/buffer and resume on the next call. */
598+
static int wolfSSH_SFTP_buffer_send_finish(WOLFSSH* ssh,
599+
WS_SFTP_BUFFER* buffer)
600+
{
601+
if (buffer == NULL || ssh == NULL) {
602+
return WS_BAD_ARGUMENT;
603+
}
604+
605+
if (wolfSSH_SFTP_buffer_idx(buffer) < wolfSSH_SFTP_buffer_size(buffer)) {
606+
ssh->error = WS_WANT_WRITE;
607+
return WS_WANT_WRITE;
608+
}
609+
#ifdef WOLFSSH_TEST_INTERNAL
610+
/* Test hook: simulate the SSH output buffer still holding queued bytes
611+
* (socket back-pressure) so the caller takes the flush-only resume path
612+
* (idx == sz, buffer_send returns WS_SUCCESS) even over loopback, where
613+
* real writes rarely block. */
614+
if (ssh->testSftpStallPending > 0) {
615+
ssh->testSftpStallPending--;
616+
ssh->error = WS_WANT_WRITE;
617+
return WS_WANT_WRITE;
618+
}
619+
#endif
620+
if (wolfSSH_OutputPending(ssh)) {
621+
ssh->error = WS_WANT_WRITE;
622+
return WS_WANT_WRITE;
623+
}
624+
return WS_SUCCESS;
625+
}
626+
627+
580628
#ifdef WOLFSSH_TEST_INTERNAL
581629
int wolfSSH_TestSftpBufferSend(WOLFSSH* ssh,
582630
byte* data, word32 sz, word32 idx)
@@ -588,6 +636,33 @@ int wolfSSH_TestSftpBufferSend(WOLFSSH* ssh,
588636
buffer.idx = idx;
589637
return wolfSSH_SFTP_buffer_send(ssh, &buffer);
590638
}
639+
640+
641+
/* Test hook: cap the number of bytes wolfSSH_SFTP_buffer_send writes per call
642+
* so a single send returns a positive-but-partial count. A cap of 0 disables
643+
* the limit. */
644+
int wolfSSH_TestSftpSendCap(WOLFSSH* ssh, word32 cap)
645+
{
646+
if (ssh == NULL) {
647+
return WS_BAD_ARGUMENT;
648+
}
649+
ssh->testSftpSendCap = cap;
650+
return WS_SUCCESS;
651+
}
652+
653+
654+
/* Test hook: make the next `count` fully-sent SFTP buffers report as still
655+
* pending in wolfSSH_SFTP_buffer_send_finish, simulating socket back-pressure
656+
* so the flush-only resume path (idx == sz, buffer_send returns WS_SUCCESS)
657+
* can be exercised deterministically. A count of 0 disables it. */
658+
int wolfSSH_TestSftpStallPending(WOLFSSH* ssh, word32 count)
659+
{
660+
if (ssh == NULL) {
661+
return WS_BAD_ARGUMENT;
662+
}
663+
ssh->testSftpStallPending = count;
664+
return WS_SUCCESS;
665+
}
591666
#endif
592667

593668

@@ -1681,16 +1756,10 @@ int wolfSSH_SFTP_read(WOLFSSH* ssh)
16811756
}
16821757
return WS_FATAL_ERROR;
16831758
}
1684-
if (wolfSSH_SFTP_buffer_idx(&state->buffer)
1685-
< wolfSSH_SFTP_buffer_size(&state->buffer)) {
1686-
ssh->error = WS_WANT_WRITE;
1687-
return WS_FATAL_ERROR;
1688-
}
1689-
/* Check if SSH layer still has pending data from WS_WANT_WRITE.
1690-
* Even if SFTP buffer is fully consumed, the data may still be
1691-
* sitting in the SSH output buffer waiting to be sent. */
1692-
if (wolfSSH_OutputPending(ssh)) {
1693-
ssh->error = WS_WANT_WRITE;
1759+
/* Resume if the request is only partially sent or is still
1760+
* queued in the SSH output buffer waiting to be flushed. */
1761+
if (wolfSSH_SFTP_buffer_send_finish(ssh, &state->buffer)
1762+
!= WS_SUCCESS) {
16941763
return WS_FATAL_ERROR;
16951764
}
16961765
ret = WS_SUCCESS;
@@ -7467,6 +7536,13 @@ int wolfSSH_SFTP_SetSTAT(WOLFSSH* ssh, char* dir, WS_SFTP_FILEATRB* atr)
74677536
return WS_FATAL_ERROR;
74687537
}
74697538

7539+
/* resume if the request is only partially sent or still queued
7540+
* in the SSH output buffer */
7541+
if (wolfSSH_SFTP_buffer_send_finish(ssh, &state->buffer)
7542+
!= WS_SUCCESS) {
7543+
return WS_FATAL_ERROR;
7544+
}
7545+
74707546
/* free up the buffer used to send data so that a new fresh buffer
74717547
* can be created when next reading the attribute packet header */
74727548
wolfSSH_SFTP_buffer_free(ssh, &state->buffer);
@@ -7641,6 +7717,12 @@ int wolfSSH_SFTP_Open(WOLFSSH* ssh, char* dir, word32 reason,
76417717
continue;
76427718
}
76437719
}
7720+
/* resume if the request is only partially sent or still queued
7721+
* in the SSH output buffer */
7722+
if (wolfSSH_SFTP_buffer_send_finish(ssh, &state->buffer)
7723+
!= WS_SUCCESS) {
7724+
return WS_FATAL_ERROR;
7725+
}
76447726
wolfSSH_SFTP_buffer_free(ssh, &state->buffer);
76457727
state->state = STATE_OPEN_GETHANDLE;
76467728
FALL_THROUGH;
@@ -7773,6 +7855,12 @@ int wolfSSH_SFTP_SendWritePacket(WOLFSSH* ssh, byte* handle, word32 handleSz,
77737855
state->state = STATE_SEND_WRITE_CLEANUP;
77747856
continue;
77757857
}
7858+
/* keep state and buffer until the header is fully flushed so
7859+
* the body is not interleaved into a half-written header */
7860+
if (wolfSSH_SFTP_buffer_send_finish(ssh, &state->buffer)
7861+
!= WS_SUCCESS) {
7862+
return WS_FATAL_ERROR;
7863+
}
77767864
state->state = STATE_SEND_WRITE_SEND_BODY;
77777865
FALL_THROUGH;
77787866

@@ -7994,6 +8082,12 @@ int wolfSSH_SFTP_SendReadPacket(WOLFSSH* ssh, byte* handle, word32 handleSz,
79948082
state->state = STATE_SEND_READ_CLEANUP;
79958083
continue;
79968084
}
8085+
/* resume if the request is only partially sent or still queued
8086+
* in the SSH output buffer */
8087+
if (wolfSSH_SFTP_buffer_send_finish(ssh, &state->buffer)
8088+
!= WS_SUCCESS) {
8089+
return WS_FATAL_ERROR;
8090+
}
79978091
wolfSSH_SFTP_buffer_free(ssh, &state->buffer);
79988092
state->state = STATE_SEND_READ_GET_HEADER;
79998093
FALL_THROUGH;
@@ -8229,6 +8323,13 @@ int wolfSSH_SFTP_MKDIR(WOLFSSH* ssh, char* dir, WS_SFTP_FILEATRB* atr)
82298323
return ret;
82308324
}
82318325

8326+
/* resume if the request is only partially sent or still queued
8327+
* in the SSH output buffer */
8328+
if (wolfSSH_SFTP_buffer_send_finish(ssh, &state->buffer)
8329+
!= WS_SUCCESS) {
8330+
return WS_FATAL_ERROR;
8331+
}
8332+
82328333
/* free data pointer to reuse it later */
82338334
wolfSSH_SFTP_buffer_free(ssh, &state->buffer);
82348335
state->state = STATE_MKDIR_GET;
@@ -8751,14 +8852,20 @@ int wolfSSH_SFTP_Rename(WOLFSSH* ssh, const char* old, const char* nw)
87518852
WLOG(WS_LOG_SFTP, "SFTP RENAME STATE: SEND");
87528853
/* send header and type specific data */
87538854
ret = wolfSSH_SFTP_buffer_send(ssh, &state->buffer);
8754-
if (ret <= 0) {
8855+
if (ret < 0) {
87558856
if (ssh->error == WS_WANT_READ ||
87568857
ssh->error == WS_WANT_WRITE) {
87578858
return WS_FATAL_ERROR;
87588859
}
87598860
state->state = STATE_RENAME_CLEANUP;
87608861
continue;
87618862
}
8863+
/* resume if the request is only partially sent or still queued
8864+
* in the SSH output buffer */
8865+
if (wolfSSH_SFTP_buffer_send_finish(ssh, &state->buffer)
8866+
!= WS_SUCCESS) {
8867+
return WS_FATAL_ERROR;
8868+
}
87628869
wolfSSH_SFTP_buffer_free(ssh, &state->buffer);
87638870
state->state = STATE_RENAME_GET_HEADER;
87648871
FALL_THROUGH;

0 commit comments

Comments
 (0)