Bind sandbox handoff authority and restore verifier gates
This commit is contained in:
parent
cf914bdf51
commit
49d5def1a9
1 changed files with 951 additions and 170 deletions
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue