- 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.
wolfSSH_SFTP_buffer_send() and ScpStreamSend() keep driving the worker when the
peer half-closes. Both return any negative status, so a WS_EOF would have
aborted a transfer that is still perfectly able to finish: the peer closed its
sending direction, not ours.
- wolfSSH_SFTP_buffer_read() reports every negative peek but a rekey instead of
spending a receive on it. A drained channel at EOF, a dead session and a
channel that is gone all mean no more data can arrive, and the poll only
overwrites the latched cause with WS_WANT_READ or blocks on a peer that has
hung up.
- A rekey is not a drained channel: peek reports it before it looks at the
buffer at all, so that one still needs the poll.
- DoScpRequest() reads its own EOF case the same way as the rest.
DoChannelClose() and wolfSSH_ChannelExit() answer only a channel whose open the
peer has confirmed. peerChannel is 0 until then and both senders resolve by peer
id, so a teardown aimed at an unconfirmed channel landed on whichever channel
held peer id 0 -- normally the live session -- latching its eofTxd and killing
its send direction.
- The close reply still retires the channel it names; there is simply nothing
to say to a peer that has not answered the open.
- wolfSSH_ChannelExit() reports WS_CHANNEL_NOT_CONF, the same as the two
send-EOF calls.
wolfSSH_ChannelExit() leaves the channel on the list once it has sent the EOF
and the close, so the application's pointer stays valid until the peer answers
and wolfSSH_worker() reports WS_CHANNEL_CLOSED. Removing it locally freed the
pointer under the caller and left the peer's close matching nothing.
- DoChannelClose() sends the EOF ahead of the close, per RFC 4254 section 5.3,
and sends both whatever the flush reports: DoPacket() consumes the peer's
close either way, so a message skipped over a blocked flush is never sent.
- It retires the channel and names it on a short write too. The debt belongs to
the output buffer, not the channel, and withholding the close signal would
leave the caller timing the teardown out.
- The worker flushes that reply, keeps WS_CHANNEL_CLOSED as the return value,
and leaves WS_WANT_WRITE latched so the caller knows to drain
wolfSSH_OutputPending() before closing the socket. ssh.h says so.
Issue: F-8839
SendChannelClose() latches closeTxd on the same terms as the EOF beside it: on
anything but the send failure that discards the output buffer. It latched
unconditionally, so a closeTxd claiming a close that never left made
wolfSSH_shutdown()'s gate skip the teardown altogether.
- Split from the EOF latch on purpose: that one widens, from success-only to
bundled, and this one narrows. Read as one change they read wrong.
SendChannelEof() commits eofTxd once the EOF is in the output buffer, not only
once the flush reports success. A short write leaves the bytes queued and they
go out on the next flush, so the retry an application makes on WS_WANT_WRITE
must not put a second EOF behind the first.
- The exception is the send failure that discards the output buffer, taking
the EOF with it: latching there would leave the channel refusing every later
send for an EOF that never went anywhere.
- A reset or a closed peer keeps the bytes, so they still count. The test is
what the buffer holds, since wolfSSH_SendPacket() reports all three the same
way.
- Both arms have a test: the discard through FailIoSend, and the reset that
keeps the bytes queued and latches.
Issue: F-8826
wolfSSH_ChannelSendEof() and wolfSSH_stream_send_eof() close an application's
sending direction and leave its receiving direction open, the half-close of
RFC 4254 section 5.3. Reads keep working until the peer sends its own EOF or
closes; data sends on the channel then report WS_EOF, while requests, the exit
status and the teardown messages still go out.
- Both refuse a channel whose open the peer has not confirmed: peerChannel is
0 until then and the send resolves by peer id, so the EOF would land on
whichever channel holds peer id 0.
- Both sit behind the disconnect gate and refuse to put a packet between
KEXINIT and NEWKEYS.
- stream_send_eof() reports WS_REKEYING itself rather than latching it, the way
stream_peek() does; ssh.h says so, since stream_send() differs.
wolfSSH_stream_read() and wolfSSH_stream_peek() hand back what the peer sent
before they report its EOF. The eofRxd test now sits behind the buffered-data
test, so a half-close no longer strands whatever arrived with it or just ahead
of it.
- stream_read() takes the head channel id before DoReceive() and keeps waiting
when the EOF belongs to another channel: the head is still open and still
has nothing buffered, so reporting it would be indistinguishable. Multi-
channel callers see that one through the worker or the callback.
- Looping only while the head is unchanged, since DoChannelClose() can free it
and the buffer pointer lives in it.
- The disconnect contract in ssh.h and internal.h drops the caveat that the
EOF outranks the drain.
- test_ChannelEofHalfClose() picks up the drain assertions, which the commit
ahead of this one cannot satisfy.
DoChannelEof() latches eofRxd and reports WS_EOF. It used to answer with an EOF
of its own, which latched eofTxd, after which the "Cannot send data after EOF"
gates in SendChannelData() and SendChannelExtendedData() refused every later
send: a peer that half-closed could never be replied to. RFC 4254 section 5.3
leaves that reply to the application.
- wolfSSH_worker() carries WS_EOF out with the channel it belongs to, and does
not mask it during a rekey; the event is raised once, on arrival.
- DoReceiveHandshake() absorbs it in the three accept/connect states that can
run with a channel already open. A legal EOF is not a handshake failure.
- wolfSSH_shutdown() treats it as the response it was waiting for.
- Tests cover the half-close, the channel id, the rekey case, the callback,
which had no test anywhere in the tree, and a handshake that survives an EOF.
- test_ConnectSurvivesChannelEof() sits in its own client region, since
wolfSSH_connect() is not built with NO_WOLFSSH_CLIENT.
Issue: F-1687
The receive mock and the packet builders sit ahead of every endpoint region
rather than inside the server half. A staged-packet IORecv and a plaintext
packet builder have nothing to do with which endpoint is built, and the tests
that follow reach for them from both halves.
- Marked WS_MAYBE_UNUSED, since either half can be the only user of any one.
- WantWriteIoSend() joins DiscardIoSend() among the send mocks.
- A relocation only: no test body changes, and the moved text is unchanged
bar the annotation. Read it with --color-moved.
RFC 8332 section 3 advises against verifying RSASSA-PKCS1-v1_5 by
applying the key and parsing the output. wolfSSH_RsaVerify() builds the
expected EMSA-PKCS1-v1_5 block, applies the key with a raw public
operation, and compares whole modulus-sized blocks.
- verify through wc_RsaFunction() and a constant-time block compare
- reject a signature not less than the modulus, which the raw operation
would otherwise reduce into a second encoding of the same signature
- reject a key too small for the digest, and a wc_RsaEncryptSize() error
- carve the four working blocks from one allocation
- add test_RsaVerify_BadPadding() and test_RsaVerify_SigRange()
Offload builds leave the modulus empty, so the range check is skipped
there. That check walks the bytes itself rather than calling memcmp(),
which some libcs compare as signed char, ordering the modulus's high
byte backwards. The padding cases pass against the old code as well;
the range case does not.
Issue: F-10574
external.test connected the client and SFTP client to a host named by
WOLFSSH_EXTERNAL_HOST. It only ran when WOLFSSH_EXTERNAL_TEST was set,
which nothing in the tree or in CI does, so it always exited 77.
- delete scripts/external.test
- drop it from scripts/include.am
The ready-file counter is shared by every create_port call and was never
reset, so the twenty iterations are a budget for the whole run rather
than per server start. A slow first start leaves later ones with no wait
at all, and the test fails with "NO ready file" instead of waiting. The
guard after the loop then disagreed with the loop as well: the wait is
for a non-empty file, but the guard only asked whether the file existed,
so a wait that ran out still went on to read an empty port.
- reset the counter in create_port, so each server start gets the full
wait and a new call site cannot forget it
- test the guard on -s, matching what the loop waited for
- drop "echo -e", undefined for POSIX sh and printed literally by shells
that do not take the flag; the escapes were only ever blank lines
- quote the expansions, use $(...) instead of backticks, and grep -q
- exit 1 rather than exit -1 from the trap handler
- drop the redundant PWD assignment before the set-directory test, and
the second copy of the wolfsftp executable check
Leaves shellcheck -s sh clean apart from SC2329 on the trap handler.
No regress case registered a channelOpenCb and a fwdCb at the same time,
which is why the v1.5.0 clobber -- fwdCb(LOCAL_SETUP) overwriting the
open callback's rejection -- shipped unnoticed. The fix is on master
already (0317c40f, 616eb681); this is the missing gate.
- TestDirectTcpipOpenCbRejectBeatsFwdCb: both callbacks registered and
the open callback rejects, so the peer must get a channel-open failure
and the forwarding hook must not run at all
- TestDirectTcpipFwdCbRejectAfterOpenCbAccept: the other half, where the
open callback accepts and the fwdCb's rejection has to reach the peer
- TestDirectTcpipFwdCbRejectsChannelId: DoChannelOpen() consults the
fwdCb twice, and only the setup rejection was covered
All three pin the recipient channel and the reason code, not just the
message id. The call counter is file scope rather than reached through
the callback ctx, so the zero the first test asserts cannot be a ctx that
stopped being delivered; the third test reads 2 on the same counter as
the positive control.
RFC 4252 section 5.1 has the server answer a request it does not accept
with USERAUTH_FAILURE. A callback returning WOLFSSH_USERAUTH_REJECTED
now always gets that reply, and always ends the session.
NO_FAILURE_ON_REJECTED suppressed the reply. The macro was never set by
configure, named in a header, or documented, and it guarded all four
method handlers alike.
- drop the macro and its four guards
- end the session on a rejected keyboard-interactive setup, the one
rejection that used to leave the peer retrying to the cap
- state the guarantee on WOLFSSH_USERAUTH_REJECTED in wolfssh/ssh.h
- add test_UserAuthRejectedSendsFailure() over the dispatched methods and
the keyboard-interactive response, asserting on the wire since the
handlers return WS_USER_AUTH_E anyway
Issue: F-11672
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.
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
wolfSSH_CTX_SetFwdEnable() and wolfSSH_SetFwdEnable() are declared in
ssh.h and defined nowhere, so calling either is a link error rather than
a way to gate forwarding. Nothing can depend on them today.
- delete both declarations
- say in the header that the WS_CallbackFwdIO passed to
wolfSSH_CTX_SetFwdCb() is stored and never called; the parameter stays
so existing calls still compile
The third dead corner, the WOLFSSH_FWD_LOCAL_CLEANUP action the library
never emits, needs a setup/cleanup pairing record and a callback that
names the channel. That work is parked on ccb-phase2-local-cleanup.
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.
A USERAUTH_REQUEST naming a service other than "ssh-connection" is
refused before method dispatch, so it drops the per-method state there
too. A following USERAUTH_INFO_RESPONSE is no longer run against a
keyboard-interactive exchange the server has already failed.
- reset ssh->authId and clear ssh->kbSetupPending on the refusal
- charge the abandoned exchange, as the dispatch path does
- leave ssh->kbAuth alone; clients free those buffers by promptCount
- add wolfSSH_TestDoUserAuthInfoResponse() for the tests
- cover the refusal and the stale INFO_RESPONSE in tests/unit.c
Issue: F-10575
RFC 4253 section 7.1 bars SERVICE_REQUEST and SERVICE_ACCEPT between a
KEXINIT and the matching NEWKEYS. The admission check now consults
ssh->isKeying, which acceptState and connectState do not track. It tests
WOLFSSH_PEER_IS_KEYING, as the KEXINIT gate below it does: a peer that
has not seen our KEXINIT yet may still legally send.
- gate MSGID_SERVICE_REQUEST on isKeying in IsMessageAllowedServer()
- gate MSGID_SERVICE_ACCEPT on isKeying in IsMessageAllowedClient()
- record the refusals as WS_MSGID_NOT_ALLOWED_E, the code DoReceive()
stores anyway, rather than WS_REKEYING, a soft retry-later status
- add unit and receive-path coverage in tests/regress.c
Issue: F-10569
RFC 4256 section 3.3 forbids an empty prompt, and RFC 4251 section 5
allows only 0 or 1 in a boolean field. Validation runs before the
payload is sized, so the sizing and building passes agree.
- reject a zero-length prompt or one with a NULL buffer
- reject a prompt count with the arrays unset
- reject a prompt over WOLFSSH_MAX_PROMPT_SZ, which the sizing pass
would otherwise sum into a wrapped payload size
- normalize the outgoing echo byte to 0 or 1
- cover the rejections in tests/api.c, the echo byte in tests/auth.c
Issue: F-10583, F-10584
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.
DoPacket() skips the whole message dispatch once ssh->disconnected is set, for
every message but a DISCONNECT. The handlers that answer must not -- a close
draws an EOF and a close of ours, a request a success or failure, an open a
confirmation, an unknown message an UNIMPLEMENTED -- and what the rest would
record is of no use to a caller that can no longer send. RFC 4253 section 11.1.
- Inbound data from here on is dropped rather than buffered, so the read path
hands back only what arrived before the disconnect. ssh.h and internal.h say
so, beside the calls and beside the flag.
- A DISCONNECT still reaches DoDisconnect(), which sends nothing and is what
latches WS_DISCONNECT; SendDisconnect() sets the flag too, so ours can be
the one that raised it.
- The frame advance steps over the whole packet, so the stream stays in step
with no payload bookkeeping of its own.
- wolfSSH_worker() gates on SendAfterDisconnect() the way wolfSSH_accept() and
wolfSSH_connect() do. With the dispatch skipped there is no non-success left
to return, so it would answer a healthy session for as long as the peer kept
talking and the SFTP and SCP drive loops would keep pumping a dead one. The
rekey test below the gate needs no disconnect of its own: the gate returns
first, and a DISCONNECT arriving mid-pass leaves ret fatal.
wolfSSH_shutdown() drops the channel on a disconnect before its own pump, so
the gate does not cost it the read it does there.
- tests/regress.c pins all four: no reply goes out, the stream stays in step
with a disconnect queued behind a skipped close, late channel data is
dropped rather than queued, and the worker reports the disconnect on the
pass that takes it and on every pass behind it.
wolfSSH_accept() and wolfSSH_connect() drive the handshake only while the
session is live. Both gate on SendAfterDisconnect() ahead of the pending-send
block, which would otherwise flush a queued disconnect and count it as the
next handshake message; the shutdown paths own that flush.
- The prototype sits ahead of both drivers, since either can be the only one
built.
- TestDisconnectGatesAccept() and TestDisconnectGatesConnect() cover a local
disconnect, one from the peer, and a queued short send. One test per
endpoint, so a single-sided build keeps the coverage that applies to it.
- A received disconnect used to reach the error-state test in wolfSSH_accept()
and report WS_INVALID_STATE_E; the gate answers WS_FATAL_ERROR first, and
both tests pin that.
- The contract comments in ssh.h and internal.h drop the ungated note.