Bind sandbox handoff authority and restore verifier gates

This commit is contained in:
ms2k 2026-07-16 15:20:13 +09:00
commit 49d5def1a9

View file

@ -18,7 +18,7 @@ use serde_json::Value;
use tempfile::TempDir;
struct Fixture {
_tree: TempDir,
tree: TempDir,
configuration: ResolvedDomainConfiguration,
terminfo_source: PathBuf,
}
@ -82,7 +82,7 @@ env = {{ UNSELECTED_SECRET = "env:UNSELECTED_HOST_SECRET" }}
fs::create_dir_all(terminfo_source.parent().unwrap()).unwrap();
fs::write(&terminfo_source, compiled_terminfo("xterm-lazaret")).unwrap();
Self {
_tree: tree,
tree,
configuration,
terminfo_source,
}
@ -98,6 +98,25 @@ env = {{ UNSELECTED_SECRET = "env:UNSELECTED_HOST_SECRET" }}
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: production_sandbox_layout().unwrap(),
})
.unwrap()
}
fn staged_terminfo_source(&self) -> PathBuf {
PathBuf::from(
String::from_utf8(
@ -123,6 +142,53 @@ env = {{ UNSELECTED_SECRET = "env:UNSELECTED_HOST_SECRET" }}
terminal: Some(self.terminal()),
}
}
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(),
"state-anchor/merged".to_owned(),
format!("sandbox-control/{container}/credentials"),
format!("sandbox-control/{container}/mailbox/requests"),
format!("sandbox-control/{container}/mailbox/results"),
format!("sandbox-control/{container}/bin"),
] {
fs::create_dir_all(runtime.join(directory)).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();
}
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> {
@ -473,3 +539,157 @@ fn production_layout_reserves_every_mount_root() {
);
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 runtime = host_path(
fixture
.configuration
.runtime_domain_root()
.canonical_label()
.as_bytes(),
);
let rootfs = runtime.join("state-anchor/merged");
fs::rename(&rootfs, runtime.join("state-anchor/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");
}