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
35 changes: 35 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 @@ -470,6 +470,40 @@ configure_hostname() {
ts "hostname=${sandbox_hostname}"
}

# OCI images commonly ship an empty /etc/hosts and rely on the container
# runtime to bind-mount a generated one. No runtime does that here, and /etc is
# read-only once the supervisor applies policy, so init supplies the loopback
# names when the file is missing or empty. A non-empty file is the image
# author's and is kept as is. Without these entries `localhost` fails to
# resolve: the guest resolver is the loopback relay, which has no record for it.
configure_hosts() {
local hosts_file
hosts_file="$(root_path /etc/hosts)"

# An absolute symlink target resolves against init's own root, not the
# workload root, so following one could write outside the workload. Leave
# an image-authored link alone.
if [ -L "$hosts_file" ]; then
ts "WARN: ${hosts_file} is a symlink; leaving it unchanged"
return 0
fi
if [ -s "$hosts_file" ]; then
return 0
fi

# Missing loopback names degrade the workload but do not make the sandbox
# unsafe, so a write failure is reported and boot continues. The shell's
# own error stays visible on the console to explain the failure.
if ! printf '%s\n' \
'127.0.0.1 localhost' \
'::1 localhost ip6-localhost ip6-loopback' \
>"$hosts_file"; then
ts "WARN: could not write ${hosts_file}; localhost will not resolve"
return 0
fi
ts "populated empty /etc/hosts with loopback names"
}

run_openshell_init_dropins() {
# Run executable drop-ins from /opt/openshell/init.d in deterministic
# ASCII-sorted order. Drop-ins are *executed* in a child shell rather
Expand Down Expand Up @@ -573,6 +607,7 @@ run_post_overlay_setup() {
setup_sandbox_workdir

configure_hostname
configure_hosts
if ! /opt/openshell/bin/openshell-vm-init prepare-network; then
ts "FATAL: failed to bring up the loopback interface"
exit 1
Expand Down
97 changes: 97 additions & 0 deletions crates/openshell-driver-vm/src/rootfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1328,6 +1328,103 @@ mod tests {
let _ = fs::remove_dir_all(&dir);
}

/// Runs the shipped guest init's `configure_hosts` against `root`.
///
/// The function is cut out of the real script so the test exercises the
/// shell that boots the guest rather than a Rust reimplementation of it.
fn run_guest_configure_hosts(root: &Path) -> std::process::Output {
let script = include_str!("../scripts/openshell-vm-sandbox-init.sh");
let start = script
.find("configure_hosts() {\n")
.expect("guest init defines configure_hosts");
let tail = &script[start..];
let end = tail
.find("\n}\n")
.expect("configure_hosts closes at column zero")
+ "\n}\n".len();
let program = format!(
"set -euo pipefail\n\
ts() {{ :; }}\n\
root_path() {{ printf '%s%s\\n' \"${{ROOT_PREFIX:-}}\" \"$1\"; }}\n\
{}\n\
configure_hosts\n",
&tail[..end]
);
Command::new("bash")
.arg("-c")
.arg(program)
.env("ROOT_PREFIX", root)
.output()
.expect("run configure_hosts")
}

#[test]
fn guest_init_populates_missing_or_empty_hosts_and_keeps_image_hosts() {
let dir = unique_temp_dir();
let etc = dir.join("etc");
fs::create_dir_all(&etc).expect("create etc");
let hosts = etc.join("hosts");
let loopback = "127.0.0.1 localhost\n::1 localhost ip6-localhost ip6-loopback\n";

// An image that ships no hosts file at all.
let output = run_guest_configure_hosts(&dir);
assert!(output.status.success(), "{output:?}");
assert_eq!(fs::read_to_string(&hosts).expect("read hosts"), loopback);

// The usual OCI placeholder: present but empty.
fs::write(&hosts, "").expect("write empty hosts");
let output = run_guest_configure_hosts(&dir);
assert!(output.status.success(), "{output:?}");
assert_eq!(fs::read_to_string(&hosts).expect("read hosts"), loopback);

// A hosts file authored by the image is not rewritten.
let authored = "10.1.2.3 build-host\n";
fs::write(&hosts, authored).expect("write authored hosts");
let output = run_guest_configure_hosts(&dir);
assert!(output.status.success(), "{output:?}");
assert_eq!(fs::read_to_string(&hosts).expect("read hosts"), authored);

// An image-authored symlink is left alone, and an absolute target
// outside the workload root is never written through.
let outside = dir.join("outside-hosts");
fs::write(&outside, "").expect("write outside hosts");
fs::remove_file(&hosts).expect("remove hosts");
std::os::unix::fs::symlink(&outside, &hosts).expect("link hosts");
let output = run_guest_configure_hosts(&dir);
assert!(output.status.success(), "{output:?}");
assert_eq!(fs::read_to_string(&outside).expect("read outside"), "");
assert!(
fs::symlink_metadata(&hosts)
.expect("stat hosts")
.file_type()
.is_symlink()
);

let _ = fs::remove_dir_all(&dir);
}

#[test]
fn guest_init_boot_path_configures_hosts_after_the_account_and_before_networking() {
// The step only helps if the boot function itself calls it, so look at
// the lines of `run_post_overlay_setup` rather than anywhere in the file.
let script = include_str!("../scripts/openshell-vm-sandbox-init.sh");
let start = script
.find("run_post_overlay_setup() {\n")
.expect("guest init defines run_post_overlay_setup");
let boot = &script[start..];
let network = boot
.find("openshell-vm-init prepare-network")
.expect("boot function brings up the loopback interface");
let lines: Vec<&str> = boot[..network].lines().map(str::trim).collect();
let position = |call: &str| lines.iter().position(|line| *line == call);
let account = position("reconcile_sandbox_account").expect("account reconciled");
let hosts = position("configure_hosts").expect("boot function calls configure_hosts");
assert!(
account < hosts,
"hosts are written after the root is set up"
);
}

#[test]
fn prepare_sandbox_rootfs_preserves_image_workdir_contents_in_rootfs() {
let dir = unique_temp_dir();
Expand Down
Loading