From 966361816daac851a5811262a34c04788a28e059 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Thu, 27 Aug 2026 14:41:58 -0700 Subject: [PATCH] wolfsshd: stop polling when the SFTP channel has nothing buffered wolfSSH_stream_peek() returns 0 for a live channel with an empty buffer, which is the ordinary idle case. None of the arms after the peek match that, so the loop falls through with the timeout still at TEST_SFTP_TIMEOUT_NONE and tcp_select() returns on its 100 us floor. An idle SFTP session keeps a core busy for as long as it stays connected. - take the peek's zero return as "nothing to do" and let the next select wait a second, the same value the want-read paths already use - sshd_sftp_idle_cpu_test.sh parks an idle SFTP session on the daemon and reads the connection process's CPU time out of /proc, failing if it spends 5 ticks or more over ten seconds The measurement the test automates: 21 ticks per 10 seconds before, 0 after. It skips where there is no /proc or no local daemon to measure. --- apps/wolfsshd/test/run_all_sshd_tests.sh | 1 + apps/wolfsshd/test/sshd_sftp_idle_cpu_test.sh | 95 +++++++++++++++++++ apps/wolfsshd/wolfsshd.c | 6 ++ 3 files changed, 102 insertions(+) create mode 100755 apps/wolfsshd/test/sshd_sftp_idle_cpu_test.sh diff --git a/apps/wolfsshd/test/run_all_sshd_tests.sh b/apps/wolfsshd/test/run_all_sshd_tests.sh index e9fa66a92..d20acb406 100755 --- a/apps/wolfsshd/test/run_all_sshd_tests.sh +++ b/apps/wolfsshd/test/run_all_sshd_tests.sh @@ -8,6 +8,7 @@ test_cases=( "sshd_term_size_test.sh" "sshd_large_sftp_test.sh" "sshd_bad_sftp_test.sh" + "sshd_sftp_idle_cpu_test.sh" "sshd_scp_fail.sh" "sshd_term_close_test.sh" "ssh_kex_algos.sh" diff --git a/apps/wolfsshd/test/sshd_sftp_idle_cpu_test.sh b/apps/wolfsshd/test/sshd_sftp_idle_cpu_test.sh new file mode 100755 index 000000000..722c24a4a --- /dev/null +++ b/apps/wolfsshd/test/sshd_sftp_idle_cpu_test.sh @@ -0,0 +1,95 @@ +#!/bin/sh + +# An idle SFTP session must not keep the server busy. wolfSSHd's SFTP loop +# polls at the select() floor whenever wolfSSH_stream_peek() reports an empty +# channel, so before the timeout was raised on that path a connected client +# that simply sat there cost most of a core for as long as it stayed open. + +ROOT_PWD=$(pwd) +. ./wolfssh_options.sh +cd ../../.. + +TEST_SFTP_CLIENT="./examples/sftpclient/wolfsftp" +PRIVATE_KEY="./keys/hansel-key-ecc.der" +PUBLIC_KEY="./keys/hansel-key-ecc.pub" + +SAMPLE_SECONDS=10 +# Ticks of CPU the connection process may use while idle. +# The poll this guards against measured 16 per 10 seconds; a server that +# waits properly measures 0. +MAX_TICKS=5 + +FIFO="/tmp/wolfssh_idle_stdin_$$" +HOLDER="" +CLIENT="" + +cleanup() { + [ -n "$CLIENT" ] && kill "$CLIENT" 2>/dev/null + [ -n "$HOLDER" ] && kill "$HOLDER" 2>/dev/null + rm -f "$FIFO" +} + +# tear down on every exit path, including an interrupt during the ten second +# measurement, so no FIFO or client is left behind +trap cleanup EXIT + +if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then + echo "expecting host, port and user as arguments" + echo "$0 127.0.0.1 22222 $USER" + exit 1 +fi + +if ! wolfssh_has SFTP || [ ! -x "$TEST_SFTP_CLIENT" ]; then + echo "SFTP client not available in this build, skipping" + exit 77 +fi + +# The measurement reads the server's CPU time out of /proc, so it only works +# against a local daemon on a system that has one. +if [ ! -r /proc/self/stat ]; then + echo "no /proc on this system, skipping" + exit 77 +fi + +PIDS_BEFORE=$(pgrep wolfsshd | sort) +if [ -z "$PIDS_BEFORE" ]; then + echo "no local wolfsshd to measure, skipping" + exit 77 +fi + +# Hold a session open without sending a single request. The client takes its +# commands from this pipe, and nothing ever writes one; the sleep bounds how +# long the pipe stays open so no part of this outlives the test. +mkfifo "$FIFO" || exit 1 +sleep $((SAMPLE_SECONDS + 30)) > "$FIFO" & +HOLDER=$! +"$TEST_SFTP_CLIENT" -u "$3" -i "$PRIVATE_KEY" -j "$PUBLIC_KEY" \ + -h "$1" -p "$2" < "$FIFO" > /dev/null 2>&1 & +CLIENT=$! +sleep 5 + +PIDS_AFTER=$(pgrep wolfsshd | sort) +CHILD=$(printf '%s\n%s\n' "$PIDS_BEFORE" "$PIDS_AFTER" | sort | uniq -u | head -1) +if [ -z "$CHILD" ] || [ ! -r "/proc/$CHILD/stat" ]; then + echo "Expecting another wolfSSHd pid after connection" + exit 1 +fi + +# Fields 14 and 15 of /proc//stat are utime and stime, in clock ticks. +BEFORE=$(awk '{print $14+$15}' "/proc/$CHILD/stat") +sleep "$SAMPLE_SECONDS" +if [ ! -r "/proc/$CHILD/stat" ]; then + echo "Connection process $CHILD exited while idle" + exit 1 +fi +AFTER=$(awk '{print $14+$15}' "/proc/$CHILD/stat") +USED=$((AFTER-BEFORE)) + +echo "idle connection used $USED ticks over $SAMPLE_SECONDS seconds" +if [ "$USED" -ge "$MAX_TICKS" ]; then + echo "Expecting an idle SFTP session to cost under $MAX_TICKS ticks" + exit 1 +fi + +cd "$ROOT_PWD" +exit 0 diff --git a/apps/wolfsshd/wolfsshd.c b/apps/wolfsshd/wolfsshd.c index d86ac426f..4f9555d30 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -967,6 +967,12 @@ static int SFTP_Subsystem(WOLFSSHD_CONNECTION* conn, WOLFSSH* ssh, if (error == WS_EOF) break; } + else { + /* Channel is live with nothing buffered. Let the next select + * block instead of polling at its 100 us floor. */ + timeout = TEST_SFTP_TIMEOUT; + continue; + } if (ret == WS_FATAL_ERROR && error == 0) { WOLFSSH_CHANNEL* channel =