Skip to content

Commit 3c72f25

Browse files
committed
release: check generated vm ids before launch
1 parent fc03ff5 commit 3c72f25

15 files changed

Lines changed: 233 additions & 206 deletions

.beads/issues.jsonl

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

Cargo.lock

Lines changed: 23 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ members = ["crates/*"]
44
exclude = ["support/m80-close-range"]
55

66
[workspace.package]
7-
version = "0.2.10"
7+
version = "0.2.11"
88
edition = "2021"
99
license = "Apache-2.0 OR MIT"
1010
repository = "https://github.com/nathanp/m80"

crates/m80-cli/tests/release/current_latest_version_cutover.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,16 @@ fn current_latest_version_cutover_names_real_repair_tag() {
2828
.expect("read installer layout fixture");
2929

3030
assert!(
31-
cargo_toml.contains("version = \"0.2.10\""),
31+
cargo_toml.contains("version = \"0.2.11\""),
3232
"workspace package version must match the current repair tag"
3333
);
3434
assert!(
35-
runbook.contains("workspace package version `0.2.10`")
36-
&& runbook.contains("expected release tag is `v0.2.10`"),
35+
runbook.contains("workspace package version `0.2.11`")
36+
&& runbook.contains("expected release tag is `v0.2.11`"),
3737
"release runbook must document the real current repair version"
3838
);
3939
assert!(
40-
behavior.contains("matching stable tag is `v0.2.10`")
40+
behavior.contains("matching stable tag is `v0.2.11`")
4141
&& behavior.contains("`v0.2.7` installer handoff"),
4242
"behavior doc must name the repaired tag and the superseded latest state"
4343
);

crates/m80-firecracker/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -669,9 +669,10 @@ an invariant fails closed.
669669
`SharedPmemErofsLayoutProbeInvalid`, `VmIdPathBudgetExceeded`). It is not a
670670
fallback bucket: it is reserved for caller configuration, CLI flag, and config
671671
merge failures where no lower crate owns a more specific typed cause.
672-
- `ConfigError::VmIdPathBudgetExceeded` is raised at `Backend::admit()` when a
673-
caller-supplied `vm_id` would produce an AF_UNIX socket path longer than the
674-
kernel `sun_path` cap (107 usable bytes). The path layout is
672+
- `ConfigError::VmIdPathBudgetExceeded` is raised at `Backend::admit()` when
673+
the selected `vm_id` would produce an AF_UNIX socket path longer than the
674+
kernel `sun_path` cap (107 usable bytes). The selected id is either the
675+
caller-supplied id or the generated `vm-{pid}-{unix_ms}` id. The path layout is
675676
`<run_root>/<vm_id>/<fc_basename>/<vm_id>/root/firecracker.sock`; vm_id
676677
appears twice because the jail layout inherits Firecracker's jailer
677678
convention. The check is pure arithmetic and runs before the admission

crates/m80-firecracker/src/backend.rs

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ impl Backend {
9292
/// Acquire one admission permit and return a [`Sandbox`] in Created state.
9393
///
9494
/// Fails fast with [`FcError::AdmissionRefused`] when no permits are
95-
/// available, or with [`ConfigError::VmIdPathBudgetExceeded`] when a
96-
/// caller-supplied `vm_id` would overflow the AF_UNIX `sun_path` cap.
95+
/// available, or with [`ConfigError::VmIdPathBudgetExceeded`] when the
96+
/// selected `vm_id` would overflow the AF_UNIX `sun_path` cap.
9797
/// Does not block.
9898
pub fn admit(self: &Arc<Self>, mut config: SandboxConfig) -> Result<Sandbox, FcError> {
9999
validate_caller_boot_args_if_present(config.boot_args.as_deref())?;
@@ -102,16 +102,13 @@ impl Backend {
102102
}
103103
validate_network_policy(&config.network)?;
104104

105-
// Caller-supplied vm_ids are validated up front so the failure
106-
// surfaces as a typed config error rather than as an opaque
107-
// `bind() AF_UNIX path too long` deep inside launch. The
108-
// auto-generated `vm-{pid}-{ts}` form (resolve_vm_id) is bounded by
109-
// construction and does not need a check here.
110-
if let Some(vm_id) = config.vm_id.as_deref() {
111-
check_vm_id_name(vm_id)?;
112-
check_vm_id_reserved_name(vm_id)?;
113-
check_vm_id_path_budget(&self.config, vm_id)?;
114-
}
105+
let vm_id = config.vm_id.get_or_insert_with(auto_vm_id);
106+
// Validate the selected vm_id up front so the failure surfaces as a
107+
// typed config error rather than as an opaque AF_UNIX failure after
108+
// launch has partially materialized a jail.
109+
check_vm_id_name(vm_id)?;
110+
check_vm_id_reserved_name(vm_id)?;
111+
check_vm_id_path_budget(&self.config, vm_id)?;
115112

116113
let mut available = self.semaphore.lock().unwrap_or_else(|p| p.into_inner());
117114

@@ -312,10 +309,15 @@ fn live_run_dir_names(run_root: &Path) -> Result<Vec<String>, FcError> {
312309
Ok(names)
313310
}
314311

315-
/// Reject a caller-supplied `vm_id` whose constructed AF_UNIX socket path
316-
/// would exceed the kernel's `sun_path` cap. The check is purely arithmetic
317-
/// (no filesystem access) so it can run before the admission permit is
318-
/// acquired.
312+
fn auto_vm_id() -> String {
313+
let pid = std::process::id();
314+
let ts = crate::runroot::unix_ms_now();
315+
format!("vm-{pid}-{ts}")
316+
}
317+
318+
/// Reject a selected `vm_id` whose constructed AF_UNIX socket path would
319+
/// exceed the kernel's `sun_path` cap. The check is purely arithmetic (no
320+
/// filesystem access) so it can run before the admission permit is acquired.
319321
fn check_vm_id_path_budget(config: &BackendConfig, vm_id: &str) -> Result<(), FcError> {
320322
let fc_basename = config
321323
.discovery

crates/m80-firecracker/src/error.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -453,15 +453,15 @@ pub enum ConfigError {
453453
/// Maximum accepted artifact byte length.
454454
max: u64,
455455
},
456-
/// The caller-supplied `vm_id` would produce an AF_UNIX socket path that
457-
/// exceeds the kernel's `sun_path` cap. Surfaces at admission time so the
458-
/// failure cannot reach `bind()` / `connect()` as an opaque IO error.
456+
/// The selected `vm_id` would produce an AF_UNIX socket path that exceeds
457+
/// the kernel's `sun_path` cap. Surfaces at admission time so the failure
458+
/// cannot reach `bind()` / `connect()` as an opaque IO error.
459459
#[error(
460460
"vm_id {vm_id:?} would produce a {path_len}-byte AF_UNIX socket path under run_root {} (cap {budget})",
461461
run_root.display()
462462
)]
463463
VmIdPathBudgetExceeded {
464-
/// Caller-supplied vm_id whose path would overflow.
464+
/// Selected vm_id whose path would overflow.
465465
vm_id: String,
466466
/// Configured run-root.
467467
run_root: PathBuf,

crates/m80-firecracker/src/launch.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,10 @@ impl Sandbox {
109109

110110
/// Resolve or auto-generate the VM identifier.
111111
fn resolve_vm_id(&self) -> String {
112-
self.config.vm_id.clone().unwrap_or_else(|| {
113-
let pid = std::process::id();
114-
let ts = crate::runroot::unix_ms_now();
115-
format!("vm-{pid}-{ts}")
116-
})
112+
self.config
113+
.vm_id
114+
.clone()
115+
.expect("Backend::admit stores the selected vm_id")
117116
}
118117

119118
/// Delete the partial run directory if launch fails.

crates/m80-firecracker/tests/path_budget.rs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
//! Admission-time validation that a caller-supplied `vm_id` would not
2-
//! produce an AF_UNIX socket path longer than the kernel's `sun_path` cap.
1+
//! Admission-time validation that the selected `vm_id` would not produce an
2+
//! AF_UNIX socket path longer than the kernel's `sun_path` cap.
33
//!
44
//! These tests run without KVM or root: they only exercise the arithmetic
55
//! check inside `Backend::admit()`. The structural fix complements the
@@ -64,13 +64,34 @@ fn admit_just_under_budget_succeeds() {
6464
}
6565

6666
#[test]
67-
fn admit_with_no_vm_id_skips_check() {
68-
// Auto-generated `vm-{pid}-{ts}` is bounded by construction; admit must
69-
// succeed without checking the budget.
70-
let backend = common::make_fake_backend(4, Path::new("/var/lib/m80-run"));
67+
fn admit_with_no_vm_id_uses_checked_generated_id() {
68+
let backend = common::make_fake_backend(4, Path::new("/tmp/m80-test"));
7169
backend
7270
.admit(config_with_vm_id(None))
73-
.expect("None vm_id must admit (auto-generated form is bounded)");
71+
.expect("None vm_id must admit when generated id fits");
72+
}
73+
74+
#[test]
75+
fn admit_with_no_vm_id_rejects_generated_id_over_budget() {
76+
let backend = common::make_fake_backend(
77+
4,
78+
Path::new("/tmp/m80-run-root-that-is-intentionally-too-long-for-auto-vm-ids"),
79+
);
80+
let err = backend
81+
.admit(config_with_vm_id(None))
82+
.expect_err("generated vm_id must be checked against the path budget");
83+
84+
let FcError::Config(ConfigError::VmIdPathBudgetExceeded {
85+
vm_id,
86+
path_len,
87+
budget,
88+
..
89+
}) = err
90+
else {
91+
panic!("expected ConfigError::VmIdPathBudgetExceeded, got {err:?}");
92+
};
93+
assert!(vm_id.starts_with("vm-"), "unexpected generated id: {vm_id}");
94+
assert!(path_len > budget);
7495
}
7596

7697
#[test]

0 commit comments

Comments
 (0)