Skip to content

bug(sandbox): Landlock not enforced on Landlock-capable kernels — best_effort silently returns Ok() #803

Closed

Description

@prekshivyas

Summary

OpenShell 0.0.26 does not enforce Landlock filesystem restrictions despite receiving a correct policy and running on Landlock-capable kernels. The sandbox process runs as unconfined (/proc/1/attr/current = "unconfined"). /sandbox is fully writable.

Reported by QA in NVIDIA/NemoClaw#1739 / nvbug 6066573.

Previous issues #584 and #664 were closed as fixed (PRs #599, #677), and those fixes shipped in v0.0.26, but the problem persists.

v0.0.27 has no Landlock changes over v0.0.26.

Environment (from QA)

All platforms affected:

  • Brev GPU: kernel 5.15.0-107, kernel 6.8.0-57
  • Standard Ubuntu: kernel 6.8.0-57
  • CONFIG_SECURITY_LANDLOCK=y
  • Landlock in LSM: lockdown,capability,landlock,yama,apparmor
  • OpenShell: 0.0.26

Kernels are well above 5.13 (Landlock V1) and 5.19 (unprivileged Landlock). This is not a kernel compatibility issue.

Reproduction

nemoclaw onboard
nemoclaw <name> connect
touch /sandbox/testfile  # Expected: Permission denied. Actual: succeeds
cat /proc/1/attr/current # Shows: "unconfined"

Policy delivered correctly

nemoclaw status --json confirms:

filesystem_policy:
  include_workdir: false
  read_only:
    - /sandbox
    - /sandbox/.openclaw
  read_write:
    - /tmp
    - /sandbox/.openclaw-data
    - /sandbox/.nemoclaw
landlock:
  compatibility: best_effort

Root cause analysis

Execution order in process.rs#L184-L187:

drop_privileges(&policy)                       // line 184: switches to sandbox user (uid 998)
sandbox::apply(&policy, workdir.as_deref())    // line 187: tries to apply Landlock AS UNPRIVILEGED USER

Inside landlock.rs:

  1. ruleset.create() — line 60
  2. try_open_path() for each policy path — lines 63-80
  3. restrict_self() — line 103

If ANY step fails and compatibility == BestEffort, the error is caught at line 107, an OCSF finding is logged, and Ok(()) is returned at line 128. The caller sees success. The sandbox runs unconfined.

Likely failure candidates

1. try_open_path() fails after drop_privileges() (MOST LIKELY)

try_open_path() does open(path, O_PATH | O_CLOEXEC) as the sandbox user (uid 998), not root. Container runtimes, AppArmor profiles, or mount configurations may restrict O_PATH opens for non-root users. If ALL paths fail, rules_applied == 0 at line 83 → error → best_effort catches it → Ok(()) → unconfined.

Fix: Open path FDs as root BEFORE drop_privileges(), then pass them into the Landlock ruleset builder.

2. Container runtime seccomp blocks Landlock syscalls

Docker's default seccomp profile may block syscalls 444 (landlock_create_ruleset), 445 (landlock_add_rule), 446 (landlock_restrict_self). This would cause ruleset.create() at line 60 to fail before OpenShell's own seccomp runs.

Fix: Verify the container runtime permits Landlock syscalls, or document the requirement.

3. landlock crate ABI negotiation failure

The code uses ABI::V2 at line 33 with CompatLevel::BestEffort. The crate (v0.4.4) might fail ABI negotiation internally despite kernel support.

4. PR_SET_NO_NEW_PRIVS not set

restrict_self() requires PR_SET_NO_NEW_PRIVS. If Docker's --security-opt no-new-privileges isn't set and the code doesn't call prctl() explicitly, restrict_self() fails with EPERM.

Suggested fixes

  1. Move path FD opening before drop_privileges() — open O_PATH FDs as root, pass them into the ruleset builder. This is the most likely fix for candidate 1.

  2. Make the failure visible — the OCSF finding at line 125 goes to structured logs nobody reads. Add a visible stderr warning and/or set an env var like OPENSHELL_LANDLOCK_ACTIVE=0 so downstream code can detect it.

  3. Log the specific error — the OCSF finding includes the error string, but it's buried in structured logging. Print the actual failure reason to stderr so operators can diagnose which candidate is failing.

  4. Two-phase Landlock — phase 1 (as root): open path FDs + create ruleset. Phase 2 (after drop_privileges()): call restrict_self() only.

Impact

  • NemoClaw's entire Landlock-based security model (PR #1121) is ineffective
  • nemoclaw onboard displays "Landlock + seccomp + netns" which is misleading
  • Only DAC protections (root:root, sticky bit, chmod 444) prevent filesystem abuse

Related

Activity

  1. johntmyers commented on Apr 10, 2026

    @johntmyers
    Collaborator

    I will look into this. @prekshivyas for reproduction, please ensure it is a pure OpenShell reproduction so we can remove as many steps as possible.

  2. mjamiv commented on Apr 10, 2026

    @mjamiv
    Contributor

    Confirmed on v0.0.25 (same behavior as reported on v0.0.26 — prior PRs for #584/#664 did not actually fix the regression on our platform).

    Host: Ubuntu 24.04.3 LTS, kernel 6.8.0-107-generic. `CONFIG_SECURITY_LANDLOCK=y`, `CONFIG_LSM=landlock,lockdown,yama,integrity,apparmor` — Landlock is compiled in and enabled in the LSM stack.

    Observation from inside a running sandbox (OpenShell v0.0.25, `claw-test`):

    ```
    $ cat /proc/1/attr/current
    unconfined

    $ grep Seccomp /proc/self/status
    Seccomp: 2
    Seccomp_filters: 1

    $ touch /sandbox/landlock-test-$$
    [no error — /sandbox is fully writable]

    $ mount | grep sandbox
    /dev/sdb on /sandbox type ext4 (rw,relatime,discard)
    ```

    Seccomp is enforced (filter mode, 1 filter) but Landlock clearly is not (`unconfined`). Matches the #803 signature exactly — silent `best_effort → Ok()` path has been shipping at least since v0.0.25.

    Happy to test PR #810 when it's ready for external validation.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions