-
Notifications
You must be signed in to change notification settings - Fork 115
Extended (stderr) data: accumulation and window flow control #1054
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1404,9 +1404,14 @@ int wolfSSH_extended_data_send(WOLFSSH* ssh, byte* buf, word32 bufSz) | |
| } | ||
|
|
||
|
|
||
| /* Reads pending data from extended data buffer. Currently can be used to get | ||
| * STDERR information sent across the channel. | ||
| * Returns the number of bytes read on success */ | ||
| /* Reads pending STDERR data from the extended data buffer. Returns bytes read | ||
| * (>= 0) or a negative WS_ error. See wolfssh/ssh.h for the public contract | ||
| * (callers MUST drain stderr; WS_REKEYING during key exchange). | ||
| * | ||
| * A successful read drains the buffer and sends a window adjust for the bytes | ||
| * consumed, releasing back-pressure. The read still succeeds if that send | ||
| * cannot complete: WS_WANT_WRITE is absorbed (adjust stays queued) and a hard | ||
| * failure is logged, not returned. ssh->error is saved/restored across it. */ | ||
| int wolfSSH_extended_data_read(WOLFSSH* ssh, byte* out, word32 outSz) | ||
| { | ||
| byte* buf; | ||
|
|
@@ -1416,6 +1421,14 @@ int wolfSSH_extended_data_read(WOLFSSH* ssh, byte* out, word32 outSz) | |
| return WS_BAD_ARGUMENT; | ||
| } | ||
|
|
||
| /* A read sends a window adjust, which must not go out between KEXINIT and | ||
| * NEWKEYS. IsMessageAllowed() gates only transport messages on isKeying, | ||
| * not connection-layer ones, so gate here like wolfSSH_stream_read(). */ | ||
| if (ssh->isKeying) { | ||
| ssh->error = WS_REKEYING; | ||
| return WS_REKEYING; | ||
| } | ||
|
|
||
| /* sanity check to make sure idx is not in a bad state */ | ||
| if (ssh->extDataBuffer.idx > ssh->extDataBuffer.length) { | ||
| 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) | |
| buf = ssh->extDataBuffer.buffer + ssh->extDataBuffer.idx; | ||
| WMEMCPY(out, buf, bufSz); | ||
| ssh->extDataBuffer.idx += bufSz; | ||
|
|
||
| /* Replenish the channel window for the bytes just consumed. The window | ||
| * was charged when the extended data arrived (DoChannelExtendedData), so | ||
| * draining it here is what releases back-pressure to the peer. If the | ||
| * channel is gone the data is still returned; there is just no window to | ||
| * adjust. */ | ||
| if (bufSz > 0) { | ||
| WOLFSSH_CHANNEL* channel; | ||
|
|
||
| channel = ChannelFind(ssh, ssh->extDataChannelId, WS_CHANNEL_ID_SELF); | ||
| if (channel != NULL) { | ||
| int adjustResult; | ||
| int savedError = ssh->error; | ||
|
|
||
| /* Credit the window locally regardless of send result, mirroring | ||
| * _UpdateChannelWindow(). On WS_WANT_WRITE the adjust is already | ||
| * bundled into outputBuffer and the peer is credited on a later | ||
| * flush; on a hard failure the channel is already failing, so a | ||
| * transient local over-advertisement is moot and crediting avoids | ||
| * permanently leaking the window for the bytes just consumed. | ||
| * | ||
| * The bytes are already in out, so this is a completed read: it | ||
| * reports the byte count and must not disturb ssh->error. Stomping | ||
| * ssh->error here would be invisible to callers that only check the | ||
| * return value and surprising to callers that inspect | ||
| * wolfSSH_get_error() after a successful read. SendChannelWindow- | ||
| * Adjust -> wolfSSH_SendPacket sets ssh->error = WS_WANT_WRITE when | ||
| * the bundled adjust cannot be flushed in non-blocking mode, so | ||
| * save ssh->error across the send and restore it; a hard send | ||
| * failure is logged, not returned. */ | ||
| adjustResult = SendChannelWindowAdjust(ssh, channel->channel, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 [Low] Extended-data read hides nonblocking WINDOW_ADJUST back-pressure The PR moves stderr receive-window replenishment from receipt time to Recommendation: Do not silently hide |
||
| bufSz); | ||
| channel->windowSz += bufSz; | ||
| ssh->error = savedError; | ||
| if (adjustResult != WS_SUCCESS && adjustResult != WS_WANT_WRITE) | ||
| WLOG(WS_LOG_ERROR, | ||
| "wolfSSH_extended_data_read: window adjust send failed " | ||
| "(%d); window credited locally, read still succeeded", | ||
| adjustResult); | ||
| } | ||
| } | ||
|
|
||
| return bufSz; | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟠 [High] Deferred discarded extended data is not charged against the channel receive window during rekey
🚫 BLOCK
The new rekey deferral path accepts discarded extended data while
ssh->isKeyingby adding peer-controlleddataSztochannel->pendingWindowAdjust, but it leaveschannel->windowSzunchanged. The only validation is per-packet (dataSz <= channel->maxPacketSzanddataSz <= channel->windowSz), so it never accounts for cumulative bytes received while window adjustments are deferred. A malicious authenticated peer can send repeated unknown extended-data packets, or repeated second-channel stderr packets that are dropped, each individually within the unchanged window, and this code accepts more bytes than were advertised. When keying completes,SendPendingChannelWindowAdjust()flushes the accumulated credit, potentially over-crediting the peer and wrapping the uncheckedword32pendingWindowAdjust += dataSz. The buffered stderr path correctly subtracts fromwindowSz, but the discarded-data rekey branches introduced by this PR do not. Data flow: encrypted peer packet -> DoPacket() -> DoChannelExtendedData() -> GetSize() -> per-packet dataSz checks -> uncharged pendingWindowAdjust accumulation. Impact is connection-level resource / flow-control DoS, not memory corruption. Severity views differ across modes: the review mode rated this High/BLOCK (cumulative window bypass + credit wrap); the security mode rated it Low (DoS only). The stricter High assessment is retained.Recommendation: Charge the receive window whenever payload bytes are accepted from the peer, even when discarded, and guard the deferred-credit addition against overflow, e.g.
if (dataSz > UINT32_MAX - channel->pendingWindowAdjust) ret = WS_OVERFLOW_E; else { channel->windowSz -= dataSz; channel->pendingWindowAdjust += dataSz; ret = WS_SUCCESS; }. InSendPendingChannelWindowAdjust(), restorewindowSzbyamount(with overflow check) afterSendChannelWindowAdjust()returnsWS_SUCCESS/WS_WANT_WRITE. Extendtest_DoChannelExtendedData_keying()to cover cumulative deferred data andpendingWindowAdjustoverflow boundaries.