Repository navigation
bug(sandbox): Landlock not enforced on Landlock-capable kernels — best_effort silently returns Ok() #803
Description
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_effortRoot 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 USERInside landlock.rs:
ruleset.create()— line 60try_open_path()for each policy path — lines 63-80restrict_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
-
Move path FD opening before
drop_privileges()— openO_PATHFDs as root, pass them into the ruleset builder. This is the most likely fix for candidate 1. -
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=0so downstream code can detect it. -
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.
-
Two-phase Landlock — phase 1 (as root): open path FDs + create ruleset. Phase 2 (after
drop_privileges()): callrestrict_self()only.
Impact
- NemoClaw's entire Landlock-based security model (PR #1121) is ineffective
nemoclaw onboarddisplays "Landlock + seccomp + netns" which is misleading- Only DAC protections (root:root, sticky bit, chmod 444) prevent filesystem abuse
Related
- NVIDIA/NemoClaw#1739 / nvbug 6066573
- Closed (fixes in v0.0.26 but problem persists): #584, #664
- Related: #783 (prepare_filesystem chowns read_write dirs)
Activity
johntmyers commentedon Apr 10, 2026
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.
fix(sandbox): add parent-side Landlock availability probe logging
test(e2e): add Landlock filesystem enforcement tests
test(e2e): add xfail tests proving #803 privilege ordering bug
fix(test): fix Landlock e2e tests based on test run results
fix(sandbox): two-phase Landlock to fix privilege ordering (#803)
fix(sandbox): two-phase Landlock to fix privilege ordering (#803)
mjamiv commentedon Apr 10, 2026
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.
fix(sandbox): two-phase Landlock to fix privilege ordering and add en…
fix(install): bump OpenShell max version to 0.0.29 for Landlock enfor…