//! What a job's sandbox needs from inside, in either tier: its //! environment, its program found in its own `PATH`, the usual `/dev` //! links and `lo`. Compiled into `flow-init` (tier 2, the microVM's PID 1) //! and into `flow` itself (tier 1, the syscall sandbox), so both start a //! job the same way. pub const PATH: &str = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; /// The file `program` names: itself when it holds a `/`, otherwise the first /// executable regular file of that name in `path`'s directories. pub fn resolve_program(program: &str, path: &str) -> Option { use std::os::unix::fs::PermissionsExt; if program.is_empty() { return None; } if program.contains('/') { return Some(program.to_string()); } path.split(':') .filter(|d| !d.is_empty()) .map(|d| format!("{d}/{program}")) .find(|p| std::fs::metadata(p).is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)) } /// A job's environment: `PATH` and `HOME=/tmp` by default, then the /// image's own (`/etc/flow/env`, written by `flow rootfs import --env`), /// then the job's (its sandbox's and its own, already merged by the host). /// Later ones win, so a pipeline can always set `PATH`. pub fn job_env(image: Option<&str>, job: &[(String, String)]) -> std::collections::BTreeMap { let mut env = std::collections::BTreeMap::new(); env.insert("PATH".to_string(), PATH.to_string()); env.insert("HOME".to_string(), "/tmp".to_string()); for line in image.unwrap_or("").lines() { if let Some((k, v)) = line.split_once('=') { if !k.is_empty() { env.insert(k.to_string(), v.to_string()); } } } for (k, v) in job { env.insert(k.clone(), v.clone()); } env } /// The links every Linux system has under /dev and devtmpfs does not /// make: `/dev/fd` (bash's process substitution, `<(…)`) and the standard /// streams, all through the job's own /proc. pub fn dev_links(dev: &str) -> Result<(), String> { for (name, target) in DEV_LINKS { let link = format!("{dev}/{name}"); if std::fs::symlink_metadata(&link).is_err() { std::os::unix::fs::symlink(target, &link).map_err(|e| format!("link {link}: {e}"))?; } } Ok(()) } const DEV_LINKS: [(&str, &str); 4] = [("fd", "/proc/self/fd"), ("stdin", "/proc/self/fd/0"), ("stdout", "/proc/self/fd/1"), ("stderr", "/proc/self/fd/2")]; /// Set `lo` up; the kernel gives it 127.0.0.1 and ::1 itself. pub fn loopback_up() -> Result<(), String> { let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0) }; if fd < 0 { return Err(format!("socket for lo: {}", std::io::Error::last_os_error())); } let mut req: libc::ifreq = unsafe { std::mem::zeroed() }; for (i, b) in b"lo".iter().enumerate() { req.ifr_name[i] = *b as libc::c_char; } let ok = unsafe { // `as _`: the request's type differs between libcs (Android's is c_int). libc::ioctl(fd, libc::SIOCGIFFLAGS as _, &mut req) == 0 && { req.ifr_ifru.ifru_flags |= libc::IFF_UP as libc::c_short; libc::ioctl(fd, libc::SIOCSIFFLAGS as _, &req) == 0 } }; let err = std::io::Error::last_os_error(); unsafe { libc::close(fd) }; if !ok { return Err(format!("bring up lo: {err}")); } Ok(()) } #[cfg(test)] mod tests { use super::{dev_links, job_env, resolve_program}; #[test] fn the_image_environment_sits_under_the_jobs() { let image = "PATH=/opt/llvm-mos/bin:/usr/bin:/bin\nX16EMU=/opt/x16emu\nnot a line\n"; let env = job_env(Some(image), &[("X16EMU".into(), "/job".into()), ("LANG".into(), "C".into())]); assert_eq!(env["PATH"], "/opt/llvm-mos/bin:/usr/bin:/bin", "the image's PATH replaces the default"); assert_eq!(env["X16EMU"], "/job", "the job's value wins"); assert_eq!(env["HOME"], "/tmp"); assert_eq!(env["LANG"], "C"); assert_eq!(job_env(None, &[])["PATH"], super::PATH); } #[test] fn dev_gets_the_usual_links_once() { let dir = std::env::temp_dir().join(format!("flow-init-dev-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let d = dir.to_str().unwrap(); dev_links(d).unwrap(); dev_links(d).unwrap(); assert_eq!(std::fs::read_link(dir.join("fd")).unwrap(), std::path::Path::new("/proc/self/fd")); assert_eq!(std::fs::read_link(dir.join("stdout")).unwrap(), std::path::Path::new("/proc/self/fd/1")); // Through the host's /proc, the link works as in a job. assert!(dir.join("fd").join("0").exists() || !std::path::Path::new("/proc/self/fd/0").exists()); std::fs::remove_dir_all(&dir).unwrap(); } use std::os::unix::fs::PermissionsExt; #[test] fn programs_are_found_in_the_jobs_path() { let dir = std::env::temp_dir().join(format!("flow-init-test-{}", std::process::id())); let (a, b) = (dir.join("a"), dir.join("b")); std::fs::create_dir_all(&a).unwrap(); std::fs::create_dir_all(&b).unwrap(); std::fs::write(a.join("tool"), "not executable").unwrap(); std::fs::write(b.join("tool"), "#!/bin/sh\n").unwrap(); std::fs::set_permissions(b.join("tool"), std::fs::Permissions::from_mode(0o755)).unwrap(); std::fs::create_dir_all(a.join("dir")).unwrap(); let path = format!("{}::{}", a.display(), b.display()); assert_eq!(resolve_program("tool", &path), Some(format!("{}/tool", b.display())), "skips a file that is not executable"); assert_eq!(resolve_program("dir", &path), None, "and directories"); assert_eq!(resolve_program("missing", &path), None); assert_eq!(resolve_program("./x", &path).as_deref(), Some("./x"), "a path is used as given"); assert_eq!(resolve_program("", &path), None); std::fs::remove_dir_all(&dir).unwrap(); } }