feat(jailer): add Landlock LSM sandboxing via --landlock flag#5771
feat(jailer): add Landlock LSM sandboxing via --landlock flag#5771pavitrabhalla wants to merge 7 commits into
Conversation
| /// Returns [`JailerError::Landlock`] if the kernel does not support Landlock (kernel < 5.13), | ||
| /// if `jail_dir` cannot be opened, or if any ruleset syscall fails. | ||
| pub fn prepare_ruleset(jail_dir: &Path) -> Result<RulesetCreated, JailerError> { | ||
| let abi = ABI::V1; |
There was a problem hiding this comment.
Please don't pin to ABI::V1 but to the latest that you actually tested on a machine. Restricting only to ABI::V1 will limit the features to only the initial ones, whereas if you take the greatest version you'll get all the ones supported by the running system (i.e. best-effort). See the documentation. BTW, this version should regularly be incremented once the new version is tested on a kernel supporting it.
| let mut parts = release.split('.'); | ||
| let major: i32 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0); | ||
| let minor: i32 = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0); | ||
| major > 5 || (major == 5 && minor >= 13) |
There was a problem hiding this comment.
Please don't check kernel version. Landlock may be available/backported to older kernels. Landlock provides a dedicated interface to check which ABI version is supported by the running kernel. You should use LandlockStatus instead, but keep in mind that there are only a few valid use case to check this support (testing makes sense here) because the normal use of the Rust crate automatically follows a best-effort approach.
| .takes_value(false) | ||
| .help("Print the binary version number."), | ||
| ) | ||
| .arg(Argument::new("landlock").takes_value(false).help( |
There was a problem hiding this comment.
This option should probably reflect the goal, not (only) the mechanism. Landlock is gaining new feature over time and the current option doesn't reflect the related description.
|
@l0kod Thanks for the feedback. I updated everything as per your comments. |
|
Hi @pavitrabhalla thanks for the PR and sorry for the delay got lost in my queue somehow. I am talking with the team to decide if this is something we want to support. However, for now, a couple of things to look at now:
Thanks again, Jack |
53f29e1 to
46dd797
Compare
|
@JackThomson2 thanks for your comments and apologies for the delay here. I addressed as per your feedback. Let me know if you have further feedback and I'll address quickly. |
| tracing = ["log-instrument", "utils/tracing"] | ||
|
|
||
| [dependencies] | ||
| landlock = "0.4" |
There was a problem hiding this comment.
For our CI it looks like we'll need to specify the patch version in the dependency, so if you could update that'd be great.
|
Hey @pavitrabhalla thanks taking a look, Looks like some doc style failures: |
a19e57b to
17b618a
Compare
Codecov Report❌ Patch coverage is Please upload reports for the commit c92cb2d to get more accurate results.
Additional details and impacted files@@ Coverage Diff @@
## main #5771 +/- ##
==========================================
- Coverage 83.08% 82.86% -0.23%
==========================================
Files 277 278 +1
Lines 30201 30120 -81
==========================================
- Hits 25094 24959 -135
- Misses 5107 5161 +54
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@JackThomson2 please approve the workflows again. The code coverage and styling issues should be resolved now. |
| /// Returns [`JailerError::Landlock`] if the kernel does not support Landlock, | ||
| /// if `jail_dir` cannot be opened, or if any ruleset syscall fails. | ||
| pub fn prepare_ruleset(jail_dir: &Path) -> Result<RulesetCreated, JailerError> { | ||
| let abi = ABI::V4; |
There was a problem hiding this comment.
How come we've chosen V4 here, I was looking through the docs seems like V4 requires 6.7 kernel version, so to support 6.1 we'd need min V2?
Firecracker recently onboarded 6.18 as the next host kernel version, so I wonder if the best setup would be, hard requirement of v2 (for 6.1 hosts) then best effort v6 (for the 6.18 hosts) seems like v5 added some nice device IOCTL features.
What do you think?
There was a problem hiding this comment.
The Landlock crate is in best-effort by default (i.e. it uses the latest available ABI, detected at runtime), which means that the abi should be the highest tested version (currently ABI::V7).
There was a problem hiding this comment.
The crate’s default SoftRequirement mode automatically downgrades at runtime to whatever the running kernel supports. So rather than a hard V2 + best-effort V6 split, I updated to ABI::V7 with Ruleset::default() (soft-requirement). On a 6.1 host it degrades gracefully; on 6.18 it uses the full V7 feature set.
| ruleset.restrict_self().map_err(|err| { | ||
| JailerError::Landlock(format!("Failed to enforce Landlock ruleset: {err}")) | ||
| })?; | ||
| Ok(()) |
There was a problem hiding this comment.
Do we need to check the okay value it looks like it returns a status docs and we'd silently continue even if this wasn't enabled?
There was a problem hiding this comment.
You’re right that the status is silently dropped. This is intentional — since we’re using soft-requirement mode, NotEnforced or PartiallyEnforced on older kernels is expected best-effort behavior rather than a failure condition. I’ve added a doc comment to enforce() in c92cb2d to make this explicit so it’s clear it’s a deliberate choice and not an oversight.
…xing Add an opt-in --landlock-restrict-fs jailer flag that uses the Linux Landlock LSM to restrict Firecracker's filesystem access to the jail directory as a defense-in-depth mechanism. When passed: - Before pivot_root: open a PathFd on the jail directory (captures the inode; survives the pivot_root boundary). - Right before exec: call restrict_self() to enforce the ruleset on the jailed Firecracker process. Uses ABI::V4 in best-effort mode; on kernels without Landlock support (< 5.13) the flag has no effect. Existing deployments are unaffected. Closes firecracker-microvm#5513 Signed-off-by: Pavitra Bhalla <pavitra@superserve.ai>
Verify that Firecracker boots and runs correctly when the jailer is started with --landlock-restrict-fs. On kernels without Landlock support the flag has no effect so the test passes regardless. Signed-off-by: Pavitra Bhalla <pavitra@superserve.ai>
- Pin `landlock` dependency to exact patch version "0.4.4" per CI requirement (was "0.4"). - Run `mdformat .` to fix markdown style failures in CI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Pavitra Bhalla <pavitra@superserve.ai>
Run mdformat to normalize blank lines between list items. Signed-off-by: Pavitra Bhalla <pavitra@superserve.ai> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add `test_enforce` to landlock.rs: forks a child process to call `enforce()` (which is irreversible in the calling process), covering the `restrict_self` path that was previously untouched by unit tests. - Add `test_new_env_with_landlock` to env.rs: exercises `make_args` and `Env::new` with `--landlock-restrict-fs` set, covering the branch in `make_args` that builds the flag and verifying the field is wired through to `Env`. The `prepare_ruleset`/`enforce` calls inside `Env::run()` remain covered only by the integration test (`test_landlock_restrict_fs`) since `run()` calls `chroot()` and is not amenable to unit testing. Signed-off-by: Pavitra Bhalla <pavitra@superserve.ai> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per the Landlock crate maintainer's guidance, the ABI parameter should be the highest version that has been tested, not a pinned older version. Ruleset::default() uses SoftRequirement by default, so the crate automatically downgrades to whatever the running kernel supports — no behaviour change on older kernels. - Bump landlock dependency from 0.4.4 to 0.4.5 (adds ABI::V7 support, Linux 6.15). - Switch from ABI::V4 to ABI::V7. - Update the prepare_ruleset doc comment to reflect best-effort semantics (no error on kernels with partial/no Landlock support). Signed-off-by: Pavitra Bhalla <pavitra@superserve.ai> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ff33f03 to
c0f6eaa
Compare
…ce() Adds a doc comment explaining why the RestrictionStatus returned by restrict_self() is not inspected: Ruleset::default() uses soft-requirement (best-effort) mode, so NotEnforced/PartiallyEnforced on older kernels is expected behaviour, not an error condition. Addresses reviewer question in PR firecracker-microvm#5771. Signed-off-by: Pavitra Bhalla <pavitra@superserve.ai>
Summary
This PR implements the feature requested in #5513 — adding opt-in Landlock LSM support to the Firecracker jailer as a defense-in-depth mechanism.
What it does
Adds a
--landlockflag to the jailer. When passed:pivot_root: a Landlock ruleset is created and aPathFdis opened on the jail directory, capturing its inodeexec:restrict_self()is called, enforcing the ruleset on the jailed Firecracker processThe ruleset grants all filesystem access rights within the jail directory and denies everything outside. This means if Firecracker escapes the
pivot_rootchroot via a kernel exploit, Landlock independently prevents access to files outside the jail — enforced by a separate LSM path in the kernel.The flag is opt-in — existing deployments are completely unaffected.
Why Landlock on top of chroot?
pivot_root+ chroot lives in the kernel's VFS layer and can be bypassed by a guest-triggered kernel exploit. Landlock is enforced by a separate LSM subsystem and independently blocks filesystem access, so it provides defense-in-depth even ifpivot_rootis defeated. The inode-based rules (not path-based) also survive thepivot_rootboundary.Verification
Tested on kernel 6.8.0-1048-gcp (Landlock V1/V2/V3 supported).
straceconfirms the correct syscalls are made:Without
--landlock— no Landlock syscalls:With
--landlock— all 4 Landlock syscalls succeed:parent_fd=3is thePathFdopened beforepivot_root— confirming the inode reference survives the chroot boundary.Changes
src/jailer/src/landlock.rs(new):prepare_ruleset()andenforce()src/jailer/src/env.rs: wires Landlock intorun()— before chroot and before execsrc/jailer/src/main.rs:--landlockCLI flag +JailerError::Landlockvariantsrc/jailer/Cargo.toml:landlock = "0.4"dependencytests/framework/jailer.py:landlockparameter inJailerContextCHANGELOG.md: entry under[Unreleased] → AddedLicense Acceptance
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
PR Checklist
tools/devtool checkbuild --allto verify that the PR passes build checks on all supported architectures.tools/devtool checkstyleto verify that the PR passes the automated style checks.CHANGELOG.md.TODO.rust-vmm.