Establish bounded descriptor budgets for sandbox domains
This commit is contained in:
parent
42e197510e
commit
503f090121
8 changed files with 191 additions and 12 deletions
|
|
@ -64,5 +64,10 @@ harness = false
|
||||||
name = "cold-start"
|
name = "cold-start"
|
||||||
path = "tests/cold_start.rs"
|
path = "tests/cold_start.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "nofile"
|
||||||
|
path = "tests/nofile.rs"
|
||||||
|
harness = false
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,9 @@ use lazaret_core::identity::{
|
||||||
};
|
};
|
||||||
use lazaret_core::records::{Domain, Session, SessionPhase};
|
use lazaret_core::records::{Domain, Session, SessionPhase};
|
||||||
use lazaret_linux::filesystem::try_exclusive_flock;
|
use lazaret_linux::filesystem::try_exclusive_flock;
|
||||||
use lazaret_runtime_supervisor::sandbox::default_sandbox_layout;
|
use lazaret_runtime_supervisor::sandbox::{DOMAIN_OPEN_FILE_LIMIT, default_sandbox_layout};
|
||||||
use rustix::fs::{Mode, OFlags, openat};
|
use rustix::fs::{Mode, OFlags, openat};
|
||||||
|
use rustix::process::{Resource, Rlimit, getrlimit, setrlimit};
|
||||||
|
|
||||||
use crate::bootstrap::{CoordinatorLifecycle, serve_control_connection_with_escalation};
|
use crate::bootstrap::{CoordinatorLifecycle, serve_control_connection_with_escalation};
|
||||||
use crate::cold_start::{BootstrapOwnerGuard, ColdStart, ColdStartError};
|
use crate::cold_start::{BootstrapOwnerGuard, ColdStart, ColdStartError};
|
||||||
|
|
@ -73,6 +74,7 @@ pub fn dispatch_private_client_bootstrap(
|
||||||
|
|
||||||
fn run_private_client_bootstrap(arguments: &[OsString]) -> Result<(), PrivateClientBootstrapError> {
|
fn run_private_client_bootstrap(arguments: &[OsString]) -> Result<(), PrivateClientBootstrapError> {
|
||||||
let parsed = parse_arguments(arguments).inspect_err(|_| signal(b'E'))?;
|
let parsed = parse_arguments(arguments).inspect_err(|_| signal(b'E'))?;
|
||||||
|
ensure_domain_nofile_limit().inspect_err(|_| signal(b'E'))?;
|
||||||
let owner = BootstrapOwnerGuard::from_stdin()
|
let owner = BootstrapOwnerGuard::from_stdin()
|
||||||
.map_err(failure)
|
.map_err(failure)
|
||||||
.inspect_err(|_| signal(b'E'))?;
|
.inspect_err(|_| signal(b'E'))?;
|
||||||
|
|
@ -158,6 +160,51 @@ fn run_private_client_bootstrap(arguments: &[OsString]) -> Result<(), PrivateCli
|
||||||
.map_err(failure)
|
.map_err(failure)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Establishes the descriptor budget inherited by one Domain runtime.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an error when the hard limit is insufficient or the soft limit cannot be raised.
|
||||||
|
pub fn ensure_domain_nofile_limit() -> Result<(), PrivateClientBootstrapError> {
|
||||||
|
let inherited = getrlimit(Resource::Nofile);
|
||||||
|
let Some(required) = required_nofile_limit(inherited)? else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
setrlimit(Resource::Nofile, required).map_err(|error| {
|
||||||
|
failure(format!(
|
||||||
|
"raising Host Coordinator RLIMIT_NOFILE from {} to {} failed: {error}",
|
||||||
|
display_limit(inherited.current),
|
||||||
|
DOMAIN_OPEN_FILE_LIMIT
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn required_nofile_limit(inherited: Rlimit) -> Result<Option<Rlimit>, PrivateClientBootstrapError> {
|
||||||
|
if inherited
|
||||||
|
.maximum
|
||||||
|
.is_some_and(|maximum| maximum < DOMAIN_OPEN_FILE_LIMIT)
|
||||||
|
{
|
||||||
|
return Err(failure(format!(
|
||||||
|
"Host Coordinator requires RLIMIT_NOFILE {DOMAIN_OPEN_FILE_LIMIT}, but inherited soft/hard limits are {}/{}",
|
||||||
|
display_limit(inherited.current),
|
||||||
|
display_limit(inherited.maximum)
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if inherited
|
||||||
|
.current
|
||||||
|
.is_none_or(|current| current >= DOMAIN_OPEN_FILE_LIMIT)
|
||||||
|
{
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Ok(Some(Rlimit {
|
||||||
|
current: Some(DOMAIN_OPEN_FILE_LIMIT),
|
||||||
|
maximum: inherited.maximum,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_limit(limit: Option<u64>) -> String {
|
||||||
|
limit.map_or_else(|| "unlimited".to_owned(), |limit| limit.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
fn serve_published(
|
fn serve_published(
|
||||||
lifecycle: &Mutex<CoordinatorLifecycle<'_, '_>>,
|
lifecycle: &Mutex<CoordinatorLifecycle<'_, '_>>,
|
||||||
waiters: &RefCell<BootstrapWaiterFace>,
|
waiters: &RefCell<BootstrapWaiterFace>,
|
||||||
|
|
@ -495,6 +542,57 @@ mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn host_coordinator_plans_a_bounded_raise_from_a_low_soft_nofile_limit() {
|
||||||
|
assert_eq!(
|
||||||
|
required_nofile_limit(Rlimit {
|
||||||
|
current: Some(1_024),
|
||||||
|
maximum: Some(DOMAIN_OPEN_FILE_LIMIT),
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
Some(Rlimit {
|
||||||
|
current: Some(DOMAIN_OPEN_FILE_LIMIT),
|
||||||
|
maximum: Some(DOMAIN_OPEN_FILE_LIMIT),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn host_coordinator_refuses_an_insufficient_hard_nofile_limit() {
|
||||||
|
let error = required_nofile_limit(Rlimit {
|
||||||
|
current: Some(1_024),
|
||||||
|
maximum: Some(DOMAIN_OPEN_FILE_LIMIT - 1),
|
||||||
|
})
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
error.to_string(),
|
||||||
|
format!(
|
||||||
|
"Host Coordinator requires RLIMIT_NOFILE {DOMAIN_OPEN_FILE_LIMIT}, but inherited soft/hard limits are 1024/{}",
|
||||||
|
DOMAIN_OPEN_FILE_LIMIT - 1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn host_coordinator_preserves_sufficient_and_unlimited_nofile_limits() {
|
||||||
|
for limit in [
|
||||||
|
Rlimit {
|
||||||
|
current: Some(DOMAIN_OPEN_FILE_LIMIT),
|
||||||
|
maximum: Some(DOMAIN_OPEN_FILE_LIMIT),
|
||||||
|
},
|
||||||
|
Rlimit {
|
||||||
|
current: Some(DOMAIN_OPEN_FILE_LIMIT + 1),
|
||||||
|
maximum: None,
|
||||||
|
},
|
||||||
|
Rlimit {
|
||||||
|
current: None,
|
||||||
|
maximum: None,
|
||||||
|
},
|
||||||
|
] {
|
||||||
|
assert_eq!(required_nofile_limit(limit).unwrap(), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn private_bootstrap_arguments_are_exact_and_public() {
|
fn private_bootstrap_arguments_are_exact_and_public() {
|
||||||
let arguments = [
|
let arguments = [
|
||||||
|
|
|
||||||
39
crates/lazaret-host-coordinator/tests/nofile.rs
Normal file
39
crates/lazaret-host-coordinator/tests/nofile.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
use std::process::ExitCode;
|
||||||
|
|
||||||
|
use lazaret_host_coordinator::client_bootstrap::ensure_domain_nofile_limit;
|
||||||
|
use lazaret_runtime_supervisor::sandbox::DOMAIN_OPEN_FILE_LIMIT;
|
||||||
|
use rustix::process::{Resource, Rlimit, getrlimit, setrlimit};
|
||||||
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
match verify_low_inherited_soft_limit() {
|
||||||
|
Ok(()) => ExitCode::SUCCESS,
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("NOFILE fixture failed: {error}");
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_low_inherited_soft_limit() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let inherited = getrlimit(Resource::Nofile);
|
||||||
|
if let Some(maximum) = inherited.maximum
|
||||||
|
&& maximum < DOMAIN_OPEN_FILE_LIMIT
|
||||||
|
{
|
||||||
|
return Err(
|
||||||
|
format!("test host hard limit {maximum} is below {DOMAIN_OPEN_FILE_LIMIT}").into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setrlimit(
|
||||||
|
Resource::Nofile,
|
||||||
|
Rlimit {
|
||||||
|
current: Some(1_024),
|
||||||
|
maximum: inherited.maximum,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
ensure_domain_nofile_limit()?;
|
||||||
|
let observed = getrlimit(Resource::Nofile);
|
||||||
|
if observed.current != Some(DOMAIN_OPEN_FILE_LIMIT) || observed.maximum != inherited.maximum {
|
||||||
|
return Err(format!("unexpected raised limit: {observed:?}").into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -19,8 +19,8 @@ pub use lazaret_sandbox_contract::{
|
||||||
};
|
};
|
||||||
use oci_spec::runtime::{
|
use oci_spec::runtime::{
|
||||||
Capabilities, Capability, LinuxBuilder, LinuxCapabilitiesBuilder, LinuxNamespaceBuilder,
|
Capabilities, Capability, LinuxBuilder, LinuxCapabilitiesBuilder, LinuxNamespaceBuilder,
|
||||||
LinuxNamespaceType, Mount, MountBuilder, ProcessBuilder, RootBuilder, Spec, SpecBuilder,
|
LinuxNamespaceType, Mount, MountBuilder, PosixRlimitBuilder, PosixRlimitType, ProcessBuilder,
|
||||||
UserBuilder,
|
RootBuilder, Spec, SpecBuilder, UserBuilder,
|
||||||
};
|
};
|
||||||
use rustix::fs::{AtFlags, FileType, Mode, OFlags, ResolveFlags, StatxFlags, openat2, statx};
|
use rustix::fs::{AtFlags, FileType, Mode, OFlags, ResolveFlags, StatxFlags, openat2, statx};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
@ -40,6 +40,7 @@ use validation::{
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const NETWORK_NAMESPACE_PATH: &str = "/proc/self/ns/net";
|
pub const NETWORK_NAMESPACE_PATH: &str = "/proc/self/ns/net";
|
||||||
|
pub const DOMAIN_OPEN_FILE_LIMIT: u64 = 524_288;
|
||||||
|
|
||||||
const OCI_VERSION: &str = "1.2.1";
|
const OCI_VERSION: &str = "1.2.1";
|
||||||
const MAX_TERMINFO_BYTES: usize = 1_048_576;
|
const MAX_TERMINFO_BYTES: usize = 1_048_576;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
use super::{
|
use super::{
|
||||||
AtFlags, Capabilities, Capability, ContainerId, File, FileIdentity, FileType, HandoffPathType,
|
AtFlags, Capabilities, Capability, ContainerId, File, FileIdentity, FileType, HandoffPathType,
|
||||||
LazyLock, LinuxBuilder, LinuxCapabilitiesBuilder, LinuxNamespaceBuilder, LinuxNamespaceType,
|
LazyLock, LinuxBuilder, LinuxCapabilitiesBuilder, LinuxNamespaceBuilder, LinuxNamespaceType,
|
||||||
Mode, Mount, NETWORK_NAMESPACE_PATH, OFlags, OsString, OwnedFd, Path, PathBuf, ProcessBuilder,
|
Mode, Mount, NETWORK_NAMESPACE_PATH, OFlags, OsString, OwnedFd, Path, PathBuf,
|
||||||
RUNTIME_FILE_DESTINATIONS, Read, ResolveFlags, ResolvedDomainConfiguration, RootProcess,
|
PosixRlimitBuilder, PosixRlimitType, ProcessBuilder, RUNTIME_FILE_DESTINATIONS, Read,
|
||||||
RunscCommand, RuntimeHandoffSource, SandboxBuildError, SandboxBuildErrorKind,
|
ResolveFlags, ResolvedDomainConfiguration, RootProcess, RunscCommand, RuntimeHandoffSource,
|
||||||
StagedHostTerminal, StatxFlags, UserBuilder, VolumeAccess, WORKSPACE_DESTINATION, bind_mount,
|
SandboxBuildError, SandboxBuildErrorKind, StagedHostTerminal, StatxFlags, UserBuilder,
|
||||||
control_mounts, hex_lower, host_path, oci_error, openat2, staging_error, standard_mounts,
|
VolumeAccess, WORKSPACE_DESTINATION, bind_mount, control_mounts, hex_lower, host_path,
|
||||||
statx, validate_absolute_normalized,
|
oci_error, openat2, staging_error, standard_mounts, statx, validate_absolute_normalized,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(super) struct RuntimePaths {
|
pub(super) struct RuntimePaths {
|
||||||
|
|
@ -353,6 +353,12 @@ pub(super) fn build_process(
|
||||||
.umask(0o022_u32)
|
.umask(0o022_u32)
|
||||||
.build()
|
.build()
|
||||||
.map_err(oci_error)?;
|
.map_err(oci_error)?;
|
||||||
|
let nofile = PosixRlimitBuilder::default()
|
||||||
|
.typ(PosixRlimitType::RlimitNofile)
|
||||||
|
.soft(super::DOMAIN_OPEN_FILE_LIMIT)
|
||||||
|
.hard(super::DOMAIN_OPEN_FILE_LIMIT)
|
||||||
|
.build()
|
||||||
|
.map_err(oci_error)?;
|
||||||
ProcessBuilder::default()
|
ProcessBuilder::default()
|
||||||
.terminal(false)
|
.terminal(false)
|
||||||
.user(user)
|
.user(user)
|
||||||
|
|
@ -360,6 +366,7 @@ pub(super) fn build_process(
|
||||||
.env(Vec::<String>::new())
|
.env(Vec::<String>::new())
|
||||||
.cwd(PathBuf::from(WORKSPACE_DESTINATION))
|
.cwd(PathBuf::from(WORKSPACE_DESTINATION))
|
||||||
.capabilities(capabilities)
|
.capabilities(capabilities)
|
||||||
|
.rlimits(vec![nofile])
|
||||||
.no_new_privileges(true)
|
.no_new_privileges(true)
|
||||||
.build()
|
.build()
|
||||||
.map_err(oci_error)
|
.map_err(oci_error)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use lazaret_core::configuration::{
|
||||||
};
|
};
|
||||||
use lazaret_core::identity::{ContainerId, DeviceId, InodeId, NamespaceIdentity};
|
use lazaret_core::identity::{ContainerId, DeviceId, InodeId, NamespaceIdentity};
|
||||||
use lazaret_runtime_supervisor::sandbox::{
|
use lazaret_runtime_supervisor::sandbox::{
|
||||||
CONTROL_ROOT_DESTINATION, CREDENTIALS_DESTINATION, DEV_DESTINATION,
|
CONTROL_ROOT_DESTINATION, CREDENTIALS_DESTINATION, DEV_DESTINATION, DOMAIN_OPEN_FILE_LIMIT,
|
||||||
ESCALATION_CLIENT_DESTINATION, HOSTNAME_DESTINATION, HOSTS_DESTINATION, HostTerminal,
|
ESCALATION_CLIENT_DESTINATION, HOSTNAME_DESTINATION, HOSTS_DESTINATION, HostTerminal,
|
||||||
MAILBOX_REQUESTS_DESTINATION, MAILBOX_RESULTS_DESTINATION, NETWORK_NAMESPACE_PATH,
|
MAILBOX_REQUESTS_DESTINATION, MAILBOX_RESULTS_DESTINATION, NETWORK_NAMESPACE_PATH,
|
||||||
PROC_DESTINATION, RESOLV_CONF_DESTINATION, RootProcess, SANDBOX_INIT_DESTINATION,
|
PROC_DESTINATION, RESOLV_CONF_DESTINATION, RootProcess, SANDBOX_INIT_DESTINATION,
|
||||||
|
|
@ -283,6 +283,14 @@ fn deterministic_snapshot_has_only_private_namespaces_and_fixed_runtime_shape()
|
||||||
assert_eq!(specification["process"]["terminal"], false);
|
assert_eq!(specification["process"]["terminal"], false);
|
||||||
assert_eq!(specification["process"]["noNewPrivileges"], true);
|
assert_eq!(specification["process"]["noNewPrivileges"], true);
|
||||||
assert_eq!(specification["process"]["cwd"], WORKSPACE_DESTINATION);
|
assert_eq!(specification["process"]["cwd"], WORKSPACE_DESTINATION);
|
||||||
|
assert_eq!(
|
||||||
|
specification["process"]["rlimits"],
|
||||||
|
serde_json::json!([{
|
||||||
|
"type": "RLIMIT_NOFILE",
|
||||||
|
"hard": DOMAIN_OPEN_FILE_LIMIT,
|
||||||
|
"soft": DOMAIN_OPEN_FILE_LIMIT
|
||||||
|
}])
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
specification["process"]["args"],
|
specification["process"]["args"],
|
||||||
serde_json::json!([
|
serde_json::json!([
|
||||||
|
|
|
||||||
|
|
@ -429,7 +429,7 @@ class VerificationTests(unittest.TestCase):
|
||||||
"not binary(capability-drop-helper) and not binary(acquisition) "
|
"not binary(capability-drop-helper) and not binary(acquisition) "
|
||||||
"and not binary(state-anchor-lifecycle) and not binary(recovery-process) "
|
"and not binary(state-anchor-lifecycle) and not binary(recovery-process) "
|
||||||
"and not binary(runtime-composition) and not binary(escalation) "
|
"and not binary(runtime-composition) and not binary(escalation) "
|
||||||
"and not binary(prepared-worker) "
|
"and not binary(prepared-worker) and not binary(nofile) "
|
||||||
"and not binary(network-runtime)"
|
"and not binary(network-runtime)"
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -493,6 +493,16 @@ class VerificationTests(unittest.TestCase):
|
||||||
"prepared-worker",
|
"prepared-worker",
|
||||||
"--locked",
|
"--locked",
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"cargo",
|
||||||
|
"test",
|
||||||
|
"--offline",
|
||||||
|
"-p",
|
||||||
|
"lazaret-host-coordinator",
|
||||||
|
"--test",
|
||||||
|
"nofile",
|
||||||
|
"--locked",
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"cargo",
|
"cargo",
|
||||||
"test",
|
"test",
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ NEXTEST_COMMAND = (
|
||||||
"not binary(capability-drop-helper) and not binary(acquisition) "
|
"not binary(capability-drop-helper) and not binary(acquisition) "
|
||||||
"and not binary(state-anchor-lifecycle) and not binary(recovery-process) "
|
"and not binary(state-anchor-lifecycle) and not binary(recovery-process) "
|
||||||
"and not binary(runtime-composition) and not binary(escalation) "
|
"and not binary(runtime-composition) and not binary(escalation) "
|
||||||
"and not binary(prepared-worker) "
|
"and not binary(prepared-worker) and not binary(nofile) "
|
||||||
"and not binary(network-runtime)"
|
"and not binary(network-runtime)"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -108,6 +108,16 @@ PREPARED_WORKER_TEST_COMMAND = (
|
||||||
"prepared-worker",
|
"prepared-worker",
|
||||||
"--locked",
|
"--locked",
|
||||||
)
|
)
|
||||||
|
NOFILE_TEST_COMMAND = (
|
||||||
|
"cargo",
|
||||||
|
"test",
|
||||||
|
"--offline",
|
||||||
|
"-p",
|
||||||
|
"lazaret-host-coordinator",
|
||||||
|
"--test",
|
||||||
|
"nofile",
|
||||||
|
"--locked",
|
||||||
|
)
|
||||||
RUNTIME_COMPOSITION_TEST_COMMAND = (
|
RUNTIME_COMPOSITION_TEST_COMMAND = (
|
||||||
"cargo",
|
"cargo",
|
||||||
"test",
|
"test",
|
||||||
|
|
@ -721,6 +731,7 @@ def run_rust_tests() -> None:
|
||||||
run(list(CAPABILITY_TEST_COMMAND))
|
run(list(CAPABILITY_TEST_COMMAND))
|
||||||
run(list(ESCALATION_TEST_COMMAND))
|
run(list(ESCALATION_TEST_COMMAND))
|
||||||
run(list(PREPARED_WORKER_TEST_COMMAND))
|
run(list(PREPARED_WORKER_TEST_COMMAND))
|
||||||
|
run(list(NOFILE_TEST_COMMAND))
|
||||||
run_physical(list(RUNTIME_COMPOSITION_TEST_COMMAND))
|
run_physical(list(RUNTIME_COMPOSITION_TEST_COMMAND))
|
||||||
run_physical(list(NETWORK_RUNTIME_TEST_COMMAND))
|
run_physical(list(NETWORK_RUNTIME_TEST_COMMAND))
|
||||||
run(list(DOCTEST_COMMAND))
|
run(list(DOCTEST_COMMAND))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue