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
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
ruleset.create() — line 60
try_open_path() for each policy path — lines 63-80
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
-
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.
-
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.
-
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()): 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
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")./sandboxis 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:
CONFIG_SECURITY_LANDLOCK=ylockdown,capability,landlock,yama,apparmorKernels are well above 5.13 (Landlock V1) and 5.19 (unprivileged Landlock). This is not a kernel compatibility issue.
Reproduction
Policy delivered correctly
nemoclaw status --jsonconfirms:Root cause analysis
Execution order in process.rs#L184-L187:
Inside landlock.rs:
ruleset.create()— line 60try_open_path()for each policy path — lines 63-80restrict_self()— line 103If ANY step fails and
compatibility == BestEffort, the error is caught at line 107, an OCSF finding is logged, andOk(())is returned at line 128. The caller sees success. The sandbox runs unconfined.Likely failure candidates
1.
try_open_path()fails afterdrop_privileges()(MOST LIKELY)try_open_path()doesopen(path, O_PATH | O_CLOEXEC)as the sandbox user (uid 998), not root. Container runtimes, AppArmor profiles, or mount configurations may restrictO_PATHopens for non-root users. If ALL paths fail,rules_applied == 0at line 83 → error →best_effortcatches 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 causeruleset.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.
landlockcrate ABI negotiation failureThe code uses
ABI::V2at line 33 withCompatLevel::BestEffort. The crate (v0.4.4) might fail ABI negotiation internally despite kernel support.4.
PR_SET_NO_NEW_PRIVSnot setrestrict_self()requiresPR_SET_NO_NEW_PRIVS. If Docker's--security-opt no-new-privilegesisn't set and the code doesn't callprctl()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 onboarddisplays "Landlock + seccomp + netns" which is misleadingRelated