Commit Graph

2970 Commits (5151732d6bb10bdd2e30dc4584b7bad34459a98e)

Author SHA1 Message Date
Yosuke Shimizu 5151732d6b wolfsftp: keep the local file when resuming a get
- 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
2026-09-01 09:19:10 -07:00
John Safranek c71202ffdb Guard remote forwards and disallowed msg ids
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
2026-09-01 10:51:48 -05:00
John Safranek 9cedb5a1e7 Keep queued replies with the folded forward
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
2026-08-31 20:35:56 -05:00
John Safranek a88404cb88 Drop a forward the peer refused twice
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
2026-08-31 20:35:56 -05:00
John Safranek f6126898e0 Test a reply landing before the request commits
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.
2026-08-31 20:35:56 -05:00
John Safranek 8c920a8767 Commit forward state before the post-send callback
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.
2026-08-31 20:35:56 -05:00
John Safranek cabbc53f8f Keep a rekey out of the mem-IO test harnesses
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.
2026-08-31 20:35:56 -05:00
John Safranek 69bfbccbf9 Add a match setting for remote forwards
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.
2026-08-31 20:35:56 -05:00
John Safranek 3267d87bf5 Cap the pending-reply queue
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.
2026-08-31 20:35:56 -05:00
John Safranek 4ec4825d86 Let the reply-queue scans see a slot still in its send window
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.
2026-08-31 20:35:56 -05:00
John Safranek 8d3f20bfde Fix interrupted and failed sends in SendPacket
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.
2026-08-31 20:35:56 -05:00
John Safranek e396a0a4b6 Match forwarded-tcpip to registered forwards
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
2026-08-31 20:35:56 -05:00
John Safranek 696339eabf Report whether a global request reached the peer
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
2026-08-31 20:35:56 -05:00
John Safranek 6c83a2bfc9 Mark the forwarding packet builders maybe-unused
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.
2026-08-31 20:35:56 -05:00
John Safranek 7325678553 echoserver: keep the EOF reply owed across a rekey
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
2026-08-31 11:53:29 -05:00
John Safranek 7b17f65b67 Tighten the stdin-stall bound
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
2026-08-31 11:53:29 -05:00
John Safranek 7c52cbe86f Wait on a child that is not taking its stdin
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
2026-08-31 11:53:29 -05:00
John Safranek fc3b0a86bd Take the worker's status before the EOF drain
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
2026-08-31 11:53:29 -05:00
John Safranek 033646ce0a Re-resolve the forwarded channel before the half-close check
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
2026-08-31 11:53:29 -05:00
John Safranek ad08a10ba6 Hold the EOF drain's unsent tail across passes
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
2026-08-31 11:53:29 -05:00
John Safranek 93f390092b Never wait on the child in the shell loop
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.
2026-08-31 11:53:29 -05:00
John Safranek ff59c723ec Handle the EOF status in apps and examples
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.
2026-08-31 11:53:29 -05:00
John Safranek 2bfdbac2b3 Carry the EOF status through SFTP and SCP
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.
2026-08-31 11:53:29 -05:00
John Safranek 74098863ad Reject teardown on an unconfirmed channel
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.
2026-08-31 11:53:29 -05:00
John Safranek fe936c8b8c Keep the channel until the peer's close
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
2026-08-31 11:53:29 -05:00
John Safranek 5911c51ef3 Drop closeTxd on a discarded close
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.
2026-08-31 11:53:29 -05:00
John Safranek 147b787798 Latch eofTxd on the bundled EOF
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
2026-08-31 11:53:29 -05:00
John Safranek 591510257b Add the channel half-close API
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.
2026-08-31 11:53:29 -05:00
John Safranek 4817f8e0d2 Drain buffered data before the EOF
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.
2026-08-31 11:53:29 -05:00
John Safranek d9c223cbae Stop echoing the peer's channel EOF
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
2026-08-31 11:53:29 -05:00
John Safranek d70f8e64d3 Hoist the shared unit test doubles
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.
2026-08-31 11:53:29 -05:00
John Safranek c5d283b024 fix: compare RSA blocks instead of parsing
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
2026-08-31 10:13:10 -05:00
John Safranek bfe6fe0f2d scripts: remove the external test
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
2026-08-30 11:24:47 -05:00
John Safranek 0af50e8301 scripts: harden sftp.test
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.
2026-08-30 11:24:47 -05:00
John Safranek c88e2e7212 tests: cover a channel-open rejection on a forwarding channel
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.
2026-08-30 11:16:05 -05:00
John Safranek 4b887680c1 fix: always send USERAUTH_FAILURE on reject
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
2026-08-28 21:53:43 -05:00
John Safranek 231772ceaa wolfsshd: stop polling when the SFTP channel has nothing buffered
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.
2026-08-28 21:36:07 -05:00
John Safranek ad059d7d0e echoserver: only continue the rekey drain on an empty read
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.
2026-08-28 18:26:25 -05:00
John Safranek 59a1ef8eec echoserver: close the agent socket on reset and handle a rekey
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
2026-08-28 18:26:25 -05:00
John Safranek 26ccac3cdd examples, tests: terminate the ES_ERROR messages with a newline
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
2026-08-28 18:22:34 -05:00
John Safranek 6c77e49649 fwd: drop the declarations with no definition
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.
2026-08-28 18:18:18 -05:00
John Safranek d3d3e7ec31 Fix the cppcheck findings
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.
2026-08-28 18:12:59 -05:00
John Safranek a98fbddb05 fix: clear userauth state on bad service
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
2026-08-28 17:55:19 -05:00
John Safranek 89b8055a0a fix: reject service messages while keying
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
2026-08-28 17:45:36 -05:00
John Safranek 5fb7be429b fix: validate keyboard-interactive prompts
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
2026-08-28 17:45:20 -05:00
John Safranek 689fec4b01 Drain the shell channel before closing stdin
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.
2026-08-28 13:30:12 -05:00
John Safranek b8b40322b0 Stop answering traffic after a disconnect
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.
2026-08-28 11:18:41 -05:00
John Safranek 239850775c Gate the handshake drivers on a disconnect
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.
2026-08-28 11:18:41 -05:00
Mark Atwood 9731cfe250 test: assert SBOM wolfSSL version comes from WOLFSSL_DIR
The dependency assertion checked only that wolfssl was present with a
DEPENDS_ON edge, never that the recorded version matched WOLFSSL_DIR, so
the SBOM could attest a version wolfSSH was not built against and still
pass. Compare versionInfo against the tree.

That assertion alone would not have caught it: the runner installs
wolfssl from the same tree it passes as WOLFSSL_DIR, so pkg-config and
wolfssl/version.h always agree and a wrong source is indistinguishable
from a right one. Add a step that forces them apart -- a stub pkg-config
reports a sentinel version, and a distclean'd copy of the tree has no
generated version.h, so the configure.ac fallback is the only route to a
correct answer. Reverting the recipe fix makes this step fail with the
sentinel, and the fallback path now has CI coverage it lacked.
2026-08-27 09:43:33 -07:00
Mark Atwood 48fc50c9bc fix: read wolfSSL version from WOLFSSL_DIR only
The wolfSSL dependency version came solely from
WOLFSSL_DIR/wolfssl/version.h. That header is generated by configure as
well as tracked, so `make distclean` in the wolfSSL tree removes it. With
it gone the --dep-version override was never passed and gen-sbom fell
back to `pkg-config --modversion wolfssl`, which reports the *installed*
wolfSSL. On a host whose installed build differs from WOLFSSL_DIR the
SBOM recorded that unrelated version (9.9.9 in the report) and still
exited 0, attesting a component wolfSSH was not built against.

Fall back to AC_INIT in WOLFSSL_DIR/configure.ac, which survives
distclean, and fail when neither source is readable rather than letting
an installed copy answer for the tree. SBOM_WOLFSSL_VERSION still
overrides both.
2026-08-27 09:43:33 -07:00