Skip to content

fix(ssh): add EMFILE backoff and exit notification to SSH accept loop - #2705

Open
politerealism wants to merge 8 commits into
NVIDIA:mainfrom
politerealism:fix/ssh-accept-backoff-and-exit-notification
Open

fix(ssh): add EMFILE backoff and exit notification to SSH accept loop#2705
politerealism wants to merge 8 commits into
NVIDIA:mainfrom
politerealism:fix/ssh-accept-backoff-and-exit-notification

Conversation

@politerealism

Copy link
Copy Markdown
Contributor

Summary

Depends on #2370 — this branch is stacked on fix/proxy-exit-detection. Please review #2370 first; once it merges the diff here will collapse to only the SSH-specific changes below.

Applies the same two-layer defense from the proxy accept loop (#2369 / #2370) to the SSH accept loop:

  • EMFILE backoff: classify accept errors as Retry (EMFILE/ENFILE/ENOBUFS — exponential backoff) vs Terminal (≥10 consecutive unknown errors → break). Mirrors classify_accept_error in openshell-supervisor-network.
  • Exit notification: thread a oneshot::Sender<()> drop-guard through run_process so the sandbox is notified when the SSH task ends for any reason. The sandbox selects on ssh_exited in both process_enabled paths and returns an error, causing Kubernetes to restart the container rather than leaving it running without SSH access.

Related Issue

Closes #2372

Changes

File Change
crates/openshell-supervisor-process/src/ssh.rs Replace bare accept().await? with classify-and-retry loop; add SshAcceptAction enum and classify_ssh_accept_error
crates/openshell-supervisor-process/src/run.rs Add ssh_exit_tx: Option<oneshot::Sender<()>> parameter; hold as drop-guard in SSH spawn block
crates/openshell-sandbox/src/lib.rs Create (ssh_exit_tx, ssh_exit_rx) channel when ssh_socket_path is Some, pin ssh_exited future, select on it in both process_enabled paths

Testing

  • cargo clippy -p openshell-sandbox -p openshell-supervisor-process — clean
  • cargo fmt --check — clean
  • Sandbox e2e: verify container exits when SSH accept loop terminates unexpectedly

Checklist

…edly

Add a oneshot notification channel to ProxyHandle that fires when the
proxy task exits for any reason (panic, abort, or loop break). The
sandbox main loop now races this signal alongside the entrypoint process
and shutdown signals, terminating with a clear OCSF error if the proxy
dies. This prevents the sandbox from continuing to report Ready with a
dead proxy.

Follows the existing sidecar control channel pattern. All five wait
paths (process+sidecar, process-only, network+sidecar, network-only
on Linux, and non-Linux) now monitor proxy liveness.

Refs NVIDIA#2337

Signed-off-by: Sean Burdine <sburdine@nvidia.com>
Signed-off-by: politerealism <burdcat17@gmail.com>
…it_receiver

Verify the oneshot drop-guard pattern fires on normal task exit and
abort, and document the take_exit_receiver contract including the
double-call hazard.

Signed-off-by: politerealism <burdcat17@gmail.com>
…unrecoverable failures

Extract accept-loop error handling into classify_accept_error() with
AcceptAction enum so the decision logic is independently testable.
Terminal errors (EBADF/EINVAL/ENOTSOCK) now exit the loop immediately,
allowing the supervisor to detect proxy death. Unknown errors exit
after 10 consecutive failures. FD exhaustion retries with exponential
backoff capped at 5s (fix unreachable cap from min(6) to min(7)).

Adds 9 unit tests covering classification, counter interactions,
backoff progression, and bounded exit behavior.

Signed-off-by: Quinn Burdine <sburdine@redhat.com>
Replace raw errno numbers with libc:: constants behind #[cfg(unix)]
gates in classify_accept_error and its tests. Refactor
ProxyHandle::exited_rx to Option<Receiver<()>> so take_exit_receiver
uses .take() instead of allocating a dummy sender. Add a
ProxyHandle-level test that verifies the full drop → abort → receiver
fires path.

Signed-off-by: Quinn Burdine <sburdine@redhat.com>
Signed-off-by: politerealism <burdcat17@gmail.com>
The refactored classifier only recognized EMFILE/ENFILE as retryable,
routing all other documented transient accept errors (ENOBUFS, ENOMEM,
ECONNABORTED, ECONNRESET, EINTR, ENETDOWN, etc.) through the unknown-
error budget which terminates the proxy after 10 consecutive failures.
Restore the complete transient errno table from main so these errors
retry indefinitely with appropriate backoff.

Signed-off-by: Quinn Burdine <sburdine@redhat.com>
Signed-off-by: politerealism <burdcat17@gmail.com>
Replace redundant closure |p| p.take_exit_receiver() with the method
reference ProxyHandle::take_exit_receiver, and change all five
select! arms from _ = &mut proxy_exited to () = &mut proxy_exited
to satisfy clippy::ignored_unit_patterns. Also run cargo fmt on the
new proxy test in proxy.rs.

Signed-off-by: Quinn Burdine <sburdine@redhat.com>
Signed-off-by: Quinn Burdine <sburdine@redhat.com>
@copy-pr-bot

copy-pr-bot Bot commented Aug 11, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions

Copy link
Copy Markdown

Thank you for your submission! We ask that you all sign our Developer Certificate of Origin before we can accept your contribution. You can sign the DCO by adding a comment below using this text:


I have read the DCO document and I hereby sign the DCO.


1 out of 2 committers have signed the DCO.
✅ (politerealism)[https://github.com/politerealism]
@quinn Burdine
Quinn Burdine seems not to be a GitHub user. You need a GitHub account to be able to sign the DCO. If you have already a GitHub account, please add the email address used for this commit to your account.
You can retrigger this bot by commenting recheck in this Pull Request. Posted by the DCO Assistant Lite bot.

@politerealism

Copy link
Copy Markdown
Contributor Author

Once #2370 merges into main, this branch will need to be rebased onto the updated main and force-pushed. The rebase should be clean — the proxy changes and SSH changes don't overlap.

Apply the same two-layer defense from the proxy accept loop (NVIDIA#2369/NVIDIA#2370)
to the SSH accept loop: classify transient vs terminal accept errors with
exponential backoff on EMFILE/resource-exhaustion, and notify the sandbox
when the accept loop exits so the container terminates instead of running
without SSH access.

- Add SshAcceptAction enum and classify_ssh_accept_error in ssh.rs,
  mirroring the proxy pattern (EMFILE/ENFILE/ENOBUFS → Retry with
  backoff, unknown errors → Terminal after 10 consecutive failures)
- Replace the bare accept().await in run_ssh_server with a classify-and-
  retry loop; resets consecutive-error counter on each successful accept
- Thread ssh_exit_tx: Option<oneshot::Sender<()>> through run_process;
  hold it as a drop-guard inside the SSH spawn so the receiver fires when
  the task ends for any reason
- Wire ssh_exited future in lib.rs (created only when ssh_socket_path is
  Some) and select! on it in both process_enabled paths, returning an
  error so the sandbox container restarts

Closes NVIDIA#2372

Signed-off-by: Quinn Burdine <119441270+politerealism@users.noreply.github.com>
@politerealism
politerealism force-pushed the fix/ssh-accept-backoff-and-exit-notification branch from 0210986 to 3e0c305 Compare August 11, 2026 18:11
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.

bug: SSH server accept loop exits permanently on transient errors (same class as #2337)

1 participant