Skip to content

fix(sandbox): report the real seccomp probe launcher error - #4051

Open
russellb wants to merge 1 commit into
NVIDIA:mainfrom
russellb:fix/surface-seccomp-probe-launcher-error
Open

russellb wants to merge 1 commit into
NVIDIA:mainfrom
russellb:fix/surface-seccomp-probe-launcher-error

Conversation

@russellb

@russellb russellb commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Summary

The seccomp notification probes hide the kernel error that actually caused them to fail. When install_listener fails on the launcher thread, the broker only observes a closed channel and reports notification launcher disappeared, discarding the errno. This makes several unrelated failures indistinguishable from the pre-5.19 WAIT_KILLABLE_RECV rejection fixed in 293fab7, which is why #3842 looks like a regression of that fix. This PR joins the launcher thread and propagates its real error.

Related Issue

Refs #3842

This is a diagnostics change that unblocks triage of #3842 (currently state:triage-needed). Investigation found no regression of 293fab7 — the EINVAL fallback, the cancellation || task_memory_writes_disabled launch invariant, and the legacy_read_only evidence plumbing are all intact in v0.1.2 and on main. The report cannot be diagnosed further until the probe surfaces the underlying errno. Analysis posted at #3842 (comment).

Changes

  • Add launcher_failure() in crates/openshell-isolation-interface/src/linux/seccomp_notify.rs, which joins the launcher thread on handover failure and returns its error with the original ErrorKind preserved, so the errno reaches the qualification report.
  • Use it on the recv() failure path of all three probes: probe_scalar_round_trip, probe_addfd_send, and probe_connected_sendto_fast_path.
  • Distinguish the remaining non-error cases: a launcher that exited without sending, and a launcher that panicked.

No behavior change for successful qualification. The probes fail in exactly the same cases as before; they now say why.

Testing

Linux verification ran in a rust:1.95-bookworm container with --security-opt seccomp=unconfined (the linux module does not build on the macOS host).

  • New test notification_probe_reports_rejected_listener_install. It spawns a disposable child process, installs a filter that refuses SECCOMP_SET_MODE_FILTER with EPERM while leaving the notification size query working, and asserts the probe surfaces PermissionDenied with os error 1. This models a restrictive host, which the EINVAL fallback cannot retry — distinct from a pre-5.19 kernel.

  • Watched it fail before the fix with left: Other, right: PermissionDenied, confirming it catches the swallowed error rather than passing vacuously.

  • cargo test -p openshell-isolation-interface: 29 lib + 25 integration tests pass.

  • cargo clippy -p openshell-isolation-interface --all-targets -- -D warnings and cargo fmt --check: clean.

  • cargo check -p openshell-sandbox -p openshell-sandbox-backend -p openshell-supervisor --all-targets: clean.

  • mise run pre-commit: passes.

  • mise run pre-commit passes

  • Unit tests added/updated

  • E2E tests added/updated (if applicable) — not applicable; the change is confined to probe error reporting and is covered by the unit test above

Checklist

  • Follows Conventional Commits
  • Commits are signed off (DCO)
  • Architecture docs updated (if applicable) — not applicable; no architecture or user-facing behavior change

@copy-pr-bot

copy-pr-bot Bot commented Oct 1, 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.

@purp

purp commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

One improvement worth carrying over from #4061: preserve the original io::Error on launcher failure.

In launcher_failure, recommend changing the Ok(Err(error)) arm to return error directly. The current io::Error::new(error.kind(), format!(...)) preserves the error kind and errno in the message, but makes raw_os_error() return None.

Please also add assert_eq!(error.raw_os_error(), Some(libc::EPERM)); to notification_probe_reports_rejected_listener_install. This keeps the simpler direct-handle helper and targeted filter-install test here while preserving the machine-readable kernel errno, as #4061 does.

The seccomp notification probes install their listener on a launcher
thread and receive it over a channel. When the install fails, the
launcher drops the sender and exits, so the broker only sees a closed
channel and returns a generic "launcher disappeared" error. The thread
is never joined, so the kernel's errno is discarded.

That collapses several distinct failures into one opaque string: a
notification struct size mismatch, a failed PR_SET_NO_NEW_PRIVS, and
any non-EINVAL SECCOMP_SET_MODE_FILTER rejection all look identical to
a pre-5.19 kernel refusing WAIT_KILLABLE_RECV, which the install path
already retries without the flag.

Join the launcher on the recv() failure path and propagate its error
unchanged, so both the ErrorKind and raw_os_error survive to the
qualification report. Apply this to all three probes.

Signed-off-by: Russell Bryant <rbryant@redhat.com>
@russellb
russellb force-pushed the fix/surface-seccomp-probe-launcher-error branch from 82f3522 to 834922c Compare October 1, 2026 19:36
@russellb

russellb commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

Done in 834922c.

launcher_failure now returns the error unchanged:

match launcher.join() {
    Ok(Err(error)) => error,
    Ok(Ok(_)) => io::Error::other(format!("{role} launcher exited before sending its listener")),
    Err(_) => io::Error::other(format!("{role} launcher panicked")),
}

install_listener_with_flags returns a bare io::Error::last_os_error(), so propagating it unchanged keeps both the kind and raw_os_error(). The doc comment now says why it isn't reformatted.

Added assert_eq!(error.raw_os_error(), Some(libc::EPERM)); to notification_probe_reports_rejected_listener_install. That assertion fails against the previous io::Error::new wrapper, so it is a real guard rather than a restatement of the kind() check.

One tradeoff worth naming: the {role} prefix is gone from the propagated error, so a failing probe no longer self-identifies as notification / ADDFD / sendto. There is no std way to re-wrap an io::Error while keeping its errno, so it is one or the other. The only caller (openshell-sandbox qualification) wraps everything as "seccomp notification probe", so the report still names the probe family but not which of the three failed. I took the errno per this suggestion and #4061; if the per-probe label turns out to matter, the cheapest fix is distinct context at the three call sites in probe_notification_api.

Verified in a Linux container (podman, aarch64, --security-opt seccomp=unconfined):

  • cargo test -p openshell-isolation-interface --lib linux::seccomp_notify — 8 passed, 0 failed, including the rejected-install test and its disposable child process
  • cargo clippy -p openshell-isolation-interface --all-targets — clean
  • mise run pre-commit on the host — passed

This branch has not been deployed

No deployments
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.

2 participants