Confine terminfo injection to runtime control state

This commit is contained in:
ms2k 2026-07-15 15:16:25 +09:00
commit 54f3a08b91

View file

@ -1,6 +1,7 @@
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::{
@ -11,16 +12,15 @@ use lazaret_runtime_supervisor::sandbox::{
CREDENTIALS_DESTINATION, ESCALATION_CLIENT_DESTINATION, HostTerminal,
MAILBOX_REQUESTS_DESTINATION, MAILBOX_RESULTS_DESTINATION, NETWORK_NAMESPACE_PATH, RootProcess,
SandboxBuildErrorKind, SandboxBuildRequest, TERMINFO_ROOT_DESTINATION, WORKSPACE_DESTINATION,
build_sandbox_bundle, production_sandbox_layout,
build_sandbox_bundle, production_sandbox_layout, stage_host_terminal,
};
use serde_json::Value;
use tempfile::TempDir;
struct Fixture {
tree: TempDir,
_tree: TempDir,
configuration: ResolvedDomainConfiguration,
terminfo_source: PathBuf,
terminfo_root: PathBuf,
}
impl Fixture {
@ -74,26 +74,44 @@ env = {{ UNSELECTED_SECRET = "env:UNSELECTED_HOST_SECRET" }}
sandbox_layout: production_sandbox_layout().unwrap(),
})
.unwrap();
let terminfo_root = tree.path().join("trusted-terminfo");
let terminfo_source = terminfo_root.join("x").join("xterm-lazaret");
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, b"compiled host terminfo capabilities").unwrap();
fs::write(&terminfo_source, compiled_terminfo("xterm-lazaret")).unwrap();
Self {
tree,
_tree: tree,
configuration,
terminfo_source,
terminfo_root,
}
}
fn terminal(&self) -> HostTerminal {
HostTerminal::from_trusted_roots(
fn terminal(&self) -> lazaret_runtime_supervisor::sandbox::StagedHostTerminal {
let terminal = HostTerminal::from_compiled_entry(
"xterm-lazaret",
Some("truecolor".to_owned()),
&self.terminfo_source,
std::slice::from_ref(&self.terminfo_root),
fs::read(&self.terminfo_source).unwrap(),
)
.unwrap()
.unwrap();
stage_host_terminal(&self.configuration, ContainerId::new([0x11; 32]), terminal).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<'_> {
@ -107,6 +125,16 @@ env = {{ UNSELECTED_SECRET = "env:UNSELECTED_HOST_SECRET" }}
}
}
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()
}
@ -255,7 +283,17 @@ fn selected_environment_volume_modes_and_host_terminal_are_preserved() {
let terminfo = &mounts["/run/lazaret/terminfo/x/xterm-lazaret"];
assert_eq!(
terminfo["source"],
fixture.terminfo_source.display().to_string()
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"]))
@ -289,7 +327,6 @@ fn configuration_document_unselected_secret_and_unselected_host_paths_do_not_ent
let specification = value(&bundle);
let allowed_external_sources = BTreeSet::from([
fixture.terminfo_source.display().to_string(),
String::from_utf8(
fixture
.configuration
@ -341,31 +378,34 @@ fn configuration_document_unselected_secret_and_unselected_host_paths_do_not_ent
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_trusted_roots(
term,
None,
&fixture.terminfo_source,
std::slice::from_ref(&fixture.terminfo_root),
)
.unwrap_err();
let error =
HostTerminal::from_compiled_entry(term, None, compiled_terminfo("xterm-lazaret"))
.unwrap_err();
assert_eq!(error.kind(), SandboxBuildErrorKind::InvalidTerminal);
}
let outside = fixture.tree.path().join("outside").join("xterm-lazaret");
fs::create_dir(outside.parent().unwrap()).unwrap();
fs::write(&outside, b"outside").unwrap();
assert!(
HostTerminal::from_trusted_roots(
"xterm-lazaret",
HostTerminal::from_compiled_entry("passwd", None, fs::read("/etc/passwd").unwrap())
.is_err()
);
assert!(
HostTerminal::from_compiled_entry(
"other-terminal",
None,
&outside,
std::slice::from_ref(&fixture.terminfo_root),
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.terminfo_source).unwrap();
fs::write(&fixture.terminfo_source, b"replacement").unwrap();
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]),