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.
f22881f to
9d8099e
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.
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