use super::*;
const WRITE_BITS: u64 = LANDLOCK_ACCESS_FS_WRITE_FILE
| LANDLOCK_ACCESS_FS_REMOVE_DIR
| LANDLOCK_ACCESS_FS_REMOVE_FILE
| LANDLOCK_ACCESS_FS_MAKE_CHAR
| LANDLOCK_ACCESS_FS_MAKE_DIR
| LANDLOCK_ACCESS_FS_MAKE_REG
| LANDLOCK_ACCESS_FS_MAKE_SOCK
| LANDLOCK_ACCESS_FS_MAKE_FIFO
| LANDLOCK_ACCESS_FS_MAKE_BLOCK
| LANDLOCK_ACCESS_FS_MAKE_SYM
| LANDLOCK_ACCESS_FS_REFER
| LANDLOCK_ACCESS_FS_TRUNCATE;
fn linux_policy_with_workspace_ops(ops: &[&str]) -> CapabilityPolicy {
CapabilityPolicy {
tools: Vec::new(),
capabilities: std::collections::BTreeMap::from([(
"workspace".to_string(),
ops.iter().map(|op| op.to_string()).collect(),
)]),
workspace_roots: vec!["/ws".to_string()],
read_only_roots: Vec::new(),
side_effect_level: Some("read_only".to_string()),
recursion_limit: None,
tool_arg_constraints: Vec::new(),
tool_annotations: std::collections::BTreeMap::new(),
sandbox_profile: SandboxProfile::Worktree,
process_sandbox: Default::default(),
process_network_proxy: None,
}
}
#[test]
fn managed_proxy_fails_closed_without_proxy_only_network_namespace() {
let mut policy = linux_policy_with_workspace_ops(&["read_text"]);
policy.side_effect_level = Some("network".to_string());
policy.process_network_proxy = Some(crate::orchestration::ProcessNetworkProxy {
http_port: 3128,
socks_port: 1080,
});
let error = match profile_setup("ignored", &policy, SandboxProfile::Worktree) {
Ok(_) => panic!("managed proxy must not widen to unrestricted Linux sockets"),
Err(error) => error,
};
assert!(
error
.to_string()
.contains("requires a proxy-only Linux network namespace"),
"{error}"
);
}
#[test]
fn no_network_excludes_addressable_sockets_but_allows_local_socketpair() {
let policy = linux_policy_with_workspace_ops(&["read_text"]);
assert_eq!(
policy.side_effect_level.as_deref(),
Some("read_only"),
"fixture must be below the network ceiling",
);
let allowed = allowed_syscalls(&policy);
assert!(
!allowed.contains(&libc::SYS_socket),
"addressable socket() must not be allowlisted without network",
);
assert!(
!allowed.contains(&libc::SYS_connect),
"connect() must not be allowlisted without network",
);
assert!(
allowed.contains(&libc::SYS_socketpair),
"socketpair() (local IPC) must be allowlisted — Cargo's jobserver needs it",
);
for call in [
libc::SYS_recvfrom,
libc::SYS_recvmsg,
libc::SYS_sendmsg,
libc::SYS_sendto,
] {
assert!(
allowed.contains(&call),
"send/recv syscall {call} must be allowlisted — local socketpair IPC (Cargo jobserver) needs it",
);
}
for call in [
libc::SYS_socket,
libc::SYS_connect,
libc::SYS_bind,
libc::SYS_listen,
libc::SYS_accept,
libc::SYS_accept4,
] {
assert!(
!allowed.contains(&call),
"egress opener {call} must stay absent without network",
);
}
}
#[test]
fn network_ceiling_allows_all_socket_syscalls() {
let mut policy = linux_policy_with_workspace_ops(&["read_text"]);
policy.side_effect_level = Some("network".to_string());
let allowed = allowed_syscalls(&policy);
for call in [
libc::SYS_socket,
libc::SYS_socketpair,
libc::SYS_connect,
libc::SYS_bind,
] {
assert!(
allowed.contains(&call),
"network ceiling must allowlist socket-family syscall {call}",
);
}
}
#[test]
fn network_ceiling_grants_exact_name_service_files_without_opening_run() {
let mut policy = linux_policy_with_workspace_ops(&["read_text"]);
assert!(network_name_service_read_roots(&policy).is_empty());
policy.side_effect_level = Some("network".to_string());
let roots = network_name_service_read_roots(&policy);
assert_eq!(
roots,
[
"/etc/resolv.conf",
"/etc/hosts",
"/etc/nsswitch.conf",
"/etc/gai.conf",
"/etc/host.conf",
]
.into_iter()
.map(PathBuf::from)
.collect::<Vec<_>>(),
);
assert!(
roots.iter().all(|root| !root.starts_with("/run")),
"the repair must grant canonical resolver files, never the mutable /run tree",
);
}
#[test]
fn process_network_ceiling_controls_real_child_socket() {
let workspace = tempfile::tempdir().expect("workspace");
let listener = std::net::TcpListener::bind(("127.0.0.1", 0)).expect("loopback listener");
let address = listener.local_addr().expect("listener address");
let args = vec![
"-c".to_string(),
format!("exec 3<>/dev/tcp/127.0.0.1/{}", address.port()),
];
let run_probe = |policy: &CapabilityPolicy| {
let mut command = Command::new("/bin/bash");
command.args(&args).current_dir(workspace.path());
let preparation = Backend::prepare_std_command(
"/bin/bash",
&args,
&mut command,
policy,
SandboxProfile::Worktree,
)
.expect("prepare sandboxed child");
assert!(matches!(preparation, PrepareOutcome::Direct));
command.output().expect("run sandboxed child")
};
let mut denied = linux_policy_with_workspace_ops(&["read_text"]);
denied.workspace_roots = vec![workspace.path().display().to_string()];
denied.side_effect_level = Some("process_exec".to_string());
let denied_output = run_probe(&denied);
assert!(
!denied_output.status.success(),
"the default process-exec ceiling must deny an addressable child socket",
);
let mut allowed = denied;
allowed.side_effect_level = Some("network".to_string());
let allowed_output = run_probe(&allowed);
assert!(
allowed_output.status.success(),
"the network ceiling must permit the child loopback socket: {}",
String::from_utf8_lossy(&allowed_output.stderr),
);
listener
.set_nonblocking(true)
.expect("set listener nonblocking");
listener
.accept()
.expect("the listener must observe the allowed child connection");
}
#[test]
fn seccomp_filter_is_default_deny_allowlist() {
let filter = compile_seccomp_program(&[libc::SYS_read, libc::SYS_write])
.expect("compile the probe filter");
assert_eq!(
filter.last().map(|entry| entry.k),
Some(libc::SECCOMP_RET_ERRNO | libc::EPERM as u32),
"seccomp fallthrough must deny unknown syscalls",
);
assert!(
filter
.iter()
.any(|entry| entry.k == libc::SECCOMP_RET_ALLOW),
"allowlisted syscalls must jump to an allow action",
);
}
#[test]
fn seccomp_filter_validates_architecture_before_syscall_number() {
let filter = compile_seccomp_program(&[libc::SYS_msync]).expect("compile the probe filter");
let arch_load = filter.first().expect("filter must not be empty");
assert_eq!(
arch_load.code,
(libc::BPF_LD | libc::BPF_W | libc::BPF_ABS) as u16,
"the first instruction must be an absolute word load",
);
assert_eq!(
arch_load.k, 4,
"the first load must read seccomp_data.arch (offset 4), not .nr (offset 0)",
);
assert_eq!(
filter.get(2).map(|entry| entry.k),
Some(libc::SECCOMP_RET_KILL_PROCESS),
"an architecture mismatch must kill the process, never return EPERM: \
EPERM would let a caller probe the whole syscall space for free",
);
assert_eq!(
filter.get(3).map(|entry| (entry.code, entry.k)),
Some(((libc::BPF_LD | libc::BPF_W | libc::BPF_ABS) as u16, 0)),
"the syscall number load must follow the architecture check",
);
}
#[test]
fn allowlist_excludes_process_introspection_and_io_uring() {
let policy = linux_policy_with_workspace_ops(&["read_text", "write_text"]);
let allowed = allowed_syscalls(&policy);
for call in [
libc::SYS_ptrace,
libc::SYS_process_vm_readv,
libc::SYS_process_vm_writev,
libc::SYS_io_uring_setup,
libc::SYS_io_uring_enter,
libc::SYS_io_uring_register,
] {
assert!(
!allowed.contains(&call),
"dangerous syscall {call} must stay outside the seccomp allowlist",
);
}
}
#[test]
fn read_only_access_grants_read_and_execute_but_never_write() {
let access = read_only_access();
assert_ne!(access & LANDLOCK_ACCESS_FS_READ_FILE, 0, "read file");
assert_ne!(access & LANDLOCK_ACCESS_FS_READ_DIR, 0, "read dir");
assert_ne!(access & LANDLOCK_ACCESS_FS_EXECUTE, 0, "execute");
assert_eq!(
access & WRITE_BITS,
0,
"read-only access must not carry any write/create/remove right",
);
}
#[test]
fn read_only_access_is_independent_of_workspace_write_capability() {
let writable = linux_policy_with_workspace_ops(&["read_text", "write_text", "delete"]);
assert_ne!(
workspace_access(&writable) & LANDLOCK_ACCESS_FS_WRITE_FILE,
0,
"writable workspace root should carry write",
);
assert_eq!(
read_only_access() & WRITE_BITS,
0,
"read-only roots stay unwritable regardless of workspace write capability",
);
}
#[test]
fn package_manager_config_roots_are_read_only() {
let temp_home = tempfile::tempdir().expect("temp home");
std::fs::write(
temp_home.path().join(".npmrc"),
"registry=https://registry.example\n",
)
.expect("write npmrc");
let roots = super::super::package_manager_config_read_roots_for_home(temp_home.path());
assert!(
roots.iter().any(|path| path.ends_with(".npmrc")),
"npmrc should be part of the package-manager preset"
);
assert!(
roots
.iter()
.any(|path| path.ends_with(".cargo/config.toml")),
"cargo config should be part of the package-manager preset"
);
assert!(
roots.iter().all(|path| path.starts_with(temp_home.path())),
"package-manager roots must stay under HOME"
);
assert_eq!(
read_only_access() & WRITE_BITS,
0,
"package-manager Landlock rules use read-only access bits"
);
}
#[test]
fn developer_toolchain_roots_are_read_only() {
let temp_home = tempfile::tempdir().expect("temp home");
let roots = super::super::developer_toolchain_read_roots_for_home(temp_home.path());
assert!(
roots.iter().any(|path| path.ends_with(".local/share/uv")),
"uv runtimes should be part of the developer-toolchain preset"
);
assert!(
roots.iter().any(|path| path.ends_with(".rustup")),
"rustup should be part of the developer-toolchain preset"
);
assert!(
roots.iter().all(|path| path.starts_with(temp_home.path())),
"developer-toolchain roots must stay under HOME"
);
assert_eq!(
read_only_access() & WRITE_BITS,
0,
"developer-toolchain Landlock rules use read-only access bits"
);
}
#[test]
fn developer_toolchains_admit_linux_vendor_installations() {
let enabled = CapabilityPolicy {
process_sandbox: crate::orchestration::ProcessSandboxPolicy {
presets: Some(vec![ProcessSandboxPreset::DeveloperToolchains]),
..Default::default()
},
..Default::default()
};
assert_eq!(
developer_toolchain_system_read_roots(&enabled),
vec![PathBuf::from("/opt")]
);
let disabled = CapabilityPolicy {
process_sandbox: crate::orchestration::ProcessSandboxPolicy {
presets: Some(Vec::new()),
..Default::default()
},
..Default::default()
};
assert!(developer_toolchain_system_read_roots(&disabled).is_empty());
}
#[test]
fn standard_device_rules_allow_common_device_files_only() {
let rules = standard_device_rules();
assert_eq!(rules.len(), 4);
assert!(rules.iter().any(
|(path, access)| path.as_path() == std::path::Path::new("/dev/null")
&& access & LANDLOCK_ACCESS_FS_READ_FILE != 0
&& access & LANDLOCK_ACCESS_FS_WRITE_FILE != 0
&& access & LANDLOCK_ACCESS_FS_IOCTL_DEV == 0
));
for device in ["/dev/zero", "/dev/random", "/dev/urandom"] {
let Some((_, access)) = rules
.iter()
.find(|(path, _)| path.as_path() == std::path::Path::new(device))
else {
panic!("missing standard device rule for {device}");
};
assert_ne!(
*access & LANDLOCK_ACCESS_FS_READ_FILE,
0,
"{device} should be readable"
);
assert_eq!(
*access & LANDLOCK_ACCESS_FS_WRITE_FILE,
0,
"{device} must not be writable"
);
assert_eq!(
*access & LANDLOCK_ACCESS_FS_IOCTL_DEV,
0,
"{device} must not receive device ioctl access"
);
}
}
#[test]
fn directory_only_access_excludes_file_applicable_rights() {
for right in [
LANDLOCK_ACCESS_FS_READ_FILE,
LANDLOCK_ACCESS_FS_WRITE_FILE,
LANDLOCK_ACCESS_FS_EXECUTE,
LANDLOCK_ACCESS_FS_TRUNCATE,
LANDLOCK_ACCESS_FS_IOCTL_DEV,
] {
assert_eq!(
DIRECTORY_ONLY_ACCESS_FS & right,
0,
"file-applicable right {right:#x} must not be directory-only",
);
}
assert_ne!(
DIRECTORY_ONLY_ACCESS_FS & LANDLOCK_ACCESS_FS_READ_DIR,
0,
"READ_DIR must be classified as directory-only",
);
}
#[test]
fn read_only_access_on_a_regular_file_drops_directory_only_bits() {
let masked = read_only_access() & !DIRECTORY_ONLY_ACCESS_FS;
assert_eq!(
masked & LANDLOCK_ACCESS_FS_READ_DIR,
0,
"READ_DIR must be stripped for non-directory rules",
);
assert_ne!(
masked & LANDLOCK_ACCESS_FS_READ_FILE,
0,
"READ_FILE must survive for non-directory rules",
);
assert_ne!(
masked & LANDLOCK_ACCESS_FS_EXECUTE,
0,
"EXECUTE must survive for non-directory rules",
);
}
#[test]
fn landlock_handled_access_tracks_device_ioctl_abi() {
assert_eq!(
landlock_handled_access(4) & LANDLOCK_ACCESS_FS_IOCTL_DEV,
0,
"ABI 4 kernels do not support device ioctl mediation",
);
assert_ne!(
landlock_handled_access(5) & LANDLOCK_ACCESS_FS_IOCTL_DEV,
0,
"ABI 5+ kernels should explicitly mediate device ioctls",
);
}
#[test]
fn proc_runtime_reads_require_restricted_yama_scope() {
for safe in ["1", "2\n", "3"] {
assert!(yama_scope_contains_process_reads(safe), "scope {safe}");
}
for unsafe_or_unknown in ["0", "", "disabled", "256"] {
assert!(
!yama_scope_contains_process_reads(unsafe_or_unknown),
"scope {unsafe_or_unknown} must not grant procfs reads",
);
}
}
fn tree(root: &std::path::Path, names: &[&str]) {
for name in names {
std::fs::create_dir_all(root.join(name)).expect("tree");
}
}
#[test]
fn report_default_denylist_expansion_cost() {
let Some(home) = crate::user_dirs::home_dir() else {
eprintln!("[landlock-cost] no home dir on this host; nothing to measure");
return;
};
let denied: Vec<PathBuf> = crate::orchestration::default_read_deny_home_paths()
.iter()
.map(|relative| home.join(relative))
.collect();
let home = home.canonicalize().unwrap_or(home);
let granted = expand_around_denied(&home, &denied).expect("expand around home");
let entries = std::fs::read_dir(&home).map(|dir| dir.count()).unwrap_or(0);
eprintln!(
"[landlock-cost] home={} home_entries={} denied={} expanded_rules={} cap={}",
home.display(),
entries,
denied.len(),
granted.len(),
MAX_DENY_EXPANSION_RULES,
);
assert!(
granted.len() <= MAX_DENY_EXPANSION_RULES,
"the product default must not trip the cap on a real home: {} rules",
granted.len()
);
assert!(
entries >= 10,
"[landlock-cost] measured '{}' with only {entries} entries, which is not a real home; \
the cap check would pass vacuously. Something rewrote HOME under this test.",
home.display()
);
}
#[test]
fn a_root_with_no_denial_inside_it_is_granted_whole() {
let temp = tempfile::TempDir::new().expect("temp");
let root = temp.path().canonicalize().expect("canonical");
tree(&root, &["a", "b"]);
let unrelated = tempfile::TempDir::new().expect("unrelated");
let unrelated = unrelated.path().canonicalize().expect("canonical");
let granted = expand_around_denied(&root, &[unrelated]).expect("expand");
assert_eq!(
granted,
vec![root],
"a denial that is not inside the root must cost nothing and leave it intact"
);
}
#[test]
fn a_denied_child_is_replaced_by_its_siblings_and_never_granted() {
let temp = tempfile::TempDir::new().expect("temp");
let root = temp.path().canonicalize().expect("canonical");
tree(&root, &["projects", "documents", ".ssh"]);
let denied = root.join(".ssh");
let granted = expand_around_denied(&root, std::slice::from_ref(&denied)).expect("expand");
assert!(
!granted.contains(&root),
"the root itself must NOT be granted; granting it would include the denied \
subtree, which is the whole failure this function exists to prevent: {granted:?}"
);
assert!(
!granted.iter().any(|path| path.starts_with(&denied)),
"no grant may lead into the denied subtree: {granted:?}"
);
assert!(
granted.contains(&root.join("projects")) && granted.contains(&root.join("documents")),
"the siblings must still be reachable, or the subtraction has silently removed \
access the policy granted: {granted:?}"
);
}
#[test]
fn a_nested_denial_keeps_siblings_at_every_level() {
let temp = tempfile::TempDir::new().expect("temp");
let root = temp.path().canonicalize().expect("canonical");
tree(&root, &["keep-me", ".config/gh", ".config/keep-this"]);
let denied = root.join(".config/gh");
let granted = expand_around_denied(&root, std::slice::from_ref(&denied)).expect("expand");
assert!(
granted.contains(&root.join("keep-me")),
"a sibling at the top level must survive: {granted:?}"
);
assert!(
granted.contains(&root.join(".config/keep-this")),
"a sibling INSIDE the denied path's parent must survive, which is what makes this \
a subtraction rather than denying the whole parent: {granted:?}"
);
assert!(
!granted.contains(&denied) && !granted.contains(&root.join(".config")),
"neither the denial nor any ancestor that contains it may be granted: {granted:?}"
);
}
#[test]
fn a_root_that_is_itself_denied_grants_nothing() {
let temp = tempfile::TempDir::new().expect("temp");
let root = temp.path().canonicalize().expect("canonical");
tree(&root, &["inside"]);
let granted = expand_around_denied(&root, std::slice::from_ref(&root)).expect("expand");
assert!(
granted.is_empty(),
"a root that IS the denial must grant nothing at all: {granted:?}"
);
}
#[test]
fn an_unreadable_ancestor_grants_nothing_beneath_it_and_never_itself() {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::TempDir::new().expect("temp");
let root = temp.path().canonicalize().expect("canonical");
tree(&root, &["locked/.ssh", "visible/keep"]);
let locked = root.join("locked");
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).expect("lock dir");
let result = expand_around_denied(&root, &[locked.join(".ssh")]);
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755)).expect("unlock");
let granted = result.expect("an unreadable ancestor must not refuse the spawn");
assert!(
!granted
.iter()
.any(|path| path == &locked || locked.starts_with(path)),
"granting the unreadable ancestor (or anything above it) would expose the \
subtree we could not enumerate around: {granted:?}"
);
assert!(
granted.contains(&root.join("visible")),
"a sibling outside the unreadable subtree must still be granted: {granted:?}"
);
}
#[test]
fn a_live_landlock_child_is_refused_a_denied_file_and_allowed_its_sibling() {
if landlock_abi_version() == 0 {
eprintln!("[landlock-live] SKIPPED: no Landlock on this kernel");
return;
}
let home = tempfile::TempDir::new().expect("temp home");
let home_path = home.path().canonicalize().expect("canonical home");
let secrets = home_path.join("secrets");
std::fs::create_dir_all(&secrets).expect("secrets dir");
let denied_file = secrets.join("id_ed25519");
std::fs::write(&denied_file, "NOT-A-REAL-KEY\n").expect("dummy key");
let allowed_file = home_path.join("readable.txt");
std::fs::write(&allowed_file, "READABLE\n").expect("control file");
let read_under = |deny: &[String], target: &std::path::Path| -> std::io::Result<bool> {
let policy = CapabilityPolicy {
workspace_roots: vec![home_path.display().to_string()],
sandbox_profile: SandboxProfile::Worktree,
process_sandbox: crate::orchestration::ProcessSandboxPolicy {
read_deny_roots: deny.to_vec(),
..Default::default()
},
..CapabilityPolicy::default()
};
crate::orchestration::push_execution_policy(policy);
let output = crate::stdlib::sandbox::command_output(
"/bin/cat",
&[target.display().to_string()],
&crate::stdlib::sandbox::ProcessCommandConfig::default(),
);
crate::orchestration::pop_execution_policy();
Ok(matches!(output, Ok(out) if out.status.success()))
};
let deny = vec![secrets.display().to_string()];
let control = read_under(&deny, &allowed_file).expect("control read");
assert!(
control,
"the control file inside the same workspace root must be readable, or the denial \
below proves nothing"
);
let denied = read_under(&deny, &denied_file).expect("denied read");
assert!(
!denied,
"a denied file must be refused even though its parent root is granted; it was read"
);
let ungated = read_under(&[], &denied_file).expect("ungated read");
assert!(
ungated,
"with the denial removed the same file must become readable, which is what proves \
the refusal was the denylist and not an unrelated accident"
);
eprintln!("[landlock-live] denied refused, sibling readable, revert readable");
}
#[test]
fn a_denial_under_a_missing_directory_costs_nothing() {
let temp = tempfile::TempDir::new().expect("temp");
let root = temp.path().canonicalize().expect("canonical");
tree(&root, &["present"]);
let granted = expand_around_denied(&root, &[root.join("absent/config")])
.expect("a denial under a missing directory must not refuse the spawn");
assert!(
granted.contains(&root.join("present")),
"the siblings must still be granted: {granted:?}"
);
}
#[test]
fn an_unexpected_enumeration_error_still_refuses_the_spawn() {
let temp = tempfile::TempDir::new().expect("temp");
let root = temp.path().canonicalize().expect("canonical");
std::fs::write(root.join("notadir"), b"x").expect("write file");
let result = expand_around_denied(&root, &[root.join("notadir/inner/secret")]);
assert!(
result.is_err(),
"an enumeration error that is neither missing nor forbidden must fail closed, \
got {result:?}"
);
}
#[test]
fn an_unreadable_optional_root_is_skipped_and_a_required_one_still_fails() {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::TempDir::new().expect("temp");
let root = temp.path().canonicalize().expect("canonical");
let locked = root.join("locked");
std::fs::create_dir_all(&locked).expect("mkdir");
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).expect("lock");
let mut profile = LandlockProfile {
ruleset_fd: -1,
rules: Vec::new(),
handled_access_fs: 0,
read_deny_roots: Vec::new(),
};
let optional = push_rule_exact(
&mut profile,
locked.clone(),
LANDLOCK_ACCESS_FS_READ_FILE,
true,
);
let required = push_rule_exact(
&mut profile,
locked.clone(),
LANDLOCK_ACCESS_FS_READ_FILE,
false,
);
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755)).expect("unlock");
assert!(
optional.is_ok(),
"an unreadable OPTIONAL root must be skipped, not refuse the spawn: {optional:?}"
);
assert!(
required.is_err(),
"an unreadable REQUIRED root must still fail closed; something asked for it by name"
);
}
#[test]
fn a_confined_child_still_spawns_when_a_preset_root_exists_but_cannot_be_read() {
use std::os::unix::fs::PermissionsExt;
if landlock_abi_version() == 0 {
eprintln!("[unreadable-root] SKIPPED: no Landlock on this kernel");
return;
}
let _env_lock = crate::runtime_paths::test_env_lock()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let home = tempfile::TempDir::new().expect("temp home");
let home_path = home.path().canonicalize().expect("canonical");
let unreadable = home_path.join(".asdf");
std::fs::create_dir_all(unreadable.join("shims")).expect("mkdir");
std::fs::set_permissions(&unreadable, std::fs::Permissions::from_mode(0o000)).expect("lock");
let previous_home = std::env::var_os("HOME");
std::env::set_var("HOME", &home_path);
let policy = CapabilityPolicy {
workspace_roots: vec![home_path.display().to_string()],
sandbox_profile: SandboxProfile::Worktree,
..CapabilityPolicy::default()
};
crate::orchestration::push_execution_policy(policy);
let output = crate::stdlib::sandbox::command_output(
"/bin/echo",
&["UNREADABLE-ROOT-PROBE-ALIVE".to_string()],
&crate::stdlib::sandbox::ProcessCommandConfig::default(),
);
crate::orchestration::pop_execution_policy();
std::fs::set_permissions(&unreadable, std::fs::Permissions::from_mode(0o755)).expect("unlock");
match previous_home {
Some(value) => std::env::set_var("HOME", value),
None => std::env::remove_var("HOME"),
}
let output = output.expect(
"an unreadable preset root must not refuse the spawn; this is the /root-under-hardened \
control and its failure is the exact outage it exists to catch",
);
assert!(
output.status.success(),
"the confined child must run: {output:?}"
);
assert!(
String::from_utf8_lossy(&output.stdout).contains("UNREADABLE-ROOT-PROBE-ALIVE"),
"the child must actually have executed, not merely exited zero"
);
eprintln!("[unreadable-root] confined child ran with an unreadable preset root present");
}