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"
|
||||
path = "tests/cold_start.rs"
|
||||
|
||||
[[test]]
|
||||
name = "nofile"
|
||||
path = "tests/nofile.rs"
|
||||
harness = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ use lazaret_core::identity::{
|
|||
};
|
||||
use lazaret_core::records::{Domain, Session, SessionPhase};
|
||||
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::process::{Resource, Rlimit, getrlimit, setrlimit};
|
||||
|
||||
use crate::bootstrap::{CoordinatorLifecycle, serve_control_connection_with_escalation};
|
||||
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> {
|
||||
let parsed = parse_arguments(arguments).inspect_err(|_| signal(b'E'))?;
|
||||
ensure_domain_nofile_limit().inspect_err(|_| signal(b'E'))?;
|
||||
let owner = BootstrapOwnerGuard::from_stdin()
|
||||
.map_err(failure)
|
||||
.inspect_err(|_| signal(b'E'))?;
|
||||
|
|
@ -158,6 +160,51 @@ fn run_private_client_bootstrap(arguments: &[OsString]) -> Result<(), PrivateCli
|
|||
.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(
|
||||
lifecycle: &Mutex<CoordinatorLifecycle<'_, '_>>,
|
||||
waiters: &RefCell<BootstrapWaiterFace>,
|
||||
|
|
@ -495,6 +542,57 @@ mod tests {
|
|||
|
||||
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]
|
||||
fn private_bootstrap_arguments_are_exact_and_public() {
|
||||
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::{
|
||||
Capabilities, Capability, LinuxBuilder, LinuxCapabilitiesBuilder, LinuxNamespaceBuilder,
|
||||
LinuxNamespaceType, Mount, MountBuilder, ProcessBuilder, RootBuilder, Spec, SpecBuilder,
|
||||
UserBuilder,
|
||||
LinuxNamespaceType, Mount, MountBuilder, PosixRlimitBuilder, PosixRlimitType, ProcessBuilder,
|
||||
RootBuilder, Spec, SpecBuilder, UserBuilder,
|
||||
};
|
||||
use rustix::fs::{AtFlags, FileType, Mode, OFlags, ResolveFlags, StatxFlags, openat2, statx};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
|
@ -40,6 +40,7 @@ use validation::{
|
|||
};
|
||||
|
||||
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 MAX_TERMINFO_BYTES: usize = 1_048_576;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use super::{
|
||||
AtFlags, Capabilities, Capability, ContainerId, File, FileIdentity, FileType, HandoffPathType,
|
||||
LazyLock, LinuxBuilder, LinuxCapabilitiesBuilder, LinuxNamespaceBuilder, LinuxNamespaceType,
|
||||
Mode, Mount, NETWORK_NAMESPACE_PATH, OFlags, OsString, OwnedFd, Path, PathBuf, ProcessBuilder,
|
||||
RUNTIME_FILE_DESTINATIONS, Read, ResolveFlags, ResolvedDomainConfiguration, RootProcess,
|
||||
RunscCommand, RuntimeHandoffSource, SandboxBuildError, SandboxBuildErrorKind,
|
||||
StagedHostTerminal, StatxFlags, UserBuilder, VolumeAccess, WORKSPACE_DESTINATION, bind_mount,
|
||||
control_mounts, hex_lower, host_path, oci_error, openat2, staging_error, standard_mounts,
|
||||
statx, validate_absolute_normalized,
|
||||
Mode, Mount, NETWORK_NAMESPACE_PATH, OFlags, OsString, OwnedFd, Path, PathBuf,
|
||||
PosixRlimitBuilder, PosixRlimitType, ProcessBuilder, RUNTIME_FILE_DESTINATIONS, Read,
|
||||
ResolveFlags, ResolvedDomainConfiguration, RootProcess, RunscCommand, RuntimeHandoffSource,
|
||||
SandboxBuildError, SandboxBuildErrorKind, StagedHostTerminal, StatxFlags, UserBuilder,
|
||||
VolumeAccess, WORKSPACE_DESTINATION, bind_mount, control_mounts, hex_lower, host_path,
|
||||
oci_error, openat2, staging_error, standard_mounts, statx, validate_absolute_normalized,
|
||||
};
|
||||
|
||||
pub(super) struct RuntimePaths {
|
||||
|
|
@ -353,6 +353,12 @@ pub(super) fn build_process(
|
|||
.umask(0o022_u32)
|
||||
.build()
|
||||
.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()
|
||||
.terminal(false)
|
||||
.user(user)
|
||||
|
|
@ -360,6 +366,7 @@ pub(super) fn build_process(
|
|||
.env(Vec::<String>::new())
|
||||
.cwd(PathBuf::from(WORKSPACE_DESTINATION))
|
||||
.capabilities(capabilities)
|
||||
.rlimits(vec![nofile])
|
||||
.no_new_privileges(true)
|
||||
.build()
|
||||
.map_err(oci_error)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use lazaret_core::configuration::{
|
|||
};
|
||||
use lazaret_core::identity::{ContainerId, DeviceId, InodeId, NamespaceIdentity};
|
||||
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,
|
||||
MAILBOX_REQUESTS_DESTINATION, MAILBOX_RESULTS_DESTINATION, NETWORK_NAMESPACE_PATH,
|
||||
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"]["noNewPrivileges"], true);
|
||||
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!(
|
||||
specification["process"]["args"],
|
||||
serde_json::json!([
|
||||
|
|
|
|||
|
|
@ -429,7 +429,7 @@ class VerificationTests(unittest.TestCase):
|
|||
"not binary(capability-drop-helper) and not binary(acquisition) "
|
||||
"and not binary(state-anchor-lifecycle) and not binary(recovery-process) "
|
||||
"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)"
|
||||
),
|
||||
],
|
||||
|
|
@ -493,6 +493,16 @@ class VerificationTests(unittest.TestCase):
|
|||
"prepared-worker",
|
||||
"--locked",
|
||||
],
|
||||
[
|
||||
"cargo",
|
||||
"test",
|
||||
"--offline",
|
||||
"-p",
|
||||
"lazaret-host-coordinator",
|
||||
"--test",
|
||||
"nofile",
|
||||
"--locked",
|
||||
],
|
||||
[
|
||||
"cargo",
|
||||
"test",
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ NEXTEST_COMMAND = (
|
|||
"not binary(capability-drop-helper) and not binary(acquisition) "
|
||||
"and not binary(state-anchor-lifecycle) and not binary(recovery-process) "
|
||||
"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)"
|
||||
),
|
||||
)
|
||||
|
|
@ -108,6 +108,16 @@ PREPARED_WORKER_TEST_COMMAND = (
|
|||
"prepared-worker",
|
||||
"--locked",
|
||||
)
|
||||
NOFILE_TEST_COMMAND = (
|
||||
"cargo",
|
||||
"test",
|
||||
"--offline",
|
||||
"-p",
|
||||
"lazaret-host-coordinator",
|
||||
"--test",
|
||||
"nofile",
|
||||
"--locked",
|
||||
)
|
||||
RUNTIME_COMPOSITION_TEST_COMMAND = (
|
||||
"cargo",
|
||||
"test",
|
||||
|
|
@ -721,6 +731,7 @@ def run_rust_tests() -> None:
|
|||
run(list(CAPABILITY_TEST_COMMAND))
|
||||
run(list(ESCALATION_TEST_COMMAND))
|
||||
run(list(PREPARED_WORKER_TEST_COMMAND))
|
||||
run(list(NOFILE_TEST_COMMAND))
|
||||
run_physical(list(RUNTIME_COMPOSITION_TEST_COMMAND))
|
||||
run_physical(list(NETWORK_RUNTIME_TEST_COMMAND))
|
||||
run(list(DOCTEST_COMMAND))
|
||||
|
|
|
|||
Loading…
Reference in a new issue