agent: refuse a channel open after a disconnect

wolfSSH_AGENT_ChannelOpen() answers a poll on a session that is over
with WS_FATAL_ERROR and WS_DISCONNECT in ssh->error, the shape every
other public sender uses: no channel opened, nothing on the wire,
RFC 4253 section 11.1. wolfSSH_accept() gates the open it drives, so
the new public entry point is the only way in.

- promote SendAfterDisconnect() to WOLFSSH_LOCAL so agent.c uses the
  same helper as every other public sender
- leave an open queued before the disconnect unflushed, the rule
  wolfSSH_shutdown() applies to all but its own disconnect
- keep WS_DISCONNECT in ssh->error at the accept() call site, which
  used to overwrite it with the status the open returns
pull/1245/head
John Safranek 2026-09-04 13:41:16 -07:00 committed by philljj
parent 41f171d982
commit 6164ec39d5
5 changed files with 69 additions and 13 deletions

View File

@ -1749,6 +1749,13 @@ int wolfSSH_AGENT_ChannelOpen(WOLFSSH* ssh)
* checks below would report a channel a client never opened. */
ret = WS_BAD_ARGUMENT;
}
else if (SendAfterDisconnect(ssh)) {
/* The session is over, so neither a new open nor the flush of one
* queued before the disconnect may go out. RFC 4253 section 11.1.
* WS_DISCONNECT is in ssh->error, where the rest of the API puts
* it. */
ret = WS_FATAL_ERROR;
}
else if (!ssh->useAgent) {
/* Nothing asked for agent forwarding on this session. */
ret = WS_BAD_ARGUMENT;

View File

@ -616,10 +616,6 @@ static int DoReceiveHandshake(WOLFSSH* ssh)
#endif /* !NO_WOLFSSH_SERVER || !NO_WOLFSSH_CLIENT */
/* Defined below, ahead of both drivers; either can be the only one built. */
static int SendAfterDisconnect(WOLFSSH* ssh);
#ifndef NO_WOLFSSH_SERVER
const char acceptError[] = "accept error: %s, %d";
@ -813,8 +809,13 @@ int wolfSSH_accept(WOLFSSH* ssh)
#endif /* WOLFSSH_SFTP and !NO_WOLFSSH_SERVER */
#ifdef WOLFSSH_AGENT
if (ssh->useAgent) {
ssh->error = wolfSSH_AGENT_ChannelOpen(ssh);
if (ssh->error < WS_SUCCESS) {
int agentRet = wolfSSH_AGENT_ChannelOpen(ssh);
if (agentRet < WS_SUCCESS) {
/* WS_FATAL_ERROR is the disconnect, which already
* recorded WS_DISCONNECT; keep that. */
if (agentRet != WS_FATAL_ERROR)
ssh->error = agentRet;
WLOG(WS_LOG_DEBUG, acceptError,
"SERVER_USERAUTH_ACCEPT_DONE", ssh->error);
return WS_FATAL_ERROR;
@ -1143,11 +1144,8 @@ int wolfSSH_connect(WOLFSSH* ssh)
#endif /* NO_WOLFSSH_CLIENT */
/* A disconnect, sent or received, ends the session, so nothing further may
* go out. RFC 4253 section 11.1. Reads are deliberately not gated on this:
* channel data that arrived before the disconnect is still the caller's.
* Call only after ssh has been checked for NULL. */
static int SendAfterDisconnect(WOLFSSH* ssh)
/* See wolfssh/internal.h for the contract. */
int SendAfterDisconnect(WOLFSSH* ssh)
{
if (ssh->disconnected) {
WLOG(WS_LOG_DEBUG, "Send attempted after a disconnect");

View File

@ -4459,6 +4459,48 @@ static void TestAgentChannelOpenWithoutRequest(void)
FreeChannelOpenHarness(&harness);
}
/* A poll after the peer disconnects must not open a channel or put anything
* on the wire. RFC 4253 section 11.1: the session is over. */
static void TestAgentChannelOpenAfterDisconnect(void)
{
ChannelOpenHarness harness;
InitChannelOpenHarness(&harness, NULL, 0);
harness.ssh->useAgent = 1;
harness.ssh->disconnected = 1;
AssertIntEQ(wolfSSH_AGENT_ChannelOpen(harness.ssh), WS_FATAL_ERROR);
AssertNull(harness.ssh->agent);
AssertIntEQ(harness.ssh->channelListSz, 0);
AssertIntEQ(harness.io.outSz, 0);
AssertIntEQ(harness.ssh->error, WS_DISCONNECT);
FreeChannelOpenHarness(&harness);
}
/* An open queued before the disconnect is not flushed either: those bytes
* belong to a session that is over, the same rule wolfSSH_shutdown() applies
* to everything but its own queued disconnect. */
static void TestAgentChannelOpenQueuedThenDisconnect(void)
{
ChannelOpenHarness harness;
InitChannelOpenHarness(&harness, NULL, 0);
harness.ssh->useAgent = 1;
harness.io.blockNext = 1;
AssertIntEQ(wolfSSH_AGENT_ChannelOpen(harness.ssh), WS_WANT_WRITE);
AssertIntEQ(harness.io.outSz, 0);
harness.ssh->disconnected = 1;
AssertIntEQ(wolfSSH_AGENT_ChannelOpen(harness.ssh), WS_FATAL_ERROR);
AssertIntEQ(harness.io.outSz, 0);
AssertIntEQ(harness.ssh->error, WS_DISCONNECT);
FreeChannelOpenHarness(&harness);
}
/* A queued open publishes the agent, so the caller's next poll must finish
* the send rather than report a success the peer never saw, and must not
* open a second channel. */
@ -13525,6 +13567,8 @@ int main(int argc, char** argv)
TestAgentChannelNullAgentSendsOpenFail();
TestAgentChannelOpenWithoutRequest();
TestAgentChannelOpenFlushesQueuedOpen();
TestAgentChannelOpenAfterDisconnect();
TestAgentChannelOpenQueuedThenDisconnect();
TestAgentChannelOpenSendFailureCleansUp();
#ifndef NO_WOLFSSH_CLIENT
TestAgentChannelOpenOnClientRefused();

View File

@ -187,11 +187,12 @@ WOLFSSH_API int wolfSSH_AGENT_enable(WOLFSSH* ssh, byte isEnabled);
* this instead. Opens one channel, then flushes what of the open is queued.
* Returns WS_SUCCESS, WS_BAD_ARGUMENT before the peer asks or on a client
* session, WS_WANT_READ or WS_WANT_WRITE while output is still queued,
* WS_FATAL_ERROR with WS_DISCONNECT in ssh->error once the session is over,
* WS_SSH_NULL_E, WS_MEMORY_E, or whatever the send reports. WS_SUCCESS says
* the open went out, not that the peer took it; a refusal reaches the
* channel-open-fail callback.
* Only the send records in ssh->error, so a poll ahead of the peer's request
* leaves the session fit for wolfSSH_accept(). */
* Only that and the send record in ssh->error, so a poll ahead of the peer's
* request leaves the session fit for wolfSSH_accept(). */
WOLFSSH_API int wolfSSH_AGENT_ChannelOpen(WOLFSSH* ssh);
WOLFSSH_LOCAL int wolfSSH_AGENT_worker(WOLFSSH* ssh);
WOLFSSH_API int wolfSSH_AGENT_Relay(WOLFSSH* ssh,

View File

@ -1679,6 +1679,12 @@ enum ChannelOpenFailReasons {
OPEN_RESOURCE_SHORTAGE
};
/* A disconnect, sent or received, ends the session, so nothing further may
* go out. RFC 4253 section 11.1. Returns 1 and records WS_DISCONNECT in
* ssh->error when the session is over, 0 otherwise. Reads are deliberately
* not gated on this: channel data that arrived before the disconnect is
* still the caller's. Call only after ssh has been checked for NULL. */
WOLFSSH_LOCAL int SendAfterDisconnect(WOLFSSH* ssh);
WOLFSSH_LOCAL int DoReceive(WOLFSSH* ssh);
WOLFSSH_LOCAL int DoProtoId(WOLFSSH* ssh);
WOLFSSH_LOCAL int wolfSSH_SendPacket(WOLFSSH* ssh);