Commit Graph

3073 Commits (10d97e151fbab0c8d06be441cdc8c2e121bca65d)

Author SHA1 Message Date
John Safranek 10d97e151f echoserver: wait to write in the SFTP loop
A send the socket refused leaves WS_WANT_WRITE, and the retry branch
continues past the only tcp_select() in the iteration, which watches
reads. Wait on write readiness there, so a peer that has stopped
reading costs a descriptor wait rather than a spin.

- a buffered send with a willing socket still goes straight around
- an error-ready or failed descriptor ends the loop
- an interrupted select() retries instead of ending the session
2026-09-16 09:55:24 -07:00
John Safranek dc03049a13 ssh: add a channel session-granted accessor
wolfSSH_ChannelGetSessionGranted() reports whether a shell, exec or
subsystem request on a channel has been answered CHANNEL_SUCCESS, so an
application can tell a second request from the first without reaching
into WOLFSSH_CHANNEL.

- the flag is still clear for the request a session callback is
  answering, so a set flag is an earlier request's grant
- wolfsshd's session callback reads the grant through it
2026-09-15 21:55:50 -07:00
John Safranek fd4d462510 scp: match the scp command as a whole token
wolfSSH_ChannelCommandIsScp() reports whether a channel's session
command starts an SCP transfer, taking "scp" only as its own token. The
divert in wolfSSH_accept() and an application's exec callback both ask
it, so the two cannot disagree about what the SCP server is handed.

- an exec of "scpbackup foo" runs as an ordinary exec
- a command carrying a NUL within the recorded command size is not an
  SCP command; ParseScpCommand() walks a C string, so a NUL would drop
  whatever follows it
- wolfsshd's session callback asks through the same helper
2026-09-15 21:55:50 -07:00
John Safranek 74a296d9c2 wolfsshd: refuse sessions it cannot serve
A shell, exec or subsystem request is answered as it arrives, through
the channel request callbacks, so a session this build cannot serve, or
a second one on a channel already running one, is refused with
CHANNEL_FAILURE rather than accepted and then dropped once the session
is up. What the daemon serves is unchanged.

- SessionRequestCb() takes a shell with WOLFSSH_SHELL, an exec with
  WOLFSSH_SHELL or an scp command with WOLFSSH_SCP, and the sftp
  subsystem with WOLFSSH_SFTP; anything else is refused and logged
- a request whose command did not fit is refused rather than read
  through a NULL
- a second program start is refused on a channel whose grant already
  stands, so sftp or scp cannot take over a running session
- the sftp name is matched whole and scp only as its own token, both
  by length and bytes, so "scpbackup" or a name with an embedded NUL
  is some other command
- sshd_bad_subsystem_test.sh asks for an unknown subsystem with the
  OpenSSH client and expects the refusal
2026-09-15 21:55:50 -07:00
John Safranek 8460f3f90c internal: log a subsystem request as a subsystem
DoChannelRequestSession() serves the shell, exec and subsystem arms
together, and its debug line labels the string it read. A subsystem
request labels that string "subsystem" rather than "command".

- unit.c: renumber the shell-after-exec failure to follow the codes
  above it
2026-09-15 21:55:50 -07:00
John Safranek 7bb39b63e7 internal: re-find the channel after the callback
The generic channel request callback may free the channel it was handed,
so DoChannelRequest() looks it up again before the type handling reads
it. Gone, the request ends there, and a reply the peer wanted fails on
the missing channel the way one after a typed session callback does.

- ssh.h says the callback may free its channel and what the request does
  from there
- regress.c frees the channel from the callback on each of the three
  answers, and on a pty-req, the type that wrote to the channel outside
  a session request
2026-09-15 15:27:16 -07:00
John Safranek 4477f4744a ssh: add generic request callbacks
wolfSSH_CTX_SetChannelReqAnyCb() and wolfSSH_CTX_SetGlobalReqAnyCb()
register a callback consulted first for a channel or global request,
with the name, the type-specific part, and whether a reply is wanted. A
tri-state answer grants, refuses, or leaves it to the handling already
there, so a policy reaches the types with no hook of their own.

- the name is the one that arrived, since a copy into a buffer truncates
  a long name and ends it at an embedded NUL, and a policy has to answer
  on what the peer sent
- a grant still parses and records what the library needs, so a granted
  session request commits the session, the typed callbacks are not
  consulted, and a granted unknown type is answered CHANNEL_SUCCESS
- a port-0 tcpip-forward skips the callback, since only the forward
  callback can report the port bound and a policy that had bound a
  listener would then have to be refused, per RFC 4254 7.1
- a client refuses tcpip-forward and cancel-tcpip-forward ahead of any
  policy, matched on the name that arrived so it holds in a build with
  no forwarding, where neither name is in the name table
- window-change, exit-status and exit-signal are cleared of a reply
  before the handling runs, so a refusal leaves them unanswered too, per
  RFC 4254 6.7 and 6.10
- regress.c covers the answers, the data delivered, the names that do
  not fit a copy, and which callbacks each answer leaves out
2026-09-15 15:27:16 -07:00
John Safranek ba8996038d tests: skip app-driven sftp in a block build
The non-blocking app-driven case runs in every other build. Under forced
blocking the two ends deadlock -- the server sits in DoReceive with
output it owes still queued, the client waits for that output -- which
is a defect of its own to fix rather than this case failing.

- blockBuild names the macro being set, which nonblockingOnly already
  stood for on a different question
2026-09-15 09:31:16 -07:00
John Safranek 46795b1ef2 tests: cover the app-driven sftp and shell paths
The -A cases in scp.test reach the exec callback and the scp handoff.
These drive the rest: the subsystem callback with the sftp accept, the
shell callback in echo mode, and both accepts on a blocking server.

- sftp.test connects to an -A server blocking and non-blocking
- sshclient.test runs a terminal session and a command session against
  an -A server
- scp.test copies from a blocking -A server, where wolfSSH_SCP_accept()
  completes in one call rather than through the retry loop
2026-09-15 09:31:16 -07:00
John Safranek 95369017da echoserver: retry a send the socket did not take
ssh_worker() reads a WS_WANT_WRITE from wolfSSH_worker() as a send the
socket has not taken yet: it waits for the socket to accept one and runs
the worker again, rather than leaving the loop. Application-driven mode
answers session requests here, and the peer waits on that reply, so a
blocked one ended a session the legacy accept() carried through.

- the worker runs on a writable socket as well as a readable one, or
  the owed send is never retried
- the ssh socket joins the select write set only while a want-write is
  outstanding
- a queued agent channel open joins the same wait, so the poll runs
  ahead of the write set
2026-09-15 09:31:16 -07:00
John Safranek ecb79ea4e3 echoserver: bind sftp and scp as the library does
The subsystem and exec callbacks pick out the same commands the accept
state machine does: sftp matched whole, by length and bytes, and scp on
the three-byte prefix ChannelCommandIsScp() takes.

- sftp with an embedded NUL is refused, rather than granted and then
  dropped by wolfSSH_SFTP_accept()
- a transfer is granted only on the head channel the accept APIs serve,
  so a request on a later channel is refused rather than answered
  success
2026-09-15 09:31:16 -07:00
John Safranek eef5346f7f echoserver: claim only a granted, usable session
ssh_worker() takes the session channel accept() established only when
the request was granted and there is somewhere to put the data. The
type and command stay set on a refusal, so they do not say what was
granted, and a refused request leaves nothing running.

- a shell build serves the channel through the pty, so with no shell
  started only echo mode can take it
- wsShellStartCb() closes the pty and reaps the child when the terminal
  setup fails, installs ChildSig() only once the session will run, and
  leaves ChildRunning alone when forkpty() fails
- the connection holds the shell's pid, so the worker loop's exit and
  an accept() whose reply failed end the child too
- the shell, exec and subsystem callbacks share SessionInUse(), so a
  second program start on the connection is refused
- the EOF drain answers a half-close only on a claimed session, rather
  than on whatever channel id 0 finds
2026-09-15 09:31:16 -07:00
John Safranek 6b954da7f7 tests: cover the application-driven SCP start
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
2026-09-15 09:31:16 -07:00
John Safranek 971f2f01e8 echoserver: answer session requests in callbacks
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
2026-09-15 09:31:16 -07:00
John Safranek 399c83b1b6 coverage: survive a truncated raw profile
The merge step takes --failure-mode=all, so a raw profile left short by
a killed process is warned about and skipped instead of aborting the
report. Tests SIGKILL their servers in cleanup traps, and a process
killed while writing its profile leaves a corrupt header behind. The
step still fails when no profile can be read at all.
2026-09-14 15:22:14 -07:00
John Safranek 65802d438c internal: commit a session only once accepted
A shell, exec or subsystem request changes the channel only once the
callback accepts it. The session type and command are set for the
callback to read and put back if it refuses, and CLIENT_DONE follows
acceptance alone, so wolfSSH_accept() stays where it is rather than
reporting a session it answered CHANNEL_FAILURE as established.

- DoChannelRequestSession() carries the three arms, which differed only
  in the type and the callback consulted
- a refusal puts the type and command back, so a grant an earlier
  request won still stands
- FreeChannelCommand() wipes and releases a command line for both
  ChannelDelete() and the refusal path
- unit.c drives a refused shell, exec and subsystem request through
  DoChannelRequest() and checks nothing was committed, and that a
  refusal after a grant puts the earlier command back whole
- regress.c checks accept() stays at ACCEPT_SERVER_CHANNEL_ACCEPT_SENT
  on a refused shell, and that the sftp gate and both diverts ask for
  the grant alone

Issue: F-8852
2026-09-14 14:48:15 -07:00
Yosuke Shimizu a14e6d0f3c ssh, apps, examples: take the worker's status from its return
- wolfSSH_OutputPending() moves from wolfssh/internal.h to
  wolfssh/ssh.h as a WOLFSSH_API taking a const WOLFSSH*, defined
  in src/ssh.c beside the other public calls.
- wolfSSH_worker() puts the send's code in the return in place of
  an event when the flush fails outright, and gates its flush on
  wolfSSH_OutputPending(). wolfssh/ssh.h states both.
- Both wolfsshd shell loops, both echoservers and
  ReceiveScpMessage() dispatch on wolfSSH_worker()'s return, and
  call wolfSSH_get_error() only to tell a transient failure from a
  terminal one. The POSIX wolfsshd loop sets wantWrite from
  wolfSSH_OutputPending().
- Five worker tests expect the send's code where they expected the
  event, and tests/testsuite.c calls wolfSSH_OutputPending().
2026-09-14 10:40:48 -07:00
Yosuke Shimizu 5ea9b736de scp: drain extended data in the SCP read path
- ScpStreamRead() calls _DumpExtendedData() and reads again when
  wolfSSH_stream_read() returns WS_EXTDATA, reporting the drain's
  own failure when it has one.
- ReceiveScpConfirmation() drops its WS_EXTDATA arm and reports a
  negative read directly.
- ReceiveScpMessage() returns _DumpExtendedData()'s failure in place
  of discarding it.
2026-09-14 10:40:48 -07:00
John Safranek 2a15157cf6 scp, apps: correct three ssh->error reads
ReceiveScpConfirmation() takes a WS_EXTDATA off ScpStreamRead()'s
return, the rule ssh.h now states for reading an event.
FlushQueuedSend() samples the owed write with the session still
locked, since the peer reader thread writes ssh->error too, and
reports a send that failed there over the event it rode in on.

- A pass that delivers stderr while its flush short-writes returns
  WS_EXTDATA with WS_WANT_WRITE in ssh->error, so the confirmation
  read aborted the transfer with the stderr undrained.
- wolfSSH_stream_read() on the reader thread clears ssh->error with
  this thread's packet still queued, which ended the flush early and
  reported it a success.
- An event return with a hard send error latched collapsed to
  WS_SUCCESS, telling the caller a queued packet reached a socket
  that was gone.
2026-09-14 10:40:48 -07:00
Yosuke Shimizu 21deb724fe scp, test.h, apps, examples: wait to write and route on the return
- tcp_select_write() joins tcp_select(), with WS_SELECT_SEND_READY
  at the end of the enum. tcp_select() passes NULL for writefds and
  cannot wait on the write side.
- The Windows wolfsshd window-change drain and sftp_worker()'s
  handshake flush retry wait on it. The drain records a give-up in
  ret, and sftp_worker() breaks only on WS_SELECT_ERROR_READY.
- The echoserver, Espressif and both wolfsshd shell loops, and
  ReceiveScpMessage(), take the event from wolfSSH_worker()'s
  return when it carries one, in place of reading ssh->error alone.
  The two echoservers cover WS_CHAN_RXD, WS_REKEYING,
  WS_CHANNEL_CLOSED and WS_EOF; the wolfsshd loops and
  ReceiveScpMessage() cover the ones they have arms for.
- The POSIX wolfsshd loop takes an owed write into wantWrite before
  the event overwrites rc, and its WS_WANT_WRITE arm runs the
  channel drain below in place of skipping it.
- FlushQueuedSend() folds the receive's own statuses into
  WS_SUCCESS inside its loop and keeps flushing while
  wolfSSH_get_error() reports WS_WANT_WRITE within the deadline, in
  place of looping on the worker's return. It reports
  WS_WANT_WRITE when the deadline leaves the packet queued.
2026-09-14 10:40:48 -07:00
Yosuke Shimizu 586b697b18 ssh, internal: always flush the worker's output
- wolfSSH_worker() calls wolfSSH_SendPacket() whenever
  ssh->outputBuffer holds bytes and the session is not
  disconnected. ssh->error keeps the receive's code when the
  receive failed, and the close's when a WS_CHANNEL_CLOSED pass
  hard-failed its flush; WS_REKEYING is withheld on a failed
  flush. Drops the second DoReceive(), the WOLFSSH_TEST_BLOCK
  fork and the separate WS_CHANNEL_CLOSED flush.
- BundlePacket() resets ssh->outputBuffer.length to
  ssh->packetStartIdx when the framing fails. wolfSSH_shutdown()
  reports WS_WANT_WRITE when its close-read leaves output queued,
  and the send's own error in place of it when that send failed.
  SendPacketFlush() records its code in ssh->error on every
  transport failure path, and wolfSSH_TriggerKeyExchange() writes
  it only when SendKexInit() fails.
- portfwd, client and scpclient accept WS_WANT_WRITE from
  wolfSSH_shutdown(); in scpclient the close-message drain runs
  on it.
- wolfssh/ssh.h drops WS_WINDOW_FULL from wolfSSH_worker() and says
  to read the return and wolfSSH_get_error() as independent channels
  on every pass.
- Twenty unit tests and the extended TestWorkerReportsDisconnect
  cover what ret and ssh->error hold after a receive, send, buffer,
  callback or framing failure.
2026-09-14 10:40:48 -07:00
Yosuke Shimizu ab979865aa apps, examples: tolerate a worker want-write
- The echoserver and Espressif shell loops and the Windows
  wolfsshd shell loop treat a WS_WANT_WRITE from wolfSSH_worker()
  as non-fatal.
2026-09-14 10:40:48 -07:00
John Safranek 28bf47d2a0 tests: pin the get_fd invalid-socket sentinel
test_wolfSSH_set_fd() compares the NULL return of wolfSSH_get_fd()
against the platform invalid-socket sentinel, the value wolfSSH_new()
initializes rfd/wfd to. The old check only asserted the result was not
WS_SUCCESS, which the previous WS_BAD_ARGUMENT return also satisfied.
2026-09-10 18:03:59 -07:00
John Safranek ed633eac0c wolfSSH_get_fd: return socket sentinel on NULL
- Return -1 on both (Windows build and not), the sentinel wolfSSH_new()
  uses for rfd/wfd.
2026-09-10 18:03:59 -07:00
John Safranek ae81d4d857 ssh: divert only into a granted session
wolfSSH_accept() hands a session to the built-in SCP or SFTP server
only when the request naming it was answered CHANNEL_SUCCESS. A
callback that refuses one sends CHANNEL_FAILURE, yet sessionType and
command are recorded ahead of that answer and stay set, so the diverts
read a refused session as a served one.

- gate both diverts on channel->sessionGranted, as
  wolfSSH_SFTP_accept() already gates the app-channels path
- the SCP divert tests the channel list itself, having no command
  lookup ahead of it to do that
- cover each divert with a refused request and a granted control
2026-09-10 17:14:23 -07:00
John Safranek 3437d6f2cc internal: drop an always-true guard
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
2026-09-10 17:14:23 -07:00
John Safranek 0a9b44a962 internal: scrub the channel command on free
The peer's exec or subsystem command line is wiped ahead of both frees
that release it, ChannelDelete() and the GetStringAlloc() that replaces
it on a repeat request, the way ChannelDelete() already wipes the
decrypted inputBuffer just above. A command line can carry a password
or a token among its arguments.

- ScrubChannelCommand() leaves the free to GetStringAlloc(), so a parse
  that fails behind it holds no dangling pointer
- cover both wipes with the retain-on-free allocator, the replacement
  through wolfSSH_TestDoChannelRequest()
- release the test's hand-built channel on a setup failure

Issue: F-8850
2026-09-10 17:14:23 -07:00
John Safranek ff1f82f547 internal: act on a session request only if parsed
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
2026-09-10 17:14:23 -07:00
John Safranek 9c8caea8b5 ssh: expose the session command length
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
- the sftp divert in wolfSSH_accept() asks the accessor for the length
- correct the trace name in wolfSSH_ChannelGetSessionCommand()
- cover a callback seeing "sftp\0evil" through exec and subsystem
2026-09-10 17:14:23 -07:00
John Safranek 44a348f01a tests: check the rejecting callback actually ran
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.
2026-09-10 17:14:23 -07:00
John Safranek 3516fb00a0 ssh, sftp: match the sftp subsystem name exactly
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
2026-09-10 17:14:23 -07:00
John Safranek 7adf524f6f sftp: require a granted subsystem to serve
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
2026-09-10 17:14:23 -07:00
John Safranek 742f327ed4 sftp: serve app-channels only on a granted sftp
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
2026-09-10 17:14:23 -07:00
John Safranek 4ac4484afd ssh: correct what a late app-channels enable does
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
2026-09-10 17:14:23 -07:00
John Safranek 2c832010d2 tests: cover application-driven channels
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
2026-09-10 17:14:23 -07:00
John Safranek b723519312 ssh: add opt-in application-driven channels
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
2026-09-10 17:14:23 -07:00
Yosuke Shimizu c039486e52 scp: return directory entries from the Zephyr entry walk
- The WOLFSSH_ZEPHYR branch of FindNextDirEntry() loops while the
  entry name is "." or "..", matching the POSIX and Windows
  branches, in place of while (1).
- tests/api.c gains test_wolfSSH_SCP_SendRecursiveEntry(), staging a
  directory holding one file and driving three
  WOLFSSH_SCP_RECURSIVE_REQUEST calls through wsScpSendCallback(),
  checking the entry name, size, and bytes placed in buf.  dirPath
  is rooted at CONFIG_WOLFSSH_SFTP_DEFAULT_DIR under WOLFSSH_ZEPHYR
  and at "./scp_recur_entry" otherwise.
- The test is gated on WOLFSSH_SCP, with WOLFSSH_SCP_USER_CALLBACKS,
  NO_FILESYSTEM and NO_WOLFSSH_DIR unset, carries an empty stub
  otherwise, and is called from wolfSSH_ApiTest().
- scpStageRecurFile() writes that fixture file.

Issue: F-13315
2026-09-10 15:28:17 -07:00
Yosuke Shimizu d3ad9fa5ed internal: gate auth-agent channel opens on the client's request
- DoChannelOpen refuses an auth-agent open on a server endpoint, and on
  a client with the agent disabled or connectState below
  CONNECT_CLIENT_CHANNEL_AGENT_REQUEST_SENT.
- Both answer OPEN_ADMINISTRATIVELY_PROHIBITED; the ssh->agent check
  stays as the resource check behind them.
- Cover the refusals before the request, with the agent disabled, over
  an accepting channelOpenCb, and on a server, plus the accepted open
  past the request; each refusal asserts ssh->error stays clean.
- Move TestAgentChannelNullAgentSendsOpenFail to a client harness, and
  have the agent tests set the agent flag explicitly.

Issue: F-13389
2026-09-10 15:28:04 -07:00
Hideki Miyazaki e04a29784f windows-check: Cache the wolfSSL build in mingw-regress
Every run of this job rebuilt wolfSSL --enable-all from source on a
Windows runner, about five minutes, even though the ref is pinned
and the configure flags never change between runs.

Add an actions/cache step keyed on wolfssl-mingw-regress-wolfssl-
<ref>-windows-latest, matching the naming singlethread-check.yml and
x509-interop.yml use. Those two split the work into a build_wolfssl
job and a build_wolfssh job, with a lookup-only cache check in the
first and a fail-on-cache-miss restore in the second. This job has
no such split, so it uses a single plain actions/cache step instead:
it restores on a hit and saves automatically after the job on a
miss.

The MSYS2 shell's $HOME lives under setup-msys2's own temp install
directory, not a path stable across runner images, so build to
${{ github.workspace }}/wolfssl-install instead: actions/cache
resolves a relative path against GITHUB_WORKSPACE, and cygpath -u
gives the msys2 shell steps the same directory as $WOLFSSL_INSTALL.
2026-09-10 15:27:50 -07:00
Hideki Miyazaki 343582a61e tests: Key regress.c's Windows portability macros on _WIN32
TEST_NULL_DEVICE, TEST_SETENV, TEST_UNSETENV, and TEST_MKDIR were
gated on USE_WINDOWS_API, while the arpa/inet.h/direct.h include
guard a few lines above keys on _WIN32. Whether mkdir(), setenv(),
and /dev/null work as expected is a libc availability question, not
a wolfSSH API-selection one, so _WIN32 is the more direct fit and
now both guards in this file agree.

No behavior change: USE_WINDOWS_API is only ever set when _WIN32 is
already defined, so this does not change which branch any current
build takes.

Verified with the MinGW cross compiler both with WOLFSSH_SFTP defined
and without, and confirmed a plain Linux build still passes
tests/regress.test and tests/unit.test.
2026-09-10 15:27:50 -07:00
Hideki Miyazaki 0bb371a4b2 windows-check: Pin wolfssl to v5.9.1-stable in mingw-regress
The wolfssl checkout in mingw-regress had no ref, so it always built
against wolfssl/wolfssl's default branch. An upstream change there
could break this job with nothing changed on the wolfssh side to
explain it, and this job in particular turned out to be sensitive to
exact wolfSSL build details while it was being brought up.

Pin it to v5.9.1-stable, the same tag singlethread-check.yml and
x509-interop.yml already use, confirmed to include wc_mlkem.c so the
ML-KEM coverage this job exercises is still built. The build and
asan-tests jobs in this file have the same unpinned checkout but are
left alone here, since they were not touched by this change.
2026-09-10 15:27:50 -07:00
Hideki Miyazaki 82032bddfe tests: Stop TestKnownHostsLastEntry depending on WMKDIR
A local run with ./configure --enable-smallstack (no --enable-sftp)
showed WMKDIR as an implicit, undeclared function under MinGW.
wolfssh/port.h only defines WMKDIR when WOLFSSH_SFTP, WOLFSSH_SCP, or
WOLFSSH_SSHD is enabled, but TestKnownHostsLastEntry itself is gated
on WOLFSSL_BASE64_ENCODE alone, so it compiles in configurations
where none of those three are on and WMKDIR does not exist.

Add a small TEST_MKDIR macro next to the existing TEST_SETENV and
TEST_UNSETENV ones, backed directly by _mkdir() under USE_WINDOWS_API
and mkdir() otherwise, and use it in place of WMKDIR. The Windows
branch needs direct.h for _mkdir's declaration; include it next to
the existing _WIN32 guard around arpa/inet.h at the top of the file.

Verified with the MinGW cross compiler both with WOLFSSH_SFTP defined
and without, and confirmed a plain Linux build configured with
--enable-smallstack (no --enable-sftp) still passes tests/regress.test
and tests/unit.test.
2026-09-10 15:27:50 -07:00
Hideki Miyazaki 53683a758c tests: Use the Windows null device in the known_hosts regression test
The next mingw-regress failure after the previous fix was
TestKnownHostsLastEntry failing its "ready" assertion. It calls
open("/dev/null", O_RDONLY) to point stdin at EOF while it drives the
known_hosts prompt, but Windows has no /dev/null; the call simply
fails there, so open() returns -1 and every check gated on ready is
skipped.

Add a TEST_NULL_DEVICE macro that resolves to "NUL" under
USE_WINDOWS_API and "/dev/null" otherwise, and use it at both call
sites in this file: TestKnownHostsLastEntry, and the same pattern in
TestPasswordEofNoCrash a bit earlier, which happens to be masked in
CI by its own isatty() guard but would hit the same bug if ever run
against a real terminal on Windows.
2026-09-10 15:27:50 -07:00
Hideki Miyazaki 4df003061a apps/wolfssh: Write known_hosts entries in binary mode
CI's mingw-regress job showed AppendKeyToFile writing a known_hosts
entry, then TestAppendKeyToFile reading it back and finding a byte
mismatch on Windows. The file was opened with WFOPEN(..., "a"), a
text mode append. On Windows the C runtime rewrites '\n' to CRLF on
write in text mode, so the entry landed on disk with a trailing
"\r\n" instead of the "\n" the test wrote and expected back.

Open the file in binary mode instead, matching the WriteTextFile test
helper a few hundred lines above in tests/regress.c, which already
uses "wb" for the same reason. known_hosts is conventionally
LF-terminated regardless of platform, so this also matches the format
other SSH clients expect from the file, not just the test.
2026-09-10 15:27:50 -07:00
Hideki Miyazaki 6d163a87d9 Fix remaining MinGW build errors in the regress/unit test build
apps/wolfssh/common.c declared a CONSOLE_SCREEN_BUFFER_INFO local in
ClientSetEcho that nothing ever read; the Windows echo toggling never
grew the code that would have used it. Drop the unused declaration.

wolfssh/port.h defined WSTRSEP as strsep(), a BSD extension MSVCRT
and MinGW do not provide. Add a portable wstrsep() in src/port.c,
matching the wstrnstr/wstrncat/wstrdup pattern already used for other
missing string functions, and route WSTRSEP through it under
USE_WINDOWS_API.

tests/regress.c called the two argument POSIX mkdir(path, mode) and
setenv()/unsetenv() directly in TestKnownHostsLastEntry. Use the
existing WMKDIR macro for the directory creation, and add small
TEST_SETENV/TEST_UNSETENV macros backed by _putenv_s() on Windows so
the HOME juggling this test does still works there.

src/wolfsftp.c had three separate issues in code paths that had never
been compiled before this job existed. wolfSSH_SFTP_RecvOpen declared
a flagsAndAttrs DWORD that nothing read, since WS_CreateFileA is
called with a hardcoded FILE_ATTRIBUTE_NORMAL instead.
wolfSSH_SFTP_RecvOpenDir compared a signed loop counter against a
sizeof expression while building ssh->driveList, so make the counter
word32. wolfSSH_SFTP_Put passed &state->rSz, an int, to ReadFile()'s
DWORD* output parameter; read into a local DWORD and copy it into
state->rSz afterward, since that field is also assigned from
WFREAD() on non-Windows builds.

src/wolfterm.c's wolfSSH_DoOSC never used its handle parameter. Mark
it with WOLFSSH_UNUSED rather than removing it, since the parameter
matches the signature its two call sites already pass and future OSC
handling such as window titles is a natural use for it.

Verified against a real x86_64-w64-mingw32 cross compiler with a
config.h edited to match the sizes and header availability the
actual Windows CI run reported (SIZEOF_LONG 4, HAVE_SYS_IOCTL_H
undefined, and so on): every file this job compiles builds cleanly
under the same -Werror flag set. Also reconfirmed a clean, unmodified
Linux build still passes both tests/regress.test and tests/unit.test.
2026-09-10 15:27:50 -07:00
Hideki Miyazaki 83a2246471 Fix three MinGW -Werror build failures in the regress/unit test build
MinGW-w64 does not ship arpa/inet.h; guard tests/regress.c's include
the same way apps/wolfsshd/auth.c already does, since htonl/ntohl end
up declared via the winsock2.h wolfSSL's headers pull in later in the
same translation unit.

wolfssh/test.h guarded its MSVC #pragma warning(disable:4996) with
USE_WINDOWS_API alone, which is also true for MinGW's gcc; gcc treats
the unrecognized pragma as an error under -Werror. Require _MSC_VER
too, matching the existing ALIGN16 pragma guard in wolfssh/internal.h.

wolfSSH_CleanPath's Windows/Nucleus drive-letter cleanup re-declared
`i` in a nested scope, shadowing the function's own `i` used by every
other loop in the function. Hoist `j` to the function's declarations
(guarded by the same #if so non-Windows/Nucleus builds don't get an
unused-variable warning) and drop the now-unnecessary block so the
loop reuses the outer `i`.

Verified locally: autoreconf + configure + make tests/regress.test
tests/unit.test builds clean and both binaries pass on Linux.
2026-09-10 15:27:50 -07:00
Hideki Miyazaki 4105c2242c windows-check: Build the .exe-suffixed regress/unit test targets on MinGW
The new run showed the ws2_32/crypt32 link fix worked (configure
passed) but make then failed: "No rule to make target
'tests/regress.test'." Reproducing the autotools build locally
confirmed why: MinGW's EXEEXT is ".exe", so automake's check_PROGRAMS
rule names the binaries tests/regress.test.exe and tests/unit.test.exe,
not the extension-less names this job was asking make to build and run.
2026-09-10 15:27:50 -07:00
Hideki Miyazaki 90ce9fb61b windows-check: Link ws2_32 and crypt32 for the mingw-regress wolfssh build
config.log from a failing run showed the wolfCrypt_Init AC_CHECK_LIB
probe pulling in ssl.c/internal.c/wolfio.c from the static
libwolfssl.a, leaving Winsock (socket, send, recv, inet_pton, ...) and
cert store (CertOpenSystemStoreA, ...) symbols unresolved. A shared
build would defer that resolution to the DLL; the static archive here
needs ws2_32 and crypt32 passed explicitly via LIBS.
2026-09-10 15:27:50 -07:00
Hideki Miyazaki 3532cc4432 windows-check: Print config.log on mingw-regress failure
The mingw-regress job's wolfssh configure step fails with
"libwolfssl is required for wolfssh" even though libwolfssl.a is
installed at the expected path. AC_CHECK_LIB only reports pass/fail;
dump config.log on failure to see the actual link error.
2026-09-10 15:27:50 -07:00
Hideki Miyazaki 39fec50df2 wolfsshd: Cover -D option parsing on Windows in CI
The Windows StartSSHD() path rebuilds argv from GetCommandLineW(). A
regression there left -D foreground mode walking the raw wide command
line, so -f and -p were ignored and the daemon used its built-in
defaults. Nothing in CI caught that.

Add sshd_dash_d_test.ps1: it starts wolfsshd with -D and a config file
at a non-default path whose Port line differs from the -p value, then
checks the listener binds the -p port and not the config port. That
holds only when -D mode parsed both -f and -p. Run it from the Windows
build job next to the existing LoginGraceTime check.
2026-09-10 15:27:50 -07:00