Commit Graph

514 Commits (master)

Author SHA1 Message Date
John Safranek b05fe9f4c9 Unbuffer stdin before a client password prompt
The password prompt reads stdin with fgets(), and a buffered stdin
keeps its own copy of the line in libc for the life of the process.
The clients make stdin unbuffered before any stdin I/O, so the typed
password lands only in the buffer ClientUserAuth() wipes.

- the wolfssh app, and the example client, sftp client and scp client
  whether or not -P was given, as keyboard-interactive reads stdin too
- ClientUnbufferStdin() acts once per process and reports a failure
- scripts/sshclient.test sends the session data right after the
  password, with no sleep

Issue: F-15669
2026-11-03 16:24:06 -06:00
John Safranek 9e206dd2f1 Wipe the client password once connect is done
ClientUserAuth() hands the library its static password buffer, and the
library sends from it after the callback returns. The clients now wipe
it, and the example's keyboard-interactive responses, as soon as
connect returns instead of holding them for the whole session.

- ClientClearPassword() in both common.c copies, called after connect
  in the wolfssh app, the example client and the sftp client
- the password arm wipes before each prompt and on failure, so a retry
  keeps no tail of a longer password
- the keyboard arm wipes its scratch copy after each response
- tests: regress and auth cases for the retry tail, the wipe and a
  failed read

Issue: F-8846
2026-11-03 16:24:06 -06:00
John Safranek 908ccef52a wolfsshd: use the exported constant-time compare
auth.c called the library-internal ConstantCompare(), which does not
resolve when the library is built with --disable-inline. Call
wolfSSH_ConstantCompare() instead.

Issue: F-7514
2026-11-03 16:02:53 -06:00
John Safranek 1c456fc843 Fix the cppcheck 2.13 findings
Restructure the two sites that left a redundant check in one build
configuration, and suppress a warning that only appears in a
configuration cppcheck invents.

- wolfsshd.c: set the -h host key file in the else of the cert store
  conflict check instead of retesting ret.
- internal.c CompositeEccSign: run wc_ecc_sig_to_rs from an else shared
  by the small-stack allocation check and the plain stack arrays.
- wolfsftp.c: suppress constStatement on WWRITE; cppcheck's -DWWRITE
  configuration turns the arguments into a comma expression.
2026-11-01 23:51:19 -06:00
John Safranek dce29c4369 wolfssh: warn when a known host adds a key type
The known_hosts search skips an entry whose host matches but whose key
type differs, so a host pinned with one type that presents another was
reported as an unknown host. The check now remembers that the host is
known with another type and says so before offering to add the new one,
as OpenSSH does.

- note the first other-type entry's key type and line in the loop
- print a warning with that entry, the host and the new fingerprint
  before the add prompt; a later same-type entry still matches or
  rejects as before
- reject a server key type that is not printable ASCII before it is
  printed or stored
- regress.c: add TestKnownHostsOtherType

Issue: ZD-22634
2026-10-30 18:00:28 -05:00
John Safranek 960e63cba0 wolfsshd: refuse a config line that does not fit
ConfigLoad() reports a line too long for its buffer and fails the load.
A cut line used to take effect with whatever fitted, since what is left
of it is read as a line of its own and a leftover of two bytes or less
is skipped as blank. The buffer is 1024 bytes now, so a long path or
forced command still loads.

- one byte is read past a full buffer, so a line that ends exactly
  there, at a newline or the end of the file, is not taken for a cut one
- the size is WOLFSSHD_MAX_LINE_SIZE in configuration.h, shared with
  the test
- test_configuration covers a long line, lines that exactly fill the
  buffer, and a line cut to leave a blank-looking remainder

Issue: ZD-22623
2026-10-30 13:51:24 -05:00
John Safranek 0f4002bc4e wolfsshd: apply ForceCommand to SFTP and SCP
A ForceCommand other than a bare "internal-sftp" now denies the SFTP
subsystem, and any ForceCommand denies SCP, as sshd_config(5) describes
it: a forced command applies to shell, command, and subsystem execution.
The session dispatch checks the configured command alongside a
certificate's, so either one on its own confines the session.

- the subsystem arm and both SCP arms consult the configured
  ForceCommand, not only a certificate's force-command
- "internal-sftp" followed by options denies SFTP, since no option is
  applied, while the shell still refuses it as "internal-sftp"
- every site testing for "internal-sftp" now shares one predicate
- root's transfer refusal no longer points at ForceCommand internal-sftp,
  which does not permit SCP
- tests require a ForceCommand to deny SFTP and SCP, read the daemon's
  decisions from its log, and pair a configured internal-sftp with a cert

Issue: ZD-22623
Issue: F-10597
2026-10-30 13:51:24 -05:00
John Safranek d04bad3a6e wolfsshd: match an unknown user by name
An unresolved user took the global methods, so a name a Match User
block covers advertised a different list depending on whether the
account existed. DefaultUserAuthTypes() now composes the blocks that
select on the name alone, with no group list, when the account does
not exist.

- a Match Group block still cannot apply to a name with no account, so
  it fails closed to the unmatched methods
- any other failure to resolve the user, or to compose its blocks,
  advertises no methods rather than the global ones, so a Match block
  that disabled a method is never skipped
- test_configuration stubs the account lookup and covers a lookup
  error, an unreadable group list and a resolved account as well as the
  Match User and Match Group blocks

Issue: ZD-22617
2026-10-30 12:11:37 -05:00
John Safranek a7df973842 wolfsshd: hide account existence in userauth
DefaultUserAuthTypes() resolved the requested user before any
authentication, so a username with no local account produced an empty
"methods that can continue" name-list. It now falls back to the global
configuration's methods when the user does not resolve, so the
USERAUTH_FAILURE reply no longer says which accounts exist.

- an unresolved user still cannot authenticate, since the same failed
  lookup rejects every method it tries
- test_configuration covers the advertisement for an unknown user,
  comparing against the global mask so it tracks the configuration

Issue: ZD-22617
2026-10-30 12:11:37 -05:00
John Safranek ad187fb7c4 wolfsshd: reject passwords containing a NUL byte
The password backends check the password as a C string, so "\0junk"
was checked as "" while its nonzero length passed the
PermitEmptyPasswords check. RequestAuthentication now refuses any
password with a NUL before the policy check and the backends.

- test_RequestAuth_embeddedNulPasswordRejected puts the NUL first,
  last and mid-password, and checks the fake crypt still runs

Issue: F-16096
2026-10-28 13:22:48 -05:00
John Safranek 48f36f753a Reject an out-of-range -p port
The -p option of wolfssh, client, wolfscp, wolfsftp and echoserver now
goes through a strtol check, so a value that is not wholly a number in
range exits with an error instead of wrapping into a different port.

- apps/wolfssh: ClientParsePort(), shared with ClientParseDestination(),
  which also rejects an empty "ssh://host:" port instead of defaulting
- examples: a ClientParsePort() in examples/client/common.c; port 0 is
  rejected in every build, not only with a main driver
- echoserver: accepts 0 to 65535, keeping 0 for the test harness
- add TestClientParsePort and an empty URI port case to regress.test

Issue: F-14763, F-14767, F-14768, F-12552
2026-10-28 13:10:29 -05:00
John Safranek d38e40bc8b wolfsshd: exec the login shell path whole
SHELL_Subsystem copied pw_shell into an 80-byte buffer before execv(),
so a longer path ran whatever its first 79 bytes named. The child now
passes pw_shell to execv() directly, and builds the login argv[0] from
its basename so a long path no longer falls back to "-sh".

- add sshd_long_shell_test.sh, which logs in as a throwaway user whose
  shell path is longer than 80 bytes and checks the shell's $0

Issue: F-14757
2026-10-28 13:10:29 -05:00
John Safranek 9ddb698e16 wolfsshd: refuse the PID file without O_NOFOLLOW
Where open() cannot refuse a final-component symlink, no pre-check can
stop a link swapped in before the open, and the fstat gate accepts a
root-owned target. wolfSSHD_ConfigSavePID now writes no PID file there
instead of relying on a racy lstat.

- test_ConfigSavePID gets a variant for that build that expects no file

Issue: F-14764
2026-10-28 13:10:29 -05:00
John Safranek 7c6f6fcb5e internal: gate channel requests and opens by role
A client refuses shell, exec, subsystem and pty-req channel requests
and direct-tcpip opens before any callback runs (RFC 4254 6.2, 6.5,
7.2). CHANNEL_OPEN_CONFIRMATION and CHANNEL_OPEN_FAILURE are a
protocol error unless the channel's own open is still outstanding.

- SshInit() installs the default WS_TermResize for servers only.
- Drop the Windows clients' raw-HANDLE resize context; nothing uses it.
- Move the channel request builders out of the server-only test block.

Issue: F-10582, F-13923, F-15533, F-15572, F-14434
2026-10-27 10:49:29 -05:00
John Safranek 63fc1fb605 wolfsshd: exit non-zero when startup fails
The POSIX StartSSHD() now returns EXIT_FAILURE when ret holds an error,
and main() passes that on. A config check (-t) of a bad config, a
failed host key load, or a bad -p now exit 1 instead of 0.

- set ret when the accept loop cannot allocate a connection, so that
  stop also exits non-zero
- PendingConnection() returns negative on a select() error other than
  EINTR, and the accept loop sets ret from it instead of quit
- log "Closing down wolfSSHD" whenever the accept loop exits, not
  only on quit, so the error exits still log it
- add sshd_exit_status_test.sh, which checks -D -t exit codes for a
  valid config and three startup failures

Issue: F-16053
2026-10-26 17:21:29 -07:00
John Safranek 8c3640db44 wolfsshd: keep accepting after a failed fork
StartSSHD() no longer stores the NewConnection() result in ret, the
accept loop's condition. A failed fork() or CreateThread() now drops
that one connection, and the daemon goes on listening.

- add sshd_fork_fail_test.sh with an LD_PRELOAD shim that fails the
  first connection's fork() with EAGAIN
- run it from run_all_sshd_tests.sh on leased port +7

Issue: F-16052
2026-10-26 17:21:29 -07:00
John Safranek d3fbc8676e internal: refuse a second session request
RFC 4254 sec 6.5 lets only one shell, exec or subsystem request succeed
per channel. DoChannelRequestSession() now refuses any request after
the first grant without asking a callback, so the granted session and
its command stand.

- a refusal before any grant leaves the channel with no command
- ssh.h documents the refusal; wolfsshd's own check is defence in depth
- regress.c sends a second exec, shell and subsystem after a granted
  exec, and a second exec over a generic-callback grant
- unit.c expects a shell after an exec to be refused and checks that a
  refused exec's command is scrubbed
2026-10-22 15:29:16 -05:00
John Safranek 1e92abf70d wolfsshd: test that a pty session ends on exit
A pty session has no stderr pipe, so the shell loop must start with
stderrEmpty set. The drain test now runs "exit 3" in a pty shell and
expects the session to end with that status rather than wait for a
stderr EOF that never comes.
2026-10-19 21:28:13 -05:00
John Safranek 5f31636aa8 wolfsshd: test the stderr a child leaves behind
A child that exits with its error output still in the pipe must have
all of it relayed. The child grows its stderr pipe and queues the
whole payload before exiting, so the result does not depend on the
pipe size or on which cat is installed.

- forty passes, each compared byte for byte with the payload
- a client failure is reported as one, not as a truncation
- fixtures readable by a login user other than the invoker
- skips without perl
- listed in extra_test_cases and local_sshd_tests
2026-10-19 21:28:13 -05:00
John Safranek 76a5e1cb3e wolfsshd: drain stderr before ending the session
The shell loop waits on the child's stderr the way it already waits on
its stdout. A child that exits with error output still in the pipe had
the rest relayed only by the single read below the loop -- one
bufferful, however much was waiting -- so the peer saw a truncated
error stream and no error. A pty session has no stderr of its own.
Neither pipe holds the loop once the peer closes the channel.
2026-10-19 21:28:13 -05:00
John Safranek dbcaa75f49 apps, examples: join readInput() on Windows
The Windows path of apps/wolfssh and examples/client sets the volatile
quit with InterlockedOr8() once readPeer() returns, repeats
CancelSynchronousIo() on the readInput() thread until it has exited,
and only then closes the handles and frees the session. readInput()
checks quit around its console read and inside its send retries, and
returns quietly when it is set.

Issue: F-11662
2026-10-16 11:54:31 -05:00
John Safranek 00b79a4d12 apps, examples: wipe the client thread buffers
readInput() and readPeer() in apps/wolfssh and examples/client clear
their stack buffer with wc_ForceZero() before returning, so neither
what was typed into the session nor the last of its output is left on
a finished thread's stack. On POSIX the main thread cancels
readInput() inside read(), so a pthread cleanup handler does the wipe
for it.

- readInput() holds only the buffer and the cleanup handler; the loop
  is readInputLoop(), since glibc's pthread_cleanup_push() is a setjmp
  and gcc -Wclobbered flags the locals of the function holding it.

Issue: F-10594, F-12647
2026-10-16 11:54:31 -05:00
John Safranek cf5df4a187 apps, examples: finish short sends and writes
readInput() in apps/wolfssh and examples/client sends again from
where a short wolfSSH_stream_send() stopped until the whole chunk is
taken, and waits out WS_WINDOW_FULL the way it waits out a rekey.
readPeer() in both writes stdout and stderr through a WriteAll() that
loops over short writes.

- A send clamped to the peer's window or packet size no longer drops
  the rest of the chunk, and the WS_WINDOW_FULL that follows the clamp
  no longer ends the stdin thread.
- The Windows stderr path writes the bytes read instead of a "%s".

Issue: F-8830, F-10548, F-10555
2026-10-16 11:54:31 -05:00
John Safranek fac8b97ef5 agent: exchange whole messages when signing
wolfSSH_AGENT_SignRequest() writes the sign request and reads the reply
through the whole-message helpers the relay uses, so a short write is
followed up and a reply is read to its declared length before it is
parsed. The setup callback's WS_AgentCbError result becomes
WS_AGENT_CXN_FAIL, and an ECDSA key's reply room fits the SSH blob.

- A short write, a hang-up, or a failed setup is WS_AGENT_CXN_FAIL;
  cleanup runs only after a setup that succeeded, and the client
  callbacks close their socket when connect() fails.
- PrepareUserAuthRequestEcc() reserves r and s as mpints a pad byte over
  the curve size, and BuildUserAuthRequestEcc() hands the agent that
  room, checks the reply's algorithm name, and bounds it by outputSz.
- A NULL sig, or a NULL digest or key blob with a size, is
  WS_BAD_ARGUMENT; agent.h documents the contract and return codes.
- api: the mock agent's short write is followed up, a reply read a byte
  at a time is reassembled, and a failed setup and NULL buffers are
  refused.
- regress: the mock agents answer as a stream; ECDSA user auth for each
  curve at, under, and over the largest blob, and a wrong curve name
  refused; a failed setup refused with nothing sent.

Issue: F-13325, F-14629, F-14631
2026-10-16 11:15:32 -05:00
John Safranek 34a9939cbc tests: report the sshd suite's pass count
The summary prints "N run, N passed, N skipped" from a PASSED counter
incremented at each success site. TOTAL counts every test reached, skips
included, so printing it under the label "passed" overstated the pass
count by exactly the number of skips.

- pair each orphan SKIPPED bump with a TOTAL bump, and count the
  local-only tests skipped against an external host by name, so the
  printed numbers are commensurable and the skip count cannot go stale
- count the host key gate's WOLFSSH_NO_HOSTKEY_PERMS exit as a pass: it
  prints PASSED and returns with TOTAL already charged, so leaving it
  uncounted would fail the new assertion on every build with that macro
- assert that passed plus skipped equals ran and, without --match, that
  ran equals the known test count, so a miscounting skip site or a name
  no call site reaches fails the run
- verified in the container: a full run reports 32 run, 30 passed, 2
  skipped against 30 PASSED lines, and --exclude sshd_forcedcmd_test.sh
  reports 32 run, 29 passed, 3 skipped against 29
2026-10-14 15:52:44 -05:00
John Safranek f5da31b17c tests: make every sshd test selectable
run_all_sshd_tests.sh validates --match and --exclude against test_cases
plus a new extra_test_cases naming the tests called in place, and every
call goes through one should_skip(). A match runs its test where it
sits, with the daemon it needs around it, so one path now serves the
whole suite. test_cases named 12 of the 32 tests the runner drives.

- gate the x509, UPN and ML-DSA daemon starts on the match, so selecting
  another test does not start them
- take the bulk skip for the tests needing a local daemon only on a full
  run, where its fixed count is right
- reject an unrecognized --exclude, and a --match that reached no call
  site: a local-only name selects nothing against an external host,
  which would otherwise exit 0 having run no test at all
- keep every test call inside one compound, so RUN_COMPLETE still fails
  a run that a shell expansion error cut short, and abort naming any
  run_test call listed in neither array
- verified in the container: a full run gives 32 run, 30 passed, 2
  skipped, and --match sshd_chroot_fail_test.sh gives 1 run, 1 passed,
  starting only the daemon that test needs
2026-10-14 15:52:44 -05:00
John Safranek 7e41ce2546 tests: cover the sshd chroot skip-after-failure
An LD_PRELOAD interposer fails one SetupChroot() step at a time and
records each chroot() plus every chdir() made after one, so a step that
ran when it should have been skipped shows up in the record. The daemon
stops after the first failed step, and nothing asserted that.

- three cases: chdir faulted, chroot faulted, and an unfaulted chroot
  into "/" that must still complete and run its command
- keep the fault injection in the interposer, with no fault code in the
  daemon or the library
- register it after the privilege-drop test, the other test that needs
  its own root-launched daemon
- skip when the example client is not built, and set the EXIT trap
  before building the interposer so no exit path leaves it behind
2026-10-14 15:52:44 -05:00
John Safranek bda30b7043 tests: fail the ossh cert test on a dead daemon
sshd_ossh_cert_test.sh polls for the listening socket and fails any case
where the daemon was not up for the whole attempt. Every rejection case
scored a pass on a non-zero client exit, so a daemon that never started
passed them all: against a wolfsshd that exits immediately, 17 of the 39
cases reported PASS.

- pin readiness to the daemon's own pid where the listening table names
  it, so a stray listener reads as a failure to bind
- check liveness after the client too; kill -0 alone will not do, since
  the disowned daemon lingers as a zombie
- stop the daemon by recorded pid and wait for it to go, rather than a
  command-line pkill that can reach another user's processes
- chmod trusted-cas.pub 0644 at creation; as a TrustedUserCAKeys anchor
  at umask 002 it lands group writable and the daemon refuses to start
- run the clients under "timeout 30", as they hung on a port that never
  speaks SSH, and fail a timed-out case rather than score a rejection
2026-10-14 15:52:44 -05:00
John Safranek c8c50adc25 tests: chmod the sshd authorized_keys at creation
create_authorized_test_file.sh sets authorized_keys_test to 0644 where
it writes it. Shell redirection left it at the process umask, 0664 under
Ubuntu's per-user groups, and wolfSSHd's StrictModes check refuses a
group or world writable file, so the suite died at its first test with
only "Couldn't connect SSH stream" on the client side.

- the check also walks every parent directory, so clearing the file mode
  alone moves the rejection up a level rather than curing it
- mirror that walk in the runner before starting a local daemon, naming
  each directory to clear, and stop rather than run tests that cannot
  authenticate
- skip the walk against an external host, whose authorized_keys file is
  not in this tree
- record the umask prerequisite in the suite's README
2026-10-14 15:52:44 -05:00
John Safranek 775a47e295 tests: poll for the settled state on term close
sshd_term_close_test.sh waits for the client to exit, then polls for the
port to settle for up to ten seconds. Its one netstat sample used to
land at the instant "timeout" killed the client, looking straight into
the teardown it measures, and netstat walks /proc/net/tcp
non-atomically, so it could read the two rows either side of the FIN.

- fail only if the port is still unsettled at the deadline, so a socket
  stuck in CLOSE_WAIT, which is what this test is for, still fails it
- compare wolfsshd pid sets and poll for the fork; a count across a
  fixed sleep missed the new child whenever an earlier test's child
  exited in the same window
- print the port's netstat lines on failure, so a real leak names the
  stuck socket
- run alternately against one daemon, the old script failed 21 of 165
  runs and its pid check 1 in 25; the new one none
2026-10-14 15:52:44 -05:00
John Safranek c0c1d3b991 tests: keep the sshd teardown off its own caller
The teardown safety net matches the daemon by process name, so it
reaches every daemon the suite starts and nothing else. As
"pkill -f wolfsshd" it also matched the runner's own shell whenever the
script was named by a path through apps/wolfsshd: the suite SIGTERMed
itself and exited 143 with every test passed.

- anchor the host key gate's kill on "/wolfsshd " and the port's own
  flag; it must match a command line to spare the shared daemon, and
  "$HK_SSHD.*$HK_PORT" made ../wolfsshd a regex
- anchor the three pkills in sshd_ossh_cert_test.sh the same way, so
  their reach is a property of the pattern, not of the config name
2026-10-14 15:52:44 -05:00
John Safranek 14be70088d tests: drive the sshd pubkey rejection path
sshd_pubkey_reject_test.sh authenticates gretel's ECC key against the
shared daemon and requires both a non-zero client exit and a new "Public
key not authorized" line in the daemon log. The suite only ever offered
a hansel key, so SearchKeysFile()'s no-line-matched path was never
driven through the daemon.

- run a positive control with the authorized key first, so the rejection
  cannot be read as a connection failure
- bail out if gretel's key turns out to be listed after all
- call it while the shared daemon is up, ahead of the StrictModes
  authorized_keys check
- poll the log for the rejection rather than sampling it once after a
  fixed sleep
2026-10-14 15:52:44 -05:00
John Safranek 46d7e3e1a2 wolfsshd: document whole-keyword config matching
ParseConfigLine() takes an options[] entry only when the keyword ends
at whitespace or the end of the line. Record what that check defends
against at the loop and pin the two arms it has. Without it an OpenSSH
"HostKeyAlgorithms" line matches "HostKey" and takes "Algorithms" as
the host key file, aborting startup.

- extend the existing prefix-match comment in test_ParseConfigLine()
  rather than open a second one; the synthetic vectors under it already
  cover the whitespace arm
- pin the end-of-line arm on the error code, not just on failure: only
  a matched keyword reaches HandleStrictModes and returns
  WS_BAD_ARGUMENT, so the check holds on every build flavor
- assert the OpenSSH spelling on its own, where the assertion survives
  HostKeyAlgorithms becoming a supported keyword
2026-10-14 10:36:16 -05:00
John Safranek 7320687a1e examples, apps: drain at shutdown everywhere
The remaining teardown sites spun wolfSSH_worker() ten times after
wolfSSH_shutdown(), the loop the client and the echoserver just
replaced. The clients now send a disconnect and drain, and wolfsshd
half closes and drains like the echoserver.

- sftpclient.c, scpclient.c, portfwd.c and the wolfssh app call
  SendDisconnectAndDrain() and treat either give-up as a clean close.
- scpclient.c and the wolfssh app report an unexpected shutdown result,
  as the client does.
- the wolfssh app waits for the channel close before draining, as the
  client does, and fails on an error that broke the wait.
- wolfsshd drained by hand after wolfSSH_free(), with no bound on a peer
  that never hung up. HalfCloseAndDrain() bounds it at ten seconds, and
  a peer that let the login grace time run out is not drained.
2026-10-13 10:45:39 -05:00
Yosuke Shimizu 4dc6e86cbb wolfsshd: bound the shell child reap and flush the held backlog
- The child's stdin pipe and the pty master are closed before the
  reap. The reap polls waitpid() with WNOHANG for
  WOLFSSHD_CHILD_REAP_TRIES x WOLFSSHD_CHILD_REAP_WAIT_US, then
  sends SIGKILL; a failed kill() is logged and ends the wait.
- ChildSig is installed before forkpty() instead of after it.
- An EIO read from the pty master ends the child's output
  instead of the loop.
- A held backlog is sent with SHELL_FlushOut() before the pipe
  drain; the drain is skipped when the backlog is not sent.
- New sshd_channel_close_test.sh (paramiko) closes the channel
  while output is held, for an exec command and a pty shell, and
  checks that the command and the connection both end; it also
  checks exit status 3 from an exec and an interactive shell.
  run_all runs it.
- sshd-test.yml and code-coverage.yml install python3-paramiko
  through install-apt-deps, and the ubuntu-24.04-tests bundle
  lists it.
2026-10-09 10:30:29 -07:00
Yosuke Shimizu d05cbd691d apps/wolfssh, examples/client: resend stdin a send did not take
- readInput() in apps/wolfssh runs FlushQueuedSend() when
  wolfSSH_stream_send() returns WS_WANT_WRITE without taking the
  buffer, then sends the same buffer again; a failed flush still
  ends in "Couldn't send data".
- readInput() in examples/client retries WS_WANT_WRITE as it
  retries WS_REKEYING, for up to SEND_RETRY_TIMEOUT (10) seconds
  per buffer.
- The comment on the queued check in apps/wolfssh describes the
  positive-return case.
2026-10-08 11:28:07 -07:00
John Safranek 96fdbe5238 apps, examples: keep readPeer up on a held reply
A held status from wolfSSH_AGENT_RelayChannel() is negative, and
leaving it in ret ended the outer while (ret >= 0) loop before the owed
reply was retried. Reset ret to WS_SUCCESS when the reply is held so
readPeer() returns to select() and retries it.

Issue: F-10545
2026-09-24 17:24:14 -05:00
Yosuke Shimizu a384999f61 agent, apps, examples: relay whole agent messages
- 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
2026-09-24 17:24:14 -05:00
John Safranek 9cd19258f2 wolfsshd: keep a 022 umask in the daemon
Daemonizing cleared the umask and nothing set one afterwards, so every
per-connection child inherited it: files created from a shell or
through the SCP receive path came out 0666, directories 0777. Hold 022,
which is what a foreground daemon already inherits from its caller.

- WOLFSSHD_DEFAULT_UMASK sets it at build time

Issue: F-3671
2026-09-22 14:09:18 -05:00
John Safranek e2d57bba7b wolfsshd: warn on unimplemented config options
Subsystem, ChallengeResponseAuthentication, UsePAM, X11Forwarding,
PrintMotd, AcceptEnv and UseDNS parse and are then dropped. An unknown
keyword is fatal, so accepting these in silence reads as support for a
setting that is not enforced. Log a warning naming the keyword instead.
Rejecting them would fail every config copied from OpenSSH.

- OptionName() maps an option tag back to its keyword for the message

Issue: F-2868
2026-09-22 14:09:18 -05:00
John Safranek dee2b1f9bf wolfsshd: give the stderr EOF test a temp dir
The runner leases a port block per run so two runs can share a host, and
this test's config and 16 MB payload are the other half of that: a
second run overwrites them, and whichever finishes first removes them
from under the other, which then reports a transfer that never ran
short. Both live in a directory of this run's own now.

- the external-host tally counts 12 local-only tests, not 10
2026-09-22 11:07:39 -05:00
John Safranek 5603f43bb7 wolfsshd: bound and guard the stderr EOF test
A byte count cannot tell a short transfer from one that never finished
and the runner sets no deadline of its own, so the client runs under
"timeout" and a 124 is reported as a hang. An empty PID means the
daemon never came up, which the count would otherwise report as the
truncation this test exists to catch.

- scratch paths held in variables, since bash rewrites PWD on the cd
  back into the test directory
- an EXIT trap removes the 16 MB scratch file and stops the daemon
2026-09-22 11:07:39 -05:00
John Safranek b3a4201c62 wolfsshd: match the stdout and pty read arms
The stdout and pty childFd reads treat EINTR and EWOULDBLOCK as
non-fatal, the set the stderr read already uses, so an interrupted read
cannot end the session with the peer's output still queued. The
"err != 0" term goes with it; read() sets errno on the -1 return that
is the only way into the test.
2026-09-22 11:07:39 -05:00
John Safranek 918c2949a2 wolfsshd: bound the wait on a drained child
A stream leaves the read set once it has reported EOF, so a child whose
output is drained while it still runs leaves the SSH socket as the only
descriptor watched, and its SIGCHLD is the only wake left. One handled
between the ChildRunning test and select() would not interrupt the
call, so wait a second at a time there. An exited child still polls.
2026-09-22 11:07:39 -05:00
John Safranek 182dd91c9a wolfsshd: test a stderr EOF with output queued
sshd_stderr_eof_test.sh streams 16 MB through a shell session whose
reader stalls, so the send window fills and the child exits while the
loop still holds a backlog. The transfer repeats twelve times because
whether the stale errno at the stderr EOF is fatal is a race.

- a short transfer is never correct, so a failure here is always real
- the fixed tree passes 48 consecutive transfers; the truncating one
  fails on the first
- runs in the sudo-only block beside sshd_window_full_test.sh, which
  reaches the same path about 1 run in 12
2026-09-22 11:07:39 -05:00
John Safranek 67de1d6663 wolfsshd: keep the shell loop past a stderr EOF
read() returns 0 at end of file and leaves errno alone, so the stderr
arm consults it only for a -1 return. A 0 marks that stream done and
drops it from the read set; stdout does the same, so neither spins on
a descriptor that stays ready once it has reported EOF.

- a stale EINTR from the child's SIGCHLD no longer ends the loop with
  the peer's output still queued
- EINTR and EWOULDBLOCK join EAGAIN as non-fatal on a -1 return
- select polls rather than blocks once the child is gone, its output
  is drained and nothing is held, so the loop still reaches its exit
2026-09-22 11:07:39 -05:00
Emma Stensland db603a86cc test_configuration: Gate composites and restore type discrimination 2026-09-21 13:34:17 -07:00
Emma Stensland 3bec025365 Fix ML-DSA composite logic from code review 2026-09-21 11:11:16 -07:00
Emma Stensland 8f6cc9f029 internal, wolfsshd: Fix code review nits 2026-09-21 11:00:11 -07:00
Yosuke Shimizu 7d8ca9bdf8 wolfsshd: initialize the WIN32 shell exit status
- processState starts at 0 in SHELL_Subsystem()'s WIN32 half, where
  it is read by wolfSSH_SetExitStatus() on paths that leave the loop
  without GetExitCodeProcess() having written it.
2026-09-17 16:11:10 -07:00