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
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
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
- 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().
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.
- 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.
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.
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.
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.
- _GetHomeDirectory loads the user's profile when WOLFSSHD_AUTH's new
profile member is NULL, setting PROFILEINFO.dwSize first and keeping
the returned hProfile there.
- _GetProfileDirectory reads the home directory with
GetUserProfileDirectoryW, in place of SHGetKnownFolderPath and the
%USERPROFILE% expansion. CheckPublicKeyWIN calls it directly, so a
caller that has not authenticated the user builds no profile.
- wolfSSHD_AuthCloseToken unloads the profile before closing the token,
calling RegCloseKey when the unload fails.
- The Windows shell cleanup calls RevertToSelf() before closing the auth
token rather than after.
- windows-sftp.yml gains a no_profile job that covers an exec session,
two overlapping sessions, and SFTP for users created with net user
alone; it skips the earlier SFTP step so its exec session connects
first.
- Both Windows workflows log testuser on once so Windows builds a real
profile, in place of writing the home directory and ProfileList entry
by hand, and the recursive icacls grants on it are gone.
Issue: F-13326
enable SHA1 with windows cert store test case
expand test cases, adjust to authorized key file, minor dead code adjustments
add more documentation, refactor duplicate code sections, clean up test cases, more adjustments to logging spamming protections
add Windows cert store test case
make windows cert feature default disabled and simplify macro guard
additional unit tests, advertise x509 and pubkey, use CN to match username, build check for WOLFSSL_SYS_CA_CERTS, fix for CM ref count
additional build test, uniform enum name, fail on unkown cert store ecc curve, tie in of loading whole cert store for sys CA's
sshd_sftp_idle_cpu_test.sh measures the connection process it forked, so
it takes the wolfsshd present after the connection and not before. The
old symmetric difference offered a pid that left during the window just
as readily, and the smallest one wins, so an earlier test's departing
child was measured through a /proc entry that no longer existed.
- compare the pid sets one way, and poll for the fork rather than
sampling a fixed five seconds in
- let the handshake and SFTP setup finish before the baseline, so their
ticks land outside the measurement rather than inside it
- print both pid sets when no child is found, since the failure says
nothing about which pids were considered
Gates ML-DSA composites behind WOLFSSH_NO_MLDSA_COMPOSITES.
Deduplicates key handling and uses heap allocation for
composite buffers when compiling for small stacks.
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.
bytes held select()'s return in a word32, so a -1 became 0xFFFFFFFF and
ran the read path on descriptor sets select() had left alone. The
SIGWINCH handler interrupts this select, so a terminal resize reaches it.
- Keep the result in an int
- Retry on EINTR, report anything else
- Same fix readPeer() in examples/client/client.c already carries
FlushQueuedSend() retried wolfSSH_worker() for as long as it reported
WS_WANT_WRITE. A peer that stops reading never lets the socket drain, so
the sending thread spun there, and at the shutdown drain that thread was
main, leaving the client unable to exit.
- Give the retry a ten second deadline
- Return the still pending WS_WANT_WRITE to the caller
- Take that for done at the shutdown drain, the socket closes next
- Mask WS_REKEYING, the worker only reports it once the send is out,
and readInput() was taking it for a send failure
On the MSVC path the input thread is never waited on, it blocks in a
console read with nothing to cancel it, so it can still be logging when
main closes the file named by -E.
- Flush the log there and let process exit close the stream
- The POSIX path joins its threads first, it still closes the file
A send the socket wasn't ready for stays queued but still reports the data
as taken, so the client waited on a reply to a message it never sent.
Flush after a queued send, a terminal size change, and at shutdown.
The shutdown drain reports its want read as WS_FATAL_ERROR, so read the
status with wolfSSH_get_error(); an ordinary shutdown was exiting 1. Time
out readPeer()'s select() so a flush can't strand the reader.
The client runs every session's I/O on threads, so it needs a threaded
wolfSSL. configure probes for SINGLE_THREADED when the client app is
enabled. Asking for the app with --enable-sshclient is an error, getting
it from --enable-all drops the app instead, so --enable-all still
configures against a single threaded wolfSSL.
The compile time check stays for the builds that never run configure.
That leaves the SINGLE_THREADED terms in the app's own guards
unreachable, so drop them.
wolfSSH_shutdown() returns WS_WANT_WRITE when the channel EOF, exit and
close messages are still queued on the non-blocking socket. Masking that
to WS_SUCCESS reported a clean exit for a session whose close messages
never reached the peer.
Mask a want write from the drain worker only, where the close messages
are already sent. The want read masking stays on both, wolfSSH_shutdown()
runs a worker of its own and passes that want read back.
- --enable-sshclient defaults to no, so the app was built only by the
configs that use --enable-all, and never under the multi-compiler
warning flags. Add it to the multi-compiler matrix.
- Add scripts/sshclient.test, run by make check. It covers the client's
sessions and the -E log file against the echoserver.
- The script is not gated on BUILD_SSHCLIENT. It exits 77 when the
client app or the echoserver isn't there, so every build runs it and
the ones without the app report it as a skip.
- Check the client and the echoserver by asking each for its usage
message, not by looking for the file. Both are libtool wrapper
scripts in the build tree, and a wrapper outlives a reconfigure that
drops the program it wraps, then runs only far enough to say so.
- The echoserver runs in echo mode and the client's stdin comes from a
fifo written a piece at a time, so the session carries data and ends
on its own. Each client run has a watchdog.
- Rename sshd-test.yml's job to cover both apps. That workflow builds
the client app along with wolfsshd.
- Check that the command reaches the server, now that the client sends
it rather than discarding it.
- Make the SINGLE_THREADED guard a preprocessor #error. The runtime
err_sys() only caught the misconfiguration in an autotools build that
got as far as running; the #error catches it at compile time for the
IDE and plain Makefile builds too.
- Treat WS_WANT_READ and WS_WANT_WRITE out of wolfSSH_worker() as a
clean shutdown. The socket is non-blocking, so the peer having
nothing ready is not a session failure.
- -E was parsed into config.logFile and printed by -G, never read.
- Install a logging callback that writes to the named file, following
what wolfsshd does for its own -E.
- Turn logging on with wolfSSH_Debugging_ON(). Installing the callback
is not enough on its own, the file came out empty in any build that
wasn't --enable-debug, including the --enable-all builds where the
library has all of its logging compiled in. wolfsshd turns logging on
the same way.
- Match DefaultLoggingCb()'s format, timestamp and level tag, so a log
written to the file and one written to stderr are comparable. That
function's GetLogStr() is private to the library, so the level names
are repeated in the app.
- Parse the command line and open the file in main(), before
wolfSSH_Init(), so the start up messages land in the file.
- Close the file after wolfSSH_Cleanup(). The callback cannot be
uninstalled, so it ran with a closed stream and segfaulted on exit.
It falls back to stderr.
- Name the stream logFileStream, apart from struct config's logFile,
which is the path it was opened from.
- Drop the always true condition around the session threads.
wolfSSH_SetChannelType() discarded an exec or subsystem name it could
not use and still returned WS_SUCCESS. SendChannelRequest() then omits
the name field entirely, which the peer rejects as malformed, dropping
the connection. Both an oversized name and an empty one reach it; the
empty case is reachable from the command line as "wolfssh -c ''".
- Return WS_BAD_ARGUMENT for a name at or above WOLFSSH_MAX_CHN_NAMESZ,
matching how the function already reports a bad type or side.
- Return WS_BAD_ARGUMENT when no name is given and none was stored by
an earlier call, and when a size arrives with no name behind it.
- Keep returning WS_SUCCESS when an earlier call stored a name, which
is what the SFTP and SCP retry loops depend on.
- Return before setting connectChannelId so a rejected call leaves
no state behind, as the server-side exec rejection does.
- Keep the stored name intact when a later call is refused.
- api.c asserts each refusal, and the largest name still admitted.
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