Skip to content

landlock: fix disable() of FS protections; reject disable(FsRefer)#90

Merged
congwang-mk merged 1 commit into
mainfrom
fix-disable-fs-protection-einval
Jun 8, 2026
Merged

landlock: fix disable() of FS protections; reject disable(FsRefer)#90
congwang-mk merged 1 commit into
mainfrom
fix-disable-fs-protection-einval

Conversation

@congwang-mk
Copy link
Copy Markdown
Contributor

Follow-up to the protection opt-out work merged in #71, addressing the blocking review comment about disable() of FS protections (#71 (comment)).

1. Blocking bug: disable() of an FS protection fails with EINVAL when the sandbox has a writable path

compute_fs_mask removes the disabled bit (REFER / TRUNCATE / IOCTL_DEV) from handled_access_fs, but the per-path write mask was derived from write_access(abi) alone. So with disable(FsTruncate) (or FsIoctlDev, or the now-rejected FsRefer) plus any writable path, the writable-path rule still requested the dropped bit, it was no longer a subset of the ruleset's handled accesses, and landlock_add_rule(2) rejected it with EINVAL. That broke nearly every real sandbox (any with a writable path). The net path was already gated this way via net_tcp_active; the FS path did not get the matching treatment.

Fix (landlock.rs): intersect the per-path write mask with the resolved handled set, so every installed rule is a subset by construction.

let fs_write_mask = write_access(abi) & handled_access_fs;

This covers both the directory rule (access) and the file rule (access & ACCESS_FILE).

Regression test: a forked confine_filesystem against the real host kernel with disable(FsTruncate) / disable(FsIoctlDev) plus fs_write("/tmp"), asserting clean confinement. It fails (EINVAL) before the fix and passes after. The existing mask_contract_tests could not catch this class because they exercise compute_fs_mask in isolation and never install a path rule.

2. disable(FsRefer) is rejected at build

Per landlock(7), REFER is denied by default by every ruleset even when it is not handled. Controlled cross-directory rename within writable areas works precisely because REFER is handled and granted on writable paths. Disabling REFER un-handles it, which can only make the sandbox stricter, never looser, so it cannot do what disable() promises and is a footgun.

build() and build_unchecked() now return SandboxError::Invalid for disable(Protection::FsRefer), surfaced uniformly through the Rust, C ABI, and Python bindings. allow_degraded(FsRefer) stays meaningful and is unchanged. The disable() rustdoc and the --disable CLI help carry the caveat.

Validation

Full workspace green on a v7 host: 334 core lib + 242 core integration + 70 FFI/CLI, plus 12 Python protection tests. New tests: disable_fs_protection_with_writable_path_does_not_einval and disable_fsrefer_is_rejected_at_build.

Signed-off-by: Cong Wang <cwang@multikernel.io>
@congwang-mk
Copy link
Copy Markdown
Contributor Author

@dzerik Please take a look

@dzerik
Copy link
Copy Markdown
Contributor

dzerik commented Jun 7, 2026

Confirmed — the fix is correct. write_access(abi) & handled_access_fs makes every per-path rule a subset of the handled set by construction, which is the invariant compute_fs_mask should have preserved from the start; the writable-path mask drifting out of sync with the handled set was the gap. The disable(FsRefer) rejection is right too — REFER is default-denied even when unhandled, so disabling it only tightens, never loosens.

This was our gap: the mask-contract tests checked compute_fs_mask's output in isolation but never its interaction with the per-path write rules through a real landlock_add_rule, and the VM matrix exercised --disable only on the v6 scopes, never --disable fs-truncate with a writable path. The regression test you added closes exactly that hole.

@congwang-mk congwang-mk merged commit 77ba681 into main Jun 8, 2026
12 checks passed
@congwang-mk congwang-mk deleted the fix-disable-fs-protection-einval branch June 8, 2026 00:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants