Skip to content

fix(sandbox): support kernels < 5.19 via seccomp WAIT_KILLABLE_RECV fallback - #3420

Open
akram wants to merge 5 commits into
NVIDIA:mainfrom
akram:fix/seccomp-wait-killable-fallback-main
Open

akram wants to merge 5 commits into
NVIDIA:mainfrom
akram:fix/seccomp-wait-killable-fallback-main

Conversation

@akram

@akram akram commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Summary

The capability-free sandbox hard-requires SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV (Linux 5.19+) to install its notification listener. On older kernels (RHEL 9.x / 5.14, i.e. current OpenShift/RHCOS nodes) the seccomp() call returns EINVAL, the listener thread dies (notification launcher disappeared), and the supervisor fails confirmation — so the sandbox cannot start.

This makes the flag optional: attempt it, and gracefully fall back to a plain NEW_LISTENER when the kernel doesn't support it. Zero-cap containment is unchanged (the listener still mediates every syscall); the only thing lost on < 5.19 is the killable-receive semantics.

Related Issue

Fixes #3417

Changes

  • openshell-isolation-interface install_listener: try WAIT_KILLABLE_RECV, and on EINVAL retry with a plain NEW_LISTENER. The resulting listener records wait_killable_recv = false.
  • SandboxConfirmEvidence::validate(): stop gating launch on the cancellation evidence (which is exactly WAIT_KILLABLE_RECV), so confirmation succeeds on < 5.19. Updated the conformance test to assert rejection on a containment-critical primitive (new_listener) instead.

Testing

  • Validated end-to-end on OpenShift / RHCOS RHEL 9.8 (kernel 5.14.0-687.35.1.el9_8): the capability-probe returns {"qualified":true, "seccomp_notification":true, "wait_killable_recv":false}, and a real sandbox created through the Kubernetes driver reaches Ready (workload + supervisor pods both Running). Before the change the same path failed with notification launcher disappeared.
  • On kernels >= 5.19 behavior is unchanged (WAIT_KILLABLE_RECV is still used; wait_killable_recv = true).
  • Built clean (musl release) in-cluster for the sandbox and supervisor images; conformance test updated. Unit/conformance suite runs in CI.

Checklist

  • Conventional Commits + --signoff (DCO)
  • Change scoped to the issue
  • Degraded semantics on < 5.19 documented in code comments
  • Maintainer review of the security-contract change (validate())

@copy-pr-bot

copy-pr-bot Bot commented Sep 17, 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.

@mrunalp

mrunalp commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Findings

  1. The fallback removes a documented security invariant. Before this PR, kernels without WAIT_KILLABLE_RECV failed closed. With the fallback, a workload can signal a notifying thread after ID_VALID but before process_vm_writev, allowing stale task-memory writes after the syscall
    resumes. The architecture explicitly makes the flag mandatory for this reason. Fallback (

    // WAIT_KILLABLE_RECV (Linux 5.19+) makes the supervisor's notification
    // receive interruptible by a fatal signal. Kernels older than 5.19 (for
    // example RHEL 9.x / 5.14 nodes) reject the flag with EINVAL. Rather than
    // refusing to start there, fall back to a plain listener: the notification
    // receive is then uninterruptible, but the sandbox is otherwise fully
    // functional. The resulting listener records `wait_killable_recv = false`
    // so callers can observe the degraded cancellation semantics.
    match install_listener_with_flags(syscalls, true) {
    Ok(listener) => Ok(listener),
    Err(error) if error.raw_os_error() == Some(libc::EINVAL) => {
    install_listener_with_flags(syscalls, false)
    }
    ), write path
    (
    listener.validate_id(notification_id)?;
    if copied != 0 {
    task_memory::write_exact(tid, address, &bytes[..copied])?;
    }
    listener.validate_id(notification_id)?;
    task_memory::write_exact(tid, length_address, &actual_length.to_ne_bytes())
    ), architecture invariant
    (
    This sandbox runtime requires Linux 6.2 or newer for Landlock ABI v3 and treats
    `SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV` as mandatory so cancelled
    notifications cannot race task-memory writes.
    ).

    The kernel documentation (https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/seccomp_filter.rst) confirms that the flag suppresses non-fatal signals after userspace receives the notification. I would retain the launch gate until the pointer-writing paths
    have another cancellation-safe design.

  2. The new comments describe the kernel semantics backwards. A plain listener is interruptible by non-fatal signals; WAIT_KILLABLE_RECV makes the notifying task ignore them after receipt. The comment currently says the fallback is “uninterruptible” and attributes the behavior to
    the supervisor’s receive operation. The unchanged architecture documentation also contradicts the new behavior.

Full transparent behavior on old kernels is difficult. The safest approach is an explicit legacy mode that forbids broker writes into workload memory.

Mode Behavior
━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Modern kernel Use WAIT_KILLABLE_RECV; retain full behavior
─────────────── ─────────────────────────────────────────────────────────────────
Legacy kernel Plain listener, but no task-memory output writes
─────────────── ─────────────────────────────────────────────────────────────────
Current PR Plain listener while retaining writes; unsafe cancellation race

Recommended changes:

  1. Keep the EINVAL fallback, but record a Killable versus LegacyReadOnly listener mode.

  2. Centralize task-memory writes behind a listener method that rejects them in legacy mode.

  3. In legacy mode:

    • Support accept/accept4 only with null address arguments.
    • Reject mediated getpeername with EOPNOTSUPP, or allow native behavior where safe.
    • Reject sendmmsg paths requiring msg_len writes.
    • Continue supporting socket creation, connect, bind, listen, sendto, and sendmsg; these use copied inputs, scalar responses, or atomic ADDFD_SEND.
  4. Change confirmation validation to accept either:

    • killable notifications with task-memory writes, or
    • legacy mode with task-memory writes disabled.
  5. Add tests that deliberately install a plain listener—even on a modern kernel—and prove every output-write path fails closed.

  6. Document the reduced syscall compatibility and emit the selected mode in qualification output.

The key invariant should be something like:

cancellation || task_memory_writes_disabled

rather than removing cancellation from validation unconditionally.

This would let outbound-oriented workloads run on RHEL 9 while preserving containment. Some server workloads may fail because common accept wrappers request the peer address. Full compatibility requires a distribution backport of WAIT_KILLABLE_RECV; userspace cannot atomically
combine arbitrary task-memory writes with a seccomp response. The kernel documentation (https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/seccomp_filter.rst) explains why non-fatal signal suppression matters after notification receipt.

@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

Thanks @mrunalp. I independently checked the signal-cancellation concern against the kernel contract, the broker's accept/getpeername/sendmmsg output-write paths, and the architecture invariant; it is reachable on the proposed plain-listener fallback and remains blocking.

Action required: @akram, preserve cancellation safety for every task-memory output write and update the comments and architecture documentation to describe the selected legacy behavior accurately.

Blocking findings:

  • GATOR-de00bfcc-01: plain-listener notification cancellation can race privileged writes into workload memory

Carried findings:

  • None

Non-blocking suggestions:

  • None
Gator metadata
  • Validation: Project-valid focused fix for accepted issue #3417
  • Docs: Missing; architecture/sandbox.md still says WAIT_KILLABLE_RECV is mandatory, and the new inline comments reverse the flag semantics
  • Checks: DCO, vouch, and Trivy pass; Branch Checks and Helm Lint await current-head test dispatch
  • E2E: test:e2e is required for sandbox lifecycle and seccomp enforcement behavior, but dispatch waits for blocking review feedback
  • Head SHA: de00bfccfca2d284da266865a6e99c74e9284d02
  • Base SHA: 8de26878f9324a822131ff6107861134843f1886
  • Merge base SHA: 6d496f540a1a2e5c49687cddd68d56dc82cd5ebb
  • Patch ID: 8efd98bbfd32d2ebec43ab50ae307293afde088e
  • Gator payload: 9
  • Review mode: initial
  • Previous reviewed SHA: none
  • Review budget exhausted: no
  • Maintainer decision required: no
  • Next state: gator:in-review

Comment thread crates/openshell-isolation-interface/src/linux/seccomp_notify.rs
@johntmyers johntmyers added the gator:in-review Gator is reviewing or awaiting PR review feedback label Sep 17, 2026
@akram

akram commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

thank you @mrunalp and @johntmyers , I'am addressing the change request in subsequent commits

@johntmyers johntmyers added gator:blocked Gator is blocked by process or repository gates and removed gator:in-review Gator is reviewing or awaiting PR review feedback labels Sep 18, 2026
@johntmyers

Copy link
Copy Markdown
Collaborator

gator-agent

Blocked

Thanks @akram. I saw your follow-up commit addressing the cancellation-safety request, including the legacy read-only listener mode and the updated architecture documentation. I cannot run the required delta review yet because GitHub reports that this head conflicts with main.

Next action: @akram, rebase or merge main, resolve the conflicts, and push the resulting head. Gator will then review the author-only delta against the open GATOR-de00bfcc-01 obligation.

Gator metadata
  • Head SHA: dc85fde79de44297ccbd5794381512d90eaa13b0
  • Gator payload: 9
  • Next state: gator:blocked
  • Blocked reason: merge_conflict

Comment on lines 262 to +265
cancellation: notification.wait_killable_recv,
// Legacy plain listener (< 5.19) disables broker output writes;
// satisfies the `cancellation || writes_disabled` launch invariant.
task_memory_writes_disabled: !notification.wait_killable_recv,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Under which conditions would cancellation and task_memory_writes_disabled not have different values?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@elezar you're right: today task_memory_writes_disabled is just !cancellation, so the two are always opposite and the cancellation || task_memory_writes_disabled check is tautological.

Instead of dropping the field, I'd make it a real independent attestation. At qualification the sandbox installs a LegacyReadOnly listener and checks that its own broker guard (write_task_output) actually fails closed with EOPNOTSUPP, then reports that result. It becomes true only when the listener is legacy and the guard is verified, so if the guard ever regressed and started writing again, the field drops to false while cancellation is already false, and validate() rejects the boundary instead of admitting the race. The two can then genuinely diverge, and the invariant has teeth. It's ~20 lines in qualify_runtime (prototype on explore/seccomp-writes-disabled-selftest).

I can fix it in this PR, or in a follow-up PR if you'd rather land the current change first.

Since this touches the cancellation-safety contract you reviewed: @mrunalp @johntmyers, does the direction work for you? The alternative is collapsing to a single ListenerMode, which makes the unsafe state unrepresentable but leaves no confirmation cross-check. I'm leaning toward the attestation since it keeps validate() meaningful.

…RECV is unavailable

SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV was added in Linux 5.19. On older
kernels (for example RHEL 9.x / 5.14 nodes such as RHCOS on OpenShift) the
flag is rejected with EINVAL, which made the capability-free sandbox fail to
start during the notification probe with "notification launcher disappeared".

Install the notification listener with WAIT_KILLABLE_RECV when the kernel
supports it and fall back to a plain NEW_LISTENER on EINVAL. The fallback
listener records wait_killable_recv = false: its notification receive is
uninterruptible, but the sandbox is otherwise fully functional.

Signed-off-by: Akram <akram.benaissi@gmail.com>
The confirmation evidence check required seccomp.cancellation
(WAIT_KILLABLE_RECV, Linux 5.19+). On kernels older than 5.19 the sandbox
falls back to a plain listener and reports cancellation=false, so requiring it
stopped the supervisor from confirming the sandbox there ("confirmation failed:
sandbox confirmation evidence is incomplete or mismatched"). Cancellation is a
robustness property, not a containment prerequisite: containment still holds via
the listener itself. Drop it from the launch gate and update the conformance
test to assert rejection on a containment-critical primitive instead.

Signed-off-by: Akram <akram.benaissi@gmail.com>
…llation invariant

Follow-up to the PR review (GATOR-de00bfcc-01 / mrunalp): make the < 5.19
fallback cancellation-safe instead of racing broker writes.

- Record an explicit ListenerMode (Killable vs LegacyReadOnly); add
  writes_disabled()/mode() and emit the selected mode in qualification output
  (seccomp_listener_mode).
- Centralize task-memory output writes behind NotificationListener::
  write_task_output; in LegacyReadOnly mode getpeername, accept/accept4 with a
  non-null address, and sendmmsg length write-backs fail closed with EOPNOTSUPP.
  accept with a null address, socket/connect/bind/listen/sendto/sendmsg keep
  working (copied inputs, scalar responses, atomic ADDFD_SEND).
- Enforce the launch invariant `cancellation || task_memory_writes_disabled` in
  SandboxConfirmEvidence::validate() rather than dropping cancellation
  unconditionally; add task_memory_writes_disabled to SeccompEvidence.
- Add per-path fail-closed tests (write_task_output, write_socket_addr, a real
  plain listener installed on a modern kernel) and confirmation-invariant tests.
- Correct the flag-semantics comments and document both modes plus the reduced
  legacy syscall compatibility in architecture/sandbox.md.

Signed-off-by: Akram <akram.benaissi@gmail.com>
@akram
akram force-pushed the fix/seccomp-wait-killable-fallback-main branch from dc85fde to 2370e50 Compare September 18, 2026 13:57
@johntmyers johntmyers added the test:e2e Requires end-to-end coverage label Sep 18, 2026
@github-actions

Copy link
Copy Markdown

Label test:e2e applied, but pull-request/3420 does not exist yet. A maintainer needs to comment /ok to test 2370e5059f9d144a65b25f0b515b9eddea929d90 to mirror this PR. Once the mirror exists, re-apply the label or re-run Branch E2E Checks from the Actions tab.

@johntmyers

Copy link
Copy Markdown
Collaborator

/ok to test 2370e50

@johntmyers

Copy link
Copy Markdown
Collaborator

gator-agent

PR Review Status

Thanks @akram. I reviewed the current head after your follow-up on the cancellation-safety request. The new legacy read-only listener mode routes sendmmsg length writes through the guarded output method and rejects accept/accept4 address output plus getpeername before task-memory access, so GATOR-de00bfcc-01 is resolved with no new code blockers.

Action required: Please document the kernel-before-5.19 LegacyReadOnly behavior and its EOPNOTSUPP limitations in the relevant public Fern docs under docs/ (for example, the OpenShift runtime guidance and support matrix). The architecture update alone does not satisfy the public documentation gate for this observable sandbox behavior.

Blocking findings:

  • No blocking code findings remain.

Carried findings:

  • GATOR-de00bfcc-01: resolved by the current head; the Gator-owned review thread is closed.
Gator metadata
  • Validation: Project-valid fix for accepted issue Capability-free sandbox fails to start on kernels < 5.19 (RHEL 9.x / 5.14): seccomp WAIT_KILLABLE_RECV EINVAL #3417, with explicit maintainer direction to proceed.
  • Docs: Public Fern docs are missing the legacy listener compatibility and syscall limitations.
  • Checks: Current-head Branch Checks, Helm Lint, and E2E workflows are queued.
  • E2E: test:e2e applied; /ok to test 2370e5059f9d144a65b25f0b515b9eddea929d90 posted; Branch E2E Checks queued.
  • Head SHA: 2370e5059f9d144a65b25f0b515b9eddea929d90
  • Base SHA: 8bd3dcc565f380ddfc464fc41b1faa936d72af3e
  • Merge base SHA: 4b2cb7f007da2f701422daf394643b0fd9d4daef
  • Patch ID: f37aa8ca1ee936368882526dbd8c95ff9f47ec37
  • Gator payload: 9
  • Review mode: follow_up
  • Previous reviewed SHA: de00bfccfca2d284da266865a6e99c74e9284d02
  • Review budget exhausted: no
  • Maintainer decision required: no
  • Next state: gator:in-review

@johntmyers johntmyers added gator:in-review Gator is reviewing or awaiting PR review feedback and removed gator:blocked Gator is blocked by process or repository gates labels Sep 18, 2026
Kernels without SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV (< 5.19, e.g. RHEL 9.x /
RHCOS 5.14) run the sandbox in a legacy read-only cancellation mode where the
broker fails closed with EOPNOTSUPP on the mediated operations that write results
back into workload memory (getpeername, accept/accept4 with a non-null address,
sendmmsg length write-backs). Document this observable behavior and its syscall
limitations in the public Fern docs: the support-matrix kernel requirements and
the OpenShift runtime guidance.

Signed-off-by: Akram <akram.benaissi@gmail.com>
@akram

akram commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

doc added @johntmyers

@johntmyers

Copy link
Copy Markdown
Collaborator

/ok to test 285085e

@johntmyers johntmyers added test:e2e Requires end-to-end coverage and removed test:e2e Requires end-to-end coverage labels Sep 18, 2026
@github-actions

Copy link
Copy Markdown

Label test:e2e applied for 285085e. Open the existing run and click Re-run all jobs to execute with the label set. The run will execute the standard E2E suite after building the required gateway, sandbox, and supervisor images once. The matching required CI gate status on this PR will flip green automatically once the run finishes.

@johntmyers johntmyers added gator:blocked Gator is blocked by process or repository gates gator:watch-pipeline Gator is monitoring PR CI/CD status gator:approval-needed Gator completed review; maintainer approval needed and removed gator:watch-pipeline Gator is monitoring PR CI/CD status gator:blocked Gator is blocked by process or repository gates gator:approval-needed Gator completed review; maintainer approval needed labels Sep 18, 2026
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 test:e2e Requires end-to-end coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Capability-free sandbox fails to start on kernels < 5.19 (RHEL 9.x / 5.14): seccomp WAIT_KILLABLE_RECV EINVAL

4 participants