A correct loop measures zero and a spinning one saturates a core, so half a
core left room for a partial spin to pass. The shorter window costs the
suite nothing, since the child's sleep set the runtime.
- Bound the reading at a tenth of a core over three seconds
- Name the settle, window, sleep and limit rather than spelling each out
Stdin is non-blocking now, so a full pipe leaves an unwritten tail for the
next pass. Nothing can come off the channel until it drains, so the channel
data that is left unread kept pending set, and pending forced a zero timeout
on select(). The loop then polled instead of waiting on the child's stdin,
which is already in the write set, and burned a core until the child read.
- Take the zero timeout only when the child has no tail owed to it
- Add sshd_stdin_stall_test.sh, which fails without this
SHELL_Subsystem() is the only reader of the child's output, so it must never be
the thing the child is waiting for. It was: the pass that writes the peer's
input to the child's stdin ran ahead of the pass that reads its stdout, and on
a pass with buffered channel data the output descriptors were left out of the
select() altogether. A child that fills its stdout pipe stops reading stdin,
the write blocks, and nothing is left to empty the pipe that would release it.
sshd_stdin_eof_test.sh case 2 is the shape that reaches it: a half-close with
the send window full leaves the whole window buffered, and the burst that
follows is up to four 32K writes with no read in between.
- The child's output is watched on every pass. A pass with work already in
hand polls with a zero timeout instead of skipping select(), so it still
sees the child's output.
- The descriptor written to is non-blocking, and what a short write leaves is
carried in channelBuffer to the next pass, which waits for the child in
select() rather than inside write(). Only EAGAIN keeps the remainder; any
other short write still ends the session.
- The child's stdin closes on the peer's EOF once that remainder is gone too,
not just once the channel is drained.
- A channel retired under us drops the remainder with the descriptor.
Every in-tree caller of wolfSSH_worker() now recognises a peer half-close.
wolfsshd's shell loop and both echoservers need it: all three ladders end in
"else if (rc != WS_WANT_READ) break", and wolfsshd's reaches
kill(childPid, SIGKILL), so without it a client half-close kills the command
it just finished feeding.
- wolfsshd closes the child's stdin off the channel's own EOF state instead of
off a worker return of zero, which no longer happens on a half-close.
- The echoservers answer the half-close off wolfSSH_ChannelGetEof() rather
than the WS_EOF status: the flush inside wolfSSH_worker() can supersede that
status, and it is raised once. They hand back the backlog first, finish a
short send, and only send the EOF once the channel is empty. Answering is
not conditional on the shell build, where an echo session is the default.
- The SFTP loops peek before leaving, so a half-close with requests still
buffered is served rather than dropped, and they report an ordinary session
end as success.
- The clients -- examples/client, scpclient, sftpclient, apps/wolfssh -- treat
it as the graceful case instead of an error. apps/wolfssh counts it as a
finished flush as well, since one worker pass can drain the queue and
consume the peer's EOF together.
- portfwd relays it to the local socket with shutdown(SHUT_WR) so a local
reader waiting on end-of-input returns, once the backlog has genuinely been
handed over: a read cut short by a rekey leaves the half-close for a later
pass.
- The Windows half of wolfsshd does not answer with an EOF of its own. That
latches eofTxd and the child's remaining output would be refused, which is
the defect this series removes from the library.
- The mplabx port drains before tearing down, the way its SFTP read path
already did; its worker arm was unreachable for a half-close until now.
wolfSSH_stream_peek() returns 0 for a live channel with an empty buffer,
which is the ordinary idle case. None of the arms after the peek match
that, so the loop falls through with the timeout still at
TEST_SFTP_TIMEOUT_NONE and tcp_select() returns on its 100 us floor. An
idle SFTP session keeps a core busy for as long as it stays connected.
- take the peek's zero return as "nothing to do" and let the next select
wait a second, the same value the want-read paths already use
- sshd_sftp_idle_cpu_test.sh parks an idle SFTP session on the daemon and
reads the connection process's CPU time out of /proc, failing if it
spends 5 ticks or more over ten seconds
The measurement the test automates: 21 ticks per 10 seconds before, 0
after. It skips where there is no /proc or no local daemon to measure.
Four sites re-tested an unchanged ret after a WOLFSSH_SMALL_STACK
allocation, which cppcheck reports as identicalInnerCondition because the
allocation that can change ret is compiled out in the default build. Test
the allocated pointer instead; that is what the check is guarding.
- certman.c, keygen.c and wolfsshd/auth.c: check the DecodedCert, MlDsaKey
and DecodedCert pointers.
- internal.c CompositeEccSign: give the fixed-buffer build pointer aliases
so both builds have the same shape, then check the r/s pointers.
- port.c: initialize fileHandle, which is only assigned when mbstowcs_s
succeeds.
- wolfsftp.c: zero localTime before WLOCALTIME, which is a per-port macro.
SHELL_Subsystem() hands the child whatever the peer sent, whichever pass it
arrived on, and closes the write end of its stdin only once that buffer is
dry. It works off the shell channel's own inputBuffer, so data held back while
the window was full is still handed over; the old read ran only on the
worker's WS_CHAN_RXD and was skipped outright while windowFull.
- The channel id comes from the head of the channel list at entry, the only
channel open there, rather than from DEFAULT_NEXT_CHANNEL, which a build
can override.
- A lookup that finds nothing is not an EOF: only a channel that is present
and drained closes the pipe.
- Data arriving behind the peer's EOF, which RFC 4254 section 5.3 forbids, is
dropped rather than written to a stdin that is already closed. The write
would fail with EBADF and end the session mid-stream.
- The short-write retry tests for a -1 return before reading errno, which
nothing else sets.
QNX system images fix the host key's owner and modes, and the daemon cannot
change either, so the secure gate refuses to load a key the integrator has no
way to correct. Add a hand-defined WOLFSSH_NO_HOSTKEY_PERMS, further
conditional on QNX, that hands only that policy to the platform.
- Fold the macro and the QNX test into the internal
WOLFSSHD_HOSTKEY_RELAX_PERMS in wolfsshd.c.
- Add a relaxPerms argument to wolfSSHD_OpenSecureFile() that skips the owner,
mode and ancestor-directory checks.
- Keep the structural checks: lstat, O_NOFOLLOW, S_ISREG and the dev/ino
recheck, so a symlink, a non-regular file or a swap during the open is still
refused.
- Set it only on the host key load, leaving the host cert, UserCAKeysFile,
authorized_keys and shadow gates unchanged.
- Log at startup when the guard is built in.
- Add six test_OpenSecureFile scenarios for the relaxed path, unreachable at
runtime off QNX and so otherwise uncovered.
Issue: ZD-22308
wolfSSHD_GetUserConf returned the first matching Match block whole, so a
keyword named only in a later matching block was dropped and the outcome
depended on the order the blocks were written.
- track in a new setMask which keywords a node set itself, so a value
inherited from the globals can be told from one the block named, with a
compile time check that no option tag shifts out of the mask
- resolve into a fresh config seeded from the globals, letting every
matching block contribute the keywords no earlier block claimed
- the resolved config now belongs to the caller, so wolfsshd and the auth
paths free it and the tests compare values rather than node identity
- put sshd_match_overlap_test.sh back in the suite
Issue: ZD-22324
OpenSSH resolves sshd_config one keyword at a time, scanning every Match
block that applies. wolfSSHD_GetUserConf returns the first matching block
whole, so a setting made only in a later matching block is dropped.
- add test_GetUserConfMatchOverlapCompose, covering a user matched by both
a Match User and a Match Group block, in either order
- add sshd_match_overlap_test.sh, the same case against a live daemon,
where the group block's ForceCommand is the setting that goes missing
- both fail until per keyword composition lands, so the unit test runs
last and the script stays commented out of run_all_sshd_tests.sh
- IsShadowExpired() in auth.c returns 1 when a shadow entry's
sp_expire date has arrived, its sp_lstchg is 0, or the day is at or
past sp_lstchg + sp_max. Negative fields leave the matching check
off; a negative day count, standing for an unavailable clock,
denies the entries that carry aging. WSSHD_SECS_PER_DAY converts
WTIME() into the unit those fields use. The helper is compiled
under HAVE_SHADOW and !WOLFSSH_USE_PAM, as its caller is.
- CheckPasswordUnix() runs the shadow entry it looked up through the
helper and, after an otherwise successful hash compare, logs the
denial and returns WSSHD_AUTH_FAILURE.
- auth.h declares IsShadowExpired() for the unit test build.
- test_configuration.c adds test_IsShadowExpired() over a table of
aging fields and day counts, and test_CheckPasswordUnix_expired()
for the denial of a correct password.
- The CheckPasswordUnix() tests share one driver,
wsshd_test_CheckPasswordUnixCase(), with the crypt() setup in
wsshd_test_LoadShadowHash() and the three fail-closed shadow
lookups gathered into test_CheckPasswordUnix_failClosed().
Issue: F-10577
stop_wolfsshd killed $PID unconditionally. With the daemon already gone the
kill failed, and under "set -e" that aborted the caller -- in
sshd_forcedcmd_test.sh before PID was cleared, so the ForceCommand-SFTP
scenario was silently skipped, the EXIT trap killed the dead pid a second
time, and the script exited 1.
- Guard on a non-empty PID, ignore a failed kill and return 0, so the
function is safe to call from an EXIT trap.
- Clear PID after stopping, so a second call cannot kill a recycled pid.
- Remove the temp key dir even when no daemon was recorded, so a daemon that
failed to start does not leak it.
- Collapse sshd_forcedcmd_test.sh's cleanup() wrapper to a bare
trap stop_wolfsshd EXIT now that the function guards itself.
- Check the cd back to the test directory in sshd_x509_upn_fail.sh; the log
it counts after the client run is the one there.
sshd_privdrop_fail_test.sh runs from apps/wolfsshd/test and handed the
example clients relative key paths, but the clients call
ChangeToWolfSshRoot() before parsing arguments. Every client died at
"Error setting private key" and the test blamed the privilege drop.
- Anchor the key, payload and client paths at the script's own directory.
- Rename the saved directory to TESTDIR so a later cd cannot clobber it.
- Report "no fork at all" separately in the timeout diagnostic.
- Print the client's own output at every failure exit, so a client that
never connects cannot be read as a daemon fault.
- Keep the client logs like log.txt, gitignored and removed on success.
Eight scripts saved their starting directory in PWD, which the shell
rewrites on every cd, so the "cd $PWD" restore landed in the repository
root. sshd_forcedcmd_test.sh's second scenario and the log count in
sshd_x509_upn_fail.sh silently never did what they cover.
- Save the starting directory in TESTDIR, as sshd_pubkey_reject_test.sh does.
- Quote "$TESTDIR" at every cd, now that the saved value is really used.
- Stop the daemon from a trap in sshd_forcedcmd_test.sh, so its now reachable
second scenario cannot leave one on the shared port when set -e aborts.
- Take start_wolfsshd's before and after daemon PID snapshots from pgrep -x
instead of scraping every digit run out of "ps -e", which mixed the TIME
field's clock digits in with the PID and, with a leftover daemon running,
stopped the wrong process.
- Let both snapshot pipelines fail, so a set -e caller survives no daemon
being up and a daemon that dies after sudo returns is reported by the
caller's own empty-PID check.
grep -c prints 0 and exits 1 when nothing matches, so the "|| echo 0"
fallback fired too and the count became "0\n0". The arithmetic error
unwound bash out of the test block, skipping the last eleven tests while
the summary still printed a pass and exited 0.
- Use the bare grep -c result and default only the empty case.
- Add a RUN_COMPLETE sentinel at the end of each branch that runs tests.
- Check the sentinel before the summary so an abort exits non-zero.
- Kill lingering daemons by process name, so the teardown does not kill
the run itself before that check when invoked by a path holding
"wolfsshd".
The scripts now read the build options from the probe instead of grepping
usage text, config.log and daemon logs. Drops the usage lines only tests read.
Without WOLFSSH_SMALL_STACK dCert is the address of a stack variable, so
the NULL check could never fire. Keep the check under the small stack
build where the WMALLOC can actually fail.
Issue: CID-573006
The drain after waitpid ignored the send return, so on a non-blocking
socket the tail of a command's output was dropped on a full window, a
rekey or a would block. Retry a bounded number of times and log when
the data still can not be sent.
Issue: CID-572907
The leftover-data drain after waitpid ignored both fcntl calls. Check
the get and the set, and skip the drain read for a pipe that could not
be made non-blocking so the read cannot hang the connection process.
Issue: CID-572931
SHELL_Subsystem() raises privileges to look up user information, but the
three pipe() failures and the forkpty() failure return WS_FATAL_ERROR
without dropping them again. HandleConnection() then runs its teardown --
wolfSSH_shutdown(), up to ten wolfSSH_worker() iterations, and the socket
drain -- still elevated when UsePrivilegeSeparation is yes or sandbox.
Drop permissions before each of the four returns, reusing the wording the
daemon already logs for a failed drop.
The forkpty() path also returned with all six pipe descriptors still
open. Close them there, as the pipe() failure paths already do, and reset
the slots to -1 to keep the child-branch invariant that an entry is
either a live descriptor or -1.
Issue: F-6980
- The zero check ran WSTRCMP() on the caller's buffer, which is a
length-bounded slice of the config line and not NUL terminated, so it
read past inSz and rejected valid "0" values whose slice had trailing
text.
- Compare num, the NUL-terminated copy that atol() was given.
Issue: F-7213
- crypt() returns a pointer into a static buffer that keeps the hashed
password after CheckPasswordHashUnix() returns; wipe it once the
comparison is done.
Issue: F-7220
- Scenarios 1 and 5 fopen() once and fstat() that handle instead of
stat()ing the path a second time.
- A failed open is now a logged error rather than an indirect rd == 0.
- The FIFO lstat() and failPath existence test stay as they are: neither
has a descriptor to stat, and mkdtemp()'s 0700 directory makes them
unraceable.
Issues: CID-651701
Add mutation-killing unit coverage flagged by Fenrir static analysis.
No production logic changes.
* F-6702 test_configuration.c: extend test_CheckPasswordHashUnix with
empty/locked-password cases pinning the empty-branch guard.
Co-authored-by: John Safranek <john@wolfssl.com>
wolfsshd could not load a host key stored as a PKCS#8 "-----BEGIN PRIVATE KEY-----"
PEM file (the form emitted for ML-DSA keys, and by `openssl genpkey`).
SetupCTX() pre-converted the file with wc_PemToDer(..., PRIVATEKEY_TYPE, ...),
which only recognizes the classic "RSA/EC PRIVATE KEY" headers. On a PKCS#8 body
that call returns success but produces a malformed DER (leading 0x04 rather than a
0x30 SEQUENCE); the bytes were then passed to wolfSSH_CTX_UsePrivateKey_buffer()
as WOLFSSH_FORMAT_ASN1, which rejected them with WS_BAD_FILETYPE_E.
Detect PEM vs DER by content and decode PEM with wc_KeyPemToDer(), which handles
PKCS#1, SEC1 and PKCS#8 private-key bodies. DER keys are unchanged.
Tested (master + wolfSSL master): an ML-DSA-65 PEM host key now loads
(wolfSSH_CTX_UsePrivateKey_buffer ret = 0, was WS_BAD_FILETYPE_E); ML-DSA-65 DER
and ECDSA SEC1 PEM host keys continue to load (no regression).
Also harden the host-key load path per review: reject an empty (0-byte)
key file explicitly instead of taking the PEM branch into WMALLOC(0), and
zeroize the raw file buffer (not just the decoded DER) before freeing so
private-key material does not linger in the heap.
SetupChroot() in wolfsshd ran chdir(chrootPath), chroot(chrootPath) and
chdir("/") in three independent if-blocks with a single return at the end.
Each ran unconditionally, so chroot() executed even when the preceding
chdir() into the target failed. That can leave the process chrooted with a
working directory still outside the new root.
Short-circuit the later steps on the ret>0 (no-failure) state so chroot()
runs only after the chdir() into the target succeeds, and chdir("/") only
after chroot() succeeds. WS_FATAL_ERROR is negative, so ret>0 is true only
while no step has failed.
Zeroize key/digest/handshake buffers before they are freed or go out
of scope, and fix a private-key leak on the CTX-full reject path.
Build-verified with ./configure --enable-all (make exit 0).
- #1278 internal.c KeyAgreeEcdhMlKem_client: WS_FORCEZERO handshake->x
ML-KEM private key before wc_MlKemKey_Free, matching the DH path.
- #2082 agent.c wolfSSH_AGENT_ID_free: WMEMSET -> WS_FORCEZERO for the
key buffer and the id struct so scrubs are not optimized away.
- #2496 internal.c SignHRsa/SignHEcdsa: WS_FORCEZERO digest (and encSig
in SignHRsa) before return.
- #2497 internal.c BuildUserAuthRequest Rsa/RsaCert/Ecc/EccCert:
WS_FORCEZERO digest (and encDigest for the RSA paths).
- #2498 internal.c DoUserAuthRequestPublicKey: WS_FORCEZERO digest before
it leaves scope on both success and failure exits.
- #2500 internal.c DoUserAuthRequestRsa: WS_FORCEZERO encDigest before
free (both SMALL_STACK and stack-array paths).
- #2501 internal.c DoUserAuthRequestRsaCert: same encDigest scrub.
- #2886 internal.c SshResourceFree: WS_FORCEZERO ssh->h and ssh->sessionId
(and reset sizes) alongside the already-zeroed KDF inputs.
- #3453 internal.c SetHostPrivateKey: on CTX-full reject, take ownership
and WS_FORCEZERO+WFREE der instead of leaking the private key.
- #3680 internal.c ChannelDelete: WS_FORCEZERO inputBuffer before free.
- #6277 wolfsftp.c ClearState/GET/PUT cleanup: WS_FORCEZERO SFTP get/put
state structs before free (all four sites).
- #6278 wolfsshd.c SHELL_Subsystem: WS_FORCEZERO channelBuffer/shellBuffer
on the data-carrying exit.
Coverity flagged unchecked return values in the wolfsshd config tests:
- Check the return code and the NULL check on the file handle returned
by WFOPEN() in test_IncludeRecursionBound().
- Cast the return on WREMOVE() to void in test_IncludeRecursionBound()
and CleanupWildcardTest().
- Check WFCLOSE() so a deferred flush failure fails the fixture setup,
and void the remaining WRMDIR()/WCLOSEDIR()/WFCLOSE() returns.
- Report a short write and a failed close separately, testing the write
first since a short write is what provokes the flush failure.
- Reset the WFILE* to WBADFILE after each close so the guard on it means
the same thing on every loop iteration.
- Pass NULL rather than 0 for the WMKDIR() filesystem handle, and
initialize the WFILE* to WBADFILE to match its later comparisons.
Issues: CID-646430, CID-646431