echoserver: answer session requests in callbacks - #1234
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Public-header documentation for new APIs/modes is currently inconsistent with the implemented return semantics and supported flows, which can mislead API consumers.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR extends wolfSSH’s server-side channel handling to support an opt-in, application-driven channel mode where wolfSSH_accept() stops after user authentication and the application drives session/channel progress via callbacks and wolfSSH_worker(). It also updates the echoserver example and adds/extends tests to cover the new behavior and retry semantics for non-blocking flows.
Changes:
- Add application-driven channel mode (
wolfSSH_CTX_SetAppChannels(),wolfSSH_SetAppChannels()) and adjustwolfSSH_accept()/channel-request handling accordingly. - Expose server-side helpers for app-driven flows (notably
wolfSSH_SCP_accept()andwolfSSH_AGENT_ChannelOpen()), and adapt SFTP accept behavior in this mode. - Update echoserver to support a
-Amode (callbacks start shell/SFTP/SCP sessions) and add regression/unit test coverage.
File summaries
| File | Description |
|---|---|
| wolfssh/wolfscp.h | Documents new server-side SCP accept API for application-driven transfers. |
| wolfssh/ssh.h | Documents new application-driven channel mode and adds public setters. |
| wolfssh/internal.h | Adds appChannels flag to WOLFSSH_CTX and WOLFSSH structs. |
| wolfssh/agent.h | Declares wolfSSH_AGENT_ChannelOpen() for app-driven agent forwarding channel open. |
| src/ssh.c | Updates wolfSSH_accept() stop-state logic; adds setters for app-channels; refactors agent open to new helper. |
| src/internal.c | Changes default “no callback” behavior for shell/exec/subsystem requests when app-driven mode is enabled. |
| src/wolfsftp.c | Makes wolfSSH_SFTP_accept() treat app-driven accept stop-state as “accept done” for callers. |
| src/wolfscp.c | Implements wolfSSH_SCP_accept() wrapper over SCP request handling with WANT propagation. |
| src/agent.c | Implements wolfSSH_AGENT_ChannelOpen() helper for server-side agent channel open. |
| tests/unit.c | Adds unit coverage ensuring no-callback session requests are refused in app-driven mode. |
| tests/regress.c | Adds regression coverage for accept stopping at userauth, callback dispatch, inheritance, and late-enable behavior. |
| tests/api.c | Adjusts keyboard-interactive test client args to avoid shell-callback refusal in echo mode. |
| examples/echoserver/echoserver.c | Adds -A mode and callback-driven session start; updates non-blocking retry loops for SFTP/SCP; refactors shell startup into callback. |
Review details
- Files reviewed: 13/13 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #1234
Scan targets checked: wolfssh-bugs, wolfssh-src
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.
78bc4db to
987a00d
Compare
A server that wants to own its channels had no way to get them: accept() ran the session state machine to the end, and a shell, exec or subsystem request with no callback registered was granted regardless. - add wolfSSH_CTX_SetAppChannels() and wolfSSH_SetAppChannels(), off by default, a byte on the context copied into the session - on, accept() returns once the user is authenticated, and a session request with no callback behind it is refused: nothing is left to serve - keep the stop state out of the pending-send advance, so a re-entry with queued output cannot step over where this call is meant to stop - stop early only while the session is short of that state, so turning the mode on afterward cannot leave the loop hunting a state it went past - teach wolfSSH_SFTP_accept() that the mode parks accept() short of an established session, so it stops redoing the handshake on every poll
wolfSSH_SetAppChannels() changes where wolfSSH_accept() stops and what becomes of a session request with no callback behind it, so both modes are exercised. - regress.c drives a server with the pivot on, one with a shell callback and one without, and checks accept() stops at ACCEPT_SERVER_USERAUTH_SENT - regress.c pins the context setter, the session's inheritance of it, and that turning it on after accept() established the session still returns - regress.c re-enters a parked accept() with output still queued, which is the one path that flushes before reading the state, and pins that it leaves the state on the stop - unit.c checks DoChannelRequest() refuses a shell, exec and subsystem request with no callback once the pivot is on
DoChannelRequest() reads ssh->appChannels when the request arrives, so turning the mode on after accept() established the session still refuses an uncallbacked shell, exec or subsystem request from then on. Only accept()'s stopping point is pinned, by the guard around stopState. - say the flag reaches the requests that follow, and that what it cannot do is move where accept() returns - drive a shell request over the wire in both modes from the late-enable test, pinning the behaviour the header now describes
In application-driven mode wolfSSH_accept() parks at userauth, so the sftp test its divert applies never runs. wolfSSH_SFTP_accept() applies it itself: the session channel must be a subsystem the application's callback granted sftp on, or the call returns WS_INVALID_STATE_E and leaves the wire alone without recording an error. - gate the app-channels branch on wolfSSH_GetSessionType() and wolfSSH_GetSessionCommand(), the same test accept() makes - ask for that grant in every accept state: below the user-auth stop accept() returns with no channel open, and past the stop there is no accept() left that could have checked anything - say in ssh.h that the mode serves SFTP through that grant and never reaches the SCP entry point - regress.c refuses the call with no channel, ahead of accept(), on a granted shell and on an established one, and serves an INIT on a granted sftp subsystem
wolfSSH_SFTP_accept() serves an application-driven session only on a channel whose subsystem request was answered CHANNEL_SUCCESS. DoChannelRequest() records the session type and command before it decides, and leaves both set on a refusal, so they cannot say by themselves whether anything was granted. - add channel->sessionGranted, set from the answer a shell, exec or subsystem request gets rather than from the request arriving - look the channel up again before recording it: a callback may close its own channel, and wolfSSH_ChannelFree() frees it - log a request's strings where they are known good: once the parse has succeeded, and ahead of a callback that may free the channel - gate the app-channels path on that flag alongside the session type and the command - cover a refusal from both sides, no callback registered and a callback that rejects, and a callback that frees its channel
The built-in SFTP server takes a session only when the subsystem name is sftp, matched whole. DoChannelRequest() keeps the parsed length in channel->commandSz, so neither wolfSSH_SFTP_accept()'s grant gate nor wolfSSH_accept()'s divert serves "sftpx" or "sftp\0evil". - cover a granted name longer than sftp, one of its length, and one running past an embedded NUL - cover the divert with those three names and a control that diverts - exec keeps its command length too Issue: F-11665
CheckSftpAcceptRefusesUngranted() drives the same refusal two ways, a registered subsystem callback saying no and app channels standing in for a missing one, and asserted nothing that told them apart. Assert the call count each case expects.
An application vetting an exec or subsystem request in its channel request callback is handed the command as a C string, which stops at an embedded NUL. wolfSSH_ChannelGetSessionCommandSz() and wolfSSH_GetSessionCommandSz() report the parsed wire length, so a callback can match a name whole the way DoChannelRequest() does. - both accessors report 0 for a NULL channel or session - wolfSSH_GetSessionCommand() defers to the channel accessor - correct the trace name in wolfSSH_ChannelGetSessionCommand() - cover a callback seeing "sftp\0evil" through exec and subsystem
DoChannelRequest() records the session type and asks the exec and subsystem callbacks whether to grant a session only when the command string parsed. A failed parse is refused on ret alone, and channel->command still holds an earlier request's value rather than the one being answered. - cover a command length header running past the end of the packet, on exec and on subsystem Issue: F-11674
ChannelDelete() wipes the peer's exec or subsystem command line before releasing it, the way it already wipes the decrypted inputBuffer just above. A command line can carry a password or a token among its arguments. - cover the wipe with the retain-on-free allocator - release the test's hand-built channel on a setup failure Issue: F-8850
DoChannelRequest() returns early when the header parse fails, so the ret == WS_SUCCESS test that followed it could never be false. The channel lookup moves into the else, which is the only way the function reaches it. Issue: F-11657
With -A the echoserver drives its own channels: accept() returns at userauth and the callbacks below start the shell, SFTP or SCP session. Off by default. The two modes are exclusive, since the callbacks answer the session requests the accept state machine otherwise answers itself. - wsShellStartCb() forks the pty, so it is registered in either mode, and claims the channel only once there is a shell behind it; a second request is refused rather than forking over the running shell - wsExecStartCb() takes an "scp " command as a transfer and any other command as a session, and is registered in either mode, since the legacy path has always started a shell for an exec request too - wsSubsysStartCb() is registered only with -A, as accept() serves sftp itself, and guards a NULL command, which a truncated request leaves behind - ssh_worker() drives the session through shellCtx.appFd, claims the channel itself when no callback did, and leaves an SFTP or SCP handoff through its cleanup so the pty master still closes - open the agent channel from the select loop, since the peer's auth-agent-req lands after accept() has returned, and read the listener from the context each pass because it appears mid-loop - resume a subsystem accept that returns a want, waiting on the socket between attempts rather than spinning - close the accepted socket again, clear fwdFd on EOF or reset, and stay in the loop on WS_REKEYING, which the read arm already handles - key ChildRunning's sig_atomic_t on WOLFSSH_SHELL, the only build with the SIGCHLD handler that writes it, so a target whose libc has no signal.h still compiles - ask for echo mode in the keyboard-interactive test, which has no account on the host for the shell callback to fork a shell for
The echoserver's -A mode runs an accepted scp command through wolfSSH_SCP_accept(). Reaching that call's want retry path takes a non-blocking server, which -N supplies. - copy to and from an app-driven server in scp.test - check the entry point's null-session argument
987a00d to
60063d2
Compare
Stacked on #1233, and it also carries #1231's commit verbatim, so only
the last commit is new here. With -A the echoserver drives its own
channels: accept() stops at userauth and the callbacks below start the
shell, SFTP or SCP session. Off by default, and the two are exclusive.
claims the channel only once there is a shell behind it.
command as an echo session; wsSubsysStartCb() guards a NULL command.
channel itself when no callback did.
between attempts rather than spinning.