Commit Graph

3057 Commits (a14e6d0f3cdc9f6a2226fab223ab4a69887891c5)

Author SHA1 Message Date
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
Mark Atwood f8f580b878 fix: fail SBOM CI when gen-sbom missing on master
The gate skipped SBOM generation and exited 0 whenever wolfssl's
scripts/gen-sbom was absent, so the job could report success having
never exercised `make sbom`. That was deliberate while the script was
unmerged, but it landed on master in wolfSSL/wolfssl#10343.

On master, a missing gen-sbom or one too old for --dep-wolfssl is a
regression, so fail instead of skipping: a green run now always means
the target was exercised and the wolfssl-dependency assertions ran.
Only a deliberately pinned older wolfssl_ref still degrades to a skip.

The ref is passed through env rather than interpolated into the shell
script, to keep the workflow expression out of the command line.
2026-08-27 09:43:33 -07:00
Mark Atwood d52677082f fix: unpin SBOM CI from merged wolfssl PR ref
wolfSSL/wolfssl#10343 merged on 2026-07-27, but the SBOM workflow still
checked out wolfssl at refs/pull/10343/head. GitHub keeps PR refs alive
after merge, so CI stayed green while validating a pre-merge snapshot
rather than master. Point wolfssl_ref and its fallback at master.

Distinguish the two gen-sbom lookup failures. A set-but-wrong
WOLFSSL_DIR previously printed the "re-run with WOLFSSL_DIR=..." advice
to someone who had already set it; it now names the path that was
probed and states that WOLFSSL_DIR is a source tree, not an install
prefix.

Refresh the README and in-recipe notes that still told users to track
the #10343 branch until it merged.
2026-08-27 09:43:33 -07:00
Sameeh Jubran 4e322f0e71 sbom: re-vendor unified canonical scripts/sbom.am
Sync scripts/sbom.am with the unified canonical copy in wolfSSL
(scripts/sbom.am), which adds the SBOM_OPTIONS_H override so one
fragment supports both options.h- and config.h/AM_CFLAGS-based macro
capture. wolfssh uses the default config.h path and does not set
SBOM_OPTIONS_H, so behavior is unchanged. Keeps the vendored fragment
byte-identical to the canonical.

Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
2026-08-27 09:43:33 -07:00
Sameeh Jubran b89b940add sbom: sync scripts/sbom.am with canonical copy and widen CI filter
Re-sync scripts/sbom.am with the canonical wolfSSL copy ($(docdir)
sbomdir, [[:space:]] version parse, GNU-make and uninstall-sbom docs)
and widen the SBOM workflow pull_request filter to '**'.

Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
2026-08-27 09:43:33 -07:00
Sameeh Jubran 2f3abf2ec4 scripts/sbom.am: capture AM_CFLAGS/CFLAGS and configurable config.h
Sync the vendored fragment with the canonical wolfSSL copy: add
$(AM_CFLAGS) $(CFLAGS) to the -dM -E build-options capture and make the
config header path overridable via SBOM_CONFIG_H (default config.h) so
the SBOM records the configured feature macros.

Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
2026-08-27 09:43:33 -07:00
Sameeh Jubran 1f57f67db7 sbom: default license to GPL-3.0-or-later and pin CI to #10343 ref
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
2026-08-27 09:43:33 -07:00
Sameeh Jubran f0e4b404d5 sbom: harden gen-sbom path resolution and align docs
Guard the SBOM_GEN wolfSSL fallback with $(if $(WOLFSSL_DIR),...) so an
unset WOLFSSL_DIR leaves it empty (yielding the intended "set WOLFSSL_DIR"
error) instead of resolving to an absolute /scripts/gen-sbom that could run
an unrelated host script. Reword the fragment header to describe the actual
gen-sbom resolution (vendored copy if present, else WOLFSSL_DIR) rather than
claiming offline tarball builds that aren't wired up yet. Condition the
README's wolfSSL-dependency statement on a sufficiently new gen-sbom to
match the existing degradation note.

Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
2026-08-27 09:43:33 -07:00
Sameeh Jubran 5323b26b0a sbom: extract shared scripts/sbom.am recipe and add CI
Move the make sbom / install-sbom / uninstall-sbom recipe out of
Makefile.am into a reusable scripts/sbom.am fragment; Makefile.am now
just declares the product facts (name, LICENSING, wolfssl dependency,
GPL-3.0-only default) and includes it.

Add a GitHub Actions workflow covering both SBOM paths:
- autotools: build wolfSSL + wolfSSH, run make sbom, assert SPDX
  validity, CycloneDX identity, reproducibility, and the wolfssl
  dependency edge.
- embedded: run gen-sbom against user_settings.h (via pcpp) + the
  wolfSSH source set, asserting the same identity/validity/reproducibility
  plus that user_settings.h options and the wolfssl dependency are
  captured.
Both gracefully skip on a gen-sbom that predates the wolfSSL SBOM
change.

Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
2026-08-27 09:43:33 -07:00
Sameeh Jubran 7ff47dc54c sbom: fix license detection and clarify wolfssl dependency note
Default SBOM_LICENSE_OVERRIDE to GPL-3.0-only so the SBOM records the
correct license instead of NOASSERTION, and point the --dep-wolfssl
note at wolfSSL/wolfssl#10343.

Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
2026-08-27 09:43:33 -07:00
Sameeh Jubran c34fdf155b fix: make SBOM reflect real config and cover all build types
- Include AM_CPPFLAGS and config.h in the options snapshot so the
  configured feature set (WOLFSSH_SCP/SFTP/NO_WOLFSSH_SERVER/...)
  appears in the SBOM instead of being empty.
- Discover static (.a), Mach-O (.dylib) and Windows (.dll/.lib)
  artifacts, not just a versioned .so.
- Set SOURCE_DATE_EPOCH from the last git commit for reproducible output.
- Clean staging tree and temp defines file via trap even on failure.
- Record wolfssl as an SBOM dependency (--dep-wolfssl), version
  auto-detected from WOLFSSL_DIR/wolfssl/version.h.
- configure.ac: detect git; drop unused version-info split + comment.

Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
2026-08-27 09:43:33 -07:00
Mark Atwood b79c3e8e89 docs: add SBOM/EU CRA Compliance section to README and build docs 2026-08-27 09:43:33 -07:00
Mark Atwood 527df4ffd6 feat: add make sbom target
Adds sbom, install-sbom, and uninstall-sbom targets.
Runs gen-sbom to produce CDX and SPDX outputs.
Requires WOLFSSL_DIR pointing to a wolfssl tree with
the feat/sbom-embedded branch (includes gen-sbom).
2026-08-27 09:43:33 -07:00
Yosuke Shimizu 78c992a5a1 ssh.c: report the byte count from a channel read that defers its credit
- 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.
2026-08-27 09:01:27 -07:00