Commit Graph

405 Commits (39fec50df20e1bfcc2d071b596bedcb4d3625054)

Author SHA1 Message Date
Aidan Keefe 4553b3c659 Skoll review and Github review
Added more test vectors as well as locking in behavior of VerifyProtoId
Added round trip test for proto id
Comment fixes
New define for proto id min size
2026-09-10 15:27:30 -07:00
Aidan Keefe 155a4e1c47 Validate a custom SSH proto ID string
wolfSSH_CTX_SetSshProtoIdStr() now rejects a string that is not
CRLF-terminated, exceeds 255 bytes counting the terminator, or
carries a CR or LF in the body. DoKexInit() subtracts the two
terminator bytes from the length when hashing it, so an
unterminated string underflowed the hash length.

Issue: F-10571

partial progress
2026-09-10 15:27:30 -07:00
John Safranek e7a05229ad agent: commit the open on a completed flush
wolfSSH_AGENT_ChannelOpen() settles its channel and agent on whether
the open reached the peer, which SendPacketDelivered() answers from the
flush count. The highwater callback runs after the last byte is out, so
its failure arrives as this send's return, and the rollback read that
as a send that never left.

- give SendPacketDelivered() external linkage, contract in internal.h
- note in agent.h that an error raised once the open is on the wire
  leaves the channel open
- test a failing highwater callback: the channel and the agent stand,
  and the poll after it is the idempotent one
2026-09-08 11:50:40 -05:00
John Safranek 6164ec39d5 agent: refuse a channel open after a disconnect
wolfSSH_AGENT_ChannelOpen() answers a poll on a session that is over
with WS_FATAL_ERROR and WS_DISCONNECT in ssh->error, the shape every
other public sender uses: no channel opened, nothing on the wire,
RFC 4253 section 11.1. wolfSSH_accept() gates the open it drives, so
the new public entry point is the only way in.

- promote SendAfterDisconnect() to WOLFSSH_LOCAL so agent.c uses the
  same helper as every other public sender
- leave an open queued before the disconnect unflushed, the rule
  wolfSSH_shutdown() applies to all but its own disconnect
- keep WS_DISCONNECT in ssh->error at the accept() call site, which
  used to overwrite it with the status the open returns
2026-09-08 11:50:40 -05:00
John Safranek 41f171d982 agent: let the application open the agent channel
The one server-side site that opens auth-agent@openssh.com sits inside
wolfSSH_accept(), so an application driving its own channels cannot
reach it: the session records the request and no channel follows.

- add wolfSSH_AGENT_ChannelOpen(), the same open lifted out of accept(),
  which still calls it
- it reports WS_BAD_ARGUMENT until the peer asks and on a client
  session, and is idempotent after, so an application can poll it
- publish the agent on a queued open too, so a retry after WS_WANT_WRITE
  finds it rather than opening a second channel and leaking the first
- flush what is left of a queued open on the next call, rather than
  reporting a success the peer never saw
- record ssh->error from the send alone, so neither a poll ahead of the
  request nor a failed allocation stops accept() continuing
2026-09-08 11:50:40 -05:00
JacobBarthelmeh 161c5ae24f expand CI test for non-fpki build, fix check for error state with CertEnumCertificatesInStore, add extended key usage type check if present, fix for EKU checks with X509 use and add test case 2026-09-05 01:32:02 -07:00
JacobBarthelmeh ed9bf644d0 use proper free for cert store version, fix echoserver on mac os 2026-09-05 01:32:02 -07:00
JacobBarthelmeh 4baba585d5 fix for flags handling, ocsp case, macro guards, unused variable, changed default from MY to required to be set
enable SHA1 with windows cert store test case

expand test cases, adjust to authorized key file, minor dead code adjustments

add more documentation, refactor duplicate code sections, clean up test cases, more adjustments to logging spamming protections
2026-09-05 01:32:02 -07:00
JacobBarthelmeh 13c2adf34b add Windows cert store use with signing and add example arguments
add Windows cert store test case

make windows cert feature default disabled and simplify macro guard

additional unit tests, advertise x509 and pubkey, use CN to match username, build check for WOLFSSL_SYS_CA_CERTS, fix for CM ref count

additional build test, uniform enum name, fail on unkown cert store ecc curve, tie in of loading whole cert store for sys CA's
2026-09-05 01:32:02 -07:00
John Safranek fb15a05ed0 tests: cover the forward cleanup callback
WOLFSSH_FWD_LOCAL_CLEANUP now fires, and what must not fire matters as
much as what must: the gate is the channel's own setup record, not its
type.

- a peer-opened direct-tcpip channel reports SETUP, CHANNEL_ID, then
  CLEANUP when it closes
- a forwarding channel this side opened reports no cleanup, and a
  session channel no forwarding action at all
- an open that fails after a successful setup still reports the cleanup,
  and a setup that reported failure draws none
- freeing the session, or the channel with wolfSSH_ChannelFree(),
  reports it, and the session free after a channel free does not
  report it again
2026-09-03 21:43:30 -05:00
John Safranek 6485062d3d internal: send LOCAL_CLEANUP on forward delete
WOLFSSH_FWD_LOCAL_CLEANUP is part of the public WS_FwdCbAction contract
and the library never sent it, so an application's handler never ran and
every peer-opened forward leaked what its setup callback allocated.
ChannelDelete() now sends it, so a peer close, an open refused after the
setup ran, wolfSSH_ChannelFree(), and freeing the session all report it.

- record the setup in a new fwdSetupTxd bit on WOLFSSH_CHANNEL: a
  locally opened forward gets no LOCAL_SETUP, so the channel type alone
  cannot say whether the application holds anything
- clear the bit when the cleanup goes out, so a channel reports it once
- pass the channel's id in the port parameter, the way
  WOLFSSH_FWD_CHANNEL_ID does, so an application with two forwards can
  tell which one ended
- TestDirectTcpipFwdCbRejectsChannelId now counts three callback calls
2026-09-03 21:43:30 -05:00
John Safranek ee7fa04bec ssh: flush any queued output at teardown
wolfSSH_shutdown() flushes whatever a short send left in the output
buffer, not just a queued disconnect. A rejected auth's
USERAUTH_FAILURE has no channel, and DoChannelClose() retires the
channel as soon as the close is bundled, so neither had anything
left to carry the retry.

- The gate still refuses a flush once the peer has disconnected,
  unless our own disconnect is the thing queued.
- A flush that finishes on a live session clears the WS_WANT_WRITE
  the short send latched, as the disconnected path already did.
- ssh.h documents the widened flush and the WS_WANT_WRITE a short
  one leaves wolfSSH_shutdown() returning.
- Tests cover the rejected auth with no channel and the close whose
  channel DoChannelClose() already retired.
- A flush that short-sends on a live channel leaves the teardown
  queued behind it. The retry adds no second EOF or close, and skips
  the close wait while its own flush is still short.
- A teardown send that drained the leftovers settles the flush; one
  that failed outranks it, since a reset leaves the buffer intact.
2026-09-03 16:12:17 -05:00
John Safranek 54610fd36e tests: cover the exec and subsystem req callbacks
Only the shell hook had coverage, so nothing held DoChannelRequest() to
handing the command and session type to the exec and subsystem callbacks
or to answering with what they return.

- assert each callback sees the session type and the command string the
  request carried, and the ctx set on the session
- assert an accepting callback draws CHANNEL_SUCCESS and a rejecting one
  CHANNEL_FAILURE, whether it rejects with a WS_ error or a bare nonzero
2026-09-03 15:31:58 -05:00
John Safranek 3c6c1e85d8 tests: cover the channel close callback
wolfSSH_CTX_SetChannelCloseCb() had no caller in tests/, examples/ or
apps/, so nothing held DoChannelClose() to running the hook before it
retires the channel.

- assert the callback runs with the closing channel's id and the ctx
  set on the session
- assert the channel is still on the list inside the callback and gone
  by the time the caller is told
- a rejecting callback changes nothing: the return is discarded and the
  close completes
2026-09-03 15:31:58 -05:00
John Safranek 6804e999e1 tests: cover the channel open response callbacks
The confirmation and failure hooks reach an application only through
DoChannelOpenConf() and DoChannelOpenFail(), and nothing in tests/ or
apps/ registered either, so both arms shipped unexercised.

- assert the confirm callback runs with the peer's channel id, window
  and packet size already recorded
- assert the failure callback runs while the channel is still findable,
  before DoChannelOpenFail() removes it
- count each hook separately, so a test can tell which one ran
- a rejecting confirm hook fails the receive and leaves the open
  unfinished; a rejecting failure hook fails it and leaves the channel
  on the list
- seed the unconfirmed channel through ChannelNew() and ChannelAppend(),
  the state an outstanding open leaves behind
2026-09-03 15:31:58 -05:00
John Safranek 79456c2002 Refuse a session channel open on a client
RFC 4254 6.1 has a session open travelling client-to-server, so
DoChannelOpen refuses one on a client endpoint in the type switch,
ahead of ChannelNew and any channelOpenCb. OpenSSH and Dropbear
refuse it outright too, with no application opt-in.

- Answer OPEN_ADMINISTRATIVELY_PROHIBITED, as the forwarded-tcpip
  direction gate below it does.
- Widen the client harness guard in regress.c so it builds without
  WOLFSSH_FWD, and hoist AcceptChannelOpenCb out of the forwarding
  block.
- Cover the refusal, and the gate outranking an accepting
  channelOpenCb.

Issue: F-8836
2026-09-03 10:40:16 -05:00
John Safranek b13704af1d Cover and document the client KEX role check
TestClientOnlyKexMsgsBlocked now asserts that 31 and 33, the ids a
client does receive, stay allowed where the handshake expects them, so
widening the role check into a 30-34 range fails the suite. The
IsMessageAllowed() comment records the receive-only policy and the
per-KEX-method id namespace.

- assert id 33 is allowed once expectMsgId is MSGID_KEXDH_GEX_REPLY
- assert expectMsgId starts at MSGID_NONE, so the role check is what
  rejects the blocked ids
- recheck all three blocked ids during a rekey on an established session
2026-09-03 10:32:18 -05:00
Aidan Keefe 557f3df42d Validate a custom SSH proto ID string
wolfSSH_CTX_SetSshProtoIdStr() now rejects a string that is not
CRLF-terminated, exceeds 255 bytes counting the terminator, or
carries a CR or LF in the body. DoKexInit() subtracts the two
terminator bytes from the length when hashing it, so an
unterminated string underflowed the hash length.

Issue: F-10571
2026-09-02 15:36:18 -07:00
Emma Stensland ea530fe05d wolfsftp: Improve POSIX and Windows write/append handling
Ensures EOF appending on all POSIX systems and fails short writes on Windows.
Consolidates the Windows SFTP flag matrix testing with shared helpers.
2026-09-02 15:35:09 -07:00
Emma Stensland 6a71292401 internal: Refactor and gate ML-DSA composite key handling
Gates ML-DSA composites behind WOLFSSH_NO_MLDSA_COMPOSITES.
Deduplicates key handling and uses heap allocation for
composite buffers when compiling for small stacks.
2026-09-02 15:35:09 -07:00
John Safranek 5e72c76683 Rebind a request in flight to the folded forward
FwdRemoteSettle() folds a port-0 forward the peer bound onto a
registration already standing at that port. A request still in its
send window resolved its forward before the send, so it names the
survivor from here on, the same as the queued replies do.

- Add FwdPendingRebind() and call it ahead of the unlink, so a cancel
  whose send pumped in the answer commits naming a forward rather
  than nothing.
- Regression test drives the port-0 answer in from the cancel's send
  callback.
2026-09-02 15:18:32 -05:00
John Safranek cb211506e2 Fold a forward the send settled onto its bind
FwdPendingCommit() looks for a registration of the bind its entry names
before linking it. An answer pumped in from the send can settle a
port-pending forward onto that port, and the fold in FwdRemoteSettle()
runs before the committing entry joins the list.

- replies queued against the folded entry move to the one that stands
  at the bind
- test drives that answer from the IO send callback; the bind takes two
  registrations without the fold, and one cancel leaves one matching
2026-09-02 15:18:32 -05:00
John Safranek e53c157049 Resolve a forward request once, before its send
A request looked its registration up again at commit, after the send had
run the application's send and highwater callbacks. A callback that
reenters the library sends behind the request that ran it, so the lookup
found what the callback did rather than what the request asked for: a
cancel from a first setup's send found nothing to take, and that setup's
commit then registered the forward the peer had just been told to drop.

- Requests in flight hang off ssh->fwdPendingHead for the length of
  their send, so a reentrant request resolves to the forward the request
  it interrupted is registering, not to nothing.
- FwdRemotePrepare() settles what the request names and holds it on the
  pending; FwdPendingCommit() uses that instead of a fresh lookup, which
  also drops the dedupe branch the double lookup needed.
- FwdRemoteUnlink() voids the pending pointers along with the queued
  slots, so freeing a forward mid-send leaves no commit naming it, and
  FwdPendingDiscard() goes through it rather than freeing raw.
- FwdRemoteMatch() reads the pending list under the rules the scan over
  registered forwards uses: a setup mid-send is on the wire and can feed
  channels; a cancel behind it or a refusal on its slot stops matching.
- Drop pend->bindAddr and pend->bindPort, which nothing reads now.
- Tests drive a cancel, a setup, an inbound forwarded-tcpip open and a
  mid-send REQUEST_FAILURE from the IO send callback, the window the
  highwater tests no longer reach.

Issue: ZD-22195
2026-09-02 15:18:32 -05:00
John Safranek 79a7f29933 Match forwarded-tcpip on every client session
The match only ran once wolfSSH_FwdRemoteSetup() had registered
something, so a client that never called it had every forwarded-tcpip
open go unchecked -- the case RFC 4254 7.2 is clearest about, since a
session that asked for no forward has nothing an open could answer for.

- Refuse the open whenever it names no registration, empty list
  included. wolfSSH_SetFwdRemoteMatch(WOLFSSH_FWD_MATCH_OFF) is the
  opt-out for an application that keeps its own bind list.
- Drop fwdRemoteTracked, which nothing reads now.
- wolfSSH_global_request() cannot carry the bind address and port RFC
  4254 7.1 puts after the want-reply boolean, so no request that
  registers a forward can be framed outside wolfSSH_FwdRemoteSetup().

Issue: ZD-22195
2026-09-02 15:18:32 -05:00
Reda Chouk 6802df0c52 Reject the KEX requests a client never receives
This is just a follow-up PR to (#1221), that mirrors pretty much the same changes for the client side too.
2026-09-02 11:20:06 -07:00
Hideki Miyazaki 88c0e34b7e Addressed review comments 2026-09-02 09:17:30 -07:00
Hideki Miyazaki 1c5fbfe0b5 Addressed review comments 2026-09-02 09:17:30 -07:00
Hideki Miyazaki 593216020e fix Windows SFTP 2026-09-02 09:17:30 -07:00
Yosuke Shimizu f1b6a99d56 wolfsftp, port: apply the attributes SETSTAT and FSETSTAT acknowledge
- SFTP_SetFileAttributes() and SFTP_SetFileAttributesHandle() carry
  out the size, ownership, permission and timestamp requests while ret
  is WS_SUCCESS, bound the size with wResolveOffset() against
  WOLFSSH_MAX_FILE_OFFSET, and set WS_UNIMPLEMENTED_E where the port
  defines no wrapper.
- wolfSSH_SFTP_RecvSetSTAT() and wolfSSH_SFTP_RecvFSetSTAT() answer
  WOLFSSH_FTP_UNSUPPORTED for WS_UNIMPLEMENTED_E; wolfSSH_SFTP_CHMOD()
  sets the attribute flags to WOLFSSH_FILEATRB_PERM before sending.
- port.h adds WTRUNCATE, WFTRUNCATE, WCHOWN and WFCHOWN for the POSIX
  port, and defines WSETTIME and WFSETTIME over the existing WUTIMES
  and WFUTIMES helpers in place of their (0) definitions.
- SFTP_SetMode() guards on _WIN32_WCE in place of USE_WINDOWS_API, and
  port.c adds WS_ChmodA(), which trims the SFTP leading root and calls
  _wchmod(); it and the WCHMOD mapping to it are left out under
  _WIN32_WCE, which keeps the _chmod mapping.
- tests/regress.c adds TestSftpSetStatAttributes(); tests/sftp.c adds
  a chmod of a directory and installs SFTP_TEST_UMASK in place of the
  sftpTestUmask static; the STATE_SET_ATR_SEND case in tests/api.c
  sets atr.flags to WOLFSSH_FILEATRB_PERM.

Issue: F-11658
2026-09-02 08:42:36 -07:00
Reda Chouk a472f1ee2b Reject the KEX replies a server never receives 2026-09-02 08:30:09 -07:00
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 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 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