From ae63cb2a7e2006cf118879ccf010011d608831cb Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 21 Oct 2026 09:14:30 -0700 Subject: [PATCH 1/7] internal: fail DoModes when the tty is unreadable wolfSSH_DoModes() now returns -1 without touching the terminal when tcgetattr() fails, instead of applying an uninitialized termios. A failed tcsetattr() is reported the same way, and wolfsshd logs it. - api.c: DoModes on a pipe returns -1 Issue: F-10546 --- apps/wolfsshd/wolfsshd.c | 5 ++++- src/internal.c | 6 ++++-- tests/api.c | 26 ++++++++++++++++++++++++++ wolfssh/ssh.h | 2 ++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index c2f033687..6577cb972 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -3065,7 +3065,10 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, /* set initial size of terminal based on saved size */ #if defined(HAVE_SYS_IOCTL_H) - wolfSSH_DoModes(ssh->modes, ssh->modesSz, childFd); + if (ssh->modesSz > 0 && + wolfSSH_DoModes(ssh->modes, ssh->modesSz, childFd) != 0) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error applying terminal modes"); + } { struct winsize s; diff --git a/src/internal.c b/src/internal.c index fb3f86fbb..ac45cccd6 100644 --- a/src/internal.c +++ b/src/internal.c @@ -13301,7 +13301,8 @@ int wolfSSH_DoModes(const byte* modes, word32 modesSz, int fd) * byte left over. */ - tcgetattr(fd, &term); + if (tcgetattr(fd, &term) != 0) + return -1; while (idx + TERMINAL_MODE_SZ <= modesSz && modes[idx] != WOLFSSH_TTY_OP_END && modes[idx] < WOLFSSH_TTY_INVALID) { @@ -13540,7 +13541,8 @@ int wolfSSH_DoModes(const byte* modes, word32 modesSz, int fd) idx += TERMINAL_MODE_SZ; } - tcsetattr(fd, TCSANOW, &term); + if (tcsetattr(fd, TCSANOW, &term) != 0) + return -1; return 0; } diff --git a/tests/api.c b/tests/api.c index 97b926d0c..3c57bf1f1 100644 --- a/tests/api.c +++ b/tests/api.c @@ -8034,6 +8034,31 @@ static void test_wolfSSH_ConvertConsole(void) } +#if defined(WOLFSSH_TERM) && !defined(NO_TERMIOS) && \ + defined(HAVE_SYS_IOCTL_H) && !defined(USE_WINDOWS_API) +#include + +static void test_wolfSSH_DoModes(void) +{ + static const byte modes[] = { + WOLFSSH_ECHO, 0x00, 0x00, 0x00, 0x01, + WOLFSSH_TTY_OP_END + }; + int fds[2]; + + AssertIntEQ(wolfSSH_DoModes(NULL, sizeof(modes), -1), -1); + + /* a pipe is not a terminal, so its attributes cannot be read */ + AssertIntEQ(pipe(fds), 0); + AssertIntEQ(wolfSSH_DoModes(modes, sizeof(modes), fds[0]), -1); + close(fds[0]); + close(fds[1]); +} +#else +static void test_wolfSSH_DoModes(void) { ; } +#endif + + static void test_wstrcat(void) { #ifndef WSTRING_USER @@ -9294,6 +9319,7 @@ int wolfSSH_ApiTest(int argc, char** argv) test_wolfSSH_SetUsername(); test_wolfSSH_SetChannelType(); test_wolfSSH_ConvertConsole(); + test_wolfSSH_DoModes(); test_wolfSSH_CTX_UsePrivateKey_buffer(); test_wolfSSH_CTX_UseCert_buffer(); test_wolfSSH_CTX_UseCert_file(); diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index a91b43734..c7ffe6635 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -1028,6 +1028,8 @@ WOLFSSH_API int wolfSSH_ConvertConsole(WOLFSSH* ssh, WOLFSSH_HANDLE handle, #endif +/* Applies the peer's pty-req terminal modes to fd. Returns 0, or -1 for a bad + * mode list or when fd's attributes cannot be read or set. */ WOLFSSH_API int wolfSSH_DoModes(const byte* modes, word32 modesSz, int fd); WOLFSSH_API WS_SessionType wolfSSH_GetSessionType(const WOLFSSH* ssh); WOLFSSH_API const char* wolfSSH_GetSessionCommand(const WOLFSSH* ssh); From ebd36b621509b256756ec0db62ade4b1e66dbae9 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 21 Oct 2026 09:15:47 -0700 Subject: [PATCH 2/7] wolfsshd: fix shell child exit and setup failures A shell child killed by a signal now reports 128 plus the signal as its exit status. A failure after the fork kills and reaps the child and closes the parent's descriptors to it, and the resize context is cleared before the terminal it points at is closed. - SHELL_AbortChild() serves the post-fork setup failures, which now run before the parent drops privileges so it can still kill the child - -r with no command now fails at the argument check in main() - sshd_exit_signal_test.sh checks a plain exit and a SIGKILL Issue: F-13327, F-14203, F-15571, F-14204 --- apps/wolfsshd/test/run_all_sshd_tests.sh | 1 + apps/wolfsshd/test/sshd_exit_signal_test.sh | 72 ++++++++++++++++ apps/wolfsshd/wolfsshd.c | 95 +++++++++++++++++---- 3 files changed, 152 insertions(+), 16 deletions(-) create mode 100755 apps/wolfsshd/test/sshd_exit_signal_test.sh diff --git a/apps/wolfsshd/test/run_all_sshd_tests.sh b/apps/wolfsshd/test/run_all_sshd_tests.sh index af2439d3c..5dc290a85 100755 --- a/apps/wolfsshd/test/run_all_sshd_tests.sh +++ b/apps/wolfsshd/test/run_all_sshd_tests.sh @@ -15,6 +15,7 @@ test_cases=( "sshd_scp_fail.sh" "sshd_term_close_test.sh" "sshd_stdin_eof_test.sh" + "sshd_exit_signal_test.sh" "sshd_stdin_stall_test.sh" "sshd_channel_close_test.sh" "ssh_kex_algos.sh" diff --git a/apps/wolfsshd/test/sshd_exit_signal_test.sh b/apps/wolfsshd/test/sshd_exit_signal_test.sh new file mode 100755 index 000000000..74438ead5 --- /dev/null +++ b/apps/wolfsshd/test/sshd_exit_signal_test.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# bash, unlike most of this directory: the option array below needs it. + +# A remote command killed by a signal must not report success. wolfSSHd sends +# 128 plus the signal number as the exit status, as a shell reports it. +# +# Needs the OpenSSH client, which exits with the remote exit status. + +if [ -z "$1" ] || [ -z "$2" ]; then + echo "expecting host and port as arguments" + echo "./sshd_exit_signal_test.sh 127.0.0.1 22222" + exit 1 +fi + +HOST="$1" +PORT="$2" +USER_NAME="${3:-`whoami`}" + +command -v ssh >/dev/null 2>&1 || { + echo "ssh not found, skipping" + exit 77 +} + +command -v timeout >/dev/null 2>&1 || { + echo "timeout not found, skipping" + exit 77 +} + +# ssh refuses a group/world readable identity file. +KEYDIR=`mktemp -d 2>/dev/null` || KEYDIR=`mktemp -d -t sshdsig` +if [ -z "$KEYDIR" ] || [ ! -d "$KEYDIR" ]; then + echo "could not create temp dir" + exit 1 +fi +trap 'rm -rf "$KEYDIR"' EXIT + +cp ../../../keys/hansel-key-ecc.pem "$KEYDIR/id_ecdsa" || exit 1 +chmod 600 "$KEYDIR/id_ecdsa" + +SSH_OPTS=(-i "$KEYDIR/id_ecdsa" -p "$PORT" + -o IdentitiesOnly=yes + -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null + -o PreferredAuthentications=publickey -o PasswordAuthentication=no + -o BatchMode=yes -o ConnectTimeout=5 -o LogLevel=ERROR) + +if ! timeout 20 ssh "${SSH_OPTS[@]}" "$USER_NAME@$HOST" true >/dev/null 2>&1 +then + echo "no session with these options, skipping" + exit 77 +fi + +# Control: a plain exit status comes through unchanged. +timeout 20 ssh "${SSH_OPTS[@]}" "$USER_NAME@$HOST" 'exit 3' \ + > /dev/null 2> "$KEYDIR/exit.err" +RESULT=$? +if [ "$RESULT" != 3 ]; then + echo "expected exit status 3, got $RESULT" + cat "$KEYDIR/exit.err" + exit 1 +fi + +# The shell running the command kills itself, so it is what the daemon reaps. +timeout 20 ssh "${SSH_OPTS[@]}" "$USER_NAME@$HOST" 'kill -KILL $$' \ + > /dev/null 2> "$KEYDIR/kill.err" +RESULT=$? +if [ "$RESULT" != 137 ]; then + echo "expected exit status 137 for SIGKILL, got $RESULT" + cat "$KEYDIR/kill.err" + exit 1 +fi + +exit 0 diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index 6577cb972..dd3dc2d68 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -2604,6 +2604,8 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, if (cnslOut != NULL) { CloseHandle(cnslOut); } + /* The resize callback must not reach ptyIn once it is closed. */ + wolfSSH_SetTerminalResizeCtx(ssh, NULL); if (ptyIn != NULL) { CloseHandle(ptyIn); } @@ -2679,6 +2681,40 @@ static int SHELL_SetNonBlocking(int fd) #define WOLFSSHD_CHILD_REAP_WAIT_US 50000 #endif +/* Close a descriptor that is still open and mark it closed. */ +static void SHELL_CloseFd(int* fd) +{ + if (*fd >= 0) { + close(*fd); + *fd = -1; + } +} + +/* Take down a shell child that the parent cannot service: kill it, reap it, + * and close the parent's descriptors to it. */ +static void SHELL_AbortChild(pid_t childPid, int* childFd, int* stdoutPipe, + int* stderrPipe, int* stdinPipe) +{ + int status; + int i; + + if (kill(childPid, SIGKILL) == 0) { + while (waitpid(childPid, &status, 0) < 0 && errno == EINTR) { + /* retry */ + } + } + else { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Unable to kill shell child %d, " + "errno %d", (int)childPid, errno); + } + SHELL_CloseFd(childFd); + for (i = 0; i < 2; i++) { + SHELL_CloseFd(&stdoutPipe[i]); + SHELL_CloseFd(&stderrPipe[i]); + SHELL_CloseFd(&stdinPipe[i]); + } +} + /* Send out the last of a shell's output once the child has been reaped. The * SSH socket is non blocking, so retry a bounded number of times on a full * window, a rekey or a would block. 'ext' selects the extended (stderr) data @@ -3024,15 +3060,6 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, exit(ret); /* exit child process and close down SSH connection */ } - if (wolfSSHD_AuthReducePermissionsUser(conn->auth, pPasswd->pw_uid, - pPasswd->pw_gid) != WS_SUCCESS) { - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error setting user ID"); - /* could not drop to the authenticated user; kill the already - * forked shell child and terminate the per-connection process - * rather than continue at a higher privilege level */ - kill(childPid, SIGKILL); - exit(1); - } sshFd = wolfSSH_get_fd(ssh); struct termios tios; @@ -3048,21 +3075,46 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, /* The shell child is already forked, and nothing below can reach the * peer without an id to address, so take it down with us. */ wolfSSH_Log(WS_LOG_ERROR, "[SSHD] No session channel to service"); - kill(childPid, SIGKILL); + SHELL_AbortChild(childPid, &childFd, stdoutPipe, stderrPipe, + stdinPipe); + if (wolfSSHD_AuthReducePermissions(conn->auth) != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Error lowering permissions level"); + exit(1); + } return WS_FATAL_ERROR; } signal(SIGINT, SIG_DFL); rc = tcgetattr(childFd, &tios); - if (rc != 0) { - return WS_FATAL_ERROR; + if (rc == 0) { + rc = tcsetattr(childFd, TCSAFLUSH, &tios); } - rc = tcsetattr(childFd, TCSAFLUSH, &tios); if (rc != 0) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error setting up the shell's pty"); + SHELL_AbortChild(childPid, &childFd, stdoutPipe, stderrPipe, + stdinPipe); + if (wolfSSHD_AuthReducePermissions(conn->auth) != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Error lowering permissions level"); + exit(1); + } return WS_FATAL_ERROR; } + /* Drop privileges only after the checks above, which may have to kill + * the child while it is still root. */ + if (wolfSSHD_AuthReducePermissionsUser(conn->auth, pPasswd->pw_uid, + pPasswd->pw_gid) != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error setting user ID"); + /* could not drop to the authenticated user; kill the already + * forked shell child and terminate the per-connection process + * rather than continue at a higher privilege level */ + kill(childPid, SIGKILL); + exit(1); + } + /* set initial size of terminal based on saved size */ #if defined(HAVE_SYS_IOCTL_H) if (ssh->modesSz > 0 && @@ -3564,6 +3616,9 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, stdinPipe[1] = -1; } + /* The resize callback must not reach childFd once it is closed. */ + wolfSSH_SetTerminalResizeCtx(ssh, NULL); + /* Close the pty master so the shell gets SIGHUP and can exit on its own */ if (childFd >= 0) { close(childFd); @@ -3603,8 +3658,16 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, "status"); } else { - if (wolfSSH_SetExitStatus(ssh, (word32)WEXITSTATUS(waitStatus)) != - WS_SUCCESS) { + word32 exitStatus = 1; + + /* A signaled child reports 128 plus the signal, as a shell does */ + if (WIFEXITED(waitStatus)) { + exitStatus = (word32)WEXITSTATUS(waitStatus); + } + else if (WIFSIGNALED(waitStatus)) { + exitStatus = 128 + (word32)WTERMSIG(waitStatus); + } + if (wolfSSH_SetExitStatus(ssh, exitStatus) != WS_SUCCESS) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Issue setting child's exit " "status"); } @@ -5053,7 +5116,7 @@ int main(int argc, char** argv) isService = 0; } if (WSTRCMP(argv[i], "-r") == 0) { - if (argc < i + 1) { + if (i + 1 >= argc) { /* was expecting command to run after -r argument */ return -1; } From 27edf994e488e1c8f4216bafe9e4b3579c9e488c Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 21 Oct 2026 09:16:25 -0700 Subject: [PATCH 3/7] wolfsshd: start SCP sessions in the user's home SCP_Subsystem() changes to pw_dir after dropping privileges, as the shell and SFTP do, so a relative remote path resolves from home rather than from the daemon's working directory, inside a chroot too. When home is unreachable, SCP and the shell start from "/", as OpenSSH does. SCP logs that only outside a chroot. - sshd_scp_home_test.sh uploads to "." and finds the file in home Issue: F-15502 --- apps/wolfsshd/test/run_all_sshd_tests.sh | 1 + apps/wolfsshd/test/sshd_scp_home_test.sh | 65 ++++++++++++++++++++++++ apps/wolfsshd/wolfsshd.c | 24 +++++++-- 3 files changed, 87 insertions(+), 3 deletions(-) create mode 100755 apps/wolfsshd/test/sshd_scp_home_test.sh diff --git a/apps/wolfsshd/test/run_all_sshd_tests.sh b/apps/wolfsshd/test/run_all_sshd_tests.sh index 5dc290a85..aa8d377fc 100755 --- a/apps/wolfsshd/test/run_all_sshd_tests.sh +++ b/apps/wolfsshd/test/run_all_sshd_tests.sh @@ -13,6 +13,7 @@ test_cases=( "sshd_sftp_idle_cpu_test.sh" "sshd_bad_subsystem_test.sh" "sshd_scp_fail.sh" + "sshd_scp_home_test.sh" "sshd_term_close_test.sh" "sshd_stdin_eof_test.sh" "sshd_exit_signal_test.sh" diff --git a/apps/wolfsshd/test/sshd_scp_home_test.sh b/apps/wolfsshd/test/sshd_scp_home_test.sh new file mode 100755 index 000000000..15e16eb0b --- /dev/null +++ b/apps/wolfsshd/test/sshd_scp_home_test.sh @@ -0,0 +1,65 @@ +#!/bin/sh + +# An SCP transfer with a relative remote path resolves it from the user's home +# directory, not from the daemon's working directory. + +# Not named PWD: the shell rewrites that variable on every cd, so a saved +# copy would not survive the cd to the repository root below. +TESTDIR=`pwd` +. ./wolfssh_options.sh +cd ../../.. + +TEST_SCP_CLIENT="./examples/scpclient/wolfscp" +USER="${3:-`whoami`}" +PRIVATE_KEY="./keys/hansel-key-ecc.der" +PUBLIC_KEY="./keys/hansel-key-ecc.pub" + +if [ -z "$1" ] || [ -z "$2" ]; then + echo "expecting host and port as arguments" + echo "./sshd_scp_home_test.sh 127.0.0.1 22222" + exit 1 +fi + +if ! wolfssh_has SCP || [ ! -x "$TEST_SCP_CLIENT" ]; then + echo "SCP client not available in this build, skipping" + exit 77 +fi + +# The check reads the file back from the home directory directly. +case "$1" in + 127.0.0.1|localhost) ;; + *) echo "remote host, skipping"; exit 77 ;; +esac + +HOMEDIR=`getent passwd "$USER" 2>/dev/null | cut -d: -f6` +[ -n "$HOMEDIR" ] || HOMEDIR=`eval echo "~$USER"` +if [ -z "$HOMEDIR" ] || [ ! -d "$HOMEDIR" ]; then + echo "no home directory for $USER, skipping" + exit 77 +fi + +NAME="scp-home-$$.dat" +OUTDIR="`pwd`/test-$$" +mkdir "$OUTDIR" || exit 1 +trap 'rm -rf "$OUTDIR"; rm -f "$HOMEDIR/$NAME"' EXIT + +if ! dd if=/dev/urandom of="$OUTDIR/$NAME" bs=1024 count=64 2>/dev/null; then + echo "Could not create the test file $OUTDIR/$NAME" + exit 1 +fi + +echo "$TEST_SCP_CLIENT -u $USER -i $PRIVATE_KEY -j $PUBLIC_KEY -L$OUTDIR/$NAME:. -H $1 -p $2" +$TEST_SCP_CLIENT -u $USER -i $PRIVATE_KEY -j $PUBLIC_KEY -L"$OUTDIR/$NAME":. \ + -H $1 -p $2 +if [ "$?" != "0" ]; then + echo "Expecting to pass transfer" + exit 1 +fi + +if ! cmp -s "$OUTDIR/$NAME" "$HOMEDIR/$NAME"; then + echo "File did not land in $HOMEDIR" + exit 1 +fi + +cd "$TESTDIR" +exit 0 diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index dd3dc2d68..77d9a4457 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -1772,8 +1772,23 @@ static int SCP_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, * privilege level */ exit(1); } + + /* Relative SCP paths resolve from home, inside a chroot too, as for + * SFTP and the shell. Like OpenSSH, a chroot without the home dir falls + * back to "/" quietly. */ + if (ret == WS_SUCCESS && chdir(pPasswd->pw_dir) != 0) { + if (error != 1) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error going to user home dir " + "%s, using /", pPasswd->pw_dir); + } + if (chdir("/") != 0) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error going to /"); + } + } #else - /* impersonate the logged on user for file permissions */ + /* Impersonate the logged on user for file permissions. Relative SCP + * paths stay relative to the daemon's directory: connections share one + * process, and SCP has no per-session default path like SFTP's. */ if (ImpersonateLoggedOnUser(wolfSSHD_GetAuthToken(conn->auth)) == FALSE) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error impersonating logged on user"); @@ -3023,8 +3038,11 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, rc = chdir(pPasswd->pw_dir); if (rc != 0) { /* not error'ing out if unable to find home directory */ - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error going to user home dir %s", - pPasswd->pw_dir); + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error going to user home dir %s, " + "using /", pPasswd->pw_dir); + if (chdir("/") != 0) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error going to /"); + } } /* default to /bin/sh if user shell is not set */ From 1f879a5c6550867b18c1f87191dc509f9f5743a9 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 21 Oct 2026 09:28:35 -0700 Subject: [PATCH 4/7] wolfsshd: keep account entries off libc storage Account lookups go through wolfSSHD_GetPwnam(), which fills a caller-owned store with getpwnam_r() and wipes it on release. Shadow entries are read the same way with getspnam_r() where libc has it. The crypt() result is wiped on every path. - auth.c and the session lookup in HandleConnection use the store - configure probes the five-argument getspnam_r() as HAVE_GETSPNAM_R - ScanShadowFile() wipes the line the loop exit reads - the getpwnam/getspnam test hooks default to NULL, the real lookup - test_GetPwnam checks the store, the wipe, and the errno contract Issue: F-11683, F-11684, F-13439, F-8842, F-12646 --- apps/wolfsshd/auth.c | 146 +++++++++++++++++++----- apps/wolfsshd/auth.h | 22 ++++ apps/wolfsshd/test/test_configuration.c | 89 ++++++++++++++- apps/wolfsshd/wolfsshd.c | 12 +- configure.ac | 10 ++ 5 files changed, 247 insertions(+), 32 deletions(-) diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index eadb0a027..8171a7599 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -120,15 +120,94 @@ int (*wsshd_setregid_cb)(WGID_T, WGID_T) = setregid; int (*wsshd_setreuid_cb)(WUID_T, WUID_T) = setreuid; int (*wsshd_setegid_cb)(WGID_T) = setegid; int (*wsshd_seteuid_cb)(WUID_T) = seteuid; -struct passwd* (*wsshd_getpwnam_cb)(const char*) = getpwnam; -#define getpwnam wsshd_getpwnam_cb +struct passwd* (*wsshd_getpwnam_cb)(const char*) = NULL; int (*wsshd_setgroups_cb)(int, const WGID_T*) = wsshd_setgroups_default; #ifdef HAVE_SHADOW -struct spwd* (*wsshd_getspnam_cb)(const char*) = getspnam; -#define getspnam wsshd_getspnam_cb +struct spwd* (*wsshd_getspnam_cb)(const char*) = NULL; #endif #endif +#ifndef _WIN32 +struct passwd* wolfSSHD_GetPwnam(const char* name, WOLFSSHD_PW* store) +{ + struct passwd* res = NULL; + int err; + + if (name == NULL || store == NULL) { + errno = EINVAL; + return NULL; + } + + WMEMSET(store, 0, sizeof(*store)); +#ifdef WOLFSSHD_UNIT_TEST + if (wsshd_getpwnam_cb != NULL) { + errno = 0; + return wsshd_getpwnam_cb(name); + } +#endif + err = getpwnam_r(name, &store->pw, store->buf, sizeof(store->buf), &res); + if (res == NULL) { + WS_FORCEZERO(store, sizeof(*store)); + if (err == ERANGE) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] passwd entry for %s exceeds " + "WOLFSSHD_PW_BUF_SZ (%d)", name, WOLFSSHD_PW_BUF_SZ); + } + /* some libcs report no such user as ENOENT */ + errno = (err == ENOENT) ? 0 : err; + } + return res; +} + +void wolfSSHD_PwFree(WOLFSSHD_PW* store) +{ + if (store != NULL) { + WS_FORCEZERO(store, sizeof(*store)); + } +} +#endif + +#ifdef HAVE_SHADOW +#ifndef WSSHD_SPWD_BUF_SZ + #define WSSHD_SPWD_BUF_SZ 1024 +#endif + +typedef struct WSSHD_SPWD { + struct spwd sp; + char buf[WSSHD_SPWD_BUF_SZ]; +} WSSHD_SPWD; + +/* Looks up the shadow entry for name into store, which the caller wipes. */ +static struct spwd* AuthGetSpnam(const char* name, WSSHD_SPWD* store) +{ + struct spwd* res = NULL; +#ifdef HAVE_GETSPNAM_R + int err; +#endif + + WMEMSET(store, 0, sizeof(*store)); +#ifdef WOLFSSHD_UNIT_TEST + if (wsshd_getspnam_cb != NULL) { + return wsshd_getspnam_cb(name); + } +#endif +#ifdef HAVE_GETSPNAM_R + err = getspnam_r(name, &store->sp, store->buf, sizeof(store->buf), &res); + if (err != 0) { + if (err == ERANGE) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] shadow entry for %s exceeds " + "WSSHD_SPWD_BUF_SZ (%d)", name, WSSHD_SPWD_BUF_SZ); + } + res = NULL; + } +#else + /* The entry stays in libc static storage, which a wipe of store does + * not reach; the caller's copy of the hash is the only one it controls. */ + res = getspnam((char*)name); +#endif + return res; +} +#endif + #ifdef WOLFSSH_OSSH_CERTS /* Peer IP string buffer; sized above INET6_ADDRSTRLEN (46) with room to spare. */ #define WOLFSSHD_PEER_IP_SZ 64 @@ -534,6 +613,8 @@ static void ScanShadowFile(WFILE* f) AddShadowLineToFakeHashCache(line); WS_FORCEZERO(line, sizeof(line)); } + /* the read that ends the loop on a full cache still fills line */ + WS_FORCEZERO(line, sizeof(line)); } #ifndef WOLFSSH_USE_PAM @@ -616,6 +697,7 @@ void wolfSSHD_AuthInit(void) #ifdef HAVE_SHADOW char tmpl[WSSHD_FAKE_HASH_SZ]; struct spwd* rootShadow; + WSSHD_SPWD rootStore; #ifndef WOLFSSHD_UNIT_TEST WFILE* f = NULL; @@ -632,7 +714,7 @@ void wolfSSHD_AuthInit(void) #endif if (numCachedFakeHashes == 0) { - rootShadow = getspnam("root"); + rootShadow = AuthGetSpnam("root", &rootStore); if (rootShadow != NULL && rootShadow->sp_pwdp != NULL) { GetFakeHashFromTemplate(rootShadow->sp_pwdp, tmpl, sizeof(tmpl)); if (tmpl[0] != '\0' && tmpl[1] != '\0' && tmpl[1] != '*') { @@ -640,7 +722,9 @@ void wolfSSHD_AuthInit(void) cachedFakeHashes[0][sizeof(cachedFakeHashes[0]) - 1] = '\0'; numCachedFakeHashes = 1; } + WS_FORCEZERO(tmpl, sizeof(tmpl)); } + WS_FORCEZERO(&rootStore, sizeof(rootStore)); } if (numCachedFakeHashes == 0) { @@ -803,18 +887,16 @@ static int CheckPasswordHashUnix(const char* input, const char* stored) if (hashedInput == NULL) { ret = WS_FATAL_ERROR; } - else if (locked) { - ret = WSSHD_AUTH_FAILURE; - } else { hashedInputSz = (word32)WSTRLEN(hashedInput); - if (hashedInputSz == 0 || hashedInput[0] == '*' || + if (locked || hashedInputSz == 0 || hashedInput[0] == '*' || hashedInputSz != storedSz || ConstantCompare((const byte*)hashedInput, (const byte*)stored, storedSz) != 0) { ret = WSSHD_AUTH_FAILURE; } + /* crypt() leaves the result in static storage */ WS_FORCEZERO(hashedInput, hashedInputSz); } } @@ -831,12 +913,12 @@ static int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz, WOLFS int ret = WS_SUCCESS; char* pwStr = NULL; struct passwd* pwInfo; + WOLFSSHD_PW pwStore; #ifdef HAVE_SHADOW struct spwd* shadowInfo; + WSSHD_SPWD spStore; time_t now; int expired = 0; - /* getspnam() returns a static buffer; copy immediately before it can - * be overwritten by any subsequent call. */ char hashBuf[WSSHD_FAKE_HASH_SZ]; #endif /* The hash of the user's password stored on the system. */ @@ -862,7 +944,7 @@ static int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz, WOLFS } if (ret == WS_SUCCESS) { - pwInfo = getpwnam((const char*)usr); + pwInfo = wolfSSHD_GetPwnam(usr, &pwStore); if (pwInfo == NULL) { /* User not found: use dummy hash to equalize timing. */ wolfSSH_Log(WS_LOG_INFO, @@ -871,11 +953,7 @@ static int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz, WOLFS else { #ifdef HAVE_SHADOW if (pwInfo->pw_passwd[0] == 'x') { -#ifdef WOLFSSH_HAVE_LIBCRYPT - shadowInfo = getspnam((const char*)usr); -#else - shadowInfo = getspnam((char*)usr); -#endif + shadowInfo = AuthGetSpnam(usr, &spStore); if (shadowInfo == NULL) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error getting user password info"); @@ -901,8 +979,6 @@ static int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz, WOLFS ret = WS_FATAL_ERROR; } else { - /* Copy before any subsequent getspnam() call - * overwrites the static buffer. */ XSTRNCPY(hashBuf, shadowInfo->sp_pwdp, sizeof(hashBuf)); hashBuf[sizeof(hashBuf) - 1] = '\0'; storedHash = hashBuf; @@ -959,7 +1035,9 @@ static int CheckPasswordUnix(const char* usr, const byte* pw, word32 pwSz, WOLFS } #ifdef HAVE_SHADOW WS_FORCEZERO(hashBuf, sizeof(hashBuf)); + WS_FORCEZERO(&spStore, sizeof(spStore)); #endif + wolfSSHD_PwFree(&pwStore); WOLFSSH_UNUSED(authCtx); return ret; @@ -1625,10 +1703,11 @@ WOLFSSHD_STATIC int SearchForPubKey(const char* path, static int CheckUserUnix(const char* name) { int ret = WSSHD_AUTH_FAILURE; struct passwd* pwInfo; + WOLFSSHD_PW pwStore; wolfSSH_Log(WS_LOG_INFO, "[SSHD] Unix check user"); errno = 0; - pwInfo = getpwnam(name); + pwInfo = wolfSSHD_GetPwnam(name, &pwStore); if (pwInfo == NULL) { if (errno != 0) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error calling getpwnam for user " @@ -1639,6 +1718,7 @@ static int CheckUserUnix(const char* name) { else { ret = WSSHD_AUTH_SUCCESS; } + wolfSSHD_PwFree(&pwStore); return ret; } @@ -1915,6 +1995,7 @@ static int CheckPublicKeyUnix(const char* name, { int ret = WSSHD_AUTH_SUCCESS; struct passwd* pwInfo; + WOLFSSHD_PW pwStore; #ifdef WOLFSSH_OSSH_CERTS if (pubKeyCtx->isOsshCert) { @@ -1933,7 +2014,7 @@ static int CheckPublicKeyUnix(const char* name, /* The requested user must be a real local account. */ if (ret == WSSHD_AUTH_SUCCESS) { errno = 0; - pwInfo = getpwnam((const char*)name); + pwInfo = wolfSSHD_GetPwnam(name, &pwStore); if (pwInfo == NULL) { if (errno != 0) { wolfSSH_Log(WS_LOG_ERROR, @@ -1974,7 +2055,7 @@ static int CheckPublicKeyUnix(const char* name, #endif /* WOLFSSH_OSSH_CERTS */ { errno = 0; - pwInfo = getpwnam((const char*)name); + pwInfo = wolfSSHD_GetPwnam(name, &pwStore); if (pwInfo == NULL) { if (errno != 0) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error calling getpwnam for user " @@ -1989,6 +2070,7 @@ static int CheckPublicKeyUnix(const char* name, wolfSSHD_ConfigGetStrictModes(authCtx->conf)); } } + wolfSSHD_PwFree(&pwStore); WOLFSSH_UNUSED(usrCaKeysFile); WOLFSSH_UNUSED(authCtx); @@ -2412,11 +2494,13 @@ static int IsRootUser(const char* usr) int isRoot = 0; #ifndef _WIN32 struct passwd* pwInfo; + WOLFSSHD_PW pwStore; - pwInfo = getpwnam(usr); + pwInfo = wolfSSHD_GetPwnam(usr, &pwStore); if ((pwInfo != NULL && pwInfo->pw_uid == 0) || XSTRCMP(usr, "root") == 0) { isRoot = 1; } + wolfSSHD_PwFree(&pwStore); #else if (XSTRCMP(usr, "root") == 0) { isRoot = 1; @@ -3163,6 +3247,7 @@ static int SetDefaultUserID(WOLFSSHD_AUTH* auth) return 0; #else struct passwd* pwInfo; + WOLFSSHD_PW pwStore; int ret = WS_SUCCESS; if (wolfSSHD_ConfigGetPrivilegeSeparation(auth->conf) == @@ -3174,7 +3259,8 @@ static int SetDefaultUserID(WOLFSSHD_AUTH* auth) return WS_SUCCESS; } - pwInfo = getpwnam(WOLFSSH_USER_STRING(WOLFSSH_SSHD_USER)); + pwInfo = wolfSSHD_GetPwnam(WOLFSSH_USER_STRING(WOLFSSH_SSHD_USER), + &pwStore); #ifdef WOLFSSHD_UNIT_TEST /* Fallback for unit tests. */ if (pwInfo == NULL) { @@ -3194,6 +3280,7 @@ static int SetDefaultUserID(WOLFSSHD_AUTH* auth) auth->sGid = getgid(); auth->sUid = getuid(); } + wolfSSHD_PwFree(&pwStore); return ret; #endif } @@ -3709,17 +3796,22 @@ WOLFSSHD_CONFIG* wolfSSHD_AuthGetUserConf(const WOLFSSHD_AUTH* auth, * Match Group directives do not apply here */ #else struct passwd* p_passwd; + WOLFSSHD_PW pwStore; + int grpRet; - p_passwd = getpwnam((const char *)usr); + p_passwd = wolfSSHD_GetPwnam(usr, &pwStore); if (p_passwd == NULL) { + wolfSSHD_PwFree(&pwStore); return NULL; } /* Resolve the full group set (primary and supplementary) so a * Match Group directive matches on any of the user's groups. * Fail closed if the groups cannot be enumerated. */ - if (wolfSSHD_GetUserGroupNames(auth->heap, usr, p_passwd->pw_gid, - &grpNames, &grpCount) != WS_SUCCESS) { + grpRet = wolfSSHD_GetUserGroupNames(auth->heap, usr, + p_passwd->pw_gid, &grpNames, &grpCount); + wolfSSHD_PwFree(&pwStore); + if (grpRet != WS_SUCCESS) { return NULL; } #endif diff --git a/apps/wolfsshd/auth.h b/apps/wolfsshd/auth.h index 7f6a80401..471a41efb 100644 --- a/apps/wolfsshd/auth.h +++ b/apps/wolfsshd/auth.h @@ -41,6 +41,27 @@ USER_NODE* AddNewUser(USER_NODE* list, byte type, const byte* username, word32 usernameSz, const byte* value, word32 valueSz); #endif +#ifndef _WIN32 +#include + +#ifndef WOLFSSHD_PW_BUF_SZ + #define WOLFSSHD_PW_BUF_SZ 4096 +#endif + +/* Caller-owned storage for an account lookup, so no copy of the entry is + * left in libc's static storage. */ +typedef struct WOLFSSHD_PW { + struct passwd pw; + char buf[WOLFSSHD_PW_BUF_SZ]; +} WOLFSSHD_PW; + +/* Looks up name into store. Returns &store->pw, or NULL when there is no such + * user (errno 0) or the lookup failed (errno set). Release the store with + * wolfSSHD_PwFree(), which wipes it, whatever the result. */ +struct passwd* wolfSSHD_GetPwnam(const char* name, WOLFSSHD_PW* store); +void wolfSSHD_PwFree(WOLFSSHD_PW* store); +#endif + int DefaultUserAuth(byte authType, WS_UserAuthData* authData, void* ctx); int DefaultUserAuthTypes(WOLFSSH* ssh, void* ctx); @@ -124,6 +145,7 @@ extern int (*wsshd_setregid_cb)(WGID_T, WGID_T); extern int (*wsshd_setreuid_cb)(WUID_T, WUID_T); extern int (*wsshd_setegid_cb)(WGID_T); extern int (*wsshd_seteuid_cb)(WUID_T); +/* NULL, the default, runs the real lookup; set, it replaces the lookup. */ extern struct passwd* (*wsshd_getpwnam_cb)(const char*); extern int (*wsshd_setgroups_cb)(int, const WGID_T*); #ifdef WOLFSSHD_HAVE_SHADOW diff --git a/apps/wolfsshd/test/test_configuration.c b/apps/wolfsshd/test/test_configuration.c index d8dc67f89..4ded133e3 100644 --- a/apps/wolfsshd/test/test_configuration.c +++ b/apps/wolfsshd/test/test_configuration.c @@ -18,6 +18,7 @@ #include #endif #ifndef _WIN32 + #include #include #include #endif @@ -2770,10 +2771,10 @@ static int test_AuthInit(void) wolfSSHD_SetCachedFakeHashForTest(NULL); - /* Force the real getspnam(), independent of what earlier tests left - * this global as. */ + /* Force the real lookup, independent of what earlier tests left this + * global as. */ savedGetspnam = wsshd_getspnam_cb; - wsshd_getspnam_cb = getspnam; + wsshd_getspnam_cb = NULL; rootShadow = getspnam("root"); if (rootShadow == NULL || rootShadow->sp_pwdp == NULL) { @@ -3030,6 +3031,85 @@ static int test_ScanShadowFile_truncatedLine(void) #endif /* WOLFSSHD_HAVE_SHADOW */ #endif /* WOLFSSH_HAVE_LIBCRYPT || WOLFSSH_HAVE_LIBLOGIN */ +#ifndef _WIN32 +/* The lookup fills the caller's store, and freeing it wipes the store. */ +static int test_GetPwnam(void) +{ + int ret = WS_SUCCESS; + struct passwd* (*savedGetpwnam)(const char*); + struct passwd* self; + struct passwd* pw; + WOLFSSHD_PW store; + char name[64]; + word32 i; + + savedGetpwnam = wsshd_getpwnam_cb; + wsshd_getpwnam_cb = NULL; + + self = getpwuid(getuid()); + if (self == NULL || self->pw_name == NULL || + WSTRLEN(self->pw_name) >= sizeof(name)) { + Log(" Skipping test: no passwd entry for this uid.\n"); + wsshd_getpwnam_cb = savedGetpwnam; + return WS_SUCCESS; + } + WSTRNCPY(name, self->pw_name, sizeof(name) - 1); + name[sizeof(name) - 1] = '\0'; + + Log(" Testing scenario: wolfSSHD_GetPwnam() for this user."); + pw = wolfSSHD_GetPwnam(name, &store); + if (pw != &store.pw || pw->pw_uid != getuid() || + WSTRCMP(pw->pw_name, name) != 0 || + (byte*)pw->pw_name < (byte*)store.buf || + (byte*)pw->pw_name >= (byte*)store.buf + sizeof(store.buf)) { + Log(" FAILED: entry not in the caller's store.\n"); + ret = WS_FATAL_ERROR; + } + wolfSSHD_PwFree(&store); + for (i = 0; ret == WS_SUCCESS && i < (word32)sizeof(store); i++) { + if (((byte*)&store)[i] != 0) { + Log(" FAILED: store not wiped.\n"); + ret = WS_FATAL_ERROR; + } + } + if (ret == WS_SUCCESS) { + Log(" PASSED.\n"); + Log(" Testing scenario: wolfSSHD_GetPwnam() for no such user."); + WMEMSET(&store, 0xA5, sizeof(store)); + errno = EIO; + if (wolfSSHD_GetPwnam("no_such_wolfsshd_user_xyz", &store) != NULL || + errno != 0) { + Log(" FAILED: expected NULL with errno 0.\n"); + ret = WS_FATAL_ERROR; + } + for (i = 0; ret == WS_SUCCESS && i < (word32)sizeof(store); i++) { + if (((byte*)&store)[i] != 0) { + Log(" FAILED: store not wiped.\n"); + ret = WS_FATAL_ERROR; + } + } + if (ret == WS_SUCCESS) { + Log(" PASSED.\n"); + } + wolfSSHD_PwFree(&store); + } + if (ret == WS_SUCCESS) { + Log(" Testing scenario: wolfSSHD_GetPwnam() with no name."); + errno = 0; + if (wolfSSHD_GetPwnam(NULL, &store) != NULL || errno == 0) { + Log(" FAILED: expected NULL with errno set.\n"); + ret = WS_FATAL_ERROR; + } + else { + Log(" PASSED.\n"); + } + } + + wsshd_getpwnam_cb = savedGetpwnam; + return ret; +} +#endif + #ifdef WOLFSSL_BASE64_ENCODE /* Build SSH wire-format public key blob. Returns size or 0 if too small. */ static word32 BuildWireKeyBlob(const char* type, const byte* payload, @@ -7364,6 +7444,9 @@ const TEST_CASE testCases[] = { TEST_DECL(test_AuthSetGroups_setgroups_fail), TEST_DECL(test_AuthSetGroups_getgrouplist_fail), #endif +#ifndef _WIN32 + TEST_DECL(test_GetPwnam), +#endif #if defined(WOLFSSH_HAVE_LIBCRYPT) || defined(WOLFSSH_HAVE_LIBLOGIN) TEST_DECL(test_CheckPasswordHashUnix), #ifdef WOLFSSHD_HAVE_SHADOW diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index 77d9a4457..b32481db3 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -4011,6 +4011,11 @@ static void* HandleConnection(void* arg) WOLFSSHD_CONFIG* usrConf; char* usr; const char* forcedCmd = NULL; + #ifndef _WIN32 + WOLFSSHD_PW pwStore; + + WMEMSET(&pwStore, 0, sizeof(pwStore)); + #endif /* get configuration for user */ usr = wolfSSH_GetUsername(ssh); @@ -4022,10 +4027,10 @@ static void* HandleConnection(void* arg) ret = WS_FATAL_ERROR; } - #ifndef WIN32 + #ifndef _WIN32 if (ret == WS_SUCCESS || ret == WS_SFTP_COMPLETE || ret == WS_SCP_INIT) { - pPasswd = getpwnam((const char *)usr); + pPasswd = wolfSSHD_GetPwnam(usr, &pwStore); if (pPasswd == NULL) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error getting user info"); ret = WS_FATAL_ERROR; @@ -4169,6 +4174,9 @@ static void* HandleConnection(void* arg) } wolfSSHD_ConfigFree(usrConf); + #ifndef _WIN32 + wolfSSHD_PwFree(&pwStore); + #endif } error = wolfSSH_get_error(ssh); diff --git a/configure.ac b/configure.ac index 9100e4097..6a3f4045a 100644 --- a/configure.ac +++ b/configure.ac @@ -507,6 +507,16 @@ AS_IF([test "x$ENABLED_SSHD" = "xyes"],[ [AC_MSG_RESULT([no]) AC_MSG_ERROR(libcrypt not found)])]) ]) + # Only the five-argument glibc/musl form; QNX and Solaris have four. + AC_MSG_CHECKING([for getspnam_r]) + AC_LINK_IFELSE( + [AC_LANG_PROGRAM([[#include ]], + [[struct spwd sp; struct spwd* res; char buf[64]; + int rc = getspnam_r("", &sp, buf, sizeof(buf), &res); + (void)rc;]])], + [AC_DEFINE([HAVE_GETSPNAM_R], [1], [five-argument getspnam_r() available]) + AC_MSG_RESULT([yes])], + [AC_MSG_RESULT([no])]) ]) AC_CONFIG_LINKS([keys/gretel-key-rsa.pub:keys/gretel-key-rsa.pub From 0f18a6ca61790b90df0ed4d5485223d1a430d3b3 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 21 Oct 2026 09:28:40 -0700 Subject: [PATCH 5/7] wolfsshd: wipe Windows shell command lines The wide-character copies of the peer's command are zeroed before they are freed, in SHELL_Subsystem() and in SetupConsole(). A CreateProcessW() failure in SetupConsole() now runs its cleanup before returning WS_FATAL_ERROR. Issue: F-8851, F-10596 --- apps/wolfsshd/wolfsshd.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index b32481db3..37357f149 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -2205,6 +2205,7 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, } if (tmp != NULL) { + WS_FORCEZERO(tmp, (word32)(sizeof(wchar_t) * cmdSz)); WFREE(tmp, NULL, DYNTYPE_SSHD); } @@ -2611,6 +2612,7 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, cleanup: if (conCmdPtr != NULL) { + WS_FORCEZERO(conCmdPtr, (word32)(sizeof(wchar_t) * conCmdSz)); WFREE(conCmdPtr, NULL, DYNTYPE_SSHD); } if (cnslIn != NULL) { @@ -2635,6 +2637,7 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, wolfSSHD_AuthCloseToken(conn->auth); } if (cmd != NULL) { + WS_FORCEZERO(cmd, (word32)(sizeof(wchar_t) * cmdSz)); WFREE(cmd, NULL, DYNTYPE_SSHD); } WS_FORCEZERO(channelBuffer, sizeof channelBuffer); @@ -5079,7 +5082,8 @@ static int SetupConsole(char* inCmd) if (CreateProcessW(NULL, cmd, NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &ext.StartupInfo, &processInfo) != TRUE) { - return WS_FATAL_ERROR; + /* the return value, once cleanup has run */ + processState = (DWORD)WS_FATAL_ERROR; } else { DWORD ava = 0; @@ -5117,6 +5121,7 @@ static int SetupConsole(char* inCmd) } if (cmd != NULL) { + WS_FORCEZERO(cmd, sizeof(wchar_t) * cmdSz); HeapFree(GetProcessHeap(), 0, cmd); } From 99c56f091fa60fb2c3f7dcbca0357200e0ad8eee Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 21 Oct 2026 09:28:45 -0700 Subject: [PATCH 6/7] wolfsshd: wipe host key copies left behind The shell child frees its session and the server context before it drops to the user, so the host private key and the session keys are wiped from its image ahead of the exec. It keeps copies of the user name and command it still needs. A short read of a key file wipes the buffer before freeing it. Issue: F-8841, F-14227 --- apps/wolfsshd/wolfsshd.c | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index 37357f149..b5c240838 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -438,6 +438,8 @@ static byte* getBufferFromFile(const char* fileName, word32* bufSz, void* heap, readSz = (word32)WFREAD(NULL, buf, 1, fileSz, file); if (readSz < (size_t)fileSz) { WFCLOSE(NULL, file); + /* whatever part of a key was read */ + WS_FORCEZERO(buf, (word32)fileSz + 1); WFREE(buf, heap, DYNTYPE_SSHD); return NULL; } @@ -2962,11 +2964,33 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, const char *args[] = {"-sh", NULL, NULL, NULL}; char cmd[MAX_COMMAND_SZ]; char shell[MAX_COMMAND_SZ]; + char* userName; int ret; signal(SIGINT, SIG_DFL); signal(SIGCHLD, SIG_DFL); + /* The shell needs none of the session, so drop the host key and the + * session keys this image inherited before giving up root. The + * command can live in the session, so keep a copy of it. */ + userName = WSTRDUP(wolfSSH_GetUsername(ssh), NULL, DYNTYPE_SSHD); + if (forcedCmd != NULL) { + forcedCmd = WSTRDUP(forcedCmd, NULL, DYNTYPE_SSHD); + if (forcedCmd == NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Error copying forced command"); + exit(1); + } + } + if (userName == NULL) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error copying user name"); + exit(1); + } + wolfSSH_free(ssh); + ssh = NULL; + wolfSSH_CTX_free(conn->ctx); + conn->ctx = NULL; + if (!ptyReq || forcedCmd) { close(stdoutPipe[0]); close(stderrPipe[0]); @@ -2993,7 +3017,7 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, } /* set additional groups if needed */ - if (wolfSSHD_AuthSetGroups(conn->auth, wolfSSH_GetUsername(ssh), + if (wolfSSHD_AuthSetGroups(conn->auth, userName, pPasswd->pw_gid) != WS_SUCCESS) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error setting groups"); exit(1); From 03173659fe015f7102be0e21b07be50aa976240a Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 21 Oct 2026 10:15:32 -0700 Subject: [PATCH 7/7] wolfsshd: hash passwords with crypt_r() if found CheckPasswordHashUnix() hashes into a heap struct crypt_data when configure finds crypt_r(), and wipes the whole struct on return, so neither the result nor crypt's working state is left behind. Builds without it, such as macOS, keep crypt() and its result wipe. - configure probes crypt_r() through as HAVE_CRYPT_R Issue: F-8842 --- apps/wolfsshd/auth.c | 44 +++++++++++++++++++++++++++++++++++++++++--- configure.ac | 10 ++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index 8171a7599..42720cbfe 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -75,6 +75,9 @@ #include #include #include +#ifdef HAVE_CRYPT_R +#include +#endif #include #include #include @@ -808,6 +811,13 @@ static int ExtractSalt(char* hash, char** salt, int saltSz) #endif #if defined(WOLFSSH_HAVE_LIBCRYPT) || defined(WOLFSSH_HAVE_LIBLOGIN) +#ifdef HAVE_CRYPT_R + /* the result lives in data, which the caller wipes */ + #define WSSHD_CRYPT(in, salt, data) crypt_r((in), (salt), (data)) +#else + #define WSSHD_CRYPT(in, salt, data) crypt((in), (salt)) +#endif + #ifdef WOLFSSHD_UNIT_TEST int CheckPasswordHashUnix(const char* input, const char* stored) #else @@ -830,6 +840,12 @@ static int CheckPasswordHashUnix(const char* input, const char* stored) int locked = 0; char* hashedInput = NULL; word32 hashedInputSz = 0, storedSz = 0; +#ifdef HAVE_CRYPT_R + /* ~128 KB on glibc, ~32 KB on libxcrypt, so not on the stack */ + struct crypt_data* cryptData = NULL; +#else + void* cryptData = NULL; +#endif if (input == NULL || stored == NULL) { ret = WS_BAD_ARGUMENT; @@ -843,6 +859,20 @@ static int CheckPasswordHashUnix(const char* input, const char* stored) return ret; } +#ifdef HAVE_CRYPT_R + if (ret == WSSHD_AUTH_SUCCESS) { + cryptData = (struct crypt_data*)WMALLOC(sizeof(*cryptData), NULL, + DYNTYPE_SSHD); + if (cryptData == NULL) { + ret = WS_MEMORY_E; + } + else { + /* zeroed, as crypt_r() requires of a fresh crypt_data */ + WMEMSET(cryptData, 0, sizeof(*cryptData)); + } + } +#endif + if (ret == WSSHD_AUTH_SUCCESS) { const char* salt = stored; @@ -870,7 +900,7 @@ static int CheckPasswordHashUnix(const char* input, const char* stored) } } - hashedInput = crypt(input, salt); + hashedInput = WSSHD_CRYPT(input, salt, cryptData); /* glibc signals an unsupported salt with a "*"-prefixed sentinel, * not NULL; check both so the fallback engages on every libc. */ if (locked) { @@ -880,7 +910,7 @@ static int CheckPasswordHashUnix(const char* input, const char* stored) && (hashedInput == NULL || hashedInput[0] == '*'); fbIdx++) { salt = fakeHashFallbacks[fbIdx]; - hashedInput = crypt(input, salt); + hashedInput = WSSHD_CRYPT(input, salt, cryptData); } } @@ -896,10 +926,18 @@ static int CheckPasswordHashUnix(const char* input, const char* stored) (const byte*)stored, storedSz) != 0) { ret = WSSHD_AUTH_FAILURE; } - /* crypt() leaves the result in static storage */ + /* the result is in crypt()'s static storage or in cryptData */ WS_FORCEZERO(hashedInput, hashedInputSz); } } +#ifdef HAVE_CRYPT_R + if (cryptData != NULL) { + WS_FORCEZERO(cryptData, sizeof(*cryptData)); + WFREE(cryptData, NULL, DYNTYPE_SSHD); + } +#else + WOLFSSH_UNUSED(cryptData); +#endif return ret; } #endif /* WOLFSSH_HAVE_LIBCRYPT || WOLFSSH_HAVE_LIBLOGIN */ diff --git a/configure.ac b/configure.ac index 6a3f4045a..50bca48c7 100644 --- a/configure.ac +++ b/configure.ac @@ -506,6 +506,16 @@ AS_IF([test "x$ENABLED_SSHD" = "xyes"],[ AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_HAVE_LIBCRYPT"], [AC_MSG_RESULT([no]) AC_MSG_ERROR(libcrypt not found)])]) + # crypt_r() keeps the hash in a buffer the daemon can wipe. Probe through + # so a positive result is build-safe; macOS has neither. + AC_MSG_CHECKING([for crypt_r]) + AC_LINK_IFELSE( + [AC_LANG_PROGRAM([[#include ]], + [[struct crypt_data d; d.initialized = 0; + (void)crypt_r("", "", &d);]])], + [AC_DEFINE([HAVE_CRYPT_R], [1], [crypt_r() available and declared]) + AC_MSG_RESULT([yes])], + [AC_MSG_RESULT([no])]) ]) # Only the five-argument glibc/musl form; QNX and Solaris have four. AC_MSG_CHECKING([for getspnam_r])