From a1603e813a387ccba046658b5ed6632ad3b29ce2 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 21 Oct 2026 09:14:30 -0700 Subject: [PATCH 1/3] 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 fails on a pipe and sets ECHO on a pty Issue: F-10546 --- apps/wolfsshd/wolfsshd.c | 5 +++- src/internal.c | 6 ++-- tests/api.c | 63 ++++++++++++++++++++++++++++++++++++++++ wolfssh/ssh.h | 2 ++ 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index 602e9db55..4b52d3dc2 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 20e13e225..800802286 100644 --- a/src/internal.c +++ b/src/internal.c @@ -13272,7 +13272,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) { @@ -13511,7 +13512,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..8d77d336b 100644 --- a/tests/api.c +++ b/tests/api.c @@ -8034,6 +8034,68 @@ static void test_wolfSSH_ConvertConsole(void) } +#if defined(WOLFSSH_TERM) && !defined(NO_TERMIOS) && \ + defined(HAVE_SYS_IOCTL_H) && !defined(USE_WINDOWS_API) +#include +#include +#ifdef HAVE_PTY_H + #include +#endif +#ifdef HAVE_UTIL_H + #include +#endif + +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]); + +#if defined(HAVE_PTY_H) || defined(HAVE_UTIL_H) + /* a pty is a terminal, so the modes land on it */ + { + static const byte echoOff[] = { + WOLFSSH_ECHO, 0x00, 0x00, 0x00, 0x00, + WOLFSSH_TTY_OP_END + }; + struct termios term; + int master, slave; + + if (openpty(&master, &slave, NULL, NULL, NULL) != 0) { + return; /* no pty to be had here */ + } + AssertIntEQ(tcgetattr(slave, &term), 0); + term.c_lflag &= ~ECHO; + AssertIntEQ(tcsetattr(slave, TCSANOW, &term), 0); + + AssertIntEQ(wolfSSH_DoModes(modes, sizeof(modes), slave), 0); + AssertIntEQ(tcgetattr(slave, &term), 0); + AssertTrue((term.c_lflag & ECHO) != 0); + + AssertIntEQ(wolfSSH_DoModes(echoOff, sizeof(echoOff), slave), 0); + AssertIntEQ(tcgetattr(slave, &term), 0); + AssertTrue((term.c_lflag & ECHO) == 0); + + close(slave); + close(master); + } +#endif +} +#else +static void test_wolfSSH_DoModes(void) { ; } +#endif + + static void test_wstrcat(void) { #ifndef WSTRING_USER @@ -9294,6 +9356,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 57a4e253e..6c7585e5b 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -1026,6 +1026,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 0cd0d59e664c123d5d812dbed6a801e663bac586 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 21 Oct 2026 09:15:47 -0700 Subject: [PATCH 2/3] 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 - -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 | 66 +++++++++++++++++-- 3 files changed, 132 insertions(+), 7 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 4b52d3dc2..edeb027c1 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,42 @@ 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 { + /* EPERM while the child is still setting up at the raised + * privilege level. Closing the pty below hangs it up. */ + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Unable to kill child process %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 @@ -3048,18 +3086,21 @@ 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); 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); return WS_FATAL_ERROR; } @@ -3564,6 +3605,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 +3647,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 +5105,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 54c8f073daa0b76c04ac1b3c7e77b316803ff4e1 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Wed, 21 Oct 2026 09:16:25 -0700 Subject: [PATCH 3/3] 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. Both log 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 | 62 ++++++++++++++++++++++++ apps/wolfsshd/wolfsshd.c | 30 ++++++++++-- 3 files changed, 89 insertions(+), 4 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..e06a61f8a --- /dev/null +++ b/apps/wolfsshd/test/sshd_scp_home_test.sh @@ -0,0 +1,62 @@ +#!/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 + +dd if=/dev/urandom of="$OUTDIR/$NAME" bs=1024 count=64 2>/dev/null + +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 edeb027c1..56d739fab 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -1772,6 +1772,19 @@ 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 */ if (ImpersonateLoggedOnUser(wolfSSHD_GetAuthToken(conn->auth)) == FALSE) { @@ -2947,6 +2960,7 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, char cmd[MAX_COMMAND_SZ]; char shell[MAX_COMMAND_SZ]; int ret; + int chrooted; signal(SIGINT, SIG_DFL); signal(SIGCHLD, SIG_DFL); @@ -2984,11 +2998,12 @@ static int SHELL_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, } rc = SetupChroot(usrConf); + chrooted = (rc == 1); if (rc < 0) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error setting chroot"); exit(1); } - else if (rc == 1) { + else if (chrooted) { rc = chdir("/"); if (rc != 0) { wolfSSH_Log(WS_LOG_ERROR, @@ -3024,9 +3039,16 @@ 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); + /* not error'ing out if unable to find home directory. Like + * OpenSSH, a chroot without the home dir falls back to "/" + * quietly. */ + if (!chrooted) { + 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 */