lazaret/crates/lazaret-runtime-supervisor/tests/sandbox_builder.rs

833 lines
29 KiB
Rust
Raw Normal View History

use std::collections::{BTreeMap, BTreeSet};
use std::ffi::OsStr;
use std::fs;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use lazaret_core::configuration::{
ConfigurationLoadRequest, ResolvedDomainConfiguration, load_trusted_configuration,
};
use lazaret_core::identity::{ContainerId, DeviceId, InodeId, NamespaceIdentity};
use lazaret_runtime_supervisor::sandbox::{
CONTROL_ROOT_DESTINATION, CREDENTIALS_DESTINATION, DEV_DESTINATION, DOMAIN_OPEN_FILE_LIMIT,
ESCALATION_CLIENT_DESTINATION, HOSTNAME_DESTINATION, HOSTS_DESTINATION, HostTerminal,
MAILBOX_REQUESTS_DESTINATION, MAILBOX_RESULTS_DESTINATION, NETWORK_NAMESPACE_PATH,
PROC_DESTINATION, RESOLV_CONF_DESTINATION, RootProcess, SANDBOX_INIT_DESTINATION,
SYS_DESTINATION, SandboxBuildErrorKind, SandboxBuildRequest, SandboxBundle,
TERMINFO_ROOT_DESTINATION, WORKSPACE_DESTINATION, build_sandbox_bundle,
build_sandbox_bundle_with_workspace, default_sandbox_layout, stage_host_terminal,
};
use serde_json::Value;
use tempfile::TempDir;
struct Fixture {
tree: TempDir,
configuration: ResolvedDomainConfiguration,
terminfo_source: PathBuf,
}
impl Fixture {
fn new() -> Self {
let tree = tempfile::tempdir().unwrap();
let configuration_directory = tree.path().join("configuration");
let workspace = tree.path().join("workspace");
let runtime = tree.path().join("runtime");
fs::create_dir(&configuration_directory).unwrap();
fs::create_dir(&workspace).unwrap();
fs::create_dir(&runtime).unwrap();
fs::create_dir(tree.path().join("prepared-root")).unwrap();
let configuration_path = configuration_directory.join("lazaret.toml");
fs::write(
&configuration_path,
format!(
r#"schema = "lazaret-trusted-host-config-v1"
[storage]
data_root = "{}"
[network]
default_allow = ["192.0.2.10/32"]
[profiles.default]
image = "example.invalid/agent:v1"
persistent_dirs = ["/root", "/usr", "/var", "/etc", "/opt"]
env = {{ SELECTED_SECRET = "env:SELECTED_HOST_SECRET" }}
volumes = [
{{ host = "{{profile_dir}}/read-only", container = "/root/read-only", kind = "directory", access = "read-only", create = true }},
{{ host = "{{profile_dir}}/read-write", container = "/root/read-write", kind = "directory", access = "read-write", create = true }},
]
[profiles.unselected]
image = "example.invalid/unselected:v1"
env = {{ UNSELECTED_SECRET = "env:UNSELECTED_HOST_SECRET" }}
"#,
tree.path().join("data").display()
),
)
.unwrap();
let environment = BTreeMap::from([
("SELECTED_HOST_SECRET".to_owned(), "selected-value".into()),
("UNSELECTED_HOST_SECRET".to_owned(), "must-not-enter".into()),
]);
let configuration = load_trusted_configuration(ConfigurationLoadRequest {
explicit_configuration_path: Some(configuration_path),
selected_profile: "default".to_owned(),
workspace,
home: tree.path().join("home"),
xdg_config_home: None,
xdg_data_home: None,
xdg_runtime_dir: runtime,
environment,
sandbox_layout: default_sandbox_layout().unwrap(),
})
.unwrap();
let terminfo_source = tree
.path()
.join("external-host-terminfo")
.join("x")
.join("xterm-lazaret");
fs::create_dir_all(terminfo_source.parent().unwrap()).unwrap();
fs::write(&terminfo_source, compiled_terminfo("xterm-lazaret")).unwrap();
Self {
tree,
configuration,
terminfo_source,
}
}
fn terminal(&self) -> lazaret_runtime_supervisor::sandbox::StagedHostTerminal {
let terminal = HostTerminal::from_compiled_entry(
"xterm-lazaret",
Some("truecolor"),
fs::read(&self.terminfo_source).unwrap(),
)
.unwrap();
stage_host_terminal(&self.configuration, ContainerId::new([0x11; 32]), terminal).unwrap()
}
fn reload_configuration(&self) -> ResolvedDomainConfiguration {
let root = self.tree.path();
load_trusted_configuration(ConfigurationLoadRequest {
explicit_configuration_path: Some(root.join("configuration/lazaret.toml")),
selected_profile: "default".to_owned(),
workspace: root.join("workspace"),
home: root.join("home"),
xdg_config_home: None,
xdg_data_home: None,
xdg_runtime_dir: root.join("runtime"),
environment: BTreeMap::from([
("SELECTED_HOST_SECRET".to_owned(), "selected-value".into()),
("UNSELECTED_HOST_SECRET".to_owned(), "must-not-enter".into()),
]),
sandbox_layout: default_sandbox_layout().unwrap(),
})
.unwrap()
}
fn staged_terminfo_source(&self) -> PathBuf {
PathBuf::from(
String::from_utf8(
self.configuration
.runtime_domain_root()
.canonical_label()
.as_bytes()
.to_vec(),
)
.unwrap(),
)
.join("sandbox-control")
.join("11".repeat(32))
.join("terminfo/x/xterm-lazaret")
}
fn request(&self, root_process: RootProcess) -> SandboxBuildRequest<'_> {
SandboxBuildRequest {
configuration: &self.configuration,
container_id: ContainerId::new([0x11; 32]),
expected_network_namespace: current_network_namespace(),
root_process,
terminal: Some(self.terminal()),
prepared_root: self.tree.path().join("prepared-root"),
}
}
fn stage_handoff(&self, bundle: &lazaret_runtime_supervisor::sandbox::SandboxBundle) {
let runtime = host_path(
self.configuration
.runtime_domain_root()
.canonical_label()
.as_bytes(),
);
let container = "11".repeat(32);
for directory in [
format!("bundles/{container}"),
"runsc".to_owned(),
format!("runsc-overlay/{container}"),
"state-anchor/merged/root".to_owned(),
"state-anchor/merged/usr".to_owned(),
"state-anchor/merged/var".to_owned(),
"state-anchor/merged/etc".to_owned(),
"state-anchor/merged/opt".to_owned(),
"state-anchor/runtime-files".to_owned(),
format!("sandbox-control/{container}/credentials"),
format!("sandbox-control/{container}/mailbox/requests"),
format!("sandbox-control/{container}/mailbox/results"),
format!("sandbox-control/{container}/bin"),
format!("sandbox-control/{container}/readiness"),
] {
fs::create_dir_all(runtime.join(directory)).unwrap();
}
for name in ["resolv.conf", "hosts", "hostname"] {
fs::write(
runtime.join("state-anchor/runtime-files").join(name),
b"fixture",
)
.unwrap();
}
fs::write(
runtime.join(format!("bundles/{container}/config.json")),
bundle.specification_json(),
)
.unwrap();
fs::write(
runtime.join(format!("sandbox-control/{container}/bin/lazaret-escalate")),
b"executable",
)
.unwrap();
fs::write(
runtime.join(format!(
"sandbox-control/{container}/bin/lazaret-sandbox-init"
)),
b"executable",
)
.unwrap();
}
fn bundle_config_path(&self) -> PathBuf {
host_path(
self.configuration
.runtime_domain_root()
.canonical_label()
.as_bytes(),
)
.join("bundles")
.join("11".repeat(32))
.join("config.json")
}
}
fn host_path(bytes: &[u8]) -> PathBuf {
PathBuf::from(OsStr::from_bytes(bytes))
}
fn compiled_terminfo(term: &str) -> Vec<u8> {
let names = format!("{term}|Lazaret test terminal\0").into_bytes();
let mut bytes = Vec::with_capacity(12 + names.len());
for value in [0x011a_u16, u16::try_from(names.len()).unwrap(), 0, 0, 0, 0] {
bytes.extend_from_slice(&value.to_le_bytes());
}
bytes.extend_from_slice(&names);
bytes
}
fn current_network_namespace() -> NamespaceIdentity {
lazaret_linux::filesystem::namespace_identity(Path::new(NETWORK_NAMESPACE_PATH)).unwrap()
}
fn root_process(arguments: impl IntoIterator<Item = String>) -> RootProcess {
RootProcess::new("/sbin/lazaret-sandbox-init", arguments).unwrap()
}
fn value(bundle: &lazaret_runtime_supervisor::sandbox::SandboxBundle) -> Value {
serde_json::from_slice(bundle.specification_json()).unwrap()
}
fn mounts(specification: &Value) -> BTreeMap<&str, &Value> {
specification["mounts"]
.as_array()
.unwrap()
.iter()
.map(|mount| (mount["destination"].as_str().unwrap(), mount))
.collect()
}
fn mount_destinations(specification: &Value) -> Vec<&str> {
specification["mounts"]
.as_array()
.unwrap()
.iter()
.map(|mount| mount["destination"].as_str().unwrap())
.collect()
}
fn options(mount: &Value) -> BTreeSet<&str> {
mount["options"]
.as_array()
.unwrap()
.iter()
.map(|option| option.as_str().unwrap())
.collect()
}
#[test]
fn deterministic_snapshot_has_only_private_namespaces_and_fixed_runtime_shape() {
let fixture = Fixture::new();
let arguments = vec!["literal;$(touch /host)".to_owned(), "two words".to_owned()];
let first = build_sandbox_bundle(fixture.request(root_process(arguments.clone()))).unwrap();
let second = build_sandbox_bundle(fixture.request(root_process(arguments.clone()))).unwrap();
assert_eq!(first, second);
let specification = value(&first);
assert_eq!(specification["ociVersion"], "1.2.1");
assert_eq!(specification["hostname"], "lazaret");
assert_eq!(specification["process"]["terminal"], false);
assert_eq!(specification["process"]["noNewPrivileges"], true);
assert_eq!(specification["process"]["cwd"], WORKSPACE_DESTINATION);
assert_eq!(
specification["process"]["rlimits"],
serde_json::json!([{
"type": "RLIMIT_NOFILE",
"hard": DOMAIN_OPEN_FILE_LIMIT,
"soft": DOMAIN_OPEN_FILE_LIMIT
}])
);
assert_eq!(
specification["process"]["args"],
serde_json::json!([
"/sbin/lazaret-sandbox-init",
"literal;$(touch /host)",
"two words"
])
);
assert_eq!(
specification["process"]["capabilities"],
serde_json::json!({
"bounding": ["CAP_SETGID", "CAP_SETUID"],
"effective": ["CAP_SETGID", "CAP_SETUID"],
"inheritable": [],
"permitted": ["CAP_SETGID", "CAP_SETUID"],
"ambient": []
})
);
let namespaces = specification["linux"]["namespaces"].as_array().unwrap();
assert_eq!(
namespaces
.iter()
.map(|namespace| namespace["type"].as_str().unwrap())
.collect::<Vec<_>>(),
["pid", "ipc", "uts", "mount", "network"]
);
for namespace in &namespaces[..4] {
assert!(namespace.get("path").is_none());
}
assert_eq!(namespaces[4]["path"], NETWORK_NAMESPACE_PATH);
assert!(specification["linux"].get("uidMappings").is_none());
assert!(specification["linux"].get("gidMappings").is_none());
assert_runtime_command(&first, &fixture);
assert_eq!(
specification["root"]["path"],
fixture
.tree
.path()
.join("prepared-root")
.display()
.to_string()
);
}
#[test]
fn routed_workspace_source_replaces_only_the_workspace_bind_source() {
let fixture = Fixture::new();
let routed = fixture.tree.path().join("runtime/routed-workspace");
let bundle =
build_sandbox_bundle_with_workspace(fixture.request(root_process([])), routed.clone())
.unwrap();
let specification = value(&bundle);
assert_eq!(
mounts(&specification)[WORKSPACE_DESTINATION]["source"],
routed.to_string_lossy().as_ref()
);
}
fn assert_runtime_command(bundle: &SandboxBundle, fixture: &Fixture) {
assert_eq!(bundle.command().program(), OsStr::new("runsc"));
let runtime_domain = String::from_utf8(
fixture
.configuration
.runtime_domain_root()
.canonical_label()
.as_bytes()
.to_vec(),
)
.unwrap();
assert_eq!(
bundle
.command()
.arguments()
.iter()
.map(|argument| argument.to_str().unwrap())
.collect::<Vec<_>>(),
[
format!("-root={runtime_domain}/runsc"),
"-network=host".to_owned(),
"-ignore-cgroups".to_owned(),
format!(
"-overlay2=root:dir={runtime_domain}/runsc-overlay/{}",
"11".repeat(32)
),
"run".to_owned(),
format!(
"-bundle={}/bundles/{}",
String::from_utf8(
fixture
.configuration
.runtime_domain_root()
.canonical_label()
.as_bytes()
.to_vec()
)
.unwrap(),
"11".repeat(32)
),
"11".repeat(32),
]
);
assert!(!bundle.command().arguments().iter().any(|argument| {
matches!(
argument.to_str(),
Some("-detach" | "-console-socket" | "-c")
)
}));
}
#[test]
fn product_init_role_has_exact_terminal_neutral_rootless_bundle_shape() {
let fixture = Fixture::new();
let root = RootProcess::new(
SANDBOX_INIT_DESTINATION,
["--lazaret-private-sandbox-init-v1".to_owned()],
)
.unwrap();
let specification = value(&build_sandbox_bundle(fixture.request(root)).unwrap());
assert_eq!(specification["process"]["terminal"], false);
assert_eq!(
specification["process"]["args"],
serde_json::json!([
SANDBOX_INIT_DESTINATION,
"--lazaret-private-sandbox-init-v1"
])
);
assert_eq!(specification["process"]["user"]["uid"], 0);
assert_eq!(specification["process"]["user"]["gid"], 0);
assert!(specification["linux"].get("uidMappings").is_none());
assert!(specification["linux"].get("gidMappings").is_none());
assert_eq!(
mounts(&specification)[SANDBOX_INIT_DESTINATION]["source"],
host_path(
fixture
.configuration
.runtime_domain_root()
.canonical_label()
.as_bytes(),
)
.join("sandbox-control")
.join("11".repeat(32))
.join("bin/lazaret-sandbox-init")
.display()
.to_string()
);
}
#[test]
fn trusted_init_environment_is_empty_and_volume_modes_are_preserved() {
let fixture = Fixture::new();
let bundle = build_sandbox_bundle(fixture.request(root_process([]))).unwrap();
let specification = value(&bundle);
let environment = specification["process"]["env"]
.as_array()
.unwrap()
.iter()
.map(|entry| entry.as_str().unwrap())
.collect::<BTreeSet<_>>();
assert!(environment.is_empty());
let mounts = mounts(&specification);
assert!(options(mounts[WORKSPACE_DESTINATION]).contains("rw"));
assert!(options(mounts["/root"]).contains("rw"));
assert!(options(mounts["/root/read-only"]).contains("ro"));
assert!(options(mounts["/root/read-write"]).contains("rw"));
let terminfo = &mounts["/run/lazaret/terminfo/x/xterm-lazaret"];
assert_eq!(
terminfo["source"],
fixture.staged_terminfo_source().display().to_string()
);
assert_eq!(
fs::read(fixture.staged_terminfo_source()).unwrap(),
fs::read(&fixture.terminfo_source).unwrap()
);
assert!(
!bundle
.specification_json()
.windows(fixture.terminfo_source.as_os_str().as_bytes().len())
.any(|window| window == fixture.terminfo_source.as_os_str().as_bytes())
);
assert!(
options(terminfo).is_superset(&BTreeSet::from(["bind", "ro", "nodev", "nosuid", "noexec"]))
);
assert!(options(mounts[CREDENTIALS_DESTINATION]).contains("ro"));
assert!(options(mounts[MAILBOX_REQUESTS_DESTINATION]).contains("rw"));
assert!(options(mounts[MAILBOX_RESULTS_DESTINATION]).contains("ro"));
assert!(options(mounts[ESCALATION_CLIENT_DESTINATION]).contains("ro"));
assert!(!options(mounts[ESCALATION_CLIENT_DESTINATION]).contains("noexec"));
for name in ["resolv.conf", "hosts", "hostname"] {
let destination = format!("/etc/{name}");
let mount = &mounts[destination.as_str()];
assert!(options(mount).contains("ro"));
assert!(
mount["source"]
.as_str()
.unwrap()
.ends_with(&format!("state-anchor/runtime-files/{name}"))
);
}
let destinations = mount_destinations(&specification);
let persistent_etc = destinations
.iter()
.position(|path| *path == "/etc")
.unwrap();
for runtime_file in [
RESOLV_CONF_DESTINATION,
HOSTS_DESTINATION,
HOSTNAME_DESTINATION,
] {
assert!(
persistent_etc
< destinations
.iter()
.position(|path| *path == runtime_file)
.unwrap()
);
}
}
#[test]
fn configuration_document_unselected_secret_and_unselected_host_paths_do_not_enter_bundle() {
let fixture = Fixture::new();
let bundle = build_sandbox_bundle(fixture.request(root_process([]))).unwrap();
let bytes = bundle.specification_json();
for forbidden in [
b"lazaret-trusted-host-config-v1".as_slice(),
b"UNSELECTED_SECRET".as_slice(),
b"must-not-enter".as_slice(),
b"example.invalid/unselected:v1".as_slice(),
b"configuration/lazaret.toml".as_slice(),
] {
assert!(
!bytes
.windows(forbidden.len())
.any(|window| window == forbidden)
);
}
let specification = value(&bundle);
let allowed_external_sources = BTreeSet::from([
String::from_utf8(
fixture
.configuration
.workspace()
.canonical_label()
.as_bytes()
.to_vec(),
)
.unwrap(),
String::from_utf8(
fixture.configuration.volumes()[0]
.record()
.canonical_host_source
.as_bytes()
.to_vec(),
)
.unwrap(),
String::from_utf8(
fixture.configuration.volumes()[1]
.record()
.canonical_host_source
.as_bytes()
.to_vec(),
)
.unwrap(),
]);
let runtime_domain = String::from_utf8(
fixture
.configuration
.runtime_domain_root()
.canonical_label()
.as_bytes()
.to_vec(),
)
.unwrap();
for mount in specification["mounts"].as_array().unwrap() {
let Some(source) = mount.get("source").and_then(Value::as_str) else {
continue;
};
if source.starts_with('/') {
assert!(
allowed_external_sources.contains(source) || source.starts_with(&runtime_domain)
);
}
}
}
#[test]
fn hostile_terminal_names_sources_and_changed_entries_fail_closed() {
let fixture = Fixture::new();
for term in ["../xterm", "xterm/name", "xterm\nname", "", &"x".repeat(65)] {
let error =
HostTerminal::from_compiled_entry(term, None, compiled_terminfo("xterm-lazaret"))
.unwrap_err();
assert_eq!(error.kind(), SandboxBuildErrorKind::InvalidTerminal);
}
assert!(
HostTerminal::from_compiled_entry("passwd", None, fs::read("/etc/passwd").unwrap())
.is_err()
);
assert!(
HostTerminal::from_compiled_entry(
"other-terminal",
None,
compiled_terminfo("xterm-lazaret")
)
.is_err()
);
for invalid in [vec![], b"not compiled terminfo".to_vec(), vec![0_u8; 12]] {
assert!(HostTerminal::from_compiled_entry("xterm-lazaret", None, invalid).is_err());
}
let terminal = fixture.terminal();
fs::remove_file(fixture.staged_terminfo_source()).unwrap();
fs::write(
fixture.staged_terminfo_source(),
compiled_terminfo("xterm-lazaret"),
)
.unwrap();
let error = build_sandbox_bundle(SandboxBuildRequest {
configuration: &fixture.configuration,
container_id: ContainerId::new([0x11; 32]),
expected_network_namespace: current_network_namespace(),
root_process: root_process([]),
terminal: Some(terminal),
prepared_root: fixture.tree.path().join("prepared-root"),
})
.unwrap_err();
assert_eq!(error.kind(), SandboxBuildErrorKind::ChangedAuthority);
}
#[test]
fn invalid_processes_and_wrong_network_identity_fail_closed() {
assert!(RootProcess::new("bin/agent", []).is_err());
assert!(RootProcess::new("/bin/agent", ["nul\0argument".to_owned()]).is_err());
let fixture = Fixture::new();
for literal in [
"$(touch /host)",
"`touch /host`",
"a; touch /host",
"a && touch /host",
"a | touch /host",
"*.secret",
"$HOME",
] {
let bundle =
build_sandbox_bundle(fixture.request(root_process([literal.to_owned()]))).unwrap();
assert_eq!(
value(&bundle)["process"]["args"],
serde_json::json!(["/sbin/lazaret-sandbox-init", literal])
);
}
let error = build_sandbox_bundle(SandboxBuildRequest {
configuration: &fixture.configuration,
container_id: ContainerId::new([0x11; 32]),
expected_network_namespace: NamespaceIdentity {
device: DeviceId::new(u64::MAX),
inode: InodeId::new(u64::MAX),
},
root_process: root_process([]),
terminal: Some(fixture.terminal()),
prepared_root: fixture.tree.path().join("prepared-root"),
})
.unwrap_err();
assert_eq!(
error.kind(),
SandboxBuildErrorKind::NetworkNamespaceMismatch
);
}
#[test]
fn default_layout_reserves_every_mount_root() {
let layout = default_sandbox_layout().unwrap();
assert_eq!(
layout.protected_destinations(),
&BTreeSet::from([
DEV_DESTINATION.to_owned(),
HOSTNAME_DESTINATION.to_owned(),
HOSTS_DESTINATION.to_owned(),
PROC_DESTINATION.to_owned(),
CONTROL_ROOT_DESTINATION.to_owned(),
RESOLV_CONF_DESTINATION.to_owned(),
SYS_DESTINATION.to_owned(),
WORKSPACE_DESTINATION.to_owned(),
])
);
assert_eq!(TERMINFO_ROOT_DESTINATION, "/run/lazaret/terminfo");
}
#[test]
fn final_handoff_refuses_replaced_workspace_and_runtime_sources() {
let fixture = Fixture::new();
let bundle = build_sandbox_bundle(fixture.request(root_process([]))).unwrap();
fixture.stage_handoff(&bundle);
let handoff = bundle
.capture_handoff_authority(&fixture.configuration)
.unwrap();
let workspace = host_path(
fixture
.configuration
.workspace()
.canonical_label()
.as_bytes(),
);
fs::rename(&workspace, workspace.with_extension("selected")).unwrap();
fs::create_dir(&workspace).unwrap();
fs::write(workspace.join("replacement-marker"), b"unchanged").unwrap();
let error = handoff.revalidate().unwrap_err();
assert_eq!(error.kind(), SandboxBuildErrorKind::ChangedAuthority);
assert_eq!(
fs::read(workspace.join("replacement-marker")).unwrap(),
b"unchanged"
);
let fixture = Fixture::new();
let bundle = build_sandbox_bundle(fixture.request(root_process([]))).unwrap();
fixture.stage_handoff(&bundle);
let handoff = bundle
.capture_handoff_authority(&fixture.configuration)
.unwrap();
let rootfs = fixture.tree.path().join("prepared-root");
fs::rename(&rootfs, fixture.tree.path().join("prepared-root-selected")).unwrap();
fs::create_dir(&rootfs).unwrap();
fs::write(rootfs.join("replacement-marker"), b"unchanged").unwrap();
let error = handoff.revalidate().unwrap_err();
assert_eq!(error.kind(), SandboxBuildErrorKind::ChangedAuthority);
assert_eq!(
fs::read(rootfs.join("replacement-marker")).unwrap(),
b"unchanged"
);
}
#[test]
fn reloaded_same_label_configuration_cannot_replace_bundle_authority() {
for selected in ["workspace", "volume", "runtime-domain"] {
let fixture = Fixture::new();
let bundle = build_sandbox_bundle(fixture.request(root_process([]))).unwrap();
fixture.stage_handoff(&bundle);
let path = match selected {
"workspace" => host_path(
fixture
.configuration
.workspace()
.canonical_label()
.as_bytes(),
),
"volume" => host_path(
fixture.configuration.volumes()[0]
.record()
.canonical_host_source
.as_bytes(),
),
"runtime-domain" => host_path(
fixture
.configuration
.runtime_domain_root()
.canonical_label()
.as_bytes(),
),
_ => unreachable!(),
};
let selected_path = path.with_extension(format!("{selected}-selected"));
fs::rename(&path, &selected_path).unwrap();
fs::create_dir(&path).unwrap();
fs::set_permissions(&path, std::os::unix::fs::PermissionsExt::from_mode(0o700)).unwrap();
let marker = path.join("replacement-marker");
fs::write(&marker, b"unchanged").unwrap();
let reloaded = fixture.reload_configuration();
let Err(error) = bundle.capture_handoff_authority(&reloaded) else {
panic!("reloaded {selected} replacement was accepted");
};
assert_eq!(error.kind(), SandboxBuildErrorKind::ChangedAuthority);
assert_eq!(fs::read(marker).unwrap(), b"unchanged");
}
}
#[test]
fn post_build_terminfo_replacement_cannot_become_handoff_authority() {
let fixture = Fixture::new();
let bundle = build_sandbox_bundle(fixture.request(root_process([]))).unwrap();
let terminfo = fixture.staged_terminfo_source();
fs::rename(&terminfo, terminfo.with_extension("selected")).unwrap();
fs::write(&terminfo, b"replacement remains unchanged").unwrap();
fixture.stage_handoff(&bundle);
let Err(error) = bundle.capture_handoff_authority(&fixture.configuration) else {
panic!("post-build terminfo replacement was accepted");
};
assert_eq!(error.kind(), SandboxBuildErrorKind::ChangedAuthority);
assert_eq!(
fs::read(terminfo).unwrap(),
b"replacement remains unchanged"
);
}
#[test]
fn bundle_authorization_distinguishes_equal_runsc_argv() {
let fixture = Fixture::new();
let first = build_sandbox_bundle(fixture.request(root_process(["first".to_owned()]))).unwrap();
let second =
build_sandbox_bundle(fixture.request(root_process(["second".to_owned()]))).unwrap();
assert_eq!(first.command().program(), second.command().program());
assert_eq!(first.command().arguments(), second.command().arguments());
assert_ne!(first.command(), second.command());
fixture.stage_handoff(&first);
let handoff = first
.capture_handoff_authority(&fixture.configuration)
.unwrap();
assert!(!handoff.matches_command(second.command()));
}
#[test]
fn staged_oci_configuration_must_match_bundle_at_capture_and_release() {
let fixture = Fixture::new();
let bundle = build_sandbox_bundle(fixture.request(root_process([]))).unwrap();
fixture.stage_handoff(&bundle);
let config = fixture.bundle_config_path();
fs::write(&config, b"pre-capture replacement").unwrap();
let Err(error) = bundle.capture_handoff_authority(&fixture.configuration) else {
panic!("mutated pre-capture OCI configuration was accepted");
};
assert_eq!(error.kind(), SandboxBuildErrorKind::ChangedAuthority);
assert_eq!(fs::read(&config).unwrap(), b"pre-capture replacement");
let fixture = Fixture::new();
let bundle = build_sandbox_bundle(fixture.request(root_process([]))).unwrap();
fixture.stage_handoff(&bundle);
let handoff = bundle
.capture_handoff_authority(&fixture.configuration)
.unwrap();
let config = fixture.bundle_config_path();
fs::write(&config, b"post-capture replacement").unwrap();
let error = handoff.revalidate().unwrap_err();
assert_eq!(error.kind(), SandboxBuildErrorKind::ChangedAuthority);
assert_eq!(fs::read(config).unwrap(), b"post-capture replacement");
}