feat(sandbox): Landlock no-op sous Posture::Allow (allow-by-default)
Un allowlist Landlock ne peut que retirer des accès (deny-by-default). Sous une fallback Posture::Allow (allow-by-default), enforcer les grants verrouillerait tout hors du project root — binaire CLI, libs et ~/.<cli> — provoquant le 'failed to open terminal'. On n'enforce donc rien sous Allow ; seuls les Deny explicites font foi (advisory via la projection LP3). Couvre permissions.json persisté avec fallback allow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -90,6 +90,20 @@ impl SandboxEnforcer for LandlockSandbox {
|
||||
fn enforce(&self, plan: &SandboxPlan) -> Result<SandboxStatus, SandboxError> {
|
||||
let abi = TARGET_ABI;
|
||||
|
||||
// Landlock is an **additive allowlist** — it can only *remove* access, i.e.
|
||||
// it is deny-by-default. A `Posture::Allow` fallback means the opposite,
|
||||
// **allow-by-default**: paths matched by no grant must stay reachable. An
|
||||
// allowlist cannot express that (it would lock everything down to the granted
|
||||
// roots, closing the CLI binary, its libs and `~/.<cli>` home — the very
|
||||
// "failed to open terminal" symptom). So under an Allow fallback we enforce
|
||||
// nothing: the policy's only teeth are explicit Denies, which are deny-islands
|
||||
// under an allow-all that an additive sandbox cannot carve out — they remain
|
||||
// advisory via the LP3 projection. This is a property of the allowlist
|
||||
// mechanism, hence it lives in the adapter, not the pure plan.
|
||||
if plan.default_posture == Posture::Allow {
|
||||
return Ok(SandboxStatus::Enforced);
|
||||
}
|
||||
|
||||
// Handle a right class only if the plan posed a grant of that class, so an
|
||||
// unposed class stays globally unrestricted (per-access-class autonomy).
|
||||
let mut handled = BitFlags::<AccessFs>::empty();
|
||||
@ -283,6 +297,59 @@ mod tests {
|
||||
assert_eq!(status, SandboxStatus::Enforced);
|
||||
}
|
||||
|
||||
/// **Allow fallback ⇒ no OS restriction even with grants posed.** Setting the
|
||||
/// policy to allow-by-default (and, in the UI, every option to Allow) emits RO|RW
|
||||
/// grants scoped to the project root. An allowlist sandbox would then close every
|
||||
/// read/write OUTSIDE that root — locking out the CLI binary, its libs and home,
|
||||
/// the exact "failed to open terminal" bug. Under `Posture::Allow` the adapter must
|
||||
/// instead enforce nothing: from the (would-be) sandboxed thread, a read AND a
|
||||
/// write OUTSIDE the only grant must both still succeed.
|
||||
#[test]
|
||||
fn allow_fallback_does_not_restrict_even_with_grants() {
|
||||
let granted = fresh_dir("allow-granted");
|
||||
let outside = fresh_dir("allow-outside");
|
||||
std::fs::write(outside.join("pre.txt"), b"pre").unwrap();
|
||||
let outside_t = outside.clone();
|
||||
|
||||
let plan = SandboxPlan {
|
||||
allowed: vec![PathGrant {
|
||||
abs_root: granted.to_string_lossy().into_owned(),
|
||||
access: PathAccess::RO.union(PathAccess::RW),
|
||||
}],
|
||||
default_posture: Posture::Allow,
|
||||
};
|
||||
|
||||
let (status, read_outside, write_outside) = std::thread::spawn(move || {
|
||||
let status = LandlockSandbox::new()
|
||||
.enforce(&plan)
|
||||
.expect("an Allow fallback never fails closed");
|
||||
let read = std::fs::read(outside_t.join("pre.txt"));
|
||||
let write =
|
||||
std::fs::write(outside_t.join("new.txt"), b"x").map_err(|e| e.kind());
|
||||
(status, read, write)
|
||||
})
|
||||
.join()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
SandboxStatus::Enforced,
|
||||
"Allow fallback is a no-op enforcement, not an error or a lock-down"
|
||||
);
|
||||
assert!(
|
||||
read_outside.is_ok(),
|
||||
"Allow fallback must keep reads outside the grant open, got {read_outside:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
write_outside,
|
||||
Ok(()),
|
||||
"Allow fallback must keep writes outside the grant open"
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&granted);
|
||||
let _ = std::fs::remove_dir_all(&outside);
|
||||
}
|
||||
|
||||
/// **CRITICAL SAFETY PROPERTY — IdeA is never sandboxed.** The adapter restricts
|
||||
/// the *current thread*; the launch path runs it on a throwaway thread. This test
|
||||
/// proves the Landlock domain is confined to that throwaway thread and does **not**
|
||||
|
||||
Reference in New Issue
Block a user