Skip to content

fix(sandbox): terminate sandbox when proxy accept loop exits unexpectedly#2370

Open
politerealism wants to merge 4 commits into
NVIDIA:mainfrom
politerealism:fix/proxy-exit-detection
Open

fix(sandbox): terminate sandbox when proxy accept loop exits unexpectedly#2370
politerealism wants to merge 4 commits into
NVIDIA:mainfrom
politerealism:fix/proxy-exit-detection

Conversation

@politerealism

@politerealism politerealism commented Jul 20, 2026

Copy link
Copy Markdown

Summary

  • 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 AppLifecycle error at High severity if the proxy dies
  • Prevents the sandbox from continuing to report Ready with a dead proxy

Related Issue

Refs #2337

Note: This is step 2 of the fix for #2337, building on PR #2369 (step 1: accept loop retry with backoff). Step 1 prevents the proxy from dying on transient errors; step 2 catches any unexpected death as defense-in-depth.

Changes

crates/openshell-supervisor-network/src/proxy.rs

  • Added exited_rx oneshot receiver field to ProxyHandle
  • Created exited_tx sender inside the spawned proxy task as _proxy_exit_guard — dropped on task exit for any reason
  • Added take_exit_receiver() method to extract the receiver for monitoring

crates/openshell-sandbox/src/lib.rs

  • Extract proxy exit receiver before the main wait section
  • Wrap it in a boxed future (std::future::pending() when no proxy exists)
  • Added proxy-death arm to all 5 tokio::select! wait paths:
    • Process-enabled + sidecar control
    • Process-enabled, no sidecar
    • Network-only + sidecar control (Linux)
    • Network-only, no sidecar (Linux)
    • Non-Linux network-only

Testing

  • cargo check -p openshell-supervisor-network — compiles clean, no warnings
  • cargo check -p openshell-sandbox — compiles clean
  • cargo test -p openshell-supervisor-network -p openshell-sandbox — all tests pass

Checklist

  • Conventional commit format
  • Signed-off for DCO compliance
  • No secrets or credentials included
  • Scoped to the issue at hand
  • Follows existing sidecar control channel pattern (lib.rs:692-710)

Note: This PR depends on PR #2369 (step 1: accept loop retry with backoff) and should be merged after it.

…exiting

The proxy accept loop unconditionally broke on any accept() error,
permanently killing the proxy while the sandbox continued to report
Ready. Replace the break with a sleep-and-continue pattern: EMFILE/ENFILE
errors get exponential backoff (100ms to 3.2s) to let file descriptors
drain, and all other accept errors get a fixed 100ms backoff matching
the existing metadata_server and edge_tunnel patterns.

Closes NVIDIA#2337

Signed-off-by: Sean Burdine <sburdine@nvidia.com>
Signed-off-by: politerealism <burdcat17@gmail.com>
…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>
@copy-pr-bot

copy-pr-bot Bot commented Jul 20, 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.

@politerealism

Copy link
Copy Markdown
Author

Follow-up filed: #2372 — the SSH server task is spawned fire-and-forget (run.rs:238), so if it exits unexpectedly the sandbox has no way to detect it. The same exit-notification pattern from this PR (oneshot drop-guard + tokio::select! arm) should be applied to the SSH server task to enforce the "Ready implies exec relay operational" invariant.

@politerealism

Copy link
Copy Markdown
Author

Rebase note: This PR was branched off #2369. Once #2369 merges, this branch needs a rebase to pick up the final version of the proxy backoff code (libc constants, extracted helpers, fixed 5s cap). No code changes required — just a rebase.

…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>
@johntmyers johntmyers self-assigned this Jul 21, 2026

@johntmyers johntmyers left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

gator-agent

PR Review Status

Validation: This is a concentrated sandbox/network-supervisor correctness fix for the reproducible stale-Ready failure in #2337, and no duplicate implementation was found.
Head SHA: 4e915eddb6d4c5111e95d44fff5c1b3e539f20b6

Thanks @politerealism, I checked your rebase note against PR #2369. Its current head differs from the retry commit stacked here; rebasing after #2369 lands should pick up the portable errno helper and corrected 5-second cap covered by the first two inline findings.

Review findings:

  • Four actionable warnings are attached inline.
  • The fail-closed proxy-exit signal is wired into every sandbox wait path and does not weaken the sandbox security boundary.

Docs: Fern docs are not needed because this restores the documented readiness invariant without changing commands, configuration, APIs, or an intentional user workflow.

Next state: gator:in-review

Please rebase after #2369 lands and address the exit-receiver contract and production-path test coverage. E2E will be required after review feedback is resolved.

// back off exponentially to let connections drain.
let is_fd_exhaustion = matches!(
err.raw_os_error(),
Some(24) | Some(23)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

gator-agent

Warning: Replace numeric errno values with libc::EMFILE and libc::ENFILE through a platform-aware helper. raw_os_error() values are platform-specific, so non-Linux targets may miss FD exhaustion and use the wrong backoff and severity. Rebasing onto the final #2369 helper should address this.

consecutive_fd_errors = consecutive_fd_errors.saturating_add(1);
let backoff_ms = 100u64
.saturating_mul(
1u64 << consecutive_fd_errors.min(6).saturating_sub(1),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

gator-agent

Warning: consecutive_fd_errors.min(6) limits the exponent to 5, so this tops out at 3,200 ms and the 5,000 ms cap is unreachable. Allow the next exponent before applying the cap and add a unit test covering the complete backoff sequence.

/// Take the exit notification receiver for health monitoring.
/// Resolves when the proxy accept loop task exits for any reason.
pub fn take_exit_receiver(&mut self) -> tokio::sync::oneshot::Receiver<()> {
let (_tx, dummy_rx) = tokio::sync::oneshot::channel();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

gator-agent

Warning: Store exited_rx as an Option and make take_exit_receiver() return Option or Result. The dropped dummy sender makes a second call indistinguishable from a real proxy exit, which can falsely terminate a healthy sandbox; the added test currently codifies that footgun.


#[tokio::test]
async fn test_exit_receiver_fires_when_task_exits() {
let (exited_tx, exited_rx) = tokio::sync::oneshot::channel::<()>();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

gator-agent

Warning: These tests recreate standalone Tokio channels, so they still pass if the production exit guard or run_sandbox selection is removed. Exercise an actual ProxyHandle task exit and add a focused sandbox-level test proving proxy notification returns the expected error and cancels the workload.

@johntmyers johntmyers added gator:in-review Gator is reviewing or awaiting PR review feedback gator:blocked Gator is blocked by process or repository gates and removed gator:in-review Gator is reviewing or awaiting PR review feedback labels Jul 21, 2026
…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>
@johntmyers

Copy link
Copy Markdown
Collaborator

gator-agent

Blocked

Head SHA: 6a1fe36ae0357d7bc0d22b91323ff8ae296b54a2

Gator is blocked because GitHub reports this PR as having merge conflicts with main (mergeStateStatus: DIRTY).

Next action: @politerealism, please rebase the branch onto the current main, resolve the conflicts, and push the updated head. Gator will review the new head and decide the required E2E label and /ok to test action after the conflict is resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gator:blocked Gator is blocked by process or repository gates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants