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:
38
.ideai/permissions.json
Normal file
38
.ideai/permissions.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"projectDefaults": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"capability": "read",
|
||||||
|
"effect": "allow",
|
||||||
|
"paths": [
|
||||||
|
"**"
|
||||||
|
],
|
||||||
|
"commands": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"capability": "write",
|
||||||
|
"effect": "allow",
|
||||||
|
"paths": [
|
||||||
|
"**"
|
||||||
|
],
|
||||||
|
"commands": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"capability": "delete",
|
||||||
|
"effect": "allow",
|
||||||
|
"paths": [
|
||||||
|
"**"
|
||||||
|
],
|
||||||
|
"commands": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"capability": "executeBash",
|
||||||
|
"effect": "allow",
|
||||||
|
"paths": [],
|
||||||
|
"commands": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fallback": "allow"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -90,6 +90,20 @@ impl SandboxEnforcer for LandlockSandbox {
|
|||||||
fn enforce(&self, plan: &SandboxPlan) -> Result<SandboxStatus, SandboxError> {
|
fn enforce(&self, plan: &SandboxPlan) -> Result<SandboxStatus, SandboxError> {
|
||||||
let abi = TARGET_ABI;
|
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
|
// 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).
|
// unposed class stays globally unrestricted (per-access-class autonomy).
|
||||||
let mut handled = BitFlags::<AccessFs>::empty();
|
let mut handled = BitFlags::<AccessFs>::empty();
|
||||||
@ -283,6 +297,59 @@ mod tests {
|
|||||||
assert_eq!(status, SandboxStatus::Enforced);
|
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
|
/// **CRITICAL SAFETY PROPERTY — IdeA is never sandboxed.** The adapter restricts
|
||||||
/// the *current thread*; the launch path runs it on a throwaway thread. This test
|
/// 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**
|
/// proves the Landlock domain is confined to that throwaway thread and does **not**
|
||||||
|
|||||||
Reference in New Issue
Block a user