Skip to content

Commit c2dbaea

Browse files
authored
Merge pull request #2 from spinupdev/feat/jail-userfaultfd
feat: recreate /dev/userfaultfd inside the jail
2 parents 80dea15 + bf51685 commit c2dbaea

6 files changed

Lines changed: 65 additions & 8 deletions

File tree

ARCHITECTURE.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ JSON manifest -> manifest validation -> Linux launch workflow
1919
or credential details.
2020
- `src/linux/jail.rs` stages the root, applies declared bind mounts, resolves
2121
allow-listed VFIO character identities, performs `pivot_root`, and creates
22-
only the KVM/TUN/entropy and declared VFIO nodes required by CH.
22+
only the KVM/TUN/entropy/userfaultfd and declared VFIO nodes required by CH.
2323
- `src/linux/cgroup.rs` owns cgroup-v2 discovery, controller delegation through
2424
`cgroup.subtree_control`, limit writes, and process attachment.
2525
- `src/linux/process.rs` owns namespaces, resource limits, descriptor and
@@ -54,7 +54,9 @@ Intentional differences:
5454
process, logs, and durable PID directly;
5555
- the API socket is created by CH inside the jail, not passed as a listener FD
5656
or bind mounted from the host;
57-
- Firecracker-specific userfaultfd support is not exposed; and
57+
- Cloud Hypervisor OnDemand restore needs `/dev/userfaultfd` inside the jail
58+
(recreated from the host character identity, owned by the unprivileged VMM).
59+
Firecracker's external uffd-handler is out of scope for this launcher; and
5860
- VFIO groups are supplied by the host allocator; this launcher validates the
5961
boundary but does not discover devices or decide assignment policy.
6062

Cargo.lock

Lines changed: 1 addition & 1 deletion
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
@@ -1,6 +1,6 @@
11
[package]
22
name = "cloud-hypervisor-jailer"
3-
version = "0.1.10"
3+
version = "0.1.11"
44
edition = "2024"
55
rust-version = "1.85"
66
description = "Cloud Hypervisor sandbox launcher for Depot"

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ On Linux, `launch` requires root and then:
2222
- mounts only declared non-symlink sources;
2323
- joins a pre-created network namespace when requested;
2424
- configures the declared cgroup-v2 values and resource limits;
25-
- creates jailed KVM, TUN, and entropy device nodes;
25+
- creates jailed KVM, TUN, entropy, and (when the host has it) userfaultfd
26+
device nodes so Cloud Hypervisor OnDemand restore can create a uffd without
27+
`vm.unprivileged_userfaultfd=1`;
2628
- recreates only explicitly declared canonical VFIO control/group character
2729
devices, without bind-mounting host `/dev` or changing host device ownership;
2830
- creates a PID namespace when requested;
@@ -35,7 +37,7 @@ On Linux, `launch` requires root and then:
3537
flowchart LR
3638
O["Host orchestrator"] -->|"versioned JSON manifest"| V["validate"]
3739
V -->|"pure checks"| L["launch as root"]
38-
L --> J["jail\nmount namespace • bind mounts • pivot_root • KVM/TUN/VFIO"]
40+
L --> J["jail\nmount namespace • bind mounts • pivot_root • KVM/TUN/userfaultfd/VFIO"]
3941
L --> C["cgroup v2\ncontroller delegation • limits • lease"]
4042
L --> P["process\nnetns/PID ns • rlimits • FD/env cleanup • UID/GID"]
4143
J --> CH["Cloud Hypervisor\n--seccomp true"]

src/linux/jail.rs

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,31 @@ pub(super) fn resolve_devices(manifest: &Manifest) -> Result<Vec<ResolvedDevice>
7070
.collect::<Result<Vec<_>>>()
7171
}
7272

73+
const USERFAULTFD_PATH: &str = "/dev/userfaultfd";
74+
75+
/// Resolve the host `/dev/userfaultfd` identity before pivot, if the node
76+
/// has one. Cloud Hypervisor OnDemand restore tries this device first, then
77+
/// the `userfaultfd(2)` syscall. Recreating the character device inside the
78+
/// jail (owned by the unprivileged VMM) is the narrow grant upstream
79+
/// recommends, instead of `vm.unprivileged_userfaultfd=1` for every process.
80+
pub(super) fn resolve_userfaultfd() -> Result<Option<ResolvedDevice>> {
81+
match fs::symlink_metadata(USERFAULTFD_PATH) {
82+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
83+
Err(err) => Err(err).context("stat /dev/userfaultfd"),
84+
Ok(metadata) => {
85+
if metadata.file_type().is_symlink() || !metadata.file_type().is_char_device() {
86+
bail!("/dev/userfaultfd is not a character device");
87+
}
88+
let device_id = metadata.rdev();
89+
Ok(Some(ResolvedDevice {
90+
destination: std::path::PathBuf::from("dev/userfaultfd"),
91+
major: libc::major(device_id) as u32,
92+
minor: libc::minor(device_id) as u32,
93+
}))
94+
}
95+
}
96+
}
97+
7398
fn resolve_device(device: &Device) -> Result<ResolvedDevice> {
7499
let metadata = fs::symlink_metadata(&device.source)
75100
.with_context(|| format!("stat device source {}", device.source.display()))?;
@@ -112,7 +137,12 @@ pub(super) fn pivot_into_jail(root: &Path) -> Result<()> {
112137
syscall_ok(unsafe { libc::rmdir(old_root.as_ptr()) }).context("remove old root")
113138
}
114139

115-
pub(super) fn create_device_nodes(uid: u32, gid: u32, devices: &[ResolvedDevice]) -> Result<()> {
140+
pub(super) fn create_device_nodes(
141+
uid: u32,
142+
gid: u32,
143+
devices: &[ResolvedDevice],
144+
userfaultfd: Option<&ResolvedDevice>,
145+
) -> Result<()> {
116146
fs::create_dir_all("/dev/net").context("create jailed dev directory")?;
117147
if !devices.is_empty() {
118148
fs::create_dir_all("/dev/vfio").context("create jailed VFIO directory")?;
@@ -123,6 +153,12 @@ pub(super) fn create_device_nodes(uid: u32, gid: u32, devices: &[ResolvedDevice]
123153
// Expose only this non-blocking entropy device; guest workloads never
124154
// receive the host /dev filesystem.
125155
create_character_device(Path::new("/dev/urandom"), 1, 9)?;
156+
if let Some(device) = userfaultfd {
157+
create_character_device(Path::new("/dev/userfaultfd"), device.major, device.minor)
158+
.context("create jailed /dev/userfaultfd")?;
159+
chown_path(Path::new("/dev/userfaultfd"), uid, gid)
160+
.context("chown jailed /dev/userfaultfd")?;
161+
}
126162
for device in devices {
127163
let destination = Path::new("/").join(&device.destination);
128164
create_character_device(&destination, device.major, device.minor)
@@ -280,6 +316,7 @@ fn mount_call(source: Option<&Path>, destination: &Path, flags: libc::c_ulong) -
280316
#[cfg(test)]
281317
mod tests {
282318
use super::artifact_mode;
319+
use super::resolve_userfaultfd;
283320

284321
#[test]
285322
fn mounted_artifact_modes_are_minimally_permissive() {
@@ -288,4 +325,19 @@ mod tests {
288325
assert_eq!(artifact_mode(true, true), 0o500);
289326
assert_eq!(artifact_mode(true, false), 0o700);
290327
}
328+
329+
#[test]
330+
fn resolve_userfaultfd_is_optional_when_the_host_has_no_device() {
331+
match resolve_userfaultfd() {
332+
Ok(None) => {}
333+
Ok(Some(device)) => {
334+
assert_eq!(
335+
device.destination,
336+
std::path::PathBuf::from("dev/userfaultfd")
337+
);
338+
assert!(device.major > 0 || device.minor > 0);
339+
}
340+
Err(err) => panic!("resolve_userfaultfd: {err}"),
341+
}
342+
}
291343
}

src/linux/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub(crate) fn launch(manifest: &Manifest) -> Result<()> {
2121
.transpose()
2222
.context("open network namespace")?;
2323
let devices = jail::resolve_devices(manifest)?;
24+
let userfaultfd = jail::resolve_userfaultfd()?;
2425

2526
jail::prepare_root(manifest)?;
2627
jail::enter_mount_namespace()?;
@@ -32,7 +33,7 @@ pub(crate) fn launch(manifest: &Manifest) -> Result<()> {
3233
// effective capabilities needed for pivot_root and device setup.
3334
process::drop_capability_bounding_set()?;
3435
jail::pivot_into_jail(&manifest.root)?;
35-
jail::create_device_nodes(manifest.uid, manifest.gid, &devices)?;
36+
jail::create_device_nodes(manifest.uid, manifest.gid, &devices, userfaultfd.as_ref())?;
3637
if let Some(netns) = netns {
3738
process::join_network_namespace(netns.as_raw_fd())?;
3839
}

0 commit comments

Comments
 (0)