The password prompt reads stdin with fgets(), and a buffered stdin
keeps its own copy of the line in libc for the life of the process.
The clients make stdin unbuffered before any stdin I/O, so the typed
password lands only in the buffer ClientUserAuth() wipes.
- the wolfssh app, and the example client, sftp client and scp client
whether or not -P was given, as keyboard-interactive reads stdin too
- ClientUnbufferStdin() acts once per process and reports a failure
- scripts/sshclient.test sends the session data right after the
password, with no sleep
Issue: F-15669
ClientUserAuth() hands the library its static password buffer, and the
library sends from it after the callback returns. The clients now wipe
it, and the example's keyboard-interactive responses, as soon as
connect returns instead of holding them for the whole session.
- ClientClearPassword() in both common.c copies, called after connect
in the wolfssh app, the example client and the sftp client
- the password arm wipes before each prompt and on failure, so a retry
keeps no tail of a longer password
- the keyboard arm wipes its scratch copy after each response
- tests: regress and auth cases for the retry tail, the wipe and a
failed read
Issue: F-8846
auth.c called the library-internal ConstantCompare(), which does not
resolve when the library is built with --disable-inline. Call
wolfSSH_ConstantCompare() instead.
Issue: F-7514
Restructure the two sites that left a redundant check in one build
configuration, and suppress a warning that only appears in a
configuration cppcheck invents.
- wolfsshd.c: set the -h host key file in the else of the cert store
conflict check instead of retesting ret.
- internal.c CompositeEccSign: run wc_ecc_sig_to_rs from an else shared
by the small-stack allocation check and the plain stack arrays.
- wolfsftp.c: suppress constStatement on WWRITE; cppcheck's -DWWRITE
configuration turns the arguments into a comma expression.
The known_hosts search skips an entry whose host matches but whose key
type differs, so a host pinned with one type that presents another was
reported as an unknown host. The check now remembers that the host is
known with another type and says so before offering to add the new one,
as OpenSSH does.
- note the first other-type entry's key type and line in the loop
- print a warning with that entry, the host and the new fingerprint
before the add prompt; a later same-type entry still matches or
rejects as before
- reject a server key type that is not printable ASCII before it is
printed or stored
- regress.c: add TestKnownHostsOtherType
Issue: ZD-22634
ConfigLoad() reports a line too long for its buffer and fails the load.
A cut line used to take effect with whatever fitted, since what is left
of it is read as a line of its own and a leftover of two bytes or less
is skipped as blank. The buffer is 1024 bytes now, so a long path or
forced command still loads.
- one byte is read past a full buffer, so a line that ends exactly
there, at a newline or the end of the file, is not taken for a cut one
- the size is WOLFSSHD_MAX_LINE_SIZE in configuration.h, shared with
the test
- test_configuration covers a long line, lines that exactly fill the
buffer, and a line cut to leave a blank-looking remainder
Issue: ZD-22623
A ForceCommand other than a bare "internal-sftp" now denies the SFTP
subsystem, and any ForceCommand denies SCP, as sshd_config(5) describes
it: a forced command applies to shell, command, and subsystem execution.
The session dispatch checks the configured command alongside a
certificate's, so either one on its own confines the session.
- the subsystem arm and both SCP arms consult the configured
ForceCommand, not only a certificate's force-command
- "internal-sftp" followed by options denies SFTP, since no option is
applied, while the shell still refuses it as "internal-sftp"
- every site testing for "internal-sftp" now shares one predicate
- root's transfer refusal no longer points at ForceCommand internal-sftp,
which does not permit SCP
- tests require a ForceCommand to deny SFTP and SCP, read the daemon's
decisions from its log, and pair a configured internal-sftp with a cert
Issue: ZD-22623
Issue: F-10597
An unresolved user took the global methods, so a name a Match User
block covers advertised a different list depending on whether the
account existed. DefaultUserAuthTypes() now composes the blocks that
select on the name alone, with no group list, when the account does
not exist.
- a Match Group block still cannot apply to a name with no account, so
it fails closed to the unmatched methods
- any other failure to resolve the user, or to compose its blocks,
advertises no methods rather than the global ones, so a Match block
that disabled a method is never skipped
- test_configuration stubs the account lookup and covers a lookup
error, an unreadable group list and a resolved account as well as the
Match User and Match Group blocks
Issue: ZD-22617
DefaultUserAuthTypes() resolved the requested user before any
authentication, so a username with no local account produced an empty
"methods that can continue" name-list. It now falls back to the global
configuration's methods when the user does not resolve, so the
USERAUTH_FAILURE reply no longer says which accounts exist.
- an unresolved user still cannot authenticate, since the same failed
lookup rejects every method it tries
- test_configuration covers the advertisement for an unknown user,
comparing against the global mask so it tracks the configuration
Issue: ZD-22617
The password backends check the password as a C string, so "\0junk"
was checked as "" while its nonzero length passed the
PermitEmptyPasswords check. RequestAuthentication now refuses any
password with a NUL before the policy check and the backends.
- test_RequestAuth_embeddedNulPasswordRejected puts the NUL first,
last and mid-password, and checks the fake crypt still runs
Issue: F-16096
The -p option of wolfssh, client, wolfscp, wolfsftp and echoserver now
goes through a strtol check, so a value that is not wholly a number in
range exits with an error instead of wrapping into a different port.
- apps/wolfssh: ClientParsePort(), shared with ClientParseDestination(),
which also rejects an empty "ssh://host:" port instead of defaulting
- examples: a ClientParsePort() in examples/client/common.c; port 0 is
rejected in every build, not only with a main driver
- echoserver: accepts 0 to 65535, keeping 0 for the test harness
- add TestClientParsePort and an empty URI port case to regress.test
Issue: F-14763, F-14767, F-14768, F-12552
SHELL_Subsystem copied pw_shell into an 80-byte buffer before execv(),
so a longer path ran whatever its first 79 bytes named. The child now
passes pw_shell to execv() directly, and builds the login argv[0] from
its basename so a long path no longer falls back to "-sh".
- add sshd_long_shell_test.sh, which logs in as a throwaway user whose
shell path is longer than 80 bytes and checks the shell's $0
Issue: F-14757
Where open() cannot refuse a final-component symlink, no pre-check can
stop a link swapped in before the open, and the fstat gate accepts a
root-owned target. wolfSSHD_ConfigSavePID now writes no PID file there
instead of relying on a racy lstat.
- test_ConfigSavePID gets a variant for that build that expects no file
Issue: F-14764
A client refuses shell, exec, subsystem and pty-req channel requests
and direct-tcpip opens before any callback runs (RFC 4254 6.2, 6.5,
7.2). CHANNEL_OPEN_CONFIRMATION and CHANNEL_OPEN_FAILURE are a
protocol error unless the channel's own open is still outstanding.
- SshInit() installs the default WS_TermResize for servers only.
- Drop the Windows clients' raw-HANDLE resize context; nothing uses it.
- Move the channel request builders out of the server-only test block.
Issue: F-10582, F-13923, F-15533, F-15572, F-14434
The POSIX StartSSHD() now returns EXIT_FAILURE when ret holds an error,
and main() passes that on. A config check (-t) of a bad config, a
failed host key load, or a bad -p now exit 1 instead of 0.
- set ret when the accept loop cannot allocate a connection, so that
stop also exits non-zero
- PendingConnection() returns negative on a select() error other than
EINTR, and the accept loop sets ret from it instead of quit
- log "Closing down wolfSSHD" whenever the accept loop exits, not
only on quit, so the error exits still log it
- add sshd_exit_status_test.sh, which checks -D -t exit codes for a
valid config and three startup failures
Issue: F-16053
StartSSHD() no longer stores the NewConnection() result in ret, the
accept loop's condition. A failed fork() or CreateThread() now drops
that one connection, and the daemon goes on listening.
- add sshd_fork_fail_test.sh with an LD_PRELOAD shim that fails the
first connection's fork() with EAGAIN
- run it from run_all_sshd_tests.sh on leased port +7
Issue: F-16052
RFC 4254 sec 6.5 lets only one shell, exec or subsystem request succeed
per channel. DoChannelRequestSession() now refuses any request after
the first grant without asking a callback, so the granted session and
its command stand.
- a refusal before any grant leaves the channel with no command
- ssh.h documents the refusal; wolfsshd's own check is defence in depth
- regress.c sends a second exec, shell and subsystem after a granted
exec, and a second exec over a generic-callback grant
- unit.c expects a shell after an exec to be refused and checks that a
refused exec's command is scrubbed
A pty session has no stderr pipe, so the shell loop must start with
stderrEmpty set. The drain test now runs "exit 3" in a pty shell and
expects the session to end with that status rather than wait for a
stderr EOF that never comes.
A child that exits with its error output still in the pipe must have
all of it relayed. The child grows its stderr pipe and queues the
whole payload before exiting, so the result does not depend on the
pipe size or on which cat is installed.
- forty passes, each compared byte for byte with the payload
- a client failure is reported as one, not as a truncation
- fixtures readable by a login user other than the invoker
- skips without perl
- listed in extra_test_cases and local_sshd_tests
The shell loop waits on the child's stderr the way it already waits on
its stdout. A child that exits with error output still in the pipe had
the rest relayed only by the single read below the loop -- one
bufferful, however much was waiting -- so the peer saw a truncated
error stream and no error. A pty session has no stderr of its own.
Neither pipe holds the loop once the peer closes the channel.
The Windows path of apps/wolfssh and examples/client sets the volatile
quit with InterlockedOr8() once readPeer() returns, repeats
CancelSynchronousIo() on the readInput() thread until it has exited,
and only then closes the handles and frees the session. readInput()
checks quit around its console read and inside its send retries, and
returns quietly when it is set.
Issue: F-11662
readInput() and readPeer() in apps/wolfssh and examples/client clear
their stack buffer with wc_ForceZero() before returning, so neither
what was typed into the session nor the last of its output is left on
a finished thread's stack. On POSIX the main thread cancels
readInput() inside read(), so a pthread cleanup handler does the wipe
for it.
- readInput() holds only the buffer and the cleanup handler; the loop
is readInputLoop(), since glibc's pthread_cleanup_push() is a setjmp
and gcc -Wclobbered flags the locals of the function holding it.
Issue: F-10594, F-12647
readInput() in apps/wolfssh and examples/client sends again from
where a short wolfSSH_stream_send() stopped until the whole chunk is
taken, and waits out WS_WINDOW_FULL the way it waits out a rekey.
readPeer() in both writes stdout and stderr through a WriteAll() that
loops over short writes.
- A send clamped to the peer's window or packet size no longer drops
the rest of the chunk, and the WS_WINDOW_FULL that follows the clamp
no longer ends the stdin thread.
- The Windows stderr path writes the bytes read instead of a "%s".
Issue: F-8830, F-10548, F-10555
wolfSSH_AGENT_SignRequest() writes the sign request and reads the reply
through the whole-message helpers the relay uses, so a short write is
followed up and a reply is read to its declared length before it is
parsed. The setup callback's WS_AgentCbError result becomes
WS_AGENT_CXN_FAIL, and an ECDSA key's reply room fits the SSH blob.
- A short write, a hang-up, or a failed setup is WS_AGENT_CXN_FAIL;
cleanup runs only after a setup that succeeded, and the client
callbacks close their socket when connect() fails.
- PrepareUserAuthRequestEcc() reserves r and s as mpints a pad byte over
the curve size, and BuildUserAuthRequestEcc() hands the agent that
room, checks the reply's algorithm name, and bounds it by outputSz.
- A NULL sig, or a NULL digest or key blob with a size, is
WS_BAD_ARGUMENT; agent.h documents the contract and return codes.
- api: the mock agent's short write is followed up, a reply read a byte
at a time is reassembled, and a failed setup and NULL buffers are
refused.
- regress: the mock agents answer as a stream; ECDSA user auth for each
curve at, under, and over the largest blob, and a wrong curve name
refused; a failed setup refused with nothing sent.
Issue: F-13325, F-14629, F-14631
The summary prints "N run, N passed, N skipped" from a PASSED counter
incremented at each success site. TOTAL counts every test reached, skips
included, so printing it under the label "passed" overstated the pass
count by exactly the number of skips.
- pair each orphan SKIPPED bump with a TOTAL bump, and count the
local-only tests skipped against an external host by name, so the
printed numbers are commensurable and the skip count cannot go stale
- count the host key gate's WOLFSSH_NO_HOSTKEY_PERMS exit as a pass: it
prints PASSED and returns with TOTAL already charged, so leaving it
uncounted would fail the new assertion on every build with that macro
- assert that passed plus skipped equals ran and, without --match, that
ran equals the known test count, so a miscounting skip site or a name
no call site reaches fails the run
- verified in the container: a full run reports 32 run, 30 passed, 2
skipped against 30 PASSED lines, and --exclude sshd_forcedcmd_test.sh
reports 32 run, 29 passed, 3 skipped against 29
run_all_sshd_tests.sh validates --match and --exclude against test_cases
plus a new extra_test_cases naming the tests called in place, and every
call goes through one should_skip(). A match runs its test where it
sits, with the daemon it needs around it, so one path now serves the
whole suite. test_cases named 12 of the 32 tests the runner drives.
- gate the x509, UPN and ML-DSA daemon starts on the match, so selecting
another test does not start them
- take the bulk skip for the tests needing a local daemon only on a full
run, where its fixed count is right
- reject an unrecognized --exclude, and a --match that reached no call
site: a local-only name selects nothing against an external host,
which would otherwise exit 0 having run no test at all
- keep every test call inside one compound, so RUN_COMPLETE still fails
a run that a shell expansion error cut short, and abort naming any
run_test call listed in neither array
- verified in the container: a full run gives 32 run, 30 passed, 2
skipped, and --match sshd_chroot_fail_test.sh gives 1 run, 1 passed,
starting only the daemon that test needs
An LD_PRELOAD interposer fails one SetupChroot() step at a time and
records each chroot() plus every chdir() made after one, so a step that
ran when it should have been skipped shows up in the record. The daemon
stops after the first failed step, and nothing asserted that.
- three cases: chdir faulted, chroot faulted, and an unfaulted chroot
into "/" that must still complete and run its command
- keep the fault injection in the interposer, with no fault code in the
daemon or the library
- register it after the privilege-drop test, the other test that needs
its own root-launched daemon
- skip when the example client is not built, and set the EXIT trap
before building the interposer so no exit path leaves it behind
sshd_ossh_cert_test.sh polls for the listening socket and fails any case
where the daemon was not up for the whole attempt. Every rejection case
scored a pass on a non-zero client exit, so a daemon that never started
passed them all: against a wolfsshd that exits immediately, 17 of the 39
cases reported PASS.
- pin readiness to the daemon's own pid where the listening table names
it, so a stray listener reads as a failure to bind
- check liveness after the client too; kill -0 alone will not do, since
the disowned daemon lingers as a zombie
- stop the daemon by recorded pid and wait for it to go, rather than a
command-line pkill that can reach another user's processes
- chmod trusted-cas.pub 0644 at creation; as a TrustedUserCAKeys anchor
at umask 002 it lands group writable and the daemon refuses to start
- run the clients under "timeout 30", as they hung on a port that never
speaks SSH, and fail a timed-out case rather than score a rejection
create_authorized_test_file.sh sets authorized_keys_test to 0644 where
it writes it. Shell redirection left it at the process umask, 0664 under
Ubuntu's per-user groups, and wolfSSHd's StrictModes check refuses a
group or world writable file, so the suite died at its first test with
only "Couldn't connect SSH stream" on the client side.
- the check also walks every parent directory, so clearing the file mode
alone moves the rejection up a level rather than curing it
- mirror that walk in the runner before starting a local daemon, naming
each directory to clear, and stop rather than run tests that cannot
authenticate
- skip the walk against an external host, whose authorized_keys file is
not in this tree
- record the umask prerequisite in the suite's README
sshd_term_close_test.sh waits for the client to exit, then polls for the
port to settle for up to ten seconds. Its one netstat sample used to
land at the instant "timeout" killed the client, looking straight into
the teardown it measures, and netstat walks /proc/net/tcp
non-atomically, so it could read the two rows either side of the FIN.
- fail only if the port is still unsettled at the deadline, so a socket
stuck in CLOSE_WAIT, which is what this test is for, still fails it
- compare wolfsshd pid sets and poll for the fork; a count across a
fixed sleep missed the new child whenever an earlier test's child
exited in the same window
- print the port's netstat lines on failure, so a real leak names the
stuck socket
- run alternately against one daemon, the old script failed 21 of 165
runs and its pid check 1 in 25; the new one none
The teardown safety net matches the daemon by process name, so it
reaches every daemon the suite starts and nothing else. As
"pkill -f wolfsshd" it also matched the runner's own shell whenever the
script was named by a path through apps/wolfsshd: the suite SIGTERMed
itself and exited 143 with every test passed.
- anchor the host key gate's kill on "/wolfsshd " and the port's own
flag; it must match a command line to spare the shared daemon, and
"$HK_SSHD.*$HK_PORT" made ../wolfsshd a regex
- anchor the three pkills in sshd_ossh_cert_test.sh the same way, so
their reach is a property of the pattern, not of the config name
sshd_pubkey_reject_test.sh authenticates gretel's ECC key against the
shared daemon and requires both a non-zero client exit and a new "Public
key not authorized" line in the daemon log. The suite only ever offered
a hansel key, so SearchKeysFile()'s no-line-matched path was never
driven through the daemon.
- run a positive control with the authorized key first, so the rejection
cannot be read as a connection failure
- bail out if gretel's key turns out to be listed after all
- call it while the shared daemon is up, ahead of the StrictModes
authorized_keys check
- poll the log for the rejection rather than sampling it once after a
fixed sleep
ParseConfigLine() takes an options[] entry only when the keyword ends
at whitespace or the end of the line. Record what that check defends
against at the loop and pin the two arms it has. Without it an OpenSSH
"HostKeyAlgorithms" line matches "HostKey" and takes "Algorithms" as
the host key file, aborting startup.
- extend the existing prefix-match comment in test_ParseConfigLine()
rather than open a second one; the synthetic vectors under it already
cover the whitespace arm
- pin the end-of-line arm on the error code, not just on failure: only
a matched keyword reaches HandleStrictModes and returns
WS_BAD_ARGUMENT, so the check holds on every build flavor
- assert the OpenSSH spelling on its own, where the assertion survives
HostKeyAlgorithms becoming a supported keyword
The remaining teardown sites spun wolfSSH_worker() ten times after
wolfSSH_shutdown(), the loop the client and the echoserver just
replaced. The clients now send a disconnect and drain, and wolfsshd
half closes and drains like the echoserver.
- sftpclient.c, scpclient.c, portfwd.c and the wolfssh app call
SendDisconnectAndDrain() and treat either give-up as a clean close.
- scpclient.c and the wolfssh app report an unexpected shutdown result,
as the client does.
- the wolfssh app waits for the channel close before draining, as the
client does, and fails on an error that broke the wait.
- wolfsshd drained by hand after wolfSSH_free(), with no bound on a peer
that never hung up. HalfCloseAndDrain() bounds it at ten seconds, and
a peer that let the login grace time run out is not drained.
- The child's stdin pipe and the pty master are closed before the
reap. The reap polls waitpid() with WNOHANG for
WOLFSSHD_CHILD_REAP_TRIES x WOLFSSHD_CHILD_REAP_WAIT_US, then
sends SIGKILL; a failed kill() is logged and ends the wait.
- ChildSig is installed before forkpty() instead of after it.
- An EIO read from the pty master ends the child's output
instead of the loop.
- A held backlog is sent with SHELL_FlushOut() before the pipe
drain; the drain is skipped when the backlog is not sent.
- New sshd_channel_close_test.sh (paramiko) closes the channel
while output is held, for an exec command and a pty shell, and
checks that the command and the connection both end; it also
checks exit status 3 from an exec and an interactive shell.
run_all runs it.
- sshd-test.yml and code-coverage.yml install python3-paramiko
through install-apt-deps, and the ubuntu-24.04-tests bundle
lists it.
- readInput() in apps/wolfssh runs FlushQueuedSend() when
wolfSSH_stream_send() returns WS_WANT_WRITE without taking the
buffer, then sends the same buffer again; a failed flush still
ends in "Couldn't send data".
- readInput() in examples/client retries WS_WANT_WRITE as it
retries WS_REKEYING, for up to SEND_RETRY_TIMEOUT (10) seconds
per buffer.
- The comment on the queued check in apps/wolfssh describes the
positive-return case.
A held status from wolfSSH_AGENT_RelayChannel() is negative, and
leaving it in ret ended the outer while (ret >= 0) loop before the owed
reply was retried. Reset ret to WS_SUCCESS when the reply is held so
readPeer() returns to select() and retries it.
Issue: F-10545
- src/agent.c gains AgentWriteAll(), AgentReadFull(),
AgentReadMessage() and AgentWriteMessage(), which loop the
agentIoCb, bound a declared length by the new
WOLFSSH_AGENT_MAX_MSG_SZ, and reconnect once when a write finds
the socket dead. wolfSSH_AGENT_Relay() is rebuilt on them.
- New WOLFSSH_API wolfSSH_AGENT_RelayChannel() frames the channel
side, holding a partial request and an unfinished reply on
WOLFSSH_AGENT_CTX in two WOLFSSH_BUFFERs whose whole allocation
is zeroed as each message is consumed. It answers WS_WANT_WRITE,
WS_WINDOW_FULL or WS_REKEYING while a reply is owed, flushes
what is left queued below under those last two, and answers
WS_WANT_WRITE while any of it is still queued.
- readPeer() in apps/wolfssh/wolfssh.c and examples/client/client.c
calls it, drops its local ato32() and 512-byte buffer, and arms a
select() write set for the transport's own want-write. While a
reply is owed it ends the read loop so the retry above select()
runs, and examples/client/client.c times its select() out at one
second.
- tests/api.c adds nine wolfSSH_AGENT_Relay() cases and
tests/regress.c twenty-two for wolfSSH_AGENT_RelayChannel().
Issue: F-10545
Daemonizing cleared the umask and nothing set one afterwards, so every
per-connection child inherited it: files created from a shell or
through the SCP receive path came out 0666, directories 0777. Hold 022,
which is what a foreground daemon already inherits from its caller.
- WOLFSSHD_DEFAULT_UMASK sets it at build time
Issue: F-3671
Subsystem, ChallengeResponseAuthentication, UsePAM, X11Forwarding,
PrintMotd, AcceptEnv and UseDNS parse and are then dropped. An unknown
keyword is fatal, so accepting these in silence reads as support for a
setting that is not enforced. Log a warning naming the keyword instead.
Rejecting them would fail every config copied from OpenSSH.
- OptionName() maps an option tag back to its keyword for the message
Issue: F-2868
The runner leases a port block per run so two runs can share a host, and
this test's config and 16 MB payload are the other half of that: a
second run overwrites them, and whichever finishes first removes them
from under the other, which then reports a transfer that never ran
short. Both live in a directory of this run's own now.
- the external-host tally counts 12 local-only tests, not 10
A byte count cannot tell a short transfer from one that never finished
and the runner sets no deadline of its own, so the client runs under
"timeout" and a 124 is reported as a hang. An empty PID means the
daemon never came up, which the count would otherwise report as the
truncation this test exists to catch.
- scratch paths held in variables, since bash rewrites PWD on the cd
back into the test directory
- an EXIT trap removes the 16 MB scratch file and stops the daemon
The stdout and pty childFd reads treat EINTR and EWOULDBLOCK as
non-fatal, the set the stderr read already uses, so an interrupted read
cannot end the session with the peer's output still queued. The
"err != 0" term goes with it; read() sets errno on the -1 return that
is the only way into the test.
A stream leaves the read set once it has reported EOF, so a child whose
output is drained while it still runs leaves the SSH socket as the only
descriptor watched, and its SIGCHLD is the only wake left. One handled
between the ChildRunning test and select() would not interrupt the
call, so wait a second at a time there. An exited child still polls.
sshd_stderr_eof_test.sh streams 16 MB through a shell session whose
reader stalls, so the send window fills and the child exits while the
loop still holds a backlog. The transfer repeats twelve times because
whether the stale errno at the stderr EOF is fatal is a race.
- a short transfer is never correct, so a failure here is always real
- the fixed tree passes 48 consecutive transfers; the truncating one
fails on the first
- runs in the sudo-only block beside sshd_window_full_test.sh, which
reaches the same path about 1 run in 12
read() returns 0 at end of file and leaves errno alone, so the stderr
arm consults it only for a -1 return. A 0 marks that stream done and
drops it from the read set; stdout does the same, so neither spins on
a descriptor that stays ready once it has reported EOF.
- a stale EINTR from the child's SIGCHLD no longer ends the loop with
the peer's output still queued
- EINTR and EWOULDBLOCK join EAGAIN as non-fatal on a -1 return
- select polls rather than blocks once the child is gone, its output
is drained and nothing is held, so the loop still reaches its exit
- processState starts at 0 in SHELL_Subsystem()'s WIN32 half, where
it is read by wolfSSH_SetExitStatus() on paths that leave the loop
without GetExitCodeProcess() having written it.