Skip to content

Commit 8c816b6

Browse files
ssh.c: report the byte count from a channel read that defers its credit
- wolfSSH_stream_read() advances inputBuffer->idx before crediting the window, and it and _ChannelRead() return the bytes copied with a non-success adjust left in ssh->error. - _ChannelRead() takes the WOLFSSH from channel->ssh, rejects an idx past inputBuffer->length, restores the entry ssh->error on a clean credit, and retires a stale WS_WANT_WRITE only when its own credit went out. - wolfSSH_SFTP_Close() checks NoticeError() only on a failed send. - The src/ssh.c block comments, and new wolfssh/ssh.h notes above wolfSSH_stream_read(), wolfSSH_ChannelRead() and wolfSSH_ChannelIdRead(), state the window-adjust and ssh->error contract. - tests/unit.c adds test_stream_read_deferredWindowAdjust() and test_ChannelIdRead_deferredWindowAdjust(): an adjust that defers, then fails, then succeeds, plus a read with nothing buffered against a seeded WS_WANT_WRITE.
1 parent 78633a1 commit 8c816b6

4 files changed

Lines changed: 286 additions & 10 deletions

File tree

src/ssh.c

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,9 +1300,9 @@ static int _ChannelReadExt(WOLFSSH_CHANNEL* channel, byte* buf, word32 bufSz);
13001300
* the SSH connection. This function handles low level operations in addition to
13011301
* the read, such as window adjustment and high water checking.
13021302
*
1303-
* In non blocking mode use the function wolfSSH_get_error(ssh) to check for
1304-
* WS_WANT_READ / WS_WANT_WRITE after a fail case was hit with
1305-
* wolfSSH_stream_read().
1303+
* In non blocking mode check wolfSSH_get_error(ssh) after the read: it holds
1304+
* WS_WANT_READ / WS_WANT_WRITE for a fail case, and for a success the status
1305+
* of a window adjust that could not be sent.
13061306
*
13071307
* Returns the number of bytes read on success, negative values on fail
13081308
*/
@@ -1394,11 +1394,17 @@ int wolfSSH_stream_read(WOLFSSH* ssh, byte* buf, word32 bufSz)
13941394
ret = WS_BUFFER_E;
13951395
else {
13961396
WMEMCPY(buf, inputBuffer->buffer + inputBuffer->idx, n);
1397+
inputBuffer->idx += n;
13971398
ret = _UpdateChannelWindow(ssh->channelList);
1398-
if (ret == WS_SUCCESS) {
1399-
inputBuffer->idx += n;
1400-
ret = n;
1399+
if (ret != WS_SUCCESS) {
1400+
ssh->error = ret;
1401+
if (ret != WS_WANT_WRITE) {
1402+
WLOG(WS_LOG_ERROR,
1403+
"wolfSSH_stream_read: window adjust send failed "
1404+
"(%d); read still succeeded", ret);
1405+
}
14011406
}
1407+
ret = n;
14021408
}
14031409
}
14041410

@@ -4000,24 +4006,59 @@ static int _UpdateChannelWindow(WOLFSSH_CHANNEL* channel)
40004006
}
40014007

40024008

4009+
/* Drains buffered channel data and credits the window for the bytes taken.
4010+
* Reports the bytes copied; an adjust that could not go out lands in
4011+
* ssh->error. */
40034012
static int _ChannelRead(WOLFSSH_CHANNEL* channel, byte* buf, word32 bufSz)
40044013
{
40054014
WOLFSSH_BUFFER* inputBuffer;
4015+
WOLFSSH* ssh;
4016+
word32 creditedSz;
40064017
int updateResult = WS_SUCCESS;
4018+
int savedError;
40074019

40084020
if (channel == NULL || buf == NULL || bufSz == 0)
40094021
return WS_BAD_ARGUMENT;
40104022

4023+
ssh = channel->ssh;
40114024
inputBuffer = &channel->inputBuffer;
4025+
4026+
if (inputBuffer->idx > inputBuffer->length) {
4027+
WLOG(WS_LOG_ERROR, "Bad internal state for buffer index");
4028+
return WS_INVALID_STATE_E;
4029+
}
4030+
40124031
bufSz = min(bufSz, inputBuffer->length - inputBuffer->idx);
40134032
WMEMCPY(buf, inputBuffer->buffer + inputBuffer->idx, bufSz);
40144033
inputBuffer->idx += bufSz;
40154034

4035+
/* Unguarded by bufSz: also compacts, and carries credit left behind. */
4036+
savedError = ssh->error;
4037+
creditedSz = inputBuffer->idx;
40164038
updateResult = _UpdateChannelWindow(channel);
4017-
if (updateResult == WS_SUCCESS)
4018-
updateResult = bufSz;
4039+
if (updateResult == WS_SUCCESS) {
4040+
/* Clear the old WS_WANT_WRITE only if this read sent an adjust of
4041+
* its own and the output buffer is now empty. */
4042+
if (savedError == WS_WANT_WRITE && creditedSz != 0
4043+
&& inputBuffer->idx == 0 && ssh->outputBuffer.length == 0) {
4044+
ssh->error = WS_SUCCESS;
4045+
}
4046+
else {
4047+
ssh->error = savedError;
4048+
}
4049+
}
4050+
else {
4051+
/* SendPacket() records only WS_WANT_WRITE, so a hard failure has to
4052+
* be recorded here; rewriting WS_WANT_WRITE is deliberate. */
4053+
ssh->error = updateResult;
4054+
if (updateResult != WS_WANT_WRITE) {
4055+
WLOG(WS_LOG_ERROR,
4056+
"_ChannelRead: window adjust send failed (%d); read still "
4057+
"succeeded", updateResult);
4058+
}
4059+
}
40194060

4020-
return updateResult;
4061+
return (int)bufSz;
40214062
}
40224063

40234064

src/wolfsftp.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8914,7 +8914,7 @@ int wolfSSH_SFTP_Close(WOLFSSH* ssh, byte* handle, word32 handleSz)
89148914
case STATE_CLOSE_SEND:
89158915
WLOG(WS_LOG_SFTP, "SFTP CLOSE STATE: SEND");
89168916
ret = SendPacketType(ssh, WOLFSSH_FTP_CLOSE, handle, handleSz);
8917-
if (NoticeError(ssh)) {
8917+
if (ret != WS_SUCCESS && NoticeError(ssh)) {
89188918
return WS_FATAL_ERROR;
89198919
}
89208920

tests/unit.c

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6488,6 +6488,220 @@ static int test_SendChannelData_zeroPeerMaxPacket(void)
64886488
return result;
64896489
}
64906490

6491+
#ifndef NO_WOLFSSH_SERVER
6492+
6493+
/* wolfSSH_stream_read() counterpart of test_ChannelExtDataCreditWantWrite():
6494+
* a deferred credit must not cost the caller the bytes already consumed, and
6495+
* an adjust that fails outright must still surface on wolfSSH_get_error(). */
6496+
static int test_stream_read_deferredWindowAdjust(void)
6497+
{
6498+
WOLFSSH_CTX* ctx = NULL;
6499+
WOLFSSH* ssh = NULL;
6500+
WOLFSSH_CHANNEL* ch = NULL;
6501+
int result = 0;
6502+
int ret;
6503+
byte in[64];
6504+
byte out[64];
6505+
word32 i;
6506+
6507+
for (i = 0; i < (word32)sizeof(in); i++) {
6508+
in[i] = (byte)i;
6509+
}
6510+
6511+
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
6512+
if (ctx == NULL)
6513+
return -6980;
6514+
wolfSSH_SetIOSend(ctx, WantWriteIoSend);
6515+
6516+
ssh = wolfSSH_new(ctx);
6517+
if (ssh == NULL) { result = -6981; goto done; }
6518+
/* Allow MSGID_CHANNEL_WINDOW_ADJUST on this bare session. */
6519+
ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT;
6520+
6521+
/* A window the size of the payload, so draining it in one read leaves
6522+
* windowSz at zero and _UpdateChannelWindow() has to credit. */
6523+
ch = ChannelNew(ssh, ID_CHANTYPE_SESSION,
6524+
(word32)sizeof(in), DEFAULT_MAX_PACKET_SZ);
6525+
if (ch == NULL) { result = -6982; goto done; }
6526+
if (ChannelAppend(ssh, ch) != WS_SUCCESS) {
6527+
ChannelDelete(ch, ssh->ctx->heap);
6528+
result = -6983;
6529+
goto done;
6530+
}
6531+
ch->openConfirmed = 1;
6532+
6533+
if (wolfSSH_TestChannelPutData(ch, in, (word32)sizeof(in)) != WS_SUCCESS) {
6534+
result = -6984; goto done;
6535+
}
6536+
if (ch->windowSz != 0) { result = -6985; goto done; }
6537+
6538+
/* Every byte is reported, and they are the bytes that were put. */
6539+
ret = wolfSSH_stream_read(ssh, out, (word32)sizeof(out));
6540+
if (ret != (int)sizeof(in)) { result = -6990; goto done; }
6541+
if (WMEMCMP(out, in, sizeof(in)) != 0) { result = -6991; goto done; }
6542+
6543+
/* The deferral is observable, the window is credited locally, and the
6544+
* bytes are consumed rather than left for a re-read. */
6545+
if (ssh->error != WS_WANT_WRITE) { result = -6992; goto done; }
6546+
if (ch->windowSz != (word32)sizeof(in)) { result = -6993; goto done; }
6547+
if (ch->inputBuffer.length - ch->inputBuffer.idx != 0) {
6548+
result = -6994; goto done;
6549+
}
6550+
6551+
/* A peer that reset rather than blocked. wolfSSH_SendPacket() records only
6552+
* WS_WANT_WRITE, so the read path has to record a hard failure itself. */
6553+
wolfSSH_SetIOSend(ctx, FailIoSend);
6554+
6555+
if (wolfSSH_TestChannelPutData(ch, in, (word32)sizeof(in)) != WS_SUCCESS) {
6556+
result = -6995; goto done;
6557+
}
6558+
if (ch->windowSz != 0) { result = -6996; goto done; }
6559+
6560+
ret = wolfSSH_stream_read(ssh, out, (word32)sizeof(out));
6561+
if (ret != (int)sizeof(in)) { result = -6997; goto done; }
6562+
if (wolfSSH_get_error(ssh) != WS_SOCKET_ERROR_E) {
6563+
result = -6998; goto done;
6564+
}
6565+
/* The send discarded what it bundled, so the credit stays owed. */
6566+
if (ch->pendingWindowAdjust != (word32)sizeof(in)) {
6567+
result = -6999; goto done;
6568+
}
6569+
6570+
/* Credit is charged for the bytes this read took, so a read that drains
6571+
* the window puts an adjust on the wire. */
6572+
wolfSSH_SetIOSend(ctx, CountIoSend);
6573+
s_extSendCount = 0;
6574+
6575+
if (wolfSSH_TestChannelPutData(ch, in, (word32)sizeof(in)) != WS_SUCCESS) {
6576+
result = -6970; goto done;
6577+
}
6578+
6579+
ret = wolfSSH_stream_read(ssh, out, (word32)sizeof(out));
6580+
if (ret != (int)sizeof(in)) { result = -6971; goto done; }
6581+
if (s_extSendCount != 1) { result = -6972; goto done; }
6582+
if (wolfSSH_get_error(ssh) != WS_SUCCESS) { result = -6973; goto done; }
6583+
/* The owed credit from the failed send rode along with this one. */
6584+
if (ch->pendingWindowAdjust != 0) { result = -6974; goto done; }
6585+
if (ch->windowSz != (word32)sizeof(in)) { result = -6975; goto done; }
6586+
6587+
done:
6588+
wolfSSH_free(ssh);
6589+
wolfSSH_CTX_free(ctx);
6590+
return result;
6591+
}
6592+
6593+
/* wolfSSH_ChannelIdRead() counterpart of
6594+
* test_stream_read_deferredWindowAdjust(): callers break out on a non-positive
6595+
* read, and this entry point has to retire the owed-flush status itself. */
6596+
static int test_ChannelIdRead_deferredWindowAdjust(void)
6597+
{
6598+
WOLFSSH_CTX* ctx = NULL;
6599+
WOLFSSH* ssh = NULL;
6600+
WOLFSSH_CHANNEL* ch = NULL;
6601+
int result = 0;
6602+
int ret;
6603+
byte in[64];
6604+
byte out[64];
6605+
word32 i;
6606+
6607+
for (i = 0; i < (word32)sizeof(in); i++) {
6608+
in[i] = (byte)i;
6609+
}
6610+
6611+
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
6612+
if (ctx == NULL)
6613+
return -7010;
6614+
wolfSSH_SetIOSend(ctx, WantWriteIoSend);
6615+
6616+
ssh = wolfSSH_new(ctx);
6617+
if (ssh == NULL) { result = -7011; goto done; }
6618+
/* Allow MSGID_CHANNEL_WINDOW_ADJUST on this bare session. */
6619+
ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT;
6620+
6621+
/* A window the size of the payload, so draining it in one read leaves
6622+
* windowSz at zero and _UpdateChannelWindow() has to credit. */
6623+
ch = ChannelNew(ssh, ID_CHANTYPE_SESSION,
6624+
(word32)sizeof(in), DEFAULT_MAX_PACKET_SZ);
6625+
if (ch == NULL) { result = -7012; goto done; }
6626+
if (ChannelAppend(ssh, ch) != WS_SUCCESS) {
6627+
ChannelDelete(ch, ssh->ctx->heap);
6628+
result = -7013;
6629+
goto done;
6630+
}
6631+
ch->openConfirmed = 1;
6632+
6633+
if (wolfSSH_TestChannelPutData(ch, in, (word32)sizeof(in)) != WS_SUCCESS) {
6634+
result = -7014; goto done;
6635+
}
6636+
if (ch->windowSz != 0) { result = -7015; goto done; }
6637+
6638+
/* Unlike wolfSSH_stream_read(), this entry point does not clear the error,
6639+
* so seed it: the assert below has to prove the read recorded it. */
6640+
ssh->error = WS_SUCCESS;
6641+
6642+
/* Every byte is reported, and they are the bytes that were put. */
6643+
ret = wolfSSH_ChannelIdRead(ssh, ch->channel, out, (word32)sizeof(out));
6644+
if (ret != (int)sizeof(in)) { result = -7016; goto done; }
6645+
if (WMEMCMP(out, in, sizeof(in)) != 0) { result = -7017; goto done; }
6646+
6647+
/* The deferral is observable, the window is credited locally, and the
6648+
* bytes are consumed rather than left for a re-read. */
6649+
if (ssh->error != WS_WANT_WRITE) { result = -7018; goto done; }
6650+
if (ch->windowSz != (word32)sizeof(in)) { result = -7019; goto done; }
6651+
if (ch->inputBuffer.length - ch->inputBuffer.idx != 0) {
6652+
result = -7020; goto done;
6653+
}
6654+
6655+
/* A peer that reset rather than blocked. wolfSSH_SendPacket() records only
6656+
* WS_WANT_WRITE, so the read path has to record a hard failure itself. */
6657+
wolfSSH_SetIOSend(ctx, FailIoSend);
6658+
6659+
if (wolfSSH_TestChannelPutData(ch, in, (word32)sizeof(in)) != WS_SUCCESS) {
6660+
result = -7021; goto done;
6661+
}
6662+
if (ch->windowSz != 0) { result = -7022; goto done; }
6663+
6664+
ret = wolfSSH_ChannelIdRead(ssh, ch->channel, out, (word32)sizeof(out));
6665+
if (ret != (int)sizeof(in)) { result = -7023; goto done; }
6666+
if (ssh->error != WS_SOCKET_ERROR_E) { result = -7024; goto done; }
6667+
/* The send discarded what it bundled, so the credit stays owed. */
6668+
if (ch->pendingWindowAdjust != (word32)sizeof(in)) {
6669+
result = -7025; goto done;
6670+
}
6671+
6672+
/* This entry point never resets ssh->error, so a credit that does go out
6673+
* has to retire the owed-flush status itself. */
6674+
wolfSSH_SetIOSend(ctx, DiscardIoSend);
6675+
ssh->error = WS_WANT_WRITE;
6676+
6677+
if (wolfSSH_TestChannelPutData(ch, in, (word32)sizeof(in)) != WS_SUCCESS) {
6678+
result = -7026; goto done;
6679+
}
6680+
6681+
ret = wolfSSH_ChannelIdRead(ssh, ch->channel, out, (word32)sizeof(out));
6682+
if (ret != (int)sizeof(in)) { result = -7027; goto done; }
6683+
if (ssh->outputBuffer.length != 0) { result = -7028; goto done; }
6684+
if (wolfSSH_get_error(ssh) != WS_SUCCESS) { result = -7029; goto done; }
6685+
/* Both the parked credit and the new one reached the peer. */
6686+
if (ch->pendingWindowAdjust != 0) { result = -7030; goto done; }
6687+
6688+
/* A read with nothing buffered sends no credit, so it has no standing to
6689+
* retire a WS_WANT_WRITE some other sender is still owed. */
6690+
ssh->error = WS_WANT_WRITE;
6691+
6692+
ret = wolfSSH_ChannelIdRead(ssh, ch->channel, out, (word32)sizeof(out));
6693+
if (ret != 0) { result = -7031; goto done; }
6694+
if (ssh->outputBuffer.length != 0) { result = -7032; goto done; }
6695+
if (wolfSSH_get_error(ssh) != WS_WANT_WRITE) { result = -7033; goto done; }
6696+
6697+
done:
6698+
wolfSSH_free(ssh);
6699+
wolfSSH_CTX_free(ctx);
6700+
return result;
6701+
}
6702+
6703+
#endif /* NO_WOLFSSH_SERVER */
6704+
64916705
/* BuildNameList() returns a C string. On an empty id list it must still
64926706
* terminate the buffer: SendKexInit() measures the result with WSTRLEN
64936707
* through AlgoListSz() and copies that many bytes into the KEXINIT. */
@@ -17194,6 +17408,18 @@ int wolfSSH_UnitTest(int argc, char** argv)
1719417408
(unitResult == 0 ? "SUCCESS" : "FAILED"));
1719517409
testResult = testResult || unitResult;
1719617410

17411+
#ifndef NO_WOLFSSH_SERVER
17412+
unitResult = test_stream_read_deferredWindowAdjust();
17413+
printf("stream_read_deferredWindowAdjust: %s\n",
17414+
(unitResult == 0 ? "SUCCESS" : "FAILED"));
17415+
testResult = testResult || unitResult;
17416+
17417+
unitResult = test_ChannelIdRead_deferredWindowAdjust();
17418+
printf("ChannelIdRead_deferredWindowAdjust: %s\n",
17419+
(unitResult == 0 ? "SUCCESS" : "FAILED"));
17420+
testResult = testResult || unitResult;
17421+
#endif /* NO_WOLFSSH_SERVER */
17422+
1719717423
unitResult = test_BuildNameList_emptySrc();
1719817424
printf("BuildNameList_emptySrc: %s\n",
1719917425
(unitResult == 0 ? "SUCCESS" : "FAILED"));

wolfssh/ssh.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,9 @@ WOLFSSH_API WOLFSSH_CHANNEL* wolfSSH_ChannelFind(WOLFSSH* ssh, word32 id,
280280
byte peer);
281281
WOLFSSH_API WOLFSSH_CHANNEL* wolfSSH_ChannelNext(WOLFSSH* ssh,
282282
WOLFSSH_CHANNEL* channel);
283+
/* Drains buffered data from the named channel: returns 0 when empty, never
284+
* receives, never reports EOF. Carries wolfSSH_stream_read()'s window-adjust
285+
* contract but does not clear ssh->error on entry; check it after the call. */
283286
WOLFSSH_API int wolfSSH_ChannelRead(WOLFSSH_CHANNEL* channel, byte* buf,
284287
word32 bufSz);
285288
WOLFSSH_API int wolfSSH_ChannelSend(WOLFSSH_CHANNEL* channel, const byte* buf,
@@ -568,6 +571,9 @@ WOLFSSH_API int wolfSSH_shutdown(WOLFSSH* ssh);
568571
* dry. A CHANNEL_EOF already received outranks that drain: both report
569572
* WS_EOF with data possibly still buffered. RFC 4253 section 11.1. */
570573
WOLFSSH_API int wolfSSH_stream_peek(WOLFSSH* ssh, byte* buf, word32 bufSz);
574+
/* Returns the bytes read; the next read clears the status. WS_WANT_WRITE
575+
* from wolfSSH_get_error() means the adjust is queued; it goes out on the
576+
* next send or a wolfSSH_worker() whose receive succeeded. Others failed. */
571577
WOLFSSH_API int wolfSSH_stream_read(WOLFSSH* ssh, byte* buf, word32 bufSz);
572578
WOLFSSH_API int wolfSSH_stream_send(WOLFSSH* ssh, byte* buf, word32 bufSz);
573579
WOLFSSH_API int wolfSSH_stream_exit(WOLFSSH* ssh, int status);
@@ -612,6 +618,9 @@ WOLFSSH_API int wolfSSH_SendIgnore(WOLFSSH* ssh, const byte* buf, word32 bufSz);
612618
WOLFSSH_API int wolfSSH_SendDisconnect(WOLFSSH* ssh, word32 reason);
613619
WOLFSSH_API int wolfSSH_global_request(WOLFSSH* ssh, const unsigned char* data,
614620
word32 dataSz, int reply);
621+
/* Reads the channel named by channelId, with wolfSSH_ChannelRead()'s
622+
* contract, except that wolfSSH_ChannelIdRead() reads during a rekey where
623+
* wolfSSH_ChannelRead() returns WS_REKEYING. */
615624
WOLFSSH_API int wolfSSH_ChannelIdRead(WOLFSSH* ssh, word32 channelId,
616625
byte* buf, word32 bufSz);
617626
WOLFSSH_API int wolfSSH_ChannelIdSend(WOLFSSH* ssh, word32 channelId,

0 commit comments

Comments
 (0)