821 lines
29 KiB
Rust
821 lines
29 KiB
Rust
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,
|
|
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"]["args"],
|
|
serde_json::json!([
|
|
"/sbin/lazaret-sandbox-init",
|
|
"literal;$(touch /host)",
|
|
"two words"
|
|
])
|
|
);
|
|
assert_eq!(
|
|
specification["process"]["capabilities"],
|
|
serde_json::json!({
|
|
"bounding": [], "effective": [], "inheritable": [], "permitted": [], "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");
|
|
}
|