Skip to content

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

@prekshivyas

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

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