Commit Graph

3106 Commits (master)

Author SHA1 Message Date
John Safranek 07fcea84d7 tests: bound the first forward parse guard too
The helper states that every bound is a subtraction because adding a
packet-derived length to the index wraps, and its own first guard was
still an addition. It could not wrap, since ParseGlobalRequestName()
already bounded the name, but the mixed forms invite the weaker one into
the next guard added here.

Issue: #1246
2026-09-18 00:01:58 +02:00
John Safranek f67b20ffca tests: pin the fixed-port forward reply terms
The reply builder is chosen by !isCancel and a zero requested port, and
the suite exercised neither term on its own. A port-0 cancel now covers
the first, and an explicit port answered by a port-reporting callback
covers the documented case where that return is ignored.

- drop the !isCancel term and the port-0 cancel test fails, finding a
  five byte payload where the bare success is one
- bound the bind-address length with a subtraction in
  ParseGlobalRequestFwdBindPort, where advancing by it wrapped word32
- add AlwaysAllocPortFwdCb, reporting a port for any remote setup

Issue: #1246
2026-09-18 00:01:58 +02:00
John Safranek 24c2139a66 internal: bare success for a fixed-port forward
RFC 4254 7.1 gives a tcpip-forward success a trailing bound-port field
only for a port-0 (dynamic) request. DoGlobalRequestFwd() now sends that
field only when the peer asked the server to allocate a port, and
answers an explicit port with a bare SSH_MSG_REQUEST_SUCCESS, as OpenSSH
and libssh do.

- Key the reply builder off requestedPort, which the port-0 compliance
  check already tracks.
- Check the reply payload length against the requested port in the
  regress global-request helper, which had baked in the trailing field.
- Add ParseGlobalRequestFwdBindPort() to recover that port from the
  request the harness fed in.
- Cover the explicit-port reply.

Issue: #1246
2026-09-18 00:01:58 +02:00
John Safranek b40148acc5 ci: build and test without RSA or ECC
os-check builds a wolfSSL with neither, then wolfssh against it, so the
Ed25519-only paths are exercised. That wolfSSL line names two things the
short option list does not imply: --enable-ed25519-stream, which wolfSSH
needs to keep Ed25519, and --enable-base64encode, which the ssh client
app needs and which defaults off away from x86_64.
2026-09-17 23:27:22 +02:00
John Safranek dd78bb6146 examples, keys: add an Ed25519 user credential
The sample clients load hansel's Ed25519 key when neither RSA nor ECDSA
is compiled in, so public key auth reaches the echoserver's matching
sample keys instead of sending a request carrying no key.

- commit keys/{hansel,gretel}-key-ed25519.{der,pem,pub}, the pair to the
  echoserver's sample authorized keys
- ClientUserAuth() fails a publickey request with no key loaded, so the
  library can offer another method
2026-09-17 23:27:22 +02:00
John Safranek 7026839c11 echoserver: add Ed25519 sample public keys
The example server loads hansel's and gretel's Ed25519 public keys when
neither RSA nor ECDSA is compiled in, so public key auth works there
instead of falling back to passwords. That arm is the "#if" and the
userEcc choice the "#else", so no empty if/else is left behind.
2026-09-17 23:27:22 +02:00
John Safranek 2ec8ad418f tests: run the Ed25519 pubkey test without ECDSA
test_pubkey_auth_ed25519_privonly_hostkey() authenticates with an
Ed25519 user key rather than hansel's ECDSA key, so it covers Ed25519
user auth alongside the host key derive fallback and needs no ECDSA.

- load_key() falls back to the Ed25519 host key when neither RSA nor
  ECDSA is available
- guard run_pubkey_test() on RSA or ECDSA, its only callers
2026-09-17 23:27:22 +02:00
John Safranek 6d1292832d internal: decode DER Ed25519 user auth keys
PrepareUserAuthRequestEd25519() tries wc_Ed25519PrivateKeyDecode() and
falls back to the OpenSSH container only when that decode fails, as the
RSA and ECDSA paths do. A private-only DER gets its public key derived
the way SendKexGetSigningKey() does, or is rejected when it cannot be.
2026-09-17 23:27:22 +02:00
John Safranek a072fcc572 internal: guard PrimeNameForId on ECDSA alone
PrimeNameForId() is called only from the three ECDSA paths, so it now
sits in its own WOLFSSH_NO_ECDSA block. wcPrimeForId() keeps the wider
"ECDSA or ECDH" condition that its curve lookups still need.
2026-09-17 23:27:22 +02:00
John Safranek 36a2ad30b6 tests: build the Ed25519 cases without ECDSA
The Ed25519 key DER helpers arrive through asn.h, which unit.c includes
only for RSA, and the host key that test_wolfSSH_SetAlgoList() installs
came only in RSA and ECDSA flavors.

- include wolfssl/wolfcrypt/asn_public.h unconditionally in unit.c
- add ./keys/server-key-ed25519.der to api.c as the last host key
  fallback
2026-09-17 23:27:22 +02:00
John Safranek 3a3ad23de3 examples, tests: drop the WOLFSSH_NO_ECC guard
Nothing defines WOLFSSH_NO_ECC, so every "#ifndef WOLFSSH_NO_ECC" block
was always compiled and the sample ECC keys behind them reached their
"#error" with no curve available. Use WOLFSSH_NO_ECDSA, which
wolfssh/internal.h derives.

- echoserver builds load_key() and its two default host keys only when
  RSA or ECDSA is there; the Ed25519 host key still loads
- ECC_PATH moves inside that guard, its only reader
- peerEcc is now read only by load_key(), so mark it used
2026-09-17 23:27:22 +02:00
John Safranek bf4f1a1964 internal, ssh: quiet unused RSA-only locals
Three locals feed only RSA and ECDSA code and draw -Werror warnings once
both are compiled out.

- digestSz in DoUserAuthRequestPublicKey(), read only by the RSA and
  ECDSA verify arms
- heap in SendKexGetSigningKey(), used only where a key is allocated
- the id parameter of CurveNameForId(), unread with no curve to name
2026-09-17 23:27:22 +02:00
John Safranek 7324d4b4da internal: shift, not divide, in the GEX check
mp_div_2() maps to sp_div_2(), which SP math compiles only for ECC, so
the safe-prime check in ValidateKexDhGexGroup() fails to link when RSA
and ECC are both off. mp_rshb() is unconditional and q is positive here,
so the two do the same thing.
2026-09-17 23:27:22 +02:00
John Safranek 9390d97926 internal: compile Ed25519 host keys without ECDSA
The ID_ED25519 arm of SendKexGetSigningKey() sat inside the
"#ifndef WOLFSSH_NO_ECDSA" block that precedes it, so a build with ECDSA
off never compiled the case that decodes and hashes an Ed25519 host key.
Close the ECDSA guard at the end of the ECDSA cases instead.
2026-09-17 23:27:22 +02:00
John Safranek 2b5855922f tests: run app-driven sftp in a block build
The non-blocking app-driven case runs under WOLFSSH_TEST_BLOCK again,
reverting ba899603. It deadlocks there until ssh_worker() waits on the
write side for a send wolfSSH_worker() reported through ssh->error, so
this wants PR 1251 landed with the wantWrite arm kept.
2026-09-17 10:26:52 -07:00
Yosuke Shimizu 1a195cb1ec examples, ide, tests: classify the worker's status at the call site
- ssh_worker() in both echoservers maps a WS_FATAL_ERROR return from
  wolfSSH_worker() to WS_WANT_READ or WS_WANT_WRITE at the call site,
  off a new local err, and keeps the result in rc.
- The terminal arm tests rc in place of re-reading wolfSSH_get_error():
  rc != WS_WANT_READ in examples/echoserver, and rc != WS_WANT_READ &&
  rc != WS_WANT_WRITE in the Espressif copy, whose empty WS_WANT_WRITE
  arm is removed.
- ssh_worker() in examples/echoserver sets wantWrite from
  wolfSSH_OutputPending() before it builds writeFds.
- The comment above rc = cnt_r in both echoservers names cnt_r and
  ssh->error as the values that are reused.
- scripts/sshclient.test and scripts/fwd-bulk.test name the client's
  missing non-blocking mode in the skip message where they echoed the
  macro name, and the comments above both skips are removed.
2026-09-17 10:26:52 -07:00
Yosuke Shimizu 47cea34929 ssh.h: correct two statements about wolfSSH_worker(), note the owed write
- The wolfSSH_worker() send-failure sentence names WS_CHANNEL_CLOSED
  and WS_FATAL_ERROR as the statuses that keep the return.
- Its channelId sentence narrows the rekey case to a WS_REKEYING
  that displaced WS_SUCCESS or WS_CHAN_RXD.
- The wolfSSH_shutdown() block adds that the read for the peer's
  close reply can leave output queued, which reports WS_WANT_WRITE,
  and that a send which failed outright there takes the return on
  the next call.
2026-09-17 09:48:37 -07:00
Yosuke Shimizu d92d1e3367 ssh: report the owed write when the worker leaves output queued
- wolfSSH_shutdown() sets ret to WS_WANT_WRITE whenever the
  wolfSSH_worker() call it makes ends with output pending. The
  sendErr local and the arm that returned wolfSSH_get_error() are
  gone.
- tests/unit.c gains EofDisconnectCb(), which sends a disconnect
  from the channel EOF dispatch, and two tests driving it:
  test_ShutdownOwedWriteAfterEofDisconnect() asserts
  wolfSSH_shutdown() returns WS_WANT_WRITE with output pending and
  WS_SUCCESS with the buffer drained on the retry;
  test_ShutdownHardEofDisconnectSurfacesOnRetry() refuses the
  disconnect's send outright and asserts the retry returns
  WS_SOCKET_ERROR_E.
2026-09-17 09:48:37 -07:00
John Safranek 4ca96ec6cb wolfsshd: name the large SFTP files per run
sshd_large_sftp_test.sh puts its pid in both the local and the remote
file name, and removes them from an EXIT trap.

- the remote name was fixed in the daemon user's home, one directory
  for the whole host, so two runs uploaded to the same path and each
  compared its own file against the other's upload
- 4.4G apiece, and the transfer runs under "set -e", so a failed run
  left both behind
2026-09-17 15:04:26 +02:00
John Safranek 3f5ceff2d4 wolfsshd: issue the test certificates per run
sshd_ossh_cert_test.sh generates its OpenSSH certificates into its own
work directory, through the OSSH_CERT_DIR that renew-ossh-certs.sh now
reads. Only the keypairs stay shared, and those are committed.

- the names carry just the login user, so two runs in one checkout
  reissued each other's certificates and the force-command case failed
  on a marker written to the other run's directory
2026-09-17 15:04:26 +02:00
John Safranek b8f5df10e7 wolfsshd: remove the temp config dir on exit
The exit teardown stops the shared daemon before sweeping the registry.
stop_wolfsshd is what removes the per-daemon temp directory, which
holds the rewritten config and root-owned copies of the trust anchors,
and it is idempotent, so a run that stopped already is unaffected.

- a run that exits early, on a daemon that will not start or on a
  failed test, reaches no stop of its own and left the directory in
  /tmp with a copy of the host key in it
2026-09-17 15:04:26 +02:00
John Safranek 536e51c874 wolfsshd: keep the registry when grep fails
Dropping a stopped daemon from the run registry now installs the
rewritten file only when grep either kept lines or matched none. Any
other status is grep failing, and the empty file it leaves behind would
become the registry, losing every other daemon's pid.

- that registry is what the end-of-run sweep works from, so blanking it
  strands whatever else the run started
2026-09-17 15:04:26 +02:00
John Safranek 277cdb173b wolfsshd: keep a daemon until it has stopped
stop_wolfsshd escalates to SIGKILL when SIGTERM has left the daemon
running after five seconds, and drops the pid from the run registry
only once it is gone. wolfsshd_alive answers whether the pid is still a
wolfsshd, which is what each of those steps turns on.

- the entry was dropped unconditionally, so a stuck daemon lost its
  last handle and held its port for the rest of the run
- kill -0 says only that some process holds the pid, and this sends
  SIGKILL as root, so a recycled pid was killed outright
- a recycled pid now counts as stopped and leaves the registry
2026-09-17 15:04:26 +02:00
John Safranek 772e83e6a6 wolfsshd: fail a daemon that never listens
start_wolfsshd now records whether the daemon's listening line appeared
in the ten seconds it waits for it. Without one it kills the daemon and
clears PID, which puts the start through the empty-PID check every
caller already has, so the run stops and names the daemon.

- the pid reaches the run registry before that check, so a daemon that
  came up but never bound is still reaped by the runner's teardown
2026-09-17 15:04:26 +02:00
John Safranek 1cb4c0650d wolfsshd: test the port block allocator
sshd_port_lease_test.sh forks processes that contend for real leases
and checks that a block is never held by two at once. It is the first
entry in test_cases, so a broken allocator is named there rather than
surfacing as a bind collision in an unrelated test much later.

- covers a contended stale block, parallel allocation, a live lease, a
  live owner this run cannot signal, release and reuse, and --port
- a winner holds its lease until the parent releases it, so a straggler
  cannot take a block already counted and read as a second winner
- the cross-user case drops to $SUDO_USER, the one arrangement that
  tells "owner gone" from "owner not mine"
- contends over 29000-29511, below the ephemeral range and away from
  the suite's daemon, with the scan pinned so --port has a block to skip
2026-09-17 15:04:26 +02:00
John Safranek 6c070dc3ab wolfsshd: lease a port block to one run only
A run's port block is leased through a directory named for the block
and for the pid holding it, in a host-wide pool under /tmp. A run
creates and removes only its own lease, and ps decides whether an
existing one is still held. The allocator moves to port_lease.sh.

- a pool under $TMPDIR was not host wide: TMPDIR is per user on macOS
  and sudo's env_reset drops it, so two runs kept private pools for one
  set of ports
- a stale lease cannot be reclaimed in place: whatever does the
  removing is authorized by an earlier read of the owner, so a second
  runner displaces the live claim the first just made
- ps -p rather than kill -0, which reports failure both for a pid that
  is gone and for one the caller may not signal -- opposite answers
  when a non-root run reads a lease held by a live root run, as in CI
- the range starts at 28000, clear of the 22000-27999 that
  scripts/fwd-bulk.test picks from and fails outright when taken
- a block holding the port passed to --port is skipped, so the shared
  daemon and a private one cannot be assigned the same port
2026-09-17 15:04:26 +02:00
John Safranek b06a3b123b wolfsshd: wait for the listener, claim the ports
start_wolfsshd returns once the daemon accepts connections, not
once it has written its pid: wolfSSHd saves the PID file just
before tcp_listen(), so a caller connecting straight away could be
refused while the daemon's log showed no connection at all. A run
also claims its port block rather than only probing it.

- wait for the daemon's own "Listening on port" line, matched on its pid
  so a previous daemon's line in the appended log cannot satisfy it
- take a block by creating its lock directory, which mkdir makes atomic,
  and release it in the teardown; probing alone let two runners pick the
  same block, and a claim whose owner is gone is treated as stale
- check that a pid from the PID file is still a wolfsshd, since kill -0
  answers only whether some process owns the number
- fail the OpenSSH cert test when mktemp gives it no work dir: an
  empty one reduced its teardown pattern to every wolfsshd present
2026-09-17 15:04:26 +02:00
John Safranek 5046945979 wolfsshd: tighten the run registry and ports
The registry now holds only daemons that are still running, and it is
cleaned up however the run ends. Both private-daemon ports come from the
runner, so they stay inside the block it probed even when --port moves
the shared daemon off it.

- drop a pid from the registry once stop_wolfsshd has stopped it, so the
  end-of-run sweep cannot reach a pid since recycled by another run
- run the sweep and the registry cleanup from an EXIT trap: every early
  exit used to skip them and leave the file in /tmp
- export WOLFSSHD_PRIVDROP_PORT rather than re-deriving the offset in
  sshd_privdrop_fail_test.sh, and correct that script's usage message
- quote the arguments to create_sshd_config.sh: an empty USER shifted
  the port into $1, silently leaving the daemon on 22222
- name the three tests that still read the whole process table, which
  are what keeps two concurrent runs from being fully independent
2026-09-17 15:04:26 +02:00
John Safranek 071a07a27d wolfsshd: name the tmux session per test run
sshd_term_size_test.sh drives the client through a tmux session named
for the port it was given. The name used to be the constant "test",
which is shared across everything the user runs, so two concurrent runs
fought over one session and each EXIT trap killed the other's.
2026-09-17 15:04:26 +02:00
John Safranek bb5d7dc2e9 wolfsshd: scope the ossh teardown to its run
The OpenSSH certificate test kills its daemon with a pkill pattern. That
pattern now carries $WORK, the mktemp directory this invocation created,
so it names one run's daemon. "sshd_config_ossh" appears on every
concurrent run's command line, so the old pattern took their daemons
down too.
2026-09-17 15:04:26 +02:00
John Safranek 96fab96a36 wolfsshd: scope a test run's ports and daemons
Two runs of the suite on one machine no longer collide. Each run takes a
block of ports, identifies the daemons it starts by the PID file in
their generated config, and at exit stops only those. wolfSSHd writes
that file after it has finished daemonizing, so the pid no longer has to
be guessed from what appeared in the process table.

- take a free block of ports per run, replacing the fixed 22222 and the
  constants the private daemons used
- honour --port for a local run, and pass the port to
  create_sshd_config.sh rather than baking it into the four configs
- read the daemon pid from a PidFile placed at the top of the generated
  config, ahead of any Match block, where it is applied
- stop only the daemons recorded during this run, not every wolfsshd on
  the machine
2026-09-17 15:04:26 +02:00
John Safranek 96d6c0db50 scripts: widen the ready-file wait to ten seconds
The sftp, scp and get-put scripts wait for the echoserver to publish its
port before connecting. Two seconds is not enough for a libtool re-exec,
the dynamic linker and the sample-key parse with a dozen sibling test
jobs on the machine, so a parallel make check failed them at their first
scenario. Ten seconds matches what sshclient.test already allows.

- raise the three wait loops from 20 to 100 iterations of 0.1 seconds
- test -s, not -e, after the loop in scp.test and get-put.test: the
  ready file is created empty and the port written afterward, so -e can
  take a file caught mid-write and yield an empty port
2026-09-17 15:04:26 +02:00
John Safranek 74491247cb scp: correct the ScpStreamRead() comment
The helper drains a WS_EXTDATA and retries the read rather than handing
it back, so it is no longer error-code transparent for that one status.
Say so where the claim is made, and condense the rest.
2026-09-16 09:55:24 -07:00
John Safranek 10d97e151f echoserver: wait to write in the SFTP loop
A send the socket refused leaves WS_WANT_WRITE, and the retry branch
continues past the only tcp_select() in the iteration, which watches
reads. Wait on write readiness there, so a peer that has stopped
reading costs a descriptor wait rather than a spin.

- a buffered send with a willing socket still goes straight around
- an error-ready or failed descriptor ends the loop
- an interrupted select() retries instead of ending the session
2026-09-16 09:55:24 -07:00
John Safranek dc03049a13 ssh: add a channel session-granted accessor
wolfSSH_ChannelGetSessionGranted() reports whether a shell, exec or
subsystem request on a channel has been answered CHANNEL_SUCCESS, so an
application can tell a second request from the first without reaching
into WOLFSSH_CHANNEL.

- the flag is still clear for the request a session callback is
  answering, so a set flag is an earlier request's grant
- wolfsshd's session callback reads the grant through it
2026-09-15 21:55:50 -07:00
John Safranek fd4d462510 scp: match the scp command as a whole token
wolfSSH_ChannelCommandIsScp() reports whether a channel's session
command starts an SCP transfer, taking "scp" only as its own token. The
divert in wolfSSH_accept() and an application's exec callback both ask
it, so the two cannot disagree about what the SCP server is handed.

- an exec of "scpbackup foo" runs as an ordinary exec
- a command carrying a NUL within the recorded command size is not an
  SCP command; ParseScpCommand() walks a C string, so a NUL would drop
  whatever follows it
- wolfsshd's session callback asks through the same helper
2026-09-15 21:55:50 -07:00
John Safranek 74a296d9c2 wolfsshd: refuse sessions it cannot serve
A shell, exec or subsystem request is answered as it arrives, through
the channel request callbacks, so a session this build cannot serve, or
a second one on a channel already running one, is refused with
CHANNEL_FAILURE rather than accepted and then dropped once the session
is up. What the daemon serves is unchanged.

- SessionRequestCb() takes a shell with WOLFSSH_SHELL, an exec with
  WOLFSSH_SHELL or an scp command with WOLFSSH_SCP, and the sftp
  subsystem with WOLFSSH_SFTP; anything else is refused and logged
- a request whose command did not fit is refused rather than read
  through a NULL
- a second program start is refused on a channel whose grant already
  stands, so sftp or scp cannot take over a running session
- the sftp name is matched whole and scp only as its own token, both
  by length and bytes, so "scpbackup" or a name with an embedded NUL
  is some other command
- sshd_bad_subsystem_test.sh asks for an unknown subsystem with the
  OpenSSH client and expects the refusal
2026-09-15 21:55:50 -07:00
John Safranek 8460f3f90c internal: log a subsystem request as a subsystem
DoChannelRequestSession() serves the shell, exec and subsystem arms
together, and its debug line labels the string it read. A subsystem
request labels that string "subsystem" rather than "command".

- unit.c: renumber the shell-after-exec failure to follow the codes
  above it
2026-09-15 21:55:50 -07:00
John Safranek 7bb39b63e7 internal: re-find the channel after the callback
The generic channel request callback may free the channel it was handed,
so DoChannelRequest() looks it up again before the type handling reads
it. Gone, the request ends there, and a reply the peer wanted fails on
the missing channel the way one after a typed session callback does.

- ssh.h says the callback may free its channel and what the request does
  from there
- regress.c frees the channel from the callback on each of the three
  answers, and on a pty-req, the type that wrote to the channel outside
  a session request
2026-09-15 15:27:16 -07:00
John Safranek 4477f4744a ssh: add generic request callbacks
wolfSSH_CTX_SetChannelReqAnyCb() and wolfSSH_CTX_SetGlobalReqAnyCb()
register a callback consulted first for a channel or global request,
with the name, the type-specific part, and whether a reply is wanted. A
tri-state answer grants, refuses, or leaves it to the handling already
there, so a policy reaches the types with no hook of their own.

- the name is the one that arrived, since a copy into a buffer truncates
  a long name and ends it at an embedded NUL, and a policy has to answer
  on what the peer sent
- a grant still parses and records what the library needs, so a granted
  session request commits the session, the typed callbacks are not
  consulted, and a granted unknown type is answered CHANNEL_SUCCESS
- a port-0 tcpip-forward skips the callback, since only the forward
  callback can report the port bound and a policy that had bound a
  listener would then have to be refused, per RFC 4254 7.1
- a client refuses tcpip-forward and cancel-tcpip-forward ahead of any
  policy, matched on the name that arrived so it holds in a build with
  no forwarding, where neither name is in the name table
- window-change, exit-status and exit-signal are cleared of a reply
  before the handling runs, so a refusal leaves them unanswered too, per
  RFC 4254 6.7 and 6.10
- regress.c covers the answers, the data delivered, the names that do
  not fit a copy, and which callbacks each answer leaves out
2026-09-15 15:27:16 -07:00
John Safranek ba8996038d tests: skip app-driven sftp in a block build
The non-blocking app-driven case runs in every other build. Under forced
blocking the two ends deadlock -- the server sits in DoReceive with
output it owes still queued, the client waits for that output -- which
is a defect of its own to fix rather than this case failing.

- blockBuild names the macro being set, which nonblockingOnly already
  stood for on a different question
2026-09-15 09:31:16 -07:00
John Safranek 46795b1ef2 tests: cover the app-driven sftp and shell paths
The -A cases in scp.test reach the exec callback and the scp handoff.
These drive the rest: the subsystem callback with the sftp accept, the
shell callback in echo mode, and both accepts on a blocking server.

- sftp.test connects to an -A server blocking and non-blocking
- sshclient.test runs a terminal session and a command session against
  an -A server
- scp.test copies from a blocking -A server, where wolfSSH_SCP_accept()
  completes in one call rather than through the retry loop
2026-09-15 09:31:16 -07:00
John Safranek 95369017da echoserver: retry a send the socket did not take
ssh_worker() reads a WS_WANT_WRITE from wolfSSH_worker() as a send the
socket has not taken yet: it waits for the socket to accept one and runs
the worker again, rather than leaving the loop. Application-driven mode
answers session requests here, and the peer waits on that reply, so a
blocked one ended a session the legacy accept() carried through.

- the worker runs on a writable socket as well as a readable one, or
  the owed send is never retried
- the ssh socket joins the select write set only while a want-write is
  outstanding
- a queued agent channel open joins the same wait, so the poll runs
  ahead of the write set
2026-09-15 09:31:16 -07:00
John Safranek ecb79ea4e3 echoserver: bind sftp and scp as the library does
The subsystem and exec callbacks pick out the same commands the accept
state machine does: sftp matched whole, by length and bytes, and scp on
the three-byte prefix ChannelCommandIsScp() takes.

- sftp with an embedded NUL is refused, rather than granted and then
  dropped by wolfSSH_SFTP_accept()
- a transfer is granted only on the head channel the accept APIs serve,
  so a request on a later channel is refused rather than answered
  success
2026-09-15 09:31:16 -07:00
John Safranek eef5346f7f echoserver: claim only a granted, usable session
ssh_worker() takes the session channel accept() established only when
the request was granted and there is somewhere to put the data. The
type and command stay set on a refusal, so they do not say what was
granted, and a refused request leaves nothing running.

- a shell build serves the channel through the pty, so with no shell
  started only echo mode can take it
- wsShellStartCb() closes the pty and reaps the child when the terminal
  setup fails, installs ChildSig() only once the session will run, and
  leaves ChildRunning alone when forkpty() fails
- the connection holds the shell's pid, so the worker loop's exit and
  an accept() whose reply failed end the child too
- the shell, exec and subsystem callbacks share SessionInUse(), so a
  second program start on the connection is refused
- the EOF drain answers a half-close only on a claimed session, rather
  than on whatever channel id 0 finds
2026-09-15 09:31:16 -07:00
John Safranek 6b954da7f7 tests: cover the application-driven SCP start
The echoserver's -A mode runs an accepted scp command through
wolfSSH_SCP_accept(). Reaching that call's want retry path takes a
non-blocking server, which -N supplies.

- copy to and from an app-driven server in scp.test
- check the entry point's null-session argument
2026-09-15 09:31:16 -07:00
John Safranek 971f2f01e8 echoserver: answer session requests in callbacks
With -A the echoserver drives its own channels: accept() returns at
userauth and the callbacks below start the shell, SFTP or SCP session.
Off by default. The two modes are exclusive, since the callbacks answer
the session requests the accept state machine otherwise answers itself.

- wsShellStartCb() forks the pty, so it is registered in either mode,
  and claims the channel only once there is a shell behind it; a second
  request is refused rather than forking over the running shell
- wsExecStartCb() takes an "scp " command as a transfer and any other
  command as a session, and is registered in either mode, since the
  legacy path has always started a shell for an exec request too
- wsSubsysStartCb() is registered only with -A, as accept() serves sftp
  itself, and guards a NULL command, which a truncated request leaves
  behind
- ssh_worker() drives the session through shellCtx.appFd, claims the
  channel itself when no callback did, and leaves an SFTP or SCP
  handoff through its cleanup so the pty master still closes
- open the agent channel from the select loop, since the peer's
  auth-agent-req lands after accept() has returned, and read the
  listener from the context each pass because it appears mid-loop
- resume a subsystem accept that returns a want, waiting on the socket
  between attempts rather than spinning
- close the accepted socket again, clear fwdFd on EOF or reset, and
  stay in the loop on WS_REKEYING, which the read arm already handles
- key ChildRunning's sig_atomic_t on WOLFSSH_SHELL, the only build
  with the SIGCHLD handler that writes it, so a target whose libc has
  no signal.h still compiles
- ask for echo mode in the keyboard-interactive test, which has no
  account on the host for the shell callback to fork a shell for
2026-09-15 09:31:16 -07:00
John Safranek 399c83b1b6 coverage: survive a truncated raw profile
The merge step takes --failure-mode=all, so a raw profile left short by
a killed process is warned about and skipped instead of aborting the
report. Tests SIGKILL their servers in cleanup traps, and a process
killed while writing its profile leaves a corrupt header behind. The
step still fails when no profile can be read at all.
2026-09-14 15:22:14 -07:00
John Safranek 65802d438c internal: commit a session only once accepted
A shell, exec or subsystem request changes the channel only once the
callback accepts it. The session type and command are set for the
callback to read and put back if it refuses, and CLIENT_DONE follows
acceptance alone, so wolfSSH_accept() stays where it is rather than
reporting a session it answered CHANNEL_FAILURE as established.

- DoChannelRequestSession() carries the three arms, which differed only
  in the type and the callback consulted
- a refusal puts the type and command back, so a grant an earlier
  request won still stands
- FreeChannelCommand() wipes and releases a command line for both
  ChannelDelete() and the refusal path
- unit.c drives a refused shell, exec and subsystem request through
  DoChannelRequest() and checks nothing was committed, and that a
  refusal after a grant puts the earlier command back whole
- regress.c checks accept() stays at ACCEPT_SERVER_CHANNEL_ACCEPT_SENT
  on a refused shell, and that the sftp gate and both diverts ask for
  the grant alone

Issue: F-8852
2026-09-14 14:48:15 -07:00
Yosuke Shimizu a14e6d0f3c ssh, apps, examples: take the worker's status from its return
- wolfSSH_OutputPending() moves from wolfssh/internal.h to
  wolfssh/ssh.h as a WOLFSSH_API taking a const WOLFSSH*, defined
  in src/ssh.c beside the other public calls.
- wolfSSH_worker() puts the send's code in the return in place of
  an event when the flush fails outright, and gates its flush on
  wolfSSH_OutputPending(). wolfssh/ssh.h states both.
- Both wolfsshd shell loops, both echoservers and
  ReceiveScpMessage() dispatch on wolfSSH_worker()'s return, and
  call wolfSSH_get_error() only to tell a transient failure from a
  terminal one. The POSIX wolfsshd loop sets wantWrite from
  wolfSSH_OutputPending().
- Five worker tests expect the send's code where they expected the
  event, and tests/testsuite.c calls wolfSSH_OutputPending().
2026-09-14 10:40:48 -07:00