Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/bin/dive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ fn main() -> Result<()> {
.make_mount(lead_pid)
.context("could not init shared mount")?;

// SAFETY: fork() requires the process to be single-threaded — the
// child only inherits the calling thread, and the child branch runs
// Rust code before exec().
dive::fork::assert_single_threaded();
match unsafe { fork()? } {
Fork::Child(_) => {
if let Err(err) = prepare_shell_environment(&shared_mount, lead_pid)
Expand Down
31 changes: 31 additions & 0 deletions src/fork.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use std::fs;

/// Assert that the process is single-threaded.
///
/// Forking a multi-threaded process is unsafe: only the calling thread
/// survives in the child, but locks and other state held by other threads
/// remain "locked forever", and only async-signal-safe functions are legal
/// in the child until `exec`. dive's fork sites do non-trivial Rust work
/// in the child, so they require the process to be single-threaded.
///
/// Counts entries under `/proc/self/task`. In debug builds a violation
/// panics; in release builds it logs a warning and proceeds.
pub fn assert_single_threaded() {
let count = match fs::read_dir("/proc/self/task") {
Ok(entries) => entries.count(),
Err(err) => {
log::warn!("could not read /proc/self/task: {err}");
return;
}
};
if count > 1 {
log::warn!(
"fork() called with {count} threads alive; \
child state may be inconsistent"
);
debug_assert!(
count == 1,
"fork() requires a single-threaded process, found {count} threads"
);
}
}
4 changes: 4 additions & 0 deletions src/image_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ impl BaseImageBuilder {
log::info!("building base image");

let tmp = tempdir()?;
// SAFETY: fork() requires the process to be single-threaded — the
// child only inherits the calling thread, and build_base_child
// runs Rust code before exit().
crate::fork::assert_single_threaded();
match unsafe { fork()? } {
Fork::Child(_) => exit(self.build_base_child(tmp.path())),
Fork::Parent(child_pid) => self.build_base_parent(child_pid),
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod base_image;
pub mod fork;
pub mod image_builder;
pub mod namespaces;
pub mod nixos;
Expand Down
3 changes: 3 additions & 0 deletions src/shared_mount.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,9 @@ fn new_userns_fd(id_mapping: &IdMaps) -> Result<OwnedFd, std::io::Error> {
}

fn clone_new_userns() -> Result<RawPid, std::io::Error> {
// Intentionally a short prefix of the kernel's `struct clone_args`:
// `set_tid`, `set_tid_size`, and `cgroup` are omitted. The kernel uses
// `args_size` to detect this and zero-fills the missing fields.
#[repr(C)]
#[allow(non_camel_case_types)]
struct clone3_args {
Expand Down
4 changes: 4 additions & 0 deletions src/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ impl Shell {
}

pub fn spawn(self) -> Result<i32> {
// SAFETY: fork() requires the process to be single-threaded — the
// child only inherits the calling thread, and dive runs Rust code
// before exec().
crate::fork::assert_single_threaded();
match unsafe { fork()? } {
Fork::Child(_) => {
let err = self.exec();
Expand Down
Loading