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
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
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
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
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
- 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().
- 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.
- 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.
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
WOLFSSH_FWD_LOCAL_CLEANUP now runs, and it runs from DoChannelClose()
ahead of the WS_CHANNEL_CLOSED the worker sees. The handler has already
closed the socket and moved the state on by then, so the recovery branch
no longer matched and left ssh_worker() holding a closed descriptor.
- guard the handler's close: the open can fail after the setup, with
nothing yet connected
- gate the handler on the channel id the library passes in the port
parameter. A channel can outlive its turn in the single forwarding
slot, and a cleanup arriving after the next forward has moved in
would close that one's live socket
- have the recovery branch clear its stale copy of the descriptor when
the handler got there first, and still do the whole teardown for a
locally opened forward, which draws no callback
- resolve the closed channel with wolfSSH_GetLastRxId(). wolfSSH_worker()
names the channel only for the data and EOF statuses, so the recovery
branch was comparing against a stale zero and ran only for a forward
that happened to be channel 0
- clear the pending direct connect as well: it is only cleared on
success, so a refused target left it set and the worker connected
again with the host name the handler had just freed
Gates ML-DSA composites behind WOLFSSH_NO_MLDSA_COMPOSITES.
Deduplicates key handling and uses heap allocation for
composite buffers when compiling for small stacks.
wolfSSH_ChannelIdRead() has no rekey guard, so a drained channel still
reports zero mid-rekey and the drain loop calls the reply in. That send
returns WS_REKEYING before it prepares a packet, so nothing is queued.
- take the send's status instead of discarding it
- latch eofAnswered and ChildRunning on every status but WS_REKEYING, so
the reply is retried on a later pass; the KEX traffic wakes it
- a short send is left latching: it bundled the EOF and set eofTxd, so a
retry queues nothing and the loop would stall in an untimed select
waiting on a peer that has already half-closed
- same change in the Espressif copy
The drain runs between wolfSSH_worker() and the get_error() that classifies
its result, and its reads and sends latch their own status: WS_WINDOW_FULL
and WS_WANT_WRITE from a send, WS_REKEYING from a read. The ladder then read
the drain's status as the worker's, matched no arm, and ended the session
with the backlog unsent and no EOF.
- Read the error once, right after the worker returns
A chunk read out of the channel is gone from it, so breaking the drain on
a non-positive send dropped whatever the send had not taken, and the echo
back to a half-closing peer came up short.
- Keep the chunk and its offset across worker passes, and read the next
chunk only once the last one is out
- Give the drain its own buffer; the read path below it reuses
channelBuffer in the same pass
- Answer the EOF off a drained flag, since a held tail means a zero read
count no longer marks an emptied channel
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_ChannelIdRead() returns a negative value for a real error, and
the rekey arm treated that the same as a zero read. Restrict the
continue to cnt_r == 0 so an error still ends the loop.
The worker drops back to APP_STATE_LISTEN when an agent connection ends,
but never closes the socket. The next accept() overwrites agentFd, so
every agent connection after the first leaks the previous descriptor. The
forward path has the same gap on its connection-reset arm, where the
socket is closed but fwdFd keeps the closed number.
A rekey was treated as a read failure and ended the session. It cannot
just be skipped either: wolfSSH_worker() reports WS_REKEYING in place of
WS_CHAN_RXD while keying, and nothing raises the data report again, so
ignoring it strands whatever arrived in that call and the peer waits on
an answer that never comes. This is the hazard the library already calls
out for WS_EXTDATA, which is exempted from the same override.
- close agentFd and clear it on both the read-zero and the
ECONNRESET/ECONNABORTED arms
- clear fwdFd on the forward reset arm, matching the read-zero arm
- clear agentCtx.appFd and fwdCtx.appFd wherever the worker closes the
socket, so the stored copy cannot outlive the descriptor
- drain the channel on WS_REKEYING as well as WS_CHAN_RXD, and take an
empty read as "nothing buffered" rather than a failure on that path.
wolfSSH_ChannelIdRead() has no isKeying gate and the window credit it
owes is parked until the rekey completes
ES_ERROR() passes its arguments straight to fprintf(stderr, ...), so a
message with no trailing newline runs into whatever the shell or the
test harness prints next.
- 7 sites in the echoserver, 6 in tests/auth.c, 8 in the Espressif copy
of the echoserver, which carries the same macro and takes this kind of
cross-cutting fix (e91ff50d, ab8058d7)
- the two wrapped format strings already ended with a newline
- Add wolfSSH_SFTP_SetConfinePath and a separate sftpConfinePath, so
the start path only says where a session begins, and only an
explicit confinement root rejects out-of-tree requests.
- Have GetAndCleanPath take the WOLFSSH and enforce the confinement
root, resolving relative requests against the start path.
- Factor the shared canonicalize-and-store work out of
wolfSSH_SFTP_SetDefaultPath into CanonicalizePath and StorePath.
- Give the echoserver -D to opt a session into confinement; without
it the -d home directory only says where SFTP starts.
- Document both settings, and the symlink and TOCTOU caveats, once in
wolfsftp.h, noting the confinement root itself is trusted.
- Cover the split in api.c, and in regress.c start a session in a
subdirectory of the confinement root: a sibling of the start
directory is reachable, anything above the root is not.
Issue: ZD-22308
- Gate header on new scpFileHeaderSent flag, not scpFileOffset==0
- A send callback returning 0 bytes first no longer re-sends header
- Skip zero-length SCP_SEND_FILE send to avoid empty CHANNEL_DATA
- Abort on a second consecutive 0-byte send callback return with file
data outstanding; skipping the send would otherwise spin
SCP_SEND_FILE -> SCP_TRANSFER with no socket I/O
- Reset offset/bufferedSz/flags in ScpSourceInit for connection reuse
- Document the WS_CallbackScpSend contract, including when a 0 return
is valid, next to the typedef
- Add test_wolfSSH_SCP_SendZeroFirst regression (func_args scp_send hook)
- Zero func_args in kex.c/testsuite.c so the new scp_send field is not read uninitialized
Issue: ZD-22176
- Test wolfSSH_ReadKey_buffer() and wc_CertPemToDer() results and skip
the entry; a malformed file gave PwMapNew() a NULL buf and an
indeterminate length.
- NULL-check both WMALLOC() results, and guard the load_file() size so
a missing file no longer reaches WMALLOC(0).
- Fall through to the loop's existing WFREE()/advance so both buffers
are freed on every path.
Issue: F-7204
- ssh_worker() cached fwdCtx.listenFd at entry, while still -1
- The listener is only made later, from WOLFSSH_FWD_REMOTE_SETUP
- FD_SET() and accept() then got a bogus fd, failing with EBADF,
which broke the worker loop and dropped the session
- Read listenFd from the context at each use, guarded on validity
- agentListenFd has the same shape but is set up before ssh_worker()
Issue: ZD-21867
- Reply to a port-0 (dynamic) tcpip-forward with the bound port.
- Add WS_FWD_PORT_CHECK (1024) as the status/port boundary in WS_FwdCbError.
- Callback returns a WS_FwdCbError status below it, the port at or above it.
- DoGlobalRequestFwd reports the port and rejects a port-0 setup with none.
- Map a callback rejection to WS_RESOURCE_E so a no-reply request keeps the link.
- Update the echoserver reference callbacks (examples and Espressif) to
recover the OS-chosen port with getsockname() and return it under the new
convention.
- Add regress coverage for the allocated-port and rejection paths.
Issue: F-5573
Co-authored-by: John Safranek <john@wolfssl.com>
1. Remove the check on authType, it is covered already.
2. Add Keyboard Setup to the existing check on authType.
3. Add a branch for User Auth None so it reaches its handler
in the map loop rather than the new error case.
4. Add else case for an error.
Fixes CID: 637345
Refactor of the echoserver example only; library is unchanged.
1. Collapse FwdStates and AGENT_STATE_* into a single WS_AppState
(INIT/LISTEN/CONNECT/CONNECTED). FWD_STATE_DIRECT becomes an
isDirect flag, cleared after connect.
2. Hoist the fields shared by the forwarding, agent, and shell paths
into a common WS_AppCtx (privateData, listenFd, appFd, channelId,
state, buffer). Shell only uses buffer.
3. Feature-specific remainder stays in WS_AgentCbActionCtx (name) and
WS_FwdCbActionCtx (host/origin name+port, isDirect). Drop the unused
pid field; inline getpid() at the call site.
4. wolfSSH_AGENT_DefaultActions and wolfSSH_FwdDefaultActions now take
a WS_AppCtx* and reach feature-specific state via
appCtx->privateData. echoserver_test() is updated to register the
WS_AppCtx (not the old CbCtx) with wolfSSH_set_agent_cb_ctx and
wolfSSH_SetFwdCbCtx.
5. Reindent ssh_worker()'s #ifdef blocks and add /* WOLFSSH_xxx */
markers.
6. fwd.test.expect: redirect echoserver and portfwd output to log files
and sync on portfwd's -R ready file instead of matching "sampled" in
the pty. Under --enable-debug the WLOG volume fills the pty and
stalls portfwd in select() before channel-open.
1. Add a test script and expect script for testing forwarding.
2. Update portfwd to have a ready file option.
3. Fix echoserver error string, needed NL.
1. Fix a couple unused variable warnings.
2. In wolfSSH_AGENT_DefaultActions(), fix comparison to the result of
snprintf() treating normal result as an error. Reset the return code
for the error state of the socket() command. Better cleanup of agent
startup failures.
When running the build tests, the terminal size gets set to 0x0. When
then tests finish, things like vim start to misbehave until you change
your term window size.
1. Echoserver will not change the terminal size when in echo mode.
2. Do not update the modes when the echo server is not in echo mode.
3. Do not set the terminal update callback context when the echo server
is not in echo mode.
4. Update the guards around the above three items to match the guards for
the modes update function.
Historically, some systems have used <sys/errno.h> in place of <errno.h>, but the C standard,
all the way back to C89/Ansi C, specifies that the correct name is `errno.h` -
and most modern compilers create a sys/errno.h compatibility shim sys/errno.h -
but musl-gcc does not have this compatibility shim,
reasoning that sys/errno.h is non-standard, and fails to compile:
```
$ CC=musl-gcc ./configure --enable-static --disable-shared --enable-scp --enable-sftp --enable-shell --with-wolfssl=wolfssl/install-musl
(...)
$ make
make -j17 all-am
make[1]: Entering directory '/home/hans/projects/wolfssh'
CC examples/client/common.o
CC examples/client/client.o
CC src/libwolfssh_la-ssh.lo
CC src/libwolfssh_la-internal.lo
CC src/libwolfssh_la-log.lo
CC src/libwolfssh_la-port.lo
CC src/libwolfssh_la-io.lo
CC src/libwolfssh_la-wolfscp.lo
CC src/libwolfssh_la-wolfsftp.lo
CC examples/echoserver/echoserver.o
CC src/libwolfssh_la-wolfterm.lo
CC examples/sftpclient/sftpclient.o
CC examples/scpclient/scpclient.o
In file included from examples/echoserver/echoserver.c:91:
/usr/include/x86_64-linux-musl/sys/errno.h:1:2: error: #warning redirecting incorrect #include <sys/errno.h> to <errno.h> [-Werror=cpp]
1 | #warning redirecting incorrect #include <sys/errno.h> to <errno.h>
| ^~~~~~~
cc1: all warnings being treated as errors
make[1]: *** [Makefile:1452: examples/echoserver/echoserver.o] Error 1
make[1]: *** Waiting for unfinished jobs....
make[1]: Leaving directory '/home/hans/projects/wolfssh'
make: *** [Makefile:1020: all] Error 2
```
so, unless we need to support pre-C89 (35+ year old C) compilers,
we can just replace sys/errno.h with errno.h.