Skip to content

wolfsshd: bound the shell child reap and flush the held backlog - #1276

Open
yosuke-wolfssl wants to merge 1 commit into
wolfSSL:masterfrom
yosuke-wolfssl:fix/wolfsshd-loop
Open

yosuke-wolfssl wants to merge 1 commit into
wolfSSL:masterfrom
yosuke-wolfssl:fix/wolfsshd-loop

Conversation

@yosuke-wolfssl

@yosuke-wolfssl yosuke-wolfssl commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Follow-up to #1217. Bugs on the path from a break in POSIX SHELL_Subsystem() to its end:

  • Pinned connection process. The post-loop reap was a blocking waitpid(). When a client closes its channel while wolfsshd holds output, the loop sends SIGINT and breaks. A command that ignores SIGINT, including any interactive shell on a pty, keeps the connection process, its socket and the child alive until the client drops the whole transport. Reproduced with paramiko (exec_command or invoke_shell, stop reading, channel.close()): the command is still running after 10 s.
  • Lost output. A break can leave unsent output in shellBuffer, and the post-loop pipe drain then read()s over it. Reached when the command exits while stdin is still arriving: the pending stdin write fails with EPIPE and the loop breaks with output held.
  • Lost SIGCHLD. The connection process starts with SIGCHLD ignored, and ChildSig was installed after forkpty(). A shell that exited in between was reaped silently, ChildRunning never cleared, and the loop waited until the client disconnected.

Fix (apps/wolfsshd/wolfsshd.c)

  • Pty master closed before the reap, so the shell gets a hangup and exits on its own.
  • Bounded reap: waitpid(WNOHANG) for WOLFSSHD_CHILD_REAP_TRIES x WOLFSSHD_CHILD_REAP_WAIT_US (0.5 s by default), then SIGKILL and a blocking wait. A normal exit is reaped on the first poll. A failed kill() is logged and ends the wait.
  • EIO from the pty master ends the output, not the loop. On Linux that is how the master reports that nothing holds the terminal. Breaking on it let the bounded reap kill a job that had only dropped the terminal (exec nohup job </dev/null >/dev/null 2>&1 in an interactive shell); the loop now waits for it, as a 0-byte read already did.
  • ChildSig installed before forkpty(), so a shell that exits at once still clears ChildRunning.
  • Held output flushed before the drain with SHELL_FlushOut(). A SHELL_SEND_NEVER backlog is dropped, and the drain is skipped whenever the backlog is not sent, so the stream never has a gap.

Tests

New apps/wolfsshd/test/sshd_channel_close_test.sh, using paramiko (the OpenSSH client never closes a channel early):

Case Asserts
exec command trapping INT and HUP, channel closed while output is held command gone and connection closed within 5 s (the SIGKILL fallback)
interactive shell on a pty, same shell exits by itself (an EXIT-trap marker SIGKILL cannot write) and the connection closes within 5 s
exit 3 from an exec and from an interactive shell client receives exit status 3

It skips (77) without python3 or paramiko, on a non-loopback host, or when paramiko cannot connect. sshd-test.yml and code-coverage.yml install python3-paramiko from apt, because the suite runs as root and cannot see a per-user pip install.

Verification

  • ubuntu:24.04 with sshd-test.yml's configure lines, suite run with sudo as a sudoer: 31 passed, 3 skipped (the kex debug test, the UPN negative on FPKI builds, ML-DSA); make check 13/13.
  • On master the new test fails: exec and pty commands are still running after 5 s. With the bounded reap but without the pty close, the pty case fails.
  • ASan + UBSan with leak detection: clean on the reap, flush and pty-close changes (the later EIO and ChildSig changes touch no buffers). gcc-13 -Werror across 6 configs: clean.

Not in this PR

  • A child killed by a signal is reported as exit-status 0 (WEXITSTATUS without WIFEXITED). Fixing that changes what goes on the wire, so it is separate.
  • A command that exits while stdin is still arriving can still lose output: the stdin write fails with EPIPE and the loop breaks instead of draining. The flush here recovers the held backlog, not the rest. Follow-up.
  • Closing the channel of an idle pty shell still leaves the shell running until the transport drops: nothing is held, so the loop never breaks.
  • Resending stderr on the list-head channel is a separate PR.

@yosuke-wolfssl yosuke-wolfssl self-assigned this Sep 28, 2026
Copilot AI lite review requested due to automatic review settings September 28, 2026 02:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Process descendants may survive cleanup, and the ESRCH race can leave children unreaped.

Review effort: Lite
Findings: 1 High severity

Open (1)
What changed in this PR

This PR bounds wolfsshd shell cleanup after channel closure and preserves pending output.

Changes:

  • Adds PTY closure, bounded reaping, and forced termination.
  • Flushes held shell output before pipe draining.
  • Adds Paramiko regression tests and CI dependencies.
File Description
apps/​wolfsshd/​wolfsshd.c Implements bounded reaping and backlog flushing.
apps/​wolfsshd/​test/​sshd_channel_close_test.sh Tests channel-close cleanup and exit statuses.
apps/​wolfsshd/​test/​run_all_sshd_tests.sh Registers the new test.
.github/​workflows/​sshd-test.yml Installs Paramiko for SSHD tests.
.github/​workflows/​code-coverage.yml Installs Paramiko for coverage runs.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/wolfsshd/wolfsshd.c

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fenrir Automated Review — PR #1276

Scan targets checked: wolfssh-src, wolfssh-bugs
Coverage: 1 of 1 in-scope changed file(s) opened by the reviewer

Fenrir result: Approved ✅

No new issues found in the changed files.

Advisory only — this automated result does not count as a GitHub approval.

Review tier: Lite

@yosuke-wolfssl
yosuke-wolfssl force-pushed the fix/wolfsshd-loop branch 2 times, most recently from 988c09a to 720f7cd Compare September 28, 2026 06:05
- The pty master is 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants