Confine terminfo injection to runtime control state
This commit is contained in:
parent
cf5948e8be
commit
54f3a08b91
4 changed files with 360 additions and 96 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -341,6 +341,7 @@ dependencies = [
|
|||
"lazaret-core",
|
||||
"lazaret-linux",
|
||||
"oci-spec",
|
||||
"rustix",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ path = "src/main.rs"
|
|||
lazaret-core = { version = "=0.0.0", path = "../lazaret-core" }
|
||||
lazaret-linux = { version = "=0.0.0", path = "../lazaret-linux" }
|
||||
oci-spec.workspace = true
|
||||
rustix.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::fmt;
|
||||
use std::fs::Metadata;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
use std::os::fd::{AsFd, OwnedFd};
|
||||
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
use lazaret_core::configuration::{ResolvedDomainConfiguration, SandboxLayoutV1};
|
||||
|
|
@ -14,6 +15,7 @@ use oci_spec::runtime::{
|
|||
LinuxNamespaceBuilder, LinuxNamespaceType, Mount, MountBuilder, ProcessBuilder, RootBuilder,
|
||||
Spec, SpecBuilder, UserBuilder,
|
||||
};
|
||||
use rustix::fs::{AtFlags, FileType, Mode, OFlags, ResolveFlags, openat2};
|
||||
|
||||
pub const WORKSPACE_DESTINATION: &str = "/workspace";
|
||||
pub const CONTROL_ROOT_DESTINATION: &str = "/run/lazaret";
|
||||
|
|
@ -26,6 +28,7 @@ pub const NETWORK_NAMESPACE_PATH: &str = "/proc/self/ns/net";
|
|||
|
||||
const OCI_VERSION: &str = "1.2.1";
|
||||
const DEFAULT_PATH: &str = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
||||
const MAX_TERMINFO_BYTES: usize = 1_048_576;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum SandboxBuildErrorKind {
|
||||
|
|
@ -122,11 +125,18 @@ struct FileIdentity {
|
|||
}
|
||||
|
||||
impl FileIdentity {
|
||||
fn from_metadata(metadata: &Metadata) -> Self {
|
||||
Self {
|
||||
device: metadata.dev(),
|
||||
inode: metadata.ino(),
|
||||
fn from_descriptor(descriptor: &OwnedFd) -> Result<Self, SandboxBuildError> {
|
||||
let stat = rustix::fs::fstat(descriptor).map_err(staging_error)?;
|
||||
if !FileType::from_raw_mode(stat.st_mode).is_file() {
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"staged terminfo artifact is not a regular file",
|
||||
));
|
||||
}
|
||||
Ok(Self {
|
||||
device: stat.st_dev,
|
||||
inode: stat.st_ino,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -134,20 +144,18 @@ impl FileIdentity {
|
|||
pub struct HostTerminal {
|
||||
term: String,
|
||||
color_term: Option<String>,
|
||||
source: PathBuf,
|
||||
identity: FileIdentity,
|
||||
compiled_entry: Vec<u8>,
|
||||
}
|
||||
|
||||
impl HostTerminal {
|
||||
/// Selects one exact compiled host terminfo entry beneath a trusted root.
|
||||
/// Accepts one bounded compiled terminfo entry without accepting a host path.
|
||||
///
|
||||
/// # Errors
|
||||
/// Invalid names, inaccessible entries, non-files, and sources outside the roots are refused.
|
||||
pub fn from_trusted_roots(
|
||||
/// Invalid names, colors, terminfo headers, aliases, and oversized entries are refused.
|
||||
pub fn from_compiled_entry(
|
||||
term: impl Into<String>,
|
||||
color_term: Option<String>,
|
||||
source: &Path,
|
||||
trusted_roots: &[PathBuf],
|
||||
compiled_entry: Vec<u8>,
|
||||
) -> Result<Self, SandboxBuildError> {
|
||||
let term = term.into();
|
||||
validate_terminal_name(&term)?;
|
||||
|
|
@ -159,60 +167,48 @@ impl HostTerminal {
|
|||
"COLORTERM contains control bytes or is empty",
|
||||
));
|
||||
}
|
||||
let source = std::fs::canonicalize(source).map_err(|error| {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::InvalidTerminal,
|
||||
format!("cannot canonicalize terminfo source: {error}"),
|
||||
)
|
||||
})?;
|
||||
if source.file_name() != Some(OsStr::new(&term)) {
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::InvalidTerminal,
|
||||
"terminfo source name differs from TERM",
|
||||
));
|
||||
}
|
||||
let beneath_trusted_root = trusted_roots.iter().any(|root| {
|
||||
std::fs::canonicalize(root)
|
||||
.ok()
|
||||
.is_some_and(|root| source.starts_with(&root) && source != root)
|
||||
});
|
||||
if !beneath_trusted_root {
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::InvalidTerminal,
|
||||
"terminfo source is outside every trusted root",
|
||||
));
|
||||
}
|
||||
let metadata = std::fs::metadata(&source).map_err(|error| {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::InvalidTerminal,
|
||||
format!("cannot inspect terminfo source: {error}"),
|
||||
)
|
||||
})?;
|
||||
if !metadata.is_file() {
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::InvalidTerminal,
|
||||
"terminfo source is not a regular file",
|
||||
));
|
||||
}
|
||||
validate_compiled_terminfo(&term, &compiled_entry)?;
|
||||
Ok(Self {
|
||||
term,
|
||||
color_term,
|
||||
source,
|
||||
identity: FileIdentity::from_metadata(&metadata),
|
||||
compiled_entry,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn revalidate(&self) -> Result<(), SandboxBuildError> {
|
||||
let metadata = std::fs::metadata(&self.source).map_err(|error| {
|
||||
SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
format!("cannot revalidate terminfo source: {error}"),
|
||||
#[derive(Debug)]
|
||||
pub struct StagedHostTerminal {
|
||||
term: String,
|
||||
color_term: Option<String>,
|
||||
compiled_entry: Vec<u8>,
|
||||
relative_source: PathBuf,
|
||||
identity: FileIdentity,
|
||||
}
|
||||
|
||||
impl StagedHostTerminal {
|
||||
fn revalidate(
|
||||
&self,
|
||||
configuration: &ResolvedDomainConfiguration,
|
||||
) -> Result<(), SandboxBuildError> {
|
||||
let root = configuration
|
||||
.runtime_domain_root()
|
||||
.open_authority()
|
||||
.map_err(changed_configuration_authority)?;
|
||||
let descriptor = openat2(
|
||||
root,
|
||||
&self.relative_source,
|
||||
OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||
Mode::empty(),
|
||||
confined_resolve_flags(),
|
||||
)
|
||||
})?;
|
||||
if !metadata.is_file() || FileIdentity::from_metadata(&metadata) != self.identity {
|
||||
.map_err(staging_error)?;
|
||||
require_staged_file(&descriptor)?;
|
||||
if FileIdentity::from_descriptor(&descriptor)? != self.identity
|
||||
|| read_descriptor(descriptor)? != self.compiled_entry
|
||||
{
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"terminfo source changed identity",
|
||||
"staged terminfo artifact changed identity or bytes",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -226,12 +222,57 @@ impl HostTerminal {
|
|||
}
|
||||
}
|
||||
|
||||
/// Copies validated terminfo bytes into the generation's fixed control authority.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses changed Runtime Domain authority, unsafe staging objects, or byte mismatches.
|
||||
pub fn stage_host_terminal(
|
||||
configuration: &ResolvedDomainConfiguration,
|
||||
container_id: ContainerId,
|
||||
terminal: HostTerminal,
|
||||
) -> Result<StagedHostTerminal, SandboxBuildError> {
|
||||
let container_name = hex_lower(container_id.as_bytes());
|
||||
let bucket = char::from(terminal.term.as_bytes()[0]).to_string();
|
||||
let components = [
|
||||
"sandbox-control",
|
||||
container_name.as_str(),
|
||||
"terminfo",
|
||||
bucket.as_str(),
|
||||
];
|
||||
let mut directory = configuration
|
||||
.runtime_domain_root()
|
||||
.open_authority()
|
||||
.map_err(changed_configuration_authority)?;
|
||||
for component in components {
|
||||
directory = open_or_create_staging_directory(directory, component)?;
|
||||
}
|
||||
let descriptor = stage_exact_file(&directory, &terminal.term, &terminal.compiled_entry)?;
|
||||
let identity = FileIdentity::from_descriptor(&descriptor)?;
|
||||
if read_descriptor(descriptor)? != terminal.compiled_entry {
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"staged terminfo bytes differ from the selected entry",
|
||||
));
|
||||
}
|
||||
Ok(StagedHostTerminal {
|
||||
relative_source: Path::new("sandbox-control")
|
||||
.join(container_name)
|
||||
.join("terminfo")
|
||||
.join(bucket)
|
||||
.join(&terminal.term),
|
||||
term: terminal.term,
|
||||
color_term: terminal.color_term,
|
||||
compiled_entry: terminal.compiled_entry,
|
||||
identity,
|
||||
})
|
||||
}
|
||||
|
||||
pub struct SandboxBuildRequest<'a> {
|
||||
pub configuration: &'a ResolvedDomainConfiguration,
|
||||
pub container_id: ContainerId,
|
||||
pub expected_network_namespace: NamespaceIdentity,
|
||||
pub root_process: RootProcess,
|
||||
pub terminal: Option<HostTerminal>,
|
||||
pub terminal: Option<StagedHostTerminal>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
|
|
@ -315,7 +356,7 @@ pub fn build_sandbox_bundle(
|
|||
.map_err(changed_configuration_authority)?;
|
||||
}
|
||||
if let Some(terminal) = &request.terminal {
|
||||
terminal.revalidate()?;
|
||||
terminal.revalidate(request.configuration)?;
|
||||
}
|
||||
|
||||
let paths = runtime_paths(request.configuration, request.container_id)?;
|
||||
|
|
@ -373,14 +414,14 @@ fn runtime_paths(
|
|||
bundle: runtime_domain.join("bundles").join(&container_name),
|
||||
runsc_root: runtime_domain.join("runsc"),
|
||||
rootfs: runtime_domain.join("state-anchor").join("merged"),
|
||||
control_sources: runtime_domain.join("sandbox-control"),
|
||||
control_sources: runtime_domain.join("sandbox-control").join(&container_name),
|
||||
container_name,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_mounts(
|
||||
configuration: &ResolvedDomainConfiguration,
|
||||
terminal: Option<&HostTerminal>,
|
||||
terminal: Option<&StagedHostTerminal>,
|
||||
control_sources: &Path,
|
||||
) -> Result<Vec<Mount>, SandboxBuildError> {
|
||||
let mut mounts = standard_mounts()?;
|
||||
|
|
@ -402,7 +443,8 @@ fn build_mounts(
|
|||
mounts.extend(control_mounts(control_sources)?);
|
||||
if let Some(terminal) = terminal {
|
||||
mounts.push(bind_mount(
|
||||
terminal.source.clone(),
|
||||
host_path(configuration.runtime_domain_root().canonical_label())?
|
||||
.join(&terminal.relative_source),
|
||||
terminal.destination(),
|
||||
true,
|
||||
true,
|
||||
|
|
@ -477,7 +519,7 @@ fn build_runsc_command(paths: &RuntimePaths) -> RunscCommand {
|
|||
fn build_process(
|
||||
configuration: &ResolvedDomainConfiguration,
|
||||
root_process: RootProcess,
|
||||
terminal: Option<&HostTerminal>,
|
||||
terminal: Option<&StagedHostTerminal>,
|
||||
) -> Result<oci_spec::runtime::Process, SandboxBuildError> {
|
||||
let mut environment = BTreeMap::from([("PATH".to_owned(), DEFAULT_PATH.to_owned())]);
|
||||
for (name, value) in configuration.ephemeral_environment() {
|
||||
|
|
@ -686,6 +728,186 @@ fn validate_terminal_name(term: &str) -> Result<(), SandboxBuildError> {
|
|||
}
|
||||
}
|
||||
|
||||
fn validate_compiled_terminfo(term: &str, bytes: &[u8]) -> Result<(), SandboxBuildError> {
|
||||
if bytes.len() < 12 || bytes.len() > MAX_TERMINFO_BYTES {
|
||||
return Err(invalid_terminfo("compiled entry has an invalid size"));
|
||||
}
|
||||
let magic = little_u16(bytes, 0);
|
||||
let number_width = match magic {
|
||||
0x011a => 2,
|
||||
0x021e => 4,
|
||||
_ => return Err(invalid_terminfo("compiled entry has an unknown magic")),
|
||||
};
|
||||
let names_size = usize::from(little_u16(bytes, 2));
|
||||
let bool_count = usize::from(little_u16(bytes, 4));
|
||||
let number_count = usize::from(little_u16(bytes, 6));
|
||||
let string_count = usize::from(little_u16(bytes, 8));
|
||||
let string_table_size = usize::from(little_u16(bytes, 10));
|
||||
let names_end = 12_usize
|
||||
.checked_add(names_size)
|
||||
.filter(|end| *end <= bytes.len())
|
||||
.ok_or_else(|| invalid_terminfo("compiled entry names exceed its length"))?;
|
||||
let names = &bytes[12..names_end];
|
||||
if names.last() != Some(&0)
|
||||
|| !names[..names.len().saturating_sub(1)]
|
||||
.split(|byte| *byte == b'|')
|
||||
.any(|name| name == term.as_bytes())
|
||||
{
|
||||
return Err(invalid_terminfo(
|
||||
"compiled entry does not name the requested terminal",
|
||||
));
|
||||
}
|
||||
let after_bools = names_end
|
||||
.checked_add(bool_count)
|
||||
.ok_or_else(|| invalid_terminfo("compiled entry size overflows"))?;
|
||||
let aligned = after_bools
|
||||
.checked_add(after_bools % 2)
|
||||
.ok_or_else(|| invalid_terminfo("compiled entry size overflows"))?;
|
||||
let required = aligned
|
||||
.checked_add(
|
||||
number_count
|
||||
.checked_mul(number_width)
|
||||
.ok_or_else(|| invalid_terminfo("compiled entry size overflows"))?,
|
||||
)
|
||||
.and_then(|size| size.checked_add(string_count.checked_mul(2)?))
|
||||
.and_then(|size| size.checked_add(string_table_size))
|
||||
.ok_or_else(|| invalid_terminfo("compiled entry size overflows"))?;
|
||||
if required > bytes.len() {
|
||||
return Err(invalid_terminfo("compiled entry sections are truncated"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn little_u16(bytes: &[u8], offset: usize) -> u16 {
|
||||
u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
|
||||
}
|
||||
|
||||
fn invalid_terminfo(detail: &str) -> SandboxBuildError {
|
||||
SandboxBuildError::new(SandboxBuildErrorKind::InvalidTerminal, detail)
|
||||
}
|
||||
|
||||
fn open_or_create_staging_directory(
|
||||
parent: OwnedFd,
|
||||
name: &str,
|
||||
) -> Result<OwnedFd, SandboxBuildError> {
|
||||
match rustix::fs::mkdirat(&parent, name, Mode::RWXU) {
|
||||
Ok(()) | Err(rustix::io::Errno::EXIST) => {}
|
||||
Err(error) => return Err(staging_error(error)),
|
||||
}
|
||||
let directory = openat2(
|
||||
parent,
|
||||
name,
|
||||
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
|
||||
Mode::empty(),
|
||||
confined_resolve_flags(),
|
||||
)
|
||||
.map_err(staging_error)?;
|
||||
let stat = rustix::fs::fstat(&directory).map_err(staging_error)?;
|
||||
if !FileType::from_raw_mode(stat.st_mode).is_dir()
|
||||
|| stat.st_uid != rustix::process::getuid().as_raw()
|
||||
|| stat.st_mode & 0o7777 != 0o700
|
||||
{
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"terminfo staging directory has unsafe ownership or mode",
|
||||
));
|
||||
}
|
||||
Ok(directory)
|
||||
}
|
||||
|
||||
fn stage_exact_file(
|
||||
directory: &OwnedFd,
|
||||
name: &str,
|
||||
bytes: &[u8],
|
||||
) -> Result<OwnedFd, SandboxBuildError> {
|
||||
match open_staged_file(directory, name) {
|
||||
Ok(descriptor) => {
|
||||
require_staged_file(&descriptor)?;
|
||||
if read_descriptor(descriptor)? != bytes {
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"existing staged terminfo artifact has different bytes",
|
||||
));
|
||||
}
|
||||
open_staged_file(directory, name).map_err(staging_error)
|
||||
}
|
||||
Err(rustix::io::Errno::NOENT) => {
|
||||
let descriptor = rustix::fs::openat(
|
||||
directory,
|
||||
name,
|
||||
OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||
Mode::RUSR | Mode::WUSR,
|
||||
)
|
||||
.map_err(staging_error)?;
|
||||
let mut file = File::from(descriptor);
|
||||
let result = (|| {
|
||||
file.write_all(bytes).map_err(staging_error)?;
|
||||
rustix::fs::fchmod(file.as_fd(), Mode::RUSR).map_err(staging_error)?;
|
||||
file.sync_all().map_err(staging_error)
|
||||
})();
|
||||
if let Err(error) = result {
|
||||
drop(file);
|
||||
let _ = rustix::fs::unlinkat(directory, name, AtFlags::empty());
|
||||
return Err(error);
|
||||
}
|
||||
drop(file);
|
||||
rustix::fs::fsync(directory).map_err(staging_error)?;
|
||||
let descriptor = open_staged_file(directory, name).map_err(staging_error)?;
|
||||
require_staged_file(&descriptor)?;
|
||||
Ok(descriptor)
|
||||
}
|
||||
Err(error) => Err(staging_error(error)),
|
||||
}
|
||||
}
|
||||
|
||||
fn open_staged_file(directory: &OwnedFd, name: &str) -> rustix::io::Result<OwnedFd> {
|
||||
openat2(
|
||||
directory,
|
||||
name,
|
||||
OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||
Mode::empty(),
|
||||
confined_resolve_flags(),
|
||||
)
|
||||
}
|
||||
|
||||
fn require_staged_file(descriptor: &OwnedFd) -> Result<(), SandboxBuildError> {
|
||||
let stat = rustix::fs::fstat(descriptor).map_err(staging_error)?;
|
||||
if !FileType::from_raw_mode(stat.st_mode).is_file()
|
||||
|| stat.st_uid != rustix::process::getuid().as_raw()
|
||||
|| stat.st_mode & 0o7777 != 0o400
|
||||
|| stat.st_nlink != 1
|
||||
{
|
||||
return Err(SandboxBuildError::new(
|
||||
SandboxBuildErrorKind::ChangedAuthority,
|
||||
"staged terminfo artifact has unsafe type, ownership, mode, or link count",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_descriptor(descriptor: OwnedFd) -> Result<Vec<u8>, SandboxBuildError> {
|
||||
let mut bytes = Vec::new();
|
||||
File::from(descriptor)
|
||||
.take((MAX_TERMINFO_BYTES + 1) as u64)
|
||||
.read_to_end(&mut bytes)
|
||||
.map_err(staging_error)?;
|
||||
if bytes.len() > MAX_TERMINFO_BYTES {
|
||||
return Err(invalid_terminfo("staged terminfo artifact is oversized"));
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn confined_resolve_flags() -> ResolveFlags {
|
||||
ResolveFlags::BENEATH
|
||||
| ResolveFlags::NO_MAGICLINKS
|
||||
| ResolveFlags::NO_SYMLINKS
|
||||
| ResolveFlags::NO_XDEV
|
||||
}
|
||||
|
||||
fn staging_error(error: impl fmt::Display) -> SandboxBuildError {
|
||||
SandboxBuildError::new(SandboxBuildErrorKind::ChangedAuthority, error.to_string())
|
||||
}
|
||||
|
||||
fn validate_absolute_normalized(path: &Path) -> Result<(), SandboxBuildError> {
|
||||
if !path.is_absolute()
|
||||
|| path.as_os_str().as_bytes().contains(&0)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
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]),
|
||||
|
|
|
|||
Loading…
Reference in a new issue