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
6 changes: 6 additions & 0 deletions crates/openshell-driver-vm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ binaries.

The driver embeds libkrun, libkrunfw, the guest OCI unpacker, the portable guest sandbox, and the custom kernel runtime. Each sandbox boots from a cached immutable bootstrap ext4 root disk plus a per-sandbox writable overlay disk. When the requested sandbox image differs from the bootstrap image, the driver prepares a read-only image ext4 disk inside a bootstrap VM and mounts that unpacked rootfs as the sandbox lowerdir.

Guest init creates missing `/dev/fd` and standard-stream links after mounting
procfs and devtmpfs, before using Bash process substitution. Existing links to
the expected procfs paths, or entries that resolve to the same files, stay
intact. Conflicting entries and unrelated dangling links fail boot with a
diagnostic rather than being replaced.

## How it fits together

Caller driver config is disabled by default, including upload workflows
Expand Down
25 changes: 25 additions & 0 deletions crates/openshell-driver-vm/scripts/openshell-vm-sandbox-init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,31 @@ mount_initial_fs() {
mount -t tmpfs tmpfs /tmp 2>/dev/null || true
mount -t tmpfs tmpfs /run 2>/dev/null || true
mount -t devtmpfs devtmpfs /dev 2>/dev/null || true
setup_fd_links /dev
}

setup_fd_links() {
local dev_dir="$1"
local name target
for name in fd stdin stdout stderr; do
case "$name" in
fd) target=/proc/self/fd ;;
stdin) target=/proc/self/fd/0 ;;
stdout) target=/proc/self/fd/1 ;;
stderr) target=/proc/self/fd/2 ;;
esac
if [[ -L "$dev_dir/$name" ]] && [[ "$(readlink "$dev_dir/$name")" == "$target" ]]; then
continue
fi
if [[ -e "$dev_dir/$name" || -L "$dev_dir/$name" ]]; then
if [[ ! "$dev_dir/$name" -ef "$target" ]]; then
printf 'guest init: %s must refer to %s\n' "$dev_dir/$name" "$target" >&2
return 1
fi
else
ln -s "$target" "$dev_dir/$name"
fi
done
}

bind_mount_into_newroot() {
Expand Down
87 changes: 87 additions & 0 deletions crates/openshell-driver-vm/src/rootfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1357,6 +1357,93 @@ mod tests {
let _ = fs::remove_dir_all(&dir);
}

#[test]
fn guest_init_creates_fd_links_and_preserves_existing_aliases() {
use std::os::unix::fs::MetadataExt as _;

let dev = tempfile::tempdir().expect("temporary guest dev");
let run = || run_guest_setup_fd_links(dev.path());
let output = run();
assert!(output.status.success(), "{output:?}");
let aliases = [
("fd", "/proc/self/fd"),
("stdin", "/proc/self/fd/0"),
("stdout", "/proc/self/fd/1"),
("stderr", "/proc/self/fd/2"),
];
let mut inodes = Vec::new();
for (name, target) in aliases {
let path = dev.path().join(name);
assert_eq!(fs::read_link(&path).expect("read alias"), Path::new(target));
inodes.push(fs::symlink_metadata(path).expect("stat alias").ino());
}
let output = run();
assert!(output.status.success(), "{output:?}");
for ((name, _), inode) in aliases.into_iter().zip(inodes) {
assert_eq!(
fs::symlink_metadata(dev.path().join(name))
.expect("stat retained alias")
.ino(),
inode,
"existing {name} alias stays intact"
);
}
#[cfg(target_os = "linux")]
{
let stdin = dev.path().join("stdin");
fs::remove_file(&stdin).expect("replace fixture alias");
std::os::unix::fs::symlink("fd/0", &stdin).expect("equivalent relative alias");
let output = run();
assert!(output.status.success(), "{output:?}");
assert_eq!(
fs::read_link(stdin).expect("retained relative alias"),
Path::new("fd/0")
);
}
}

#[test]
fn guest_init_rejects_conflicting_fd_entries_without_replacing_them() {
let dev = tempfile::tempdir().expect("temporary guest dev");
let fd = dev.path().join("fd");
fs::write(&fd, b"keep this entry").expect("conflicting file");
let output = run_guest_setup_fd_links(dev.path());
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("must refer to /proc/self/fd"));
assert_eq!(fs::read(&fd).expect("retained file"), b"keep this entry");

fs::remove_file(&fd).expect("remove fixture file");
std::os::unix::fs::symlink("missing-target", &fd).expect("dangling link");
let output = run_guest_setup_fd_links(dev.path());
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("must refer to /proc/self/fd"));
assert_eq!(
fs::read_link(fd).expect("retained link"),
Path::new("missing-target")
);
}

fn run_guest_setup_fd_links(dev: &Path) -> std::process::Output {
let script = include_str!("../scripts/openshell-vm-sandbox-init.sh");
let start = script
.find("setup_fd_links() {\n")
.expect("fd setup function");
let tail = &script[start..];
let end = tail.find("\n}\n").expect("fd setup function end") + "\n}\n".len();
Command::new("bash")
.args([
"-c",
&format!(
"set -euo pipefail\n{}\nsetup_fd_links \"$1\"\n",
&tail[..end]
),
"test",
])
.arg(dev)
.output()
.expect("run shipped fd setup")
}

/// Runs the shipped guest init's `configure_hosts` against `root`.
///
/// The function is cut out of the real script so the test exercises the
Expand Down
Loading