Skip to content

Commit f97de57

Browse files
committed
Window flow control for extended data
- Charge the channel window on receipt; replenish only when the application consumes bytes via wolfSSH_extended_data_read. - Track extDataChannelId so the read path adjusts the right window. - Reject extended data larger than the advertised window. - stderr now shares the channel window with normal data (RFC 4254 section 5.2 back-pressure). - Gate wolfSSH_extended_data_read on isKeying: a window adjust is a connection-layer message IsMessageAllowed does not gate, so the read must not emit one between KEXINIT and NEWKEYS (RFC 4253 section 7.1). - Fail safe when a second channel sends stderr while another still has unread data: drop it and replenish its window instead of leaking the displaced channel's window and over-crediting the survivor. - Document the mandatory-drain contract in the public header. Issue: F-864
1 parent d612c49 commit f97de57

5 files changed

Lines changed: 294 additions & 20 deletions

File tree

src/internal.c

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10211,20 +10211,50 @@ static int DoChannelExtendedData(WOLFSSH* ssh,
1021110211
ret = WS_INVALID_CHANID;
1021210212
else if (dataSz > channel->maxPacketSz)
1021310213
ret = WS_RECV_OVERFLOW_E;
10214+
else if (dataSz > channel->windowSz)
10215+
/* Peer sent more than the window we advertised. */
10216+
ret = WS_RECV_OVERFLOW_E;
1021410217
else if (dataTypeCode == CHANNEL_EXTENDED_DATA_STDERR) {
1021510218
/* Buffer the stderr data for the application to read. The
1021610219
* extended data buffer accumulates unread data instead of
1021710220
* overwriting it, so a blob arriving before the application
10218-
* reads the previous one is no longer silently lost. */
10219-
ret = PutBuffer(&ssh->extDataBuffer, buf + begin, dataSz);
10220-
#ifdef DEBUG_WOLFSSH
10221-
DumpOctetString(buf + begin, dataSz);
10222-
#endif
10223-
if (ret == WS_SUCCESS)
10221+
* reads the previous one is no longer silently lost.
10222+
*
10223+
* Extended data consumes channel window the same as ordinary
10224+
* data (RFC 4254 section 5.2). Charge the window now and only
10225+
* replenish it once the application drains the data in
10226+
* wolfSSH_extended_data_read(). That gives stderr real
10227+
* back-pressure instead of replenishing on receipt, which
10228+
* previously let the peer keep clobbering the buffer. */
10229+
int hadPending = ssh->extDataBuffer.idx < ssh->extDataBuffer.length;
10230+
10231+
if (hadPending && ssh->extDataChannelId != channel->channel) {
10232+
/* Buffer and extDataChannelId are shared (single-session
10233+
* limitation, see WOLFSSH.extDataBuffer in internal.h), so only
10234+
* one channel's stderr buffers at a time. Buffering a second
10235+
* would desync windows (displaced channel never credited, this
10236+
* one over-credited). Fail safe: drop it and replenish its
10237+
* window now so nothing leaks. */
10238+
WLOG(WS_LOG_ERROR,
10239+
"Extended data from channel %u dropped; channel %u still "
10240+
"has unread stderr buffered (only one channel's stderr "
10241+
"can be buffered at a time)",
10242+
channel->channel, ssh->extDataChannelId);
1022410243
ret = SendChannelWindowAdjust(ssh, channel->channel, dataSz);
10225-
if (ret == WS_SUCCESS) {
10226-
ssh->lastRxId = channelId;
10227-
ret = WS_EXTDATA;
10244+
if (ret == WS_SUCCESS)
10245+
ssh->lastRxId = channelId;
10246+
}
10247+
else {
10248+
ret = PutBuffer(&ssh->extDataBuffer, buf + begin, dataSz);
10249+
#ifdef DEBUG_WOLFSSH
10250+
DumpOctetString(buf + begin, dataSz);
10251+
#endif
10252+
if (ret == WS_SUCCESS) {
10253+
channel->windowSz -= dataSz;
10254+
ssh->extDataChannelId = channel->channel;
10255+
ssh->lastRxId = channelId;
10256+
ret = WS_EXTDATA;
10257+
}
1022810258
}
1022910259
}
1023010260
else {

src/ssh.c

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,9 +1404,14 @@ int wolfSSH_extended_data_send(WOLFSSH* ssh, byte* buf, word32 bufSz)
14041404
}
14051405

14061406

1407-
/* Reads pending data from extended data buffer. Currently can be used to get
1408-
* STDERR information sent across the channel.
1409-
* Returns the number of bytes read on success */
1407+
/* Reads pending STDERR data from the extended data buffer. Returns bytes read
1408+
* (>= 0) or a negative WS_ error. See wolfssh/ssh.h for the public contract
1409+
* (callers MUST drain stderr; WS_REKEYING during key exchange).
1410+
*
1411+
* A successful read drains the buffer and sends a window adjust for the bytes
1412+
* consumed, releasing back-pressure. The read still succeeds if that send
1413+
* cannot complete: WS_WANT_WRITE is absorbed (adjust stays queued) and a hard
1414+
* failure is logged, not returned. ssh->error is saved/restored across it. */
14101415
int wolfSSH_extended_data_read(WOLFSSH* ssh, byte* out, word32 outSz)
14111416
{
14121417
byte* buf;
@@ -1416,6 +1421,14 @@ int wolfSSH_extended_data_read(WOLFSSH* ssh, byte* out, word32 outSz)
14161421
return WS_BAD_ARGUMENT;
14171422
}
14181423

1424+
/* A read sends a window adjust, which must not go out between KEXINIT and
1425+
* NEWKEYS. IsMessageAllowed() gates only transport messages on isKeying,
1426+
* not connection-layer ones, so gate here like wolfSSH_stream_read(). */
1427+
if (ssh->isKeying) {
1428+
ssh->error = WS_REKEYING;
1429+
return WS_REKEYING;
1430+
}
1431+
14191432
/* sanity check to make sure idx is not in a bad state */
14201433
if (ssh->extDataBuffer.idx > ssh->extDataBuffer.length) {
14211434
WLOG(WS_LOG_ERROR, "Bad internal state for buffer index");
@@ -1425,6 +1438,48 @@ int wolfSSH_extended_data_read(WOLFSSH* ssh, byte* out, word32 outSz)
14251438
buf = ssh->extDataBuffer.buffer + ssh->extDataBuffer.idx;
14261439
WMEMCPY(out, buf, bufSz);
14271440
ssh->extDataBuffer.idx += bufSz;
1441+
1442+
/* Replenish the channel window for the bytes just consumed. The window
1443+
* was charged when the extended data arrived (DoChannelExtendedData), so
1444+
* draining it here is what releases back-pressure to the peer. If the
1445+
* channel is gone the data is still returned; there is just no window to
1446+
* adjust. */
1447+
if (bufSz > 0) {
1448+
WOLFSSH_CHANNEL* channel;
1449+
1450+
channel = ChannelFind(ssh, ssh->extDataChannelId, WS_CHANNEL_ID_SELF);
1451+
if (channel != NULL) {
1452+
int adjustResult;
1453+
int savedError = ssh->error;
1454+
1455+
/* Credit the window locally regardless of send result, mirroring
1456+
* _UpdateChannelWindow(). On WS_WANT_WRITE the adjust is already
1457+
* bundled into outputBuffer and the peer is credited on a later
1458+
* flush; on a hard failure the channel is already failing, so a
1459+
* transient local over-advertisement is moot and crediting avoids
1460+
* permanently leaking the window for the bytes just consumed.
1461+
*
1462+
* The bytes are already in out, so this is a completed read: it
1463+
* reports the byte count and must not disturb ssh->error. Stomping
1464+
* ssh->error here would be invisible to callers that only check the
1465+
* return value and surprising to callers that inspect
1466+
* wolfSSH_get_error() after a successful read. SendChannelWindow-
1467+
* Adjust -> wolfSSH_SendPacket sets ssh->error = WS_WANT_WRITE when
1468+
* the bundled adjust cannot be flushed in non-blocking mode, so
1469+
* save ssh->error across the send and restore it; a hard send
1470+
* failure is logged, not returned. */
1471+
adjustResult = SendChannelWindowAdjust(ssh, channel->channel,
1472+
bufSz);
1473+
channel->windowSz += bufSz;
1474+
ssh->error = savedError;
1475+
if (adjustResult != WS_SUCCESS && adjustResult != WS_WANT_WRITE)
1476+
WLOG(WS_LOG_ERROR,
1477+
"wolfSSH_extended_data_read: window adjust send failed "
1478+
"(%d); window credited locally, read still succeeded",
1479+
adjustResult);
1480+
}
1481+
}
1482+
14281483
return bufSz;
14291484
}
14301485

tests/unit.c

Lines changed: 174 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1769,10 +1769,16 @@ static int test_DoChannelExtendedData_overflow(void)
17691769
return result;
17701770
}
17711771

1772-
/* Exercises the accumulating extended-data buffer: two stderr blobs that
1773-
* arrive before the application reads must both be preserved (no silent
1774-
* overwrite), and an unknown extended data type must be ignored rather than
1775-
* rejected. */
1772+
/* Exercises the accumulating extended-data buffer and its window
1773+
* back-pressure: two stderr blobs that arrive before the application reads
1774+
* must both be preserved (no silent overwrite), the channel window must be
1775+
* charged on receipt and replenished on read, a partial read followed by an
1776+
* append must preserve byte ordering and window accounting across the
1777+
* GrowBuffer compaction, and an unknown extended data type must be ignored
1778+
* rather than rejected. A final case pins the documented single-session
1779+
* limitation: stderr arriving on a second channel before the first is drained
1780+
* is dropped (its window replenished immediately) rather than corrupting the
1781+
* shared buffer's window accounting. */
17761782
static int test_DoChannelExtendedData_flow(void)
17771783
{
17781784
WOLFSSH_CTX* ctx = NULL;
@@ -1824,17 +1830,20 @@ static int test_DoChannelExtendedData_flow(void)
18241830
goto done;
18251831
}
18261832

1827-
/* First stderr blob: buffered. */
1833+
/* First stderr blob: buffered, window charged 128 -> 118. */
18281834
idx = 0;
18291835
ret = wolfSSH_TestDoChannelExtendedData(ssh, (byte*)blob1,
18301836
(word32)sizeof(blob1), &idx);
18311837
if (ret != WS_EXTDATA) { result = -604; goto done; }
1838+
if (ch->windowSz != 118) { result = -605; goto done; }
18321839

1833-
/* Second stderr blob before any read: must accumulate, not overwrite. */
1840+
/* Second stderr blob before any read: must accumulate, not overwrite.
1841+
* Window charged 118 -> 108. */
18341842
idx = 0;
18351843
ret = wolfSSH_TestDoChannelExtendedData(ssh, (byte*)blob2,
18361844
(word32)sizeof(blob2), &idx);
18371845
if (ret != WS_EXTDATA) { result = -606; goto done; }
1846+
if (ch->windowSz != 108) { result = -607; goto done; }
18381847

18391848
/* Read everything: both blobs present and in order (no data loss). */
18401849
ret = wolfSSH_extended_data_read(ssh, out, (word32)sizeof(out));
@@ -1846,20 +1855,178 @@ static int test_DoChannelExtendedData_flow(void)
18461855
for (i = 10; i < 20; i++)
18471856
if (out[i] != 0x22) { result = -610; goto done; }
18481857
}
1858+
/* Draining replenishes the window: 108 + 20 -> 128. */
1859+
if (ch->windowSz != 128) { result = -611; goto done; }
18491860

18501861
/* Buffer drained; a further read returns 0. */
18511862
ret = wolfSSH_extended_data_read(ssh, out, (word32)sizeof(out));
18521863
if (ret != 0) { result = -612; goto done; }
18531864

1865+
/* Partial-read-then-append: read part of one blob (leaving idx > 0), then
1866+
* receive another blob. The append forces GrowBuffer to compact a non-zero
1867+
* idx in place (WMEMMOVE) before adding the new data, and the window must
1868+
* track a read that is split across the receipt of more data. */
1869+
idx = 0;
1870+
ret = wolfSSH_TestDoChannelExtendedData(ssh, (byte*)blob1,
1871+
(word32)sizeof(blob1), &idx);
1872+
if (ret != WS_EXTDATA) { result = -630; goto done; }
1873+
if (ch->windowSz != 118) { result = -631; goto done; }
1874+
1875+
/* Read 4 of the 10 buffered bytes: window 118 + 4 -> 122, idx left at 4. */
1876+
ret = wolfSSH_extended_data_read(ssh, out, 4);
1877+
if (ret != 4) { result = -632; goto done; }
1878+
{
1879+
int i;
1880+
for (i = 0; i < 4; i++)
1881+
if (out[i] != 0x11) { result = -633; goto done; }
1882+
}
1883+
if (ch->windowSz != 122) { result = -634; goto done; }
1884+
1885+
/* Append blob2 with 6 unread bytes still pending (idx=4, length=10): the
1886+
* 6 remaining 0x11 bytes must be preserved ahead of the new 0x22 bytes.
1887+
* Window charged 122 -> 112. */
1888+
idx = 0;
1889+
ret = wolfSSH_TestDoChannelExtendedData(ssh, (byte*)blob2,
1890+
(word32)sizeof(blob2), &idx);
1891+
if (ret != WS_EXTDATA) { result = -635; goto done; }
1892+
if (ch->windowSz != 112) { result = -636; goto done; }
1893+
1894+
/* Read the rest: 6 leftover 0x11 followed by 10 0x22, in order. Window
1895+
* 112 + 16 -> 128. */
1896+
ret = wolfSSH_extended_data_read(ssh, out, (word32)sizeof(out));
1897+
if (ret != 16) { result = -637; goto done; }
1898+
{
1899+
int i;
1900+
for (i = 0; i < 6; i++)
1901+
if (out[i] != 0x11) { result = -638; goto done; }
1902+
for (i = 6; i < 16; i++)
1903+
if (out[i] != 0x22) { result = -639; goto done; }
1904+
}
1905+
if (ch->windowSz != 128) { result = -640; goto done; }
1906+
1907+
/* Drained again. */
1908+
ret = wolfSSH_extended_data_read(ssh, out, (word32)sizeof(out));
1909+
if (ret != 0) { result = -641; goto done; }
1910+
18541911
/* Unknown extended data type: ignored (consumed), not rejected. Nothing
1855-
* is buffered for the application. */
1912+
* is buffered and the window is left intact (replenished on receipt). */
18561913
idx = 0;
18571914
ret = wolfSSH_TestDoChannelExtendedData(ssh, (byte*)unknownBlob,
18581915
(word32)sizeof(unknownBlob), &idx);
18591916
if (ret != WS_SUCCESS) { result = -613; goto done; }
1917+
if (ch->windowSz != 128) { result = -614; goto done; }
18601918
ret = wolfSSH_extended_data_read(ssh, out, (word32)sizeof(out));
18611919
if (ret != 0) { result = -615; goto done; }
18621920

1921+
/* Window-overflow branch: draw the window down below a blob that is
1922+
* still <= maxPacketSz, then confirm the next blob is rejected by the
1923+
* "dataSz > windowSz" branch and not the maxPacketSz branch. dataSz=60
1924+
* stays under maxPacketSz=64 so the maxPacketSz check cannot fire first. */
1925+
{
1926+
byte big[12 + 60];
1927+
int i;
1928+
1929+
WMEMSET(big, 0, sizeof(big));
1930+
big[7] = 0x01; /* type = stderr */
1931+
big[11] = 0x3C; /* dataSz = 60 */
1932+
for (i = 12; i < (int)sizeof(big); i++)
1933+
big[i] = 0x44;
1934+
1935+
/* window 128 -> 68 */
1936+
idx = 0;
1937+
ret = wolfSSH_TestDoChannelExtendedData(ssh, big,
1938+
(word32)sizeof(big), &idx);
1939+
if (ret != WS_EXTDATA) { result = -616; goto done; }
1940+
if (ch->windowSz != 68) { result = -617; goto done; }
1941+
1942+
/* window 68 -> 8 */
1943+
idx = 0;
1944+
ret = wolfSSH_TestDoChannelExtendedData(ssh, big,
1945+
(word32)sizeof(big), &idx);
1946+
if (ret != WS_EXTDATA) { result = -618; goto done; }
1947+
if (ch->windowSz != 8) { result = -619; goto done; }
1948+
1949+
/* dataSz=60 <= maxPacketSz but > windowSz=8: window-overflow reject,
1950+
* window left unchanged. */
1951+
idx = 0;
1952+
ret = wolfSSH_TestDoChannelExtendedData(ssh, big,
1953+
(word32)sizeof(big), &idx);
1954+
if (ret != WS_RECV_OVERFLOW_E) { result = -620; goto done; }
1955+
if (ch->windowSz != 8) { result = -621; goto done; }
1956+
}
1957+
1958+
/* Cross-channel fail safe: the extData buffer and extDataChannelId are
1959+
* shared across channels, so only one channel's stderr can be buffered at a
1960+
* time. Stderr arriving on a second channel before the first is drained is
1961+
* dropped (its window replenished immediately) instead of corrupting the
1962+
* window accounting. This pins the documented single-session limitation.
1963+
* First drain the 120 bytes still pending on channel 0 from the overflow
1964+
* block (8 + 120 -> 128 on read) to reach a clean state. */
1965+
{
1966+
WOLFSSH_CHANNEL* chB = NULL;
1967+
/* channelId=1, type=1 (stderr), dataSz=10, payload all 0x22 */
1968+
static const byte blobB[] = {
1969+
0x00, 0x00, 0x00, 0x01,
1970+
0x00, 0x00, 0x00, 0x01,
1971+
0x00, 0x00, 0x00, 0x0A,
1972+
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22
1973+
};
1974+
1975+
ret = wolfSSH_extended_data_read(ssh, out, (word32)sizeof(out));
1976+
if (ret != 32) { result = -650; goto done; }
1977+
ret = wolfSSH_extended_data_read(ssh, out, (word32)sizeof(out));
1978+
if (ret != 32) { result = -651; goto done; }
1979+
ret = wolfSSH_extended_data_read(ssh, out, (word32)sizeof(out));
1980+
if (ret != 32) { result = -652; goto done; }
1981+
ret = wolfSSH_extended_data_read(ssh, out, (word32)sizeof(out));
1982+
if (ret != 24) { result = -653; goto done; }
1983+
ret = wolfSSH_extended_data_read(ssh, out, (word32)sizeof(out));
1984+
if (ret != 0) { result = -654; goto done; }
1985+
if (ch->windowSz != 128) { result = -655; goto done; }
1986+
1987+
/* Second channel, id 1, same window/packet sizes as channel 0. */
1988+
chB = ChannelNew(ssh, ID_CHANTYPE_SESSION, 128, 64);
1989+
if (chB == NULL) { result = -656; goto done; }
1990+
if (ChannelAppend(ssh, chB) != WS_SUCCESS) {
1991+
ChannelDelete(chB, ssh->ctx->heap);
1992+
result = -657;
1993+
goto done;
1994+
}
1995+
1996+
/* Channel 0 buffers stderr first: charged 128 -> 118, extDataChannelId
1997+
* becomes 0, no warning (nothing was pending). */
1998+
idx = 0;
1999+
ret = wolfSSH_TestDoChannelExtendedData(ssh, (byte*)blob1,
2000+
(word32)sizeof(blob1), &idx);
2001+
if (ret != WS_EXTDATA) { result = -658; goto done; }
2002+
if (ch->windowSz != 118) { result = -659; goto done; }
2003+
2004+
/* Channel 1 sends stderr while channel 0 still has unread data. The
2005+
* shared single-session buffer can only hold one channel's stderr, so
2006+
* this blob is dropped (fail safe) rather than corrupting the window
2007+
* accounting. Its window is replenished immediately (SendChannelWindow-
2008+
* Adjust returns WS_SUCCESS), so chB stays at 128, the data is not
2009+
* buffered, and extDataChannelId is left at channel 0. */
2010+
idx = 0;
2011+
ret = wolfSSH_TestDoChannelExtendedData(ssh, (byte*)blobB,
2012+
(word32)sizeof(blobB), &idx);
2013+
if (ret != WS_SUCCESS) { result = -660; goto done; }
2014+
if (chB->windowSz != 128) { result = -661; goto done; }
2015+
2016+
/* One read drains only channel 0's buffered 10 bytes (channel 1's were
2017+
* dropped, not buffered) and credits them back to channel 0:
2018+
* 118 + 10 -> 128. No window is leaked and no channel is over-credited. */
2019+
ret = wolfSSH_extended_data_read(ssh, out, (word32)sizeof(out));
2020+
if (ret != 10) { result = -662; goto done; }
2021+
{
2022+
int i;
2023+
for (i = 0; i < 10; i++)
2024+
if (out[i] != 0x11) { result = -663; goto done; }
2025+
}
2026+
if (ch->windowSz != 128) { result = -665; goto done; }
2027+
if (chB->windowSz != 128) { result = -666; goto done; }
2028+
}
2029+
18632030
done:
18642031
wolfSSH_free(ssh);
18652032
wolfSSH_CTX_free(ctx);

wolfssh/internal.h

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -874,7 +874,22 @@ struct WOLFSSH {
874874

875875
WOLFSSH_BUFFER inputBuffer;
876876
WOLFSSH_BUFFER outputBuffer;
877-
WOLFSSH_BUFFER extDataBuffer; /* extended data ready to be read */
877+
WOLFSSH_BUFFER extDataBuffer; /* extended data ready to be read. Shared
878+
* across channels, so stderr buffering and
879+
* its window back-pressure are effectively
880+
* single-channel: only extDataChannelId is
881+
* replenished on read. */
882+
word32 extDataChannelId; /* self channel id whose window the buffered
883+
* extended data was charged against; its
884+
* window is replenished as the app reads.
885+
* If a second channel buffers stderr before
886+
* the app drains the buffer, this is
887+
* overwritten: the first channel's window is
888+
* never credited, and on read this channel is
889+
* over-credited by the first channel's bytes
890+
* (the whole shared buffer is credited here).
891+
* Buffered stderr is intended for the
892+
* single-session case. */
878893
WC_RNG* rng;
879894

880895
byte h[WC_MAX_DIGEST_SIZE];

0 commit comments

Comments
 (0)