Match forwarded-tcpip opens to registered forwards - #1148
Conversation
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #1148
Scan targets checked: wolfssh-bugs, wolfssh-src
Findings: 2
2 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
There was a problem hiding this comment.
Pull request overview
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
This PR tightens forwarded-tcpip channel-open handling to only accept opens that correspond to tcpip-forward registrations made via wolfSSH_FwdRemoteSetup() (per RFC 4254 §7.2), and fixes two wolfSSH_SendPacket() error-handling issues discovered during testing.
Changes:
- Track per-session remote-forward registrations and enforce matching for inbound
forwarded-tcpipopens (with wildcard + cancel/overlap semantics). - Introduce a send-order reply queue to pair REQUEST_SUCCESS/FAILURE with want-reply global requests/forward requests.
- Fix
wolfSSH_SendPacket()handling forWS_CBIO_ERR_ISRandWS_CBIO_ERR_GENERAL, and expand regression/API tests accordingly.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| wolfssh/ssh.h | Documents updated remote-forward enforcement semantics and global-request reply-queue behavior. |
| wolfssh/internal.h | Adds forward tracking structs, reply queue state, and a flush counter to disambiguate post-send errors. |
| src/ssh.c | Integrates forward prepare/commit/discard around sends; enforces port-0 want-reply; queues want-reply global requests. |
| src/internal.c | Implements forward tracking + reply pairing; enforces forwarded-tcpip matching; fixes wolfSSH_SendPacket() ISR/general error behavior. |
| tests/regress.c | Adds extensive regression coverage for matching, cancellation semantics, reentrancy, send-order pairing, and send error paths. |
| tests/api.c | Adds API bad-args coverage for port-0 without want-reply. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
9d8099e to
f976e85
Compare
3233925 to
ad011b0
Compare
BuildDirectTcpipExtra() and BuildGlobalRequestFwdPacket() have callers in several conditional blocks, and a build with none of them left the two functions unused, which -Werror turns into a build failure. ReadUint32() next to them already carries the attribute for the same reason. - --disable-server builds regress.c again.
A send's return code cannot tell a caller its request is on its way. The highwater callback runs after the last byte goes out, so a rekey's errors surface as the send's, and WS_WANT_WRITE leaves the packet framed for the next flush. - Count the flushes wolfSSH_SendPacket() completes. - Compare that count across a send to tell those outcomes apart. - SendGlobalRequest() and SendGlobalRequestFwd() carry the answer in an optional out-param. - Both callers pass NULL, so nothing acts on it yet. Issue: ZD-22195
RFC 4254 7.2 says a forwarded-tcpip open answers a forward the client asked for, so refuse an open naming anything else. Enforcement starts at the first wolfSSH_FwdRemoteSetup(), leaving a client that frames tcpip-forward itself unaffected. - Register each setup per session. A wildcard bind matches on port alone. - Port 0 now requires want-reply, since only the reply names the port. - Repeat setups of one bind share a registration, so one cancel undoes it. - A cancel stops matching as it goes out, but a want-reply cancel stays registered until the peer answers: a refusal leaves the listener up. - Replies carry no request id, so a per-session queue pairs them in send order. A want-reply wolfSSH_global_request() takes a slot as well. - Registration is split around the send: allocate first, link once the request reached the wire. - A request resolves its registration on commit, since sending runs application callbacks that can reenter the library. - Tests cover matching, cancel, overlapping requests, send-order pairing, port 0, registration around the send, and reentrancy from a callback. - They drive a client session, so they sit outside the server-only block in regress.c and run in a --disable-server build. The harness struct, its teardown, the channel-open-failure helpers and the forwarding callback moved out with them, shared with the server-side tests. Contracts for wolfSSH_FwdRemoteSetup(), wolfSSH_FwdRemoteCancel() and wolfSSH_global_request() are in wolfssh/ssh.h. Issue: ZD-22195
A signal was turned into a fatal error, and a refused send left its packet counted in the output buffer. Both fixes are in wolfSSH_SendPacket(), so they cover every sender. - WS_CBIO_ERR_ISR fell through to WS_SOCKET_ERROR_E. Nothing went out and the session is unharmed, so retry, as ReceiveData() already does. - Callers that discard a packet on error, like the KEX and userauth sends, were throwing away framed output the peer never refused. - On WS_CBIO_ERR_GENERAL the buffer was shrunk with the packet still counted in plainSz, so SendChannelData() flushed nothing and called it a success. Clear it with the packet it described. - Tests pin the retry from a forwarding sender and a plain global request, and drive a channel send through a would-block and a refused flush.
A slot named its forward only once its request committed, so the scans could not see a request mid-send. A cancel the peer confirms while a fresh setup for the same bind is still going out then found nothing standing for the forward and unlinked it, leaving the peer with a listener the client refuses every open for. - Name a slot by its bind until it commits. - FwdReplyNames() answers for both FwdReplyHasSetup() and FwdReplyNewest(). - The bind is borrowed from the caller and dropped at commit, the lifetime WOLFSSH_FWD_PENDING already assumes for it. - A pointer to the entry would not do: the send runs callbacks that can free and remake it, which is why the entry is re-resolved at commit. - Test drives a confirmed cancel against a re-setup still in its send window.
Only the peer gives a slot back, by answering, so a peer that never answers a want-reply global request lets the queue grow for the life of the session, and the queue scans run once per forward on every inbound forwarded-tcpip open. - WOLFSSH_MAX_FWD_REPLIES bounds the queue. - Refuse before the request is framed: one whose slot was never queued would mispair every later reply. - Return WS_RESOURCE_E, not the WS_MEMORY_E that nothing failing to allocate would have yielded. - Test fills the queue, then covers the refusal, that a refused setup registers nothing, and a slot coming back on an answer.
The forwarded-tcpip check is a behaviour change on a shipped API, so an application that trips over it needs a way out that isn't abandoning wolfSSH_FwdRemoteSetup(). wolfSSH_SetFwdRemoteMatch() relaxes the check for the session. - STRICT is the default and keeps the bind-plus-port rule. - PORT compares the port alone, for a peer that rewrites the bind address it echoes back, which STRICT refuses every open from. - OFF accepts any open, as wolfSSH did before the check existed. - Tests cover each setting, and a refused one leaving the default in place.
Filling the reply queue sends enough to cross a lowered highwater mark. The rekey that fires sends a KEXINIT the 256-byte mem buffer cannot hold. - Disable the highwater; a bigger buffer only moves the cliff. - Both harnesses, since the buffer is the same size on each.
RFC 4254 7.2 says a
forwarded-tcpipopen answers a forward the client asked for, but we accepted any of them. This tracks whatwolfSSH_FwdRemoteSetup()registered and refuses opens naming anything else. Enforcement starts at the first registration, so a client that framestcpip-forwarditself is unaffected. TwowolfSSH_SendPacket()bugs found while testing it are fixed here too.WS_CBIO_ERR_ISRfell through toWS_SOCKET_ERROR_E, discarding framed output the peer never refused; retry instead, asReceiveData()does. AndWS_CBIO_ERR_GENERALleft the discarded packet counted inplainSz, soSendChannelData()flushed nothing and called it a success.tests/regress.ccover matching, cancel, overlapping requests, send-order pairing, port 0, registration around the send, reentrancy from a callback, and both send paths.The three affected API contracts are documented in
wolfssh/ssh.h; no signatures changed.Issue: ZD-22195