Establish bounded descriptor budgets for sandbox domains

This commit is contained in:
ms2k 2026-07-24 02:18:30 +09:00
commit 503f090121

View file

@ -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 = [