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
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_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
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
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
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
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.
- wolfSSH_stream_read() advances inputBuffer->idx before crediting the
window, and it and _ChannelRead() return the bytes copied with a
non-success adjust left in ssh->error.
- _ChannelRead() takes the WOLFSSH from channel->ssh, rejects an idx
past inputBuffer->length, restores the entry ssh->error on a clean
credit, and retires a stale WS_WANT_WRITE only when its own credit
went out.
- wolfSSH_SFTP_Close() checks NoticeError() only on a failed send.
- The src/ssh.c block comments, and new wolfssh/ssh.h notes above
wolfSSH_stream_read(), wolfSSH_ChannelRead() and
wolfSSH_ChannelIdRead(), state the window-adjust and ssh->error
contract.
- tests/unit.c adds test_stream_read_deferredWindowAdjust() and
test_ChannelIdRead_deferredWindowAdjust(): an adjust that defers,
then fails, then succeeds, plus a read with nothing buffered against
a seeded WS_WANT_WRITE.
- STATE_SEND_READ_FTP_DATA trims state->buffer to UINT32_SZ with
wolfSSH_SFTP_buffer_set_size(), reads the data string length into
it with wolfSSH_SFTP_buffer_read(), and decodes it with
wolfSSH_SFTP_buffer_rewind() and wolfSSH_SFTP_buffer_ato32().
- The outSz bound is applied to the decoded length before
wolfSSH_SFTP_buffer_create() allocates for it, and that call's
return is checked.
- ssh->error is set to WS_BUFFER_E when the size or decode helper
fails, to WS_RECV_OVERFLOW_E when the decoded length exceeds
outSz, and to WS_MEMORY_E when the allocation fails.
- The szFlat stack array is removed from
wolfSSH_SFTP_SendReadPacket().
- tests/unit.c gains test_SftpSendReadPacketSplit() and
test_SftpSendReadPacketOverflow(), both registered in
wolfSSH_UnitTest(), with the SftpBuildData() and
SftpClientDriveReadSplit() helpers. The first drives a DATA reply
split at each of the four points across the length prefix; the
second drives one whose string length exceeds the caller's buffer
and checks for WS_RECV_OVERFLOW_E with no retained send read
state.
Issue: F-8828
- DoKexDhInit() sends SSH_MSG_DISCONNECT with KEY_EXCHANGE_FAILED on
WS_CRYPTO_FAILED and WS_PUBKEY_REJECTED_E, DoKexDhGexGroup() on
WS_CRYPTO_FAILED and WS_DH_SIZE_E.
- DoKexDhReply() sends KEY_EXCHANGE_FAILED on WS_CRYPTO_FAILED and
HOST_KEY_NOT_VERIFIABLE on WS_PUBKEY_REJECTED_E.
- DuplexEndpoint records the reason code of a plaintext outbound
disconnect, and InitKexReplyHarnessKex() takes an explicit KEX
algorithm.
- New mutator modes shorten f and e, write a zero-length e, cut the
GEX prime below the requested floor and set the GEX generator to 1;
LocateSinglePacketPayload() finds the payload for all three
single-packet rewriters.
- The harness KEX algorithm falls back to curve25519-sha256, then
ecdh-sha2-nistp256, when no plain diffie-hellman-group is built.
- Tests assert the reason code on the wire for each new mode and for
host key rejection, and assert no disconnect on a successful
handshake.
Issue: F-8838
The flag was set in SendDisconnect() and never cleared, so it meant "a
disconnect was sent" rather than "a flush is owed". Once ours had gone
out, the next teardown call still pushed whatever the internal senders
had queued behind it: measured, a CHANNEL_EOF from DoChannelEof() went
on the wire after the disconnect. wolfSSH_SendPacket() now clears it.
Issue: F-8837
wolfSSH_shutdown() set ssh->error to WS_DISCONNECT only inside the
channel branch, so a flush that emptied the buffer with the channel
already retired left behind the WS_WANT_WRITE that queued it. echoserver
and sftpclient read that error and burn ten wolfSSH_worker() calls on a
write that is already done. TestShutdownFlushesWithNoChannel asserts it.
Issue: F-8837
wolfSSH_stream_peek() and wolfSSH_stream_read() test isKeying before
disconnected, so a peer that rekeys then disconnects wedges both: only
NEWKEYS clears isKeying and none is coming. Callers spin on WS_REKEYING
and never get the buffered data. Both gates and read's copy step now
defer to disconnected. Covered by TestDisconnectOutranksRekey.
Issue: F-8837
The two disconnect tests ran on a session that had never finished user
auth, so IsMessageAllowed() blocked the sends on its own and the "nothing
on the wire" assertions held even with the gates removed. Both now sit
past user auth. With only the shutdown gate reverted the test measures 72
bytes out and both teardown flags set, where before it measured nothing.
- wolfSSH_stream_peek() reports WS_DISCONNECT when the channel is gone,
the way wolfSSH_stream_read() already did; a missing channel used to
read as a bad argument on a session that had simply ended
- the drain test covers the no-channel case for both calls
Issue: F-8837
The disconnect gate left three ways for traffic to reach a peer that had
already ended the session, and it made the default highwater callback
report a failure for a packet that had gone out fine.
- wolfSSH_shutdown() drops the channel when ssh->disconnected is set, so
the EOF, exit status and close are skipped along with the wait for a
close the peer will never send
- wsHighwater() skips the rekey request on a disconnected session, so a
firing high water mark no longer turns SendDisconnect() and
SendChannelEof() into failures
- wolfSSH_ChangeTerminalSize() gained the SendAfterDisconnect() gate,
making the ssh.h contract true for every send declared below it
- regress covers all three, including that shutdown leaves eofTxd and
closeTxd clear and puts nothing on the wire
A disconnect of our own left queued by a short send still reaches the peer.
SendDisconnect() records disconnectTxd once the packet is bundled, and one
FlushQueuedDisconnect() helper gates the retry in wolfSSH_SendDisconnect()
and wolfSSH_shutdown() on that. Keying it on disconnected alone would push
whatever was queued, since the peer's disconnect sets that flag too and
leaves only unrelated traffic behind.
wolfSSH_shutdown() flushes ahead of the channel-list test, so the peer's
close retiring the last channel does not strand the disconnect, and an
unfinished flush outranks WS_CHANNEL_CLOSED. Its WS_WANT_WRITE stays in
ssh->error as well, since callers gate their shutdown retry on that.
The highwater guard sits in HighwaterCheck(), not in the default callback:
the return that fails the send comes from whatever callback the application
installed, and it propagates out through wolfSSH_SendPacket().
Issue: F-8837
The ssh.h comment promised that every send call below it reports
WS_DISCONNECT, but three did not: wolfSSH_TriggerKeyExchange(),
wolfSSH_SendIgnore() and wolfSSH_SendDisconnect().
- All three now take the SendAfterDisconnect() gate, so the sentence in
ssh.h describes the code rather than the intent.
- TriggerKeyExchange() is the highwater callback's rekey trigger, so this
also stops a rekey starting on a session the peer has ended.
- SendIgnore() and SendDisconnect() gained the NULL check the gate needs;
both already reported WS_BAD_ARGUMENT for that from the callee.
- A second disconnect is refused: one ends the session.
- regress.c: the three calls join the send sweep.
Issue: F-8837
wolfSSH_stream_peek() is how the shell loops decide whether a channel is
drained. It had no disconnect check, so a dead session looked exactly like
a drained one: zero bytes available, nothing to tell them apart.
- Report WS_DISCONNECT once the buffered data runs dry, the same shape
wolfSSH_stream_read() uses. What is still buffered comes back first.
- ssh.h and internal.h name peek alongside the read call, and no longer
claim the read side is ungated outright.
- regress.c: peek sees the buffered byte, then sees the disconnect.
Raised from the channel-eof branch, where peek becomes the drain gate for
the wolfsshd and echoserver shell loops.
Issue: F-8837
The disconnect flag gated wolfSSH_stream_read() and wolfSSH_stream_send(),
which is the client-side API. wolfsshd and echoserver drive their channels
through the channel-id calls, so the daemon was never gated at all.
- New SendAfterDisconnect() helper, used by the six send entry points:
stream_send, stream_exit, ChannelIdSend, ChannelIdSendExt,
extended_data_send and global_request.
- Reads stay open, since data that arrived before the disconnect is still
the caller's. wolfSSH_stream_read() drains its buffer and reports
WS_DISCONNECT only once it runs dry.
- wolfSSH_worker() stays ungated; the shutdown paths still pump it.
- ssh.h and internal.h describe the split.
- regress.c: buffered data survives the disconnect, and every send call
refuses without a byte leaving the session.
Issue: F-8837
Every public send call means every one: the channel-pointer sends
(wolfSSH_ChannelSend, wolfSSH_ChannelSendExt, wolfSSH_ChannelExit), the
forwarding requests and both wolfSSH_ChannelFwdNew* opens carry the gate
too, and none of them had a message-filter backstop.
ChannelCreditWindow() parks its credit rather than sending. The reads that
drain what arrived before the disconnect credit the window for the bytes
taken, and that credit went straight to the transport: each drain put a
CHANNEL_WINDOW_ADJUST on the wire after the session was over, and a failing
send replaced the byte count already copied for the caller.
SSH_MSG_DISCONNECT left nothing behind but ssh->error, which
wolfSSH_stream_read() clears on entry. An application looping on the
stream calls lost the code and went back to a connection already over.
- Add WOLFSSH.disconnected, set by DoDisconnect() and SendDisconnect().
- DoDisconnect() sets it before decoding the payload, so a malformed
message still ends the session. RFC 4253 section 11.1.
- wolfSSH_stream_read() and wolfSSH_stream_send() report WS_DISCONNECT
from the flag instead of reaching for the transport again.
- Both guards run ahead of the channelList NULL test, so a torn-down
session reports the disconnect rather than WS_BAD_ARGUMENT.
- ssh.h states that undrained channel data goes with the session;
internal.h states which calls the flag gates and which it does not.
- regress.c: the receive side, the send side, and both of those again on
a session with an open channel.
Issue: F-8837
The test channel credits the peer's window too. Left at 0, SendChannelData()
bails with WS_WINDOW_FULL before the wire, and the "nothing went out" checks
would hold with the gate removed.
wolfSSH_shutdown() searched for the session channel by the peer's channel
ID while telling ChannelFind() to match the local ID field. Each side
numbers its channels independently, so the search usually found nothing.
- The session channel is the head of the list; take it directly instead
of searching for what is already in hand.
- Restores the EOF, exit-status and close sends, and the drain that waits
on the peer's close, all skipped on the NULL result.
- Only bit when the two IDs differ, so the single-channel tests, where
both sides pick 0, never saw it.
- unit.c: shut down a channel whose peer ID is not its local ID, then
check that EOF and close went out.
Issue: F-8817
The refusals added for names the peer cannot use changed the return
contract of a public API whose block comment still promised only
WS_SUCCESS. There is no dox_comments entry, so that comment is all an
embedder has.
- Spell out each WS_BAD_ARGUMENT case, the keep-the-stored-name rule,
and that a refused call leaves the selected type alone.
- api.c asserts connectChannelId across the refusals. It is the field
SendChannelRequest() switches on, so moving the checks back below the
assignment would otherwise pass.
A window-change arriving before any pty-req had nothing to resize, but
the size was stored and the resize callback run anyway. Dropbear refuses
the same request for the same reason.
- Reject it with the existing rej path, so no reply is sent for a
request RFC 4254 sec 6.7 says takes none, and the session continues.
- unit.c covers the rejection, and now drives a real pty-req, which
had no coverage on the receive side at all.
The four pty-req and window-change dimensions were decoded straight into
the WOLFSSH fields and handed to the resize callback unchecked. The
consumers copy them into the unsigned short fields of a struct winsize
for TIOCSWINSZ, so anything above 65535 wraps, and 0x10000 arrives as a
0x0 terminal.
- Add SetTerminalSize() and route both the pty-req and window-change
branches through it, so pty-req stops decoding straight into the
WOLFSSH fields.
- Clamp all four to TERMINAL_DIMENSION_MAX. Others truncate at the
ioctl and accept it, but wolfSSH hands the word32 values to
termResizeCb first, so an unclamped dimension escapes the library
rather than being cut down on the way to the ioctl.
- Take a zero dimension as sent. Others do too, and a zero is how a
peer reports a dimension it has no information about.
- unit.c drives all four dimensions from one table, covering the zero,
single-zero and wrapping cases, in an error code range no other case
in the function claims.
F-8833 recommended ignoring a zero dimension. That is declined above:
Others take zeros as sent, and a zero is how a peer reports a dimension
it has no information about. The finding's symptom, a 0x0 terminal, is
also reached by a route it did not identify, a dimension above 65535
wrapping, and the clamp closes that one.
Issue: F-8833
DoPacket stepped to the next packet using payloadIdx, which handlers
set to however much they read. The default case reads none of an
unimplemented message's payload, leaving the cursor short by that much.
- Advance inputBuffer.idx by UINT32_SZ + curSz from the packet start,
the length DoReceive already bounds-checked, so a handler that
ignores trailing bytes cannot move the next packet's start.
- Snapshot curSz on entry beside the packet start, so the frame is
computed entirely from entry-time state. Reading it back after the
handler switch would describe the next packet if a handler ever
re-entered the receive path.
- Covers DoIgnore, DoDebug, DoUnimplemented, DoChannelSuccess and
DoChannelFailure, which are all short on a padded payload.
- Clamp to the buffer length on the WS_BUFFER_E path.
- unit.c pins the cursor across an unimplemented message, driving
DoPacket through a new wolfSSH_TestDoPacket() hook. The ShrinkBuffer()
noted below zeroes the cursor, so DoReceive() cannot be in the path.
Note the short cursor is not currently observable: DoReceive calls
ShrinkBuffer() with forcedFree, which drops the rest of the buffer
after every packet. This is hardening, not a live desync.
Issue: F-8825
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.
MAX_PACKET_SZ caps the whole SSH binary packet, but the channel
maxPacketSz it was compared against counts only channel payload. A
peer honoring the advertised 35000 overruns the receiver's own check.
- Derive MAX_CHANNEL_PACKET_SZ in internal.h: MAX_PACKET_SZ less the
transport framing, the CHANNEL_EXTENDED_DATA header, the worst-case
padding BundlePacket() picks, and MAX_HMAC_SZ. 34899 by default.
- Name that overhead twice, once for the compiler and once as a
literal for the preprocessor, which reads the wolfCrypt enum
constants in the first form as zero. The #error guarding
DEFAULT_MAX_PACKET_SZ uses the second rather than its own copy.
- MAX_CHANNEL_PACKET_SZ is derived rather than a tunable, so it is
not overridable; an override defeated the bound it enforces.
- wolfSSH_CTX_SetWindowPacketSize() bounds maxPacketSz against that
instead of MAX_PACKET_SZ; DEFAULT_MAX_PACKET_SZ is unaffected.
- api.c tests the new edge and that MAX_PACKET_SZ is now rejected.
Issue: F-8835
- BuildUserAuthRequestEd25519() signs with wolfSSH_AGENT_SignRequest()
when the agent is enabled, writing the returned signature blob
length-prefixed into the reserved payload and advancing idx past it.
The capacity handed to the agent is the room the prepare phase set
aside: two lengths plus the signature and public key type sizes.
- The buffer the local signing path fills is allocated in that path
rather than at the top of the function; sig starts NULL and the
small-stack free at the tail already null-checks it.
- PrepareUserAuthRequestEd25519() notes that the agent holds the
private key and loads none locally.
- tests/regress.c gains TestAgentEd25519UserAuthEmitsSignature(),
TestAgentEd25519UserAuthPropagatesAgentError() and
TestAgentEd25519UserAuthRejectsOversizeSignature(), which drive
SendUserAuthRequest() over a mock agent and parse the emitted
USERAUTH_REQUEST down to its signature field.
- InitAgentEd25519Ctx() takes the signature size the mock agent
answers with, so a caller can hand back a blob past the capacity.
- ParsePayloadLen() and BuildExtInfoSigAlgs() move to the shared test
helper section so the new tests and the existing callers share them.
Issue: F-11660