WOLFSSH_FWD_LOCAL_CLEANUP is part of the public WS_FwdCbAction contract
and the library never sent it, so an application's handler never ran and
every peer-opened forward leaked what its setup callback allocated.
ChannelDelete() now sends it, so a peer close, an open refused after the
setup ran, wolfSSH_ChannelFree(), and freeing the session all report it.
- record the setup in a new fwdSetupTxd bit on WOLFSSH_CHANNEL: a
locally opened forward gets no LOCAL_SETUP, so the channel type alone
cannot say whether the application holds anything
- clear the bit when the cleanup goes out, so a channel reports it once
- pass the channel's id in the port parameter, the way
WOLFSSH_FWD_CHANNEL_ID does, so an application with two forwards can
tell which one ended
- TestDirectTcpipFwdCbRejectsChannelId now counts three callback calls
scripts/fwd.test sends eight short lines, so nothing in the suite drives
a forwarding channel past its first window, and a forward that stalled
once the window needed crediting would go unnoticed.
- push a payload several windows long through a local direct-tcpip
forward and compare the bytes that arrive
- end a second transfer one window plus a short tail in, where the tail
is what portfwd still holds at end-of-input, and repeat it since the
window credit beating that tail is a race
- give the listening nc its stdin from a fifo a sleep holds open:
reading end-of-input makes nc close the connection, which truncates
the transfer and looks exactly like a stall
- bail out early once the byte count stops moving, so a real stall
reports in seconds
- dump the logs on failure, check the listening nc came up, and take
ports clear of the ones fwd.test.expect hardcodes, so a squatted or
shared port is not reported as a stall
A zero read on the local socket left the loop at once, and anything
read but not yet accepted by wolfSSH_ChannelSend() went with it. With
the peer's window full that tail is up to a buffer's worth, so a
transfer that ends while the window is being credited comes out short.
- stop polling the socket on end-of-input and keep looping until the
buffer is empty
- leave anyway once the channel is gone, so a forward whose open failed
cannot hold the loop on a buffer nothing can take
wolfSSH_shutdown() flushes whatever a short send left in the output
buffer, not just a queued disconnect. A rejected auth's
USERAUTH_FAILURE has no channel, and DoChannelClose() retires the
channel as soon as the close is bundled, so neither had anything
left to carry the retry.
- The gate still refuses a flush once the peer has disconnected,
unless our own disconnect is the thing queued.
- A flush that finishes on a live session clears the WS_WANT_WRITE
the short send latched, as the disconnected path already did.
- ssh.h documents the widened flush and the WS_WANT_WRITE a short
one leaves wolfSSH_shutdown() returning.
- Tests cover the rejected auth with no channel and the close whose
channel DoChannelClose() already retired.
- A flush that short-sends on a live channel leaves the teardown
queued behind it. The retry adds no second EOF or close, and skips
the close wait while its own flush is still short.
- A teardown send that drained the leftovers settles the flush; one
that failed outranks it, since a reset leaves the buffer intact.
Only the shell hook had coverage, so nothing held DoChannelRequest() to
handing the command and session type to the exec and subsystem callbacks
or to answering with what they return.
- assert each callback sees the session type and the command string the
request carried, and the ctx set on the session
- assert an accepting callback draws CHANNEL_SUCCESS and a rejecting one
CHANNEL_FAILURE, whether it rejects with a WS_ error or a bare nonzero
wolfSSH_CTX_SetChannelCloseCb() had no caller in tests/, examples/ or
apps/, so nothing held DoChannelClose() to running the hook before it
retires the channel.
- assert the callback runs with the closing channel's id and the ctx
set on the session
- assert the channel is still on the list inside the callback and gone
by the time the caller is told
- a rejecting callback changes nothing: the return is discarded and the
close completes
The confirmation and failure hooks reach an application only through
DoChannelOpenConf() and DoChannelOpenFail(), and nothing in tests/ or
apps/ registered either, so both arms shipped unexercised.
- assert the confirm callback runs with the peer's channel id, window
and packet size already recorded
- assert the failure callback runs while the channel is still findable,
before DoChannelOpenFail() removes it
- count each hook separately, so a test can tell which one ran
- a rejecting confirm hook fails the receive and leaves the open
unfinished; a rejecting failure hook fails it and leaves the channel
on the list
- seed the unconfirmed channel through ChannelNew() and ChannelAppend(),
the state an outstanding open leaves behind
When a test is configured out, its entry function compiles down to
"return 77", the automake skip code, and the main() that forwards to it
inlines to the same two instructions. Apple's ld folds the two identical
bodies together and then writes LC_MAIN entryoff 0, so the binary starts
executing at the Mach-O header and dies with SIGILL instead of skipping.
tests/kex.test, tests/api.test and tests/auth.test all hit this under
--enable-all CPPFLAGS=-DWOLFSSH_TEST_BLOCK, and kex.test hits it again
under --disable-server.
Probe for -Wl,-no_deduplicate and add it to AM_LDFLAGS when the linker
takes it. GNU ld rejects the flag, so the check leaves Linux alone.
Making the skipped body differ does not help: anything added to the entry
function inlines into main() and the two fold again.
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
RFC 4254 6.1 has a session open travelling client-to-server, so
DoChannelOpen refuses one on a client endpoint in the type switch,
ahead of ChannelNew and any channelOpenCb. OpenSSH and Dropbear
refuse it outright too, with no application opt-in.
- Answer OPEN_ADMINISTRATIVELY_PROHIBITED, as the forwarded-tcpip
direction gate below it does.
- Widen the client harness guard in regress.c so it builds without
WOLFSSH_FWD, and hoist AcceptChannelOpenCb out of the forwarding
block.
- Cover the refusal, and the gate outranking an accepting
channelOpenCb.
Issue: F-8836
TestClientOnlyKexMsgsBlocked now asserts that 31 and 33, the ids a
client does receive, stay allowed where the handshake expects them, so
widening the role check into a 30-34 range fails the suite. The
IsMessageAllowed() comment records the receive-only policy and the
per-KEX-method id namespace.
- assert id 33 is allowed once expectMsgId is MSGID_KEXDH_GEX_REPLY
- assert expectMsgId starts at MSGID_NONE, so the role check is what
rejects the blocked ids
- recheck all three blocked ids during a rekey on an established session
wolfSSH_CTX_SetSshProtoIdStr() now rejects a string that is not
CRLF-terminated, exceeds 255 bytes counting the terminator, or
carries a CR or LF in the body. DoKexInit() subtracts the two
terminator bytes from the length when hashing it, so an
unterminated string underflowed the hash length.
Issue: F-10571
Gates ML-DSA composites behind WOLFSSH_NO_MLDSA_COMPOSITES.
Deduplicates key handling and uses heap allocation for
composite buffers when compiling for small stacks.
FwdRemoteSettle() folds a port-0 forward the peer bound onto a
registration already standing at that port. A request still in its
send window resolved its forward before the send, so it names the
survivor from here on, the same as the queued replies do.
- Add FwdPendingRebind() and call it ahead of the unlink, so a cancel
whose send pumped in the answer commits naming a forward rather
than nothing.
- Regression test drives the port-0 answer in from the cancel's send
callback.
FwdPendingCommit() looks for a registration of the bind its entry names
before linking it. An answer pumped in from the send can settle a
port-pending forward onto that port, and the fold in FwdRemoteSettle()
runs before the committing entry joins the list.
- replies queued against the folded entry move to the one that stands
at the bind
- test drives that answer from the IO send callback; the bind takes two
registrations without the fold, and one cancel leaves one matching
A request looked its registration up again at commit, after the send had
run the application's send and highwater callbacks. A callback that
reenters the library sends behind the request that ran it, so the lookup
found what the callback did rather than what the request asked for: a
cancel from a first setup's send found nothing to take, and that setup's
commit then registered the forward the peer had just been told to drop.
- Requests in flight hang off ssh->fwdPendingHead for the length of
their send, so a reentrant request resolves to the forward the request
it interrupted is registering, not to nothing.
- FwdRemotePrepare() settles what the request names and holds it on the
pending; FwdPendingCommit() uses that instead of a fresh lookup, which
also drops the dedupe branch the double lookup needed.
- FwdRemoteUnlink() voids the pending pointers along with the queued
slots, so freeing a forward mid-send leaves no commit naming it, and
FwdPendingDiscard() goes through it rather than freeing raw.
- FwdRemoteMatch() reads the pending list under the rules the scan over
registered forwards uses: a setup mid-send is on the wire and can feed
channels; a cancel behind it or a refusal on its slot stops matching.
- Drop pend->bindAddr and pend->bindPort, which nothing reads now.
- Tests drive a cancel, a setup, an inbound forwarded-tcpip open and a
mid-send REQUEST_FAILURE from the IO send callback, the window the
highwater tests no longer reach.
Issue: ZD-22195
The match only ran once wolfSSH_FwdRemoteSetup() had registered
something, so a client that never called it had every forwarded-tcpip
open go unchecked -- the case RFC 4254 7.2 is clearest about, since a
session that asked for no forward has nothing an open could answer for.
- Refuse the open whenever it names no registration, empty list
included. wolfSSH_SetFwdRemoteMatch(WOLFSSH_FWD_MATCH_OFF) is the
opt-out for an application that keeps its own bind list.
- Drop fwdRemoteTracked, which nothing reads now.
- wolfSSH_global_request() cannot carry the bind address and port RFC
4254 7.1 puts after the want-reply boolean, so no request that
registers a forward can be framed outside wolfSSH_FwdRemoteSetup().
Issue: ZD-22195
- SFTP_SetFileAttributes() and SFTP_SetFileAttributesHandle() carry
out the size, ownership, permission and timestamp requests while ret
is WS_SUCCESS, bound the size with wResolveOffset() against
WOLFSSH_MAX_FILE_OFFSET, and set WS_UNIMPLEMENTED_E where the port
defines no wrapper.
- wolfSSH_SFTP_RecvSetSTAT() and wolfSSH_SFTP_RecvFSetSTAT() answer
WOLFSSH_FTP_UNSUPPORTED for WS_UNIMPLEMENTED_E; wolfSSH_SFTP_CHMOD()
sets the attribute flags to WOLFSSH_FILEATRB_PERM before sending.
- port.h adds WTRUNCATE, WFTRUNCATE, WCHOWN and WFCHOWN for the POSIX
port, and defines WSETTIME and WFSETTIME over the existing WUTIMES
and WFUTIMES helpers in place of their (0) definitions.
- SFTP_SetMode() guards on _WIN32_WCE in place of USE_WINDOWS_API, and
port.c adds WS_ChmodA(), which trims the SFTP leading root and calls
_wchmod(); it and the WCHMOD mapping to it are left out under
_WIN32_WCE, which keeps the _chmod mapping.
- tests/regress.c adds TestSftpSetStatAttributes(); tests/sftp.c adds
a chmod of a directory and installs SFTP_TEST_UMASK in place of the
sftpTestUmask static; the STATE_SET_ATR_SEND case in tests/api.c
sets atr.flags to WOLFSSH_FILEATRB_PERM.
Issue: F-11658
- The Windows local open in wolfSSH_SFTP_Get() passes OPEN_EXISTING
when the write offset is nonzero and CREATE_ALWAYS otherwise, and
drops FILE_APPEND_DATA from the desired access. A new
DWORD creationDisp replaces the block-scoped desiredAccess.
- That open reports INVALID_HANDLE_VALUE as WS_BAD_FILE_E and moves to
STATE_GET_CLEANUP; the OVERLAPPED offset is set from gOfst on every
open rather than only when resuming.
- STATE_GET_LOOKUP_OFFSET clears a saved offset when the remote size
STATE_GET_LSTAT stored in state->attrib is no larger than it, and
again when the local destination does not hold exactly that many
bytes. The destination stat overwrites state->attrib.
- tests/api.c adds test_wolfSSH_SFTP_GetResume() and its
sftpGetToCompletion() helper, six cases over the resume paths, built
where the hosted file wrappers are available.
- test_wolfSSH_SFTP_PutResume() and test_wolfSSH_SFTP_GetResume() drop
the WOLFSSH_ZEPHYR k_sleep() block their bodies exclude.
- .gitignore covers every wolfssh_*.tmp the api tests leave behind on
an aborted run, replacing the known_hosts-only entry.
Issue: F-12547
A client answers tcpip-forward and cancel-tcpip-forward with a failure,
RFC 4254 section 7.1, before the request body is parsed. DoPacket()
disconnects on a refused id this build implements and on any refused id
of 80 or higher, the range RFC 4252 section 6 names; a refused id below
80 is answered UNIMPLEMENTED, RFC 4253 section 11.4.
- MsgIdKnown() carries DoPacket()'s dispatch ids, build guards included
- answer a refused id off the dispatch, so one cannot reach a handler
if MsgIdKnown() falls behind
- skip the disconnect once the session is over, RFC 4253 section 11.1
forbids sending after one
- cover ids 53, 79, 200 and a channel open before user auth on one
keyed-server helper
- cover a client refusing both request names, with a reply asked for and
without, and a server still succeeding
Issue: #1047 (3), #1047 (7), F-10576, F-10581, F-12574
A port-0 reply naming a port another registration stands for merges the
two, since one bind gets one registration. The requests still queued
against the entry that goes named that same bind, so FwdReplyRebind()
hands them to the survivor. Left naming nothing, a cancel among them
settles no forward and the merged one keeps matching opens.
- FwdReplyRebind() repoints queued slots before the stale entry is
unlinked and FwdReplyVoid() clears them
- Test folds a port-0 reply onto a forward with a cancel already
queued, and checks the cancel takes the survivor down
A cancel refusal leaves the forward standing, since the peer keeps a
listener it would not drop. There is no listener to keep when the setup
was refused too, so FwdRemoteSettle() unwinds a registration nothing
establishes and nothing is still owed an answer on, rather than leaving
it unconfirmed and unmatchable until the session ends.
- A cancel refusal unlinks an unconfirmed forward with no setup queued
- Test refuses a want-reply setup and the cancel behind it, and checks
the registration is gone
The mid-send window closed to the highwater callback when the commit
moved ahead of it, and the tests written for that window went with it:
they now answer a settled slot and reach the ordinary path. The IO send
callback is where a peer's answer can still land while the sender owns
its slot, so drive the reply from there.
- FwdReplyFromSendCb() answers the request from inside the flush, with
a short-write and a fail-next mode for a send that loses the rest of
the request after the answer is in.
- Tests cover a parked success, a parked refusal, a parked port-0 reply,
a failed send giving the answered slot back, and an answer settling an
earlier request while a cancel naming the same bind is mid-send.
- The highwater tests stay, for the committed path they now cover; their
comments said they held an uncommitted slot.
The forward a "tcpip-forward" or "cancel-tcpip-forward" establishes was
committed after SendGlobalRequestFwd() returned, which is after the
post-send highwater callback had run. A request that callback sends goes
out behind this one but committed ahead of it, so the earlier request
had the last word and the client ended up on the opposite side of the
forward from the peer. A first setup whose callback cancels it left the
forward registered with no listener on the peer, and a cancel whose
callback re-establishes the forward unlinked it, refusing every open for
a listener the peer holds.
- Split the post-send highwater check off wolfSSH_SendPacket() as
SendPacketFlush(), for a sender with state to commit first.
- SendGlobalRequestFwd() takes the pending forward and settles it inside
the send window, then runs the check.
- Commit order is send order now, so the last request sent governs,
whichever call made it.
- FwdPendingCommit() still re-resolves the entry: the IO send callback
can reenter mid-flush, which no ordering fixes.
- Tests cover a reentrant cancel of a first setup, a reentrant setup
during a cancel, and an inbound forwarded-tcpip open pumped from the
callback.
Filling the reply queue sends enough to cross a lowered highwater mark.
The rekey that fires sends a KEXINIT the 256-byte mem buffer cannot hold.
- Disable the highwater; a bigger buffer only moves the cliff.
- Both harnesses, since the buffer is the same size on each.
The forwarded-tcpip check is a behaviour change on a shipped API, so an
application that trips over it needs a way out that isn't abandoning
wolfSSH_FwdRemoteSetup(). wolfSSH_SetFwdRemoteMatch() relaxes the check
for the session.
- STRICT is the default and keeps the bind-plus-port rule.
- PORT compares the port alone, for a peer that rewrites the bind
address it echoes back, which STRICT refuses every open from.
- OFF accepts any open, as wolfSSH did before the check existed.
- Tests cover each setting, and a refused one leaving the default in
place.
Only the peer gives a slot back, by answering, so a peer that never
answers a want-reply global request lets the queue grow for the life of
the session, and the queue scans run once per forward on every inbound
forwarded-tcpip open.
- WOLFSSH_MAX_FWD_REPLIES bounds the queue.
- Refuse before the request is framed: one whose slot was never queued
would mispair every later reply.
- Return WS_RESOURCE_E, not the WS_MEMORY_E that nothing failing to
allocate would have yielded.
- Test fills the queue, then covers the refusal, that a refused setup
registers nothing, and a slot coming back on an answer.
A slot named its forward only once its request committed, so the scans
could not see a request mid-send. A cancel the peer confirms while a
fresh setup for the same bind is still going out then found nothing
standing for the forward and unlinked it, leaving the peer with a
listener the client refuses every open for.
- Name a slot by its bind until it commits.
- FwdReplyNames() answers for both FwdReplyHasSetup() and
FwdReplyNewest().
- The bind is borrowed from the caller and dropped at commit, the
lifetime WOLFSSH_FWD_PENDING already assumes for it.
- A pointer to the entry would not do: the send runs callbacks that can
free and remake it, which is why the entry is re-resolved at commit.
- Test drives a confirmed cancel against a re-setup still in its send
window.
A signal was turned into a fatal error, and a refused send left its
packet counted in the output buffer. Both fixes are in
wolfSSH_SendPacket(), so they cover every sender.
- WS_CBIO_ERR_ISR fell through to WS_SOCKET_ERROR_E. Nothing went out
and the session is unharmed, so retry, as ReceiveData() already does.
- Callers that discard a packet on error, like the KEX and userauth
sends, were throwing away framed output the peer never refused.
- On WS_CBIO_ERR_GENERAL the buffer was shrunk with the packet still
counted in plainSz, so SendChannelData() flushed nothing and called it
a success. Clear it with the packet it described.
- Tests pin the retry from a forwarding sender and a plain global
request, and drive a channel send through a would-block and a refused
flush.
RFC 4254 7.2 says a forwarded-tcpip open answers a forward the client
asked for, so refuse an open naming anything else. Enforcement starts at
the first wolfSSH_FwdRemoteSetup(), leaving a client that frames
tcpip-forward itself unaffected.
- Register each setup per session. A wildcard bind matches on port alone.
- Port 0 now requires want-reply, since only the reply names the port.
- Repeat setups of one bind share a registration, so one cancel undoes
it.
- A cancel stops matching as it goes out, but a want-reply cancel stays
registered until the peer answers: a refusal leaves the listener up.
- Replies carry no request id, so a per-session queue pairs them in send
order. A want-reply wolfSSH_global_request() takes a slot as well.
- Registration is split around the send: allocate first, link once the
request reached the wire.
- A request resolves its registration on commit, since sending runs
application callbacks that can reenter the library.
- Tests cover matching, cancel, overlapping requests, send-order
pairing, port 0, registration around the send, and reentrancy from a
callback.
- They drive a client session, so they sit outside the server-only block
in regress.c and run in a --disable-server build. The harness struct,
its teardown, the channel-open-failure helpers and the forwarding
callback moved out with them, shared with the server-side tests.
Contracts for wolfSSH_FwdRemoteSetup(), wolfSSH_FwdRemoteCancel() and
wolfSSH_global_request() are in wolfssh/ssh.h.
Issue: ZD-22195
A send's return code cannot tell a caller its request is on its way. The
highwater callback runs after the last byte goes out, so a rekey's
errors surface as the send's, and WS_WANT_WRITE leaves the packet framed
for the next flush.
- Count the flushes wolfSSH_SendPacket() completes.
- Compare that count across a send to tell those outcomes apart.
- SendGlobalRequest() and SendGlobalRequestFwd() carry the answer in an
optional out-param.
- Both callers pass NULL, so nothing acts on it yet.
Issue: ZD-22195
BuildDirectTcpipExtra() and BuildGlobalRequestFwdPacket() have callers in
several conditional blocks, and a build with none of them left the two
functions unused, which -Werror turns into a build failure.
ReadUint32() next to them already carries the attribute for the same
reason.
- --disable-server builds regress.c again.
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
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
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 refused channel open frees the channel and surfaces as a fatal error
rather than a close, so the guard that clears fwdChannel never runs and
the half-close check read freed memory on every refused -L forward.
- Stash the channel id wherever the channel is created or adopted
- Look the channel up by id each pass and stop once it is gone
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
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.