mirror of https://github.com/wolfSSL/wolfssh.git
wolfsshd: keep accepting after a failed fork
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-16052pull/1352/head
parent
9281b03898
commit
8c3640db44
|
|
@ -71,6 +71,8 @@ apps/wolfsshd/wolfsshd
|
|||
apps/wolfsshd/test/test_configuration
|
||||
apps/wolfsshd/test/sshd_privdrop_preload.so
|
||||
apps/wolfsshd/test/privdrop_client_*.log
|
||||
apps/wolfsshd/test/sshd_fork_preload.so
|
||||
apps/wolfsshd/test/fork_client_*.log
|
||||
apps/wolfsshd/test/log.txt
|
||||
apps/wolfsshd/test/sshd_config_*
|
||||
apps/wolfsshd/test/authorized_keys_test
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ extra_test_cases=(
|
|||
"sshd_login_grace_test.sh"
|
||||
"sshd_privdrop_fail_test.sh"
|
||||
"sshd_chroot_fail_test.sh"
|
||||
"sshd_fork_fail_test.sh"
|
||||
"sshd_x509_test.sh"
|
||||
"sshd_x509_upn_fail.sh"
|
||||
"sshd_mldsa_composite_test.sh"
|
||||
|
|
@ -127,7 +128,10 @@ done
|
|||
# +0 shared wolfSSHd (TEST_PORT) +3 host key ownership/symlink gate
|
||||
# +1 StrictModes negative test +4 OpenSSH certificate test
|
||||
# +2 AuthorizedUPNDomains negative +5 privilege-drop test
|
||||
# +6 chroot skip-after-failure test
|
||||
# +6 chroot skip-after-failure test +7 fork failure test
|
||||
#
|
||||
# That fills the PORT_BLOCK_SIZE (8) ports of a block; another private-daemon
|
||||
# port needs a larger PORT_BLOCK_SIZE in port_lease.sh.
|
||||
#
|
||||
# The range deliberately starts above everything else in the repo that binds
|
||||
# a port: CI steps bind 22222, 22225 and 22226, and scripts/fwd-bulk.test
|
||||
|
|
@ -165,6 +169,7 @@ HOSTKEY_PERM_PORT=$((PORT_BASE + 3))
|
|||
export WOLFSSHD_TEST_PORT=$((PORT_BASE + 4))
|
||||
export WOLFSSHD_PRIVDROP_PORT=$((PORT_BASE + 5))
|
||||
export WOLFSSHD_CHROOT_PORT=$((PORT_BASE + 6))
|
||||
export WOLFSSHD_FORK_PORT=$((PORT_BASE + 7))
|
||||
|
||||
# Registry of the daemons started during this run, appended to by
|
||||
# start_wolfsshd in every test script that sources start_sshd.sh. The exit
|
||||
|
|
@ -766,6 +771,7 @@ local_sshd_tests=(
|
|||
"sshd_login_grace_test.sh"
|
||||
"sshd_privdrop_fail_test.sh"
|
||||
"sshd_chroot_fail_test.sh"
|
||||
"sshd_fork_fail_test.sh"
|
||||
)
|
||||
if [ "$USING_LOCAL_HOST" == 1 ]; then
|
||||
for test in "${local_sshd_tests[@]}"; do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,171 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Accept-loop regression: a fork() that fails for one connection must drop that
|
||||
# connection only. The daemon used to store the failure in its accept loop's
|
||||
# control variable and shut down, so one EAGAIN at a process limit stopped it
|
||||
# listening for good. The failure is injected against the stock wolfsshd with
|
||||
# an LD_PRELOAD interposer (sshd_fork_preload.c) that fails the first
|
||||
# connection's fork() once.
|
||||
|
||||
if [ -z "$1" ] || [ -z "$2" ]; then
|
||||
echo "expecting host and the runner's shared port as arguments;"
|
||||
echo "this test binds WOLFSSHD_FORK_PORT, or that port + 7"
|
||||
echo "./sshd_fork_fail_test.sh 127.0.0.1 22300"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Not PWD: that is bash's own variable and the shell rewrites it on every cd,
|
||||
# so a saved copy is gone by the time it is read.
|
||||
TESTDIR=`pwd`
|
||||
USER=`whoami`
|
||||
TEST_HOST="$1"
|
||||
|
||||
# Own daemon on a port leased by the runner, so the isolation holds against
|
||||
# another run of the suite on the same host. Standalone, fall back to an
|
||||
# offset from the port passed in.
|
||||
TEST_PORT="${WOLFSSHD_FORK_PORT:-$(( $2 + 7 ))}"
|
||||
|
||||
SSHD_BIN="../wolfsshd"
|
||||
if [ ! -x "$SSHD_BIN" ]; then
|
||||
echo "SKIP: $SSHD_BIN not built"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
# macOS strips DYLD_INSERT_LIBRARIES from the sudo-launched daemon, so Linux only.
|
||||
if [ "`uname -s`" != "Linux" ]; then
|
||||
echo "SKIP: fork fault injection needs Linux LD_PRELOAD"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
# start_sshd.sh word-splits SSHD_ENV to build "sudo env NAME=VALUE ...", so a
|
||||
# preload path holding whitespace would be torn in half.
|
||||
case "$TESTDIR" in
|
||||
*[[:space:]]*)
|
||||
echo "SKIP: test directory path contains whitespace"
|
||||
exit 77
|
||||
;;
|
||||
esac
|
||||
|
||||
ROOT="$TESTDIR/../../.."
|
||||
TEST_CLIENT="$ROOT/examples/client/client"
|
||||
if [ ! -x "$TEST_CLIENT" ]; then
|
||||
echo "SKIP: $TEST_CLIENT not built"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
# Absolute: the example client calls ChangeToWolfSshRoot() before parsing
|
||||
# anything, so a relative path would be resolved from the repository root.
|
||||
PRIVATE_KEY="$ROOT/keys/hansel-key-ecc.der"
|
||||
PUBLIC_KEY="$ROOT/keys/hansel-key-ecc.pub"
|
||||
|
||||
source ./start_sshd.sh
|
||||
|
||||
# Teardown on every exit path; log.txt and the client logs are kept for
|
||||
# debugging like the other tests.
|
||||
cleanup() {
|
||||
stop_wolfsshd
|
||||
rm -f sshd_config_test_fork "$PRELOAD_LIB"
|
||||
return 0
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Build the interposer next to this script; skip if no compiler. Relative "./"
|
||||
# path so the LD_PRELOAD value has no space (see SSHD_ENV in start_sshd.sh).
|
||||
PRELOAD_SRC="sshd_fork_preload.c"
|
||||
PRELOAD_LIB="./sshd_fork_preload.so"
|
||||
CC_BIN="${CC:-cc}"
|
||||
if ! "$CC_BIN" -shared -fPIC -o "$PRELOAD_LIB" "$PRELOAD_SRC" -ldl 2>/dev/null; then
|
||||
echo "SKIP: could not build $PRELOAD_LIB with $CC_BIN"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
# The runner creates this in its setup step; a standalone run does not get that.
|
||||
if [ ! -f authorized_keys_test ]; then
|
||||
./create_authorized_test_file.sh || exit 1
|
||||
fi
|
||||
|
||||
touch log.txt 2>/dev/null
|
||||
|
||||
cat <<EOF > sshd_config_test_fork
|
||||
Port $TEST_PORT
|
||||
Protocol 2
|
||||
LoginGraceTime 600
|
||||
PermitRootLogin yes
|
||||
PasswordAuthentication yes
|
||||
PermitEmptyPasswords no
|
||||
UsePrivilegeSeparation no
|
||||
UseDNS no
|
||||
HostKey $ROOT/keys/server-key.pem
|
||||
AuthorizedKeysFile $TESTDIR/authorized_keys_test
|
||||
EOF
|
||||
|
||||
# Read through sudo: the daemon runs under sudo, so log.txt may be root-owned.
|
||||
log_count() {
|
||||
local n
|
||||
n=`sudo grep -c "$1" log.txt 2>/dev/null`
|
||||
echo "${n:-0}"
|
||||
}
|
||||
|
||||
DEADLINE=30
|
||||
TIMEOUT=""
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
TIMEOUT="timeout $DEADLINE"
|
||||
fi
|
||||
|
||||
# Runs one exec session; output in $CLIENT_LOG, exit status in $CLIENT_RET.
|
||||
run_client() {
|
||||
CLIENT_LOG="$TESTDIR/fork_client_$1.log"
|
||||
$TIMEOUT "$TEST_CLIENT" -q -c "echo $TOKEN" -u "$USER" -i "$PRIVATE_KEY" \
|
||||
-j "$PUBLIC_KEY" -h "$TEST_HOST" -p "$TEST_PORT" \
|
||||
> "$CLIENT_LOG" 2>&1
|
||||
CLIENT_RET=$?
|
||||
}
|
||||
|
||||
client_said() {
|
||||
echo " client said: `tail -n 3 "$CLIENT_LOG" | tr '\n' '|'`"
|
||||
echo " full client output in $CLIENT_LOG"
|
||||
}
|
||||
|
||||
# Echoed by the exec session. Present only when a shell actually ran.
|
||||
TOKEN="wolfsshd_fork_ok_$$"
|
||||
|
||||
BEFORE_FORK_FAIL=`log_count "Issue spawning new process"`
|
||||
|
||||
SSHD_ENV="LD_PRELOAD=$PRELOAD_LIB WOLFSSHD_FAULT_FORK=1"
|
||||
export SSHD_BIN SSHD_ENV
|
||||
start_wolfsshd "sshd_config_test_fork"
|
||||
if [ -z "$PID" ]; then
|
||||
echo "FAIL: fork-fault daemon did not start"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# First connection: its fork() is failed, so the daemon drops it. The client's
|
||||
# own result does not matter, only that the daemon reached the failure.
|
||||
run_client "faulted"
|
||||
AFTER_FORK_FAIL=`log_count "Issue spawning new process"`
|
||||
if [ "$AFTER_FORK_FAIL" -le "$BEFORE_FORK_FAIL" ]; then
|
||||
echo "FAIL: injected fork() failure never reached the daemon"
|
||||
client_said
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The daemon must still be up. Give a daemon that is on its way out the time to
|
||||
# finish exiting, so this does not pass on a race.
|
||||
sleep 1
|
||||
if ! wolfsshd_alive "$PID"; then
|
||||
echo "FAIL: daemon exited after one connection's fork() failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Second connection: the interposer forwards this fork(), so the session must
|
||||
# run end to end.
|
||||
run_client "after"
|
||||
if [ "$CLIENT_RET" -ne 0 ] || ! grep -q "$TOKEN" "$CLIENT_LOG"; then
|
||||
echo "FAIL: daemon did not serve a connection after a failed fork()"
|
||||
client_said
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f "$TESTDIR"/fork_client_*.log
|
||||
echo "PASS: daemon keeps accepting after a failed fork()"
|
||||
exit 0
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/* sshd_fork_preload.c
|
||||
*
|
||||
* Copyright (C) 2014-2026 wolfSSL Inc.
|
||||
*
|
||||
* This file is part of wolfSSH.
|
||||
*
|
||||
* wolfSSH is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* wolfSSH is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with wolfSSH. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* LD_PRELOAD interposer for sshd_fork_fail_test.sh only. */
|
||||
/* When WOLFSSHD_FAULT_FORK is set, the first fork() after the first accept()
|
||||
* returns EAGAIN, as it does at a process limit. Keyed on accept() so the two
|
||||
* forks made while daemonizing are left alone. One shot: every later fork,
|
||||
* including those in connection children, is forwarded. */
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <dlfcn.h>
|
||||
|
||||
static int wsshd_accepted = 0;
|
||||
static int wsshd_failed = 0;
|
||||
|
||||
int accept(int fd, struct sockaddr* addr, socklen_t* addrLen)
|
||||
{
|
||||
int (*real)(int, struct sockaddr*, socklen_t*);
|
||||
int ret;
|
||||
|
||||
real = (int (*)(int, struct sockaddr*, socklen_t*))
|
||||
dlsym(RTLD_NEXT, "accept");
|
||||
if (real == NULL) {
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
ret = real(fd, addr, addrLen);
|
||||
if (ret >= 0) {
|
||||
wsshd_accepted++;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
pid_t fork(void)
|
||||
{
|
||||
pid_t (*real)(void);
|
||||
|
||||
if (getenv("WOLFSSHD_FAULT_FORK") != NULL && wsshd_accepted == 1
|
||||
&& !wsshd_failed) {
|
||||
wsshd_failed = 1;
|
||||
errno = EAGAIN;
|
||||
return -1;
|
||||
}
|
||||
real = (pid_t (*)(void))dlsym(RTLD_NEXT, "fork");
|
||||
if (real == NULL) {
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
return real();
|
||||
}
|
||||
|
|
@ -4818,8 +4818,10 @@ static int StartSSHD(int argc, char** argv)
|
|||
conn->auth = connAuth;
|
||||
}
|
||||
#endif
|
||||
ret = NewConnection(conn);
|
||||
if (ret != WS_SUCCESS) {
|
||||
/* Not assigned to ret: a failed fork() or CreateThread()
|
||||
* belongs to this connection only, and ret is the loop
|
||||
* condition, so it would stop the daemon accepting. */
|
||||
if (NewConnection(conn) != WS_SUCCESS) {
|
||||
/* on failure the connection was not handed off, it is
|
||||
still owned here and needs cleaned up */
|
||||
WCLOSESOCKET(conn->fd);
|
||||
|
|
|
|||
Loading…
Reference in New Issue