agent, apps, examples: relay whole agent messages - #1262
yosuke-wolfssl wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate issues remain in channel routing, retry handling, output flushing, message validation, and reply-delivery accounting.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR moves SSH-agent framing and relay state into the library, updates client integrations, and expands test coverage.
Changes:
- Adds bounded full-message I/O, reconnect handling, and stateful channel relaying.
- Updates the CLI and example client integrations.
- Adds API and regression tests for fragmentation, retries, limits, and backpressure.
File summaries
| File | Summary |
|---|---|
wolfssh/agent.h |
Exposes relay state and the channel relay API. |
tests/regress.c |
Tests channel fragmentation, retries, and reply backpressure. |
tests/api.c |
Tests framed relay behavior, limits, short writes, and reconnects. |
src/agent.c |
Implements framing, buffering, reconnects, and channel relaying; unresolved review findings remain. |
examples/client/client.c |
Integrates channel relaying; unresolved routing and retry issues remain. |
apps/wolfssh/wolfssh.c |
Integrates channel relaying; unresolved routing and retry issues remain. |
Review details
Suppressed comments (7)
apps/wolfssh/wolfssh.c:726
- When this call returns
WS_REKEYING,AgentRelayHeld()correctly leavesagentOwedset, but the code then continues intowolfSSH_stream_read(). In this read loop aWS_FATAL_ERRORwithwolfSSH_get_error() == WS_REKEYINGfalls through toerr_sys()(unlikeexamples/client/client.c:567-570), so a valid held reply during a rekey terminates the app instead of driving and retrying the rekey. HandleWS_REKEYINGin this loop before treating the stream read failure as fatal.
ret = wolfSSH_AGENT_RelayChannel(args->ssh, channel);
agentChannel = channel;
agentOwed = AgentRelayHeld(ret, &agentWantsWrite);
apps/wolfssh/wolfssh.c:684
readPeer()is also governed bywhile (ret >= 0). A held retry result is negative, so this assignment leaves the loop condition false and abandons the owed reply instead of waiting for the transport/window/rekey to clear. ResetrettoWS_SUCCESSwheneveragentOwedis true.
agentOwed = AgentRelayHeld(relayRet, &agentWantsWrite);
examples/client/client.c:510
readPeer()exits on the outerwhile (ret >= 0)condition. When this retry returns a held status (all are negative),agentOwedremains true butretstays negative, so the thread leaves immediately and never retries the reply. ResetrettoWS_SUCCESSfor held statuses while retainingagentOwed.
agentOwed = AgentRelayHeld(relayRet, &agentWantsWrite);
examples/client/client.c:553
- The first relay call has the same outer-loop failure:
WS_WANT_WRITE,WS_WINDOW_FULL, andWS_REKEYINGare negative, so leaving the code inretmakeswhile (ret >= 0)terminate after this iteration even thoughagentOwedsays the reply must be retried. Normalize held results toWS_SUCCESSbefore continuing.
agentOwed = AgentRelayHeld(ret, &agentWantsWrite);
src/agent.c:2259
- wolfSSH_ChannelIdRead() can return bytes successfully while leaving a WINDOW_ADJUST queued and ssh->error set to WS_WANT_WRITE. If this call only accumulates a partial request, no reply is present for AgentRelaySendReply() to flush, so this path returns WS_SUCCESS and the applications disarm their write set; on a nonblocking transport the queued window adjust is then never flushed and the peer can stall. Propagate a pending output/WS_WANT_WRITE status before returning success even when no agent reply is owed.
break;
}
if (ret != WS_SUCCESS && ret != WS_WANT_WRITE && ret != WS_WINDOW_FULL
&& ret != WS_REKEYING && ret != WS_CHAN_RXD && agent != NULL)
src/agent.c:2183
- The new state has one
relayBufand onerxBuf, and this branch clears both whenchannelIdchanges. Both updatedreadPeer()loops callwolfSSH_AGENT_RelayChannel()for everyWS_CHAN_RXD, including shell or other channel traffic while an agent request/reply is pending; that silently drops the partial request or owed reply and desynchronizes the agent protocol. Defer unrelated channel events or make the relay state per channel before using this API in those loops.
if (!agent->relayActive || agent->relayChannel != channelId) {
AgentBufferReset(agent->relayBuf);
AgentBufferReset(agent->rxBuf);
src/agent.c:2073
wolfSSH_AGENT_Relay()passes the caller-providedmsgSzdirectly toAgentWriteMessage()without checking the framing. For*msgSz == 0,AgentWriteAll()'s empty loop returnsWS_SUCCESS, soRelay()reads a reply without sending a request; a four-byte zero or oversized header is also forwarded. Validate exactly one non-empty length-prefixed message againstWOLFSSH_AGENT_MAX_MSG_SZbefore writing, asRelayChannel()does.
ret = AgentWriteMessage(ssh, msg, *msgSz);
- Files reviewed: 6/6 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
02a2638 to
97fa98e
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Critical relay progress and pending-reply handling issues remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (3)
src/agent.c:2216
- AgentBufferPrep() passes 512 to GrowBuffer() on every loop, but GrowBuffer() treats that argument as additional free capacity. With one-byte channel fragments, once the partial message is within 512 bytes of capacity this reallocates and copies the entire accumulated message for each byte (up to roughly 512 reallocations for a maximum-sized message), making attacker-controlled fragmentation disproportionately expensive. Grow geometrically or only expand when the current capacity is exhausted.
ret = AgentBufferPrep(agent, &agent->relayBuf,
WOLFSSH_AGENT_RELAY_CHUNK_SZ);
src/agent.c:2228
- A positive
wolfSSH_ChannelIdRead()result is not sufficient here:_ChannelRead()deliberately returns the byte count even when its window-adjust send fails and records that failure inssh->error. If this read only completes a partial request, no reply is queued, soAgentRelaySendReply()can return success and hide errors such asWS_SOCKET_ERROR_EorWS_OVERFLOW_E; callers then continue driving a broken channel. Propagate the post-read non-transient transport error (while preserving the documented transient wants) before reporting success.
else if (rxd > 0) {
in->length += (word32)rxd;
progress = 1;
}
wolfssh/agent.h:216
- The return contract here does not match AgentRelaySendReply(): after a positive partial send leaves bytes in rxBuf, the implementation returns WS_WANT_WRITE whenever the reply tail remains (src/agent.c:2151-2154), even if that send has just reduced peerWindowSz to zero; the regression test expects exactly that at tests/regress.c:3737-3746. Consumers therefore arm write readiness for a reply actually held by the peer. Either return WS_WINDOW_FULL at that point or document WS_WANT_WRITE as a generic retry status rather than transport-only.
* same channelId must come back for the rest of either. While a reply is
* owed it names what holds it: WS_WANT_WRITE the transport, WS_WINDOW_FULL
* or WS_REKEYING the peer. Call again until WS_SUCCESS. Any other
- Files reviewed: 6/6 changed files
- Comments generated: 9
- Review effort level: Lite
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #1262
Scan targets checked: wolfssh-src, wolfssh-bugs
Findings: 4
4 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Reported findings require changes before merge.
97fa98e to
2f30b1c
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #1262
Scan targets checked: wolfssh-src, wolfssh-bugs
Findings: 1
1 finding(s) posted as inline comments (see file-level comments below)
💬 2 finding(s) from an earlier review are still open and were not re-posted:
- Switching agent channels discards relay state —
src/agent.c:2190 - Agent reconnect leaks the previous socket —
src/agent.c:1951
This review was generated automatically by Fenrir. Reported findings require changes before merge.
- src/agent.c gains AgentWriteAll(), AgentReadFull(), AgentReadMessage() and AgentWriteMessage(), which loop the agentIoCb, bound a declared length by the new WOLFSSH_AGENT_MAX_MSG_SZ, and reconnect once when a write finds the socket dead. wolfSSH_AGENT_Relay() is rebuilt on them. - New WOLFSSH_API wolfSSH_AGENT_RelayChannel() frames the channel side, holding a partial request and an unfinished reply on WOLFSSH_AGENT_CTX in two WOLFSSH_BUFFERs whose whole allocation is zeroed as each message is consumed. It answers WS_WANT_WRITE, WS_WINDOW_FULL or WS_REKEYING while a reply is owed, flushes what is left queued below under those last two, and answers WS_WANT_WRITE while any of it is still queued. - readPeer() in apps/wolfssh/wolfssh.c and examples/client/client.c calls it, drops its local ato32() and 512-byte buffer, and arms a select() write set for the transport's own want-write. While a reply is owed it ends the read loop so the retry above select() runs, and examples/client/client.c times its select() out at one second. - tests/api.c adds nine wolfSSH_AGENT_Relay() cases and tests/regress.c twenty-two for wolfSSH_AGENT_RelayChannel(). Issue: F-10545
- WOLFSSH_AGENT_MAX_MSG_SZ defaults to 262144.
2f30b1c to
6532128
Compare
Problem
Agent forwarding relays a length-prefixed message protocol across two byte
streams — the SSH channel and the local ssh-agent socket — and neither side
respected message boundaries.
readPeer()inapps/wolfssh/wolfssh.candexamples/client/client.creadinto a 512-byte stack buffer, did one top-up read, then relayed whatever
they had.
wolfSSH_AGENT_Relay()did a singleagentIoCbwrite whose shortwrite was reported as success, and a single read declared a complete reply.
A remote server controls how its agent requests are sized and fragmented, so
one over 508 bytes was truncated, and one split across three or more
deliveries could never be assembled — malformed messages reached the local
agent and desynced the agent protocol. Five defects in all: no size check, no
reassembly state across calls, a negative read return folded into a length,
trailing bytes of the next message counted into the relayed length, and an
unchecked
wolfSSH_GetLastRxId().Fix (
src/agent.c)Framing moves into the library.
AgentWriteAll(),AgentReadFull(),AgentReadMessage()loop theagentIoCband read a length prefix bounded by a newWOLFSSH_AGENT_MAX_MSG_SZ(256 KB, overridable).AgentWriteMessage()reconnects once when a write finds the agent socket dead.
wolfSSH_AGENT_Relay()is rebuilt on them with its signature unchanged, sointegrators inherit the read fix without changing code.
wolfSSH_AGENT_RelayChannel()frames the channel side, holding apartial request and an unfinished reply on
WOLFSSH_AGENT_CTXbetweencalls. Its return names what holds an owed reply:
WS_SUCCESSWS_WANT_WRITEWS_WINDOW_FULL,WS_REKEYINGIt also drives the transport when a held status leaves bytes queued below —
SendChannelData()returnsWS_REKEYINGahead of its own flush — and thenanswers
WS_WANT_WRITE, so the caller waits on the socket that owns them.Both
readPeer()copies collapse towolfSSH_GetLastRxId()+wolfSSH_AGENT_RelayChannel(), dropping their localato32()and 512-bytebuffer, and arm a
select()write set only for the transport's ownwant-write so a full peer window cannot spin the read thread.
Closes f-10545.
Tests
Nine cases for
wolfSSH_AGENT_Relay()intests/api.cand twenty-two forwolfSSH_AGENT_RelayChannel()intests/regress.c, covering fragmentation,trailing bytes, oversize and zero lengths, short writes, agent reconnect,
buffer scrubbing, and each reason a reply stays owed.
Verification
make check13/13.-Werrorclean across all six preflight configs.Not in this PR
SendChannelData()returns the fulldataSzwhen its second pre-send flush(
src/internal.c:23207) leaves output queued, having framed nothing — thefirst flush returns early on that, the second falls through to
if (ret == WS_SUCCESS || ret == WS_WANT_WRITE) ret = dataSz;. Every callerreads a count for bytes that reached no buffer,
wolfSSH_stream_send()included, so it is raised on its own rather than worked around here.
The relay reaches it when a channel read defers a window adjust
(
outputBuffer.length != 0withplainSz == 0) and the socket is still fullat the reply send: the reply is dropped and reported as delivered.