OXYGENHUB

Coverage / crates/flow-init/src/job.rs 78.6%

77 of 98 lines run, 10 of 12 functions · run #202 on main · run missed

1
//! What a job's sandbox needs from inside, in either tier: its
2
//! environment, its program found in its own `PATH`, the usual `/dev`
3
//! links and `lo`. Compiled into `flow-init` (tier 2, the microVM's PID 1)
4
//! and into `flow` itself (tier 1, the syscall sandbox), so both start a
5
//! job the same way.
6
7
pub const PATH: &str = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
8
9
/// The file `program` names: itself when it holds a `/`, otherwise the first
10
/// executable regular file of that name in `path`'s directories.
115×
pub fn resolve_program(program: &str, path: &str) -> Option<String> {
12
    use std::os::unix::fs::PermissionsExt;
135×
    if program.is_empty() {
141×
        return None;
154×
    }
164×
    if program.contains('/') {
171×
        return Some(program.to_string());
183×
    }
193×
    path.split(':')
209×
        .filter(|d| !d.is_empty())
216×
        .map(|d| format!("{d}/{program}"))
226×
        .find(|p| std::fs::metadata(p).is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0))
235×
}
24
25
/// A job's environment: `PATH` and `HOME=/tmp` by default, then the
26
/// image's own (`/etc/flow/env`, written by `flow rootfs import --env`),
27
/// then the job's (its sandbox's and its own, already merged by the host).
28
/// Later ones win, so a pipeline can always set `PATH`.
292×
pub fn job_env(image: Option<&str>, job: &[(String, String)]) -> std::collections::BTreeMap<String, String> {
302×
    let mut env = std::collections::BTreeMap::new();
312×
    env.insert("PATH".to_string(), PATH.to_string());
322×
    env.insert("HOME".to_string(), "/tmp".to_string());
333×
    for line in image.unwrap_or("").lines() {
343×
        if let Some((k, v)) = line.split_once('=') {
352×
            if !k.is_empty() {
362×
                env.insert(k.to_string(), v.to_string());
372×
            }
381×
        }
39
    }
402×
    for (k, v) in job {
412×
        env.insert(k.clone(), v.clone());
422×
    }
432×
    env
442×
}
45
46
/// The links every Linux system has under /dev and devtmpfs does not
47
/// make: `/dev/fd` (bash's process substitution, `<(…)`) and the standard
48
/// streams, all through the job's own /proc.
492×
pub fn dev_links(dev: &str) -> Result<(), String> {
508×
    for (name, target) in DEV_LINKS {
518×
        let link = format!("{dev}/{name}");
528×
        if std::fs::symlink_metadata(&link).is_err() {
534×
            std::os::unix::fs::symlink(target, &link).map_err(|e| format!("link {link}: {e}"))?;
544×
        }
55
    }
562×
    Ok(())
572×
}
58
59
const DEV_LINKS: [(&str, &str); 4] =
60
    [("fd", "/proc/self/fd"), ("stdin", "/proc/self/fd/0"), ("stdout", "/proc/self/fd/1"), ("stderr", "/proc/self/fd/2")];
61
62
/// Set `lo` up; the kernel gives it 127.0.0.1 and ::1 itself.
630×
pub fn loopback_up() -> Result<(), String> {
640×
    let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0) };
650×
    if fd < 0 {
660×
        return Err(format!("socket for lo: {}", std::io::Error::last_os_error()));
670×
    }
680×
    let mut req: libc::ifreq = unsafe { std::mem::zeroed() };
690×
    for (i, b) in b"lo".iter().enumerate() {
700×
        req.ifr_name[i] = *b as libc::c_char;
710×
    }
720×
    let ok = unsafe {
73
        // `as _`: the request's type differs between libcs (Android's is c_int).
740×
        libc::ioctl(fd, libc::SIOCGIFFLAGS as _, &mut req) == 0 && {
750×
            req.ifr_ifru.ifru_flags |= libc::IFF_UP as libc::c_short;
760×
            libc::ioctl(fd, libc::SIOCSIFFLAGS as _, &req) == 0
77
        }
78
    };
790×
    let err = std::io::Error::last_os_error();
800×
    unsafe { libc::close(fd) };
810×
    if !ok {
820×
        return Err(format!("bring up lo: {err}"));
830×
    }
840×
    Ok(())
850×
}
86
87
#[cfg(test)]
88
mod tests {
89
    use super::{dev_links, job_env, resolve_program};
90
91
    #[test]
921×
    fn the_image_environment_sits_under_the_jobs() {
931×
        let image = "PATH=/opt/llvm-mos/bin:/usr/bin:/bin\nX16EMU=/opt/x16emu\nnot a line\n";
941×
        let env = job_env(Some(image), &[("X16EMU".into(), "/job".into()), ("LANG".into(), "C".into())]);
951×
        assert_eq!(env["PATH"], "/opt/llvm-mos/bin:/usr/bin:/bin", "the image's PATH replaces the default");
961×
        assert_eq!(env["X16EMU"], "/job", "the job's value wins");
971×
        assert_eq!(env["HOME"], "/tmp");
981×
        assert_eq!(env["LANG"], "C");
991×
        assert_eq!(job_env(None, &[])["PATH"], super::PATH);
1001×
    }
101
102
    #[test]
1031×
    fn dev_gets_the_usual_links_once() {
1041×
        let dir = std::env::temp_dir().join(format!("flow-init-dev-{}", std::process::id()));
1051×
        let _ = std::fs::remove_dir_all(&dir);
1061×
        std::fs::create_dir_all(&dir).unwrap();
1071×
        let d = dir.to_str().unwrap();
1081×
        dev_links(d).unwrap();
1091×
        dev_links(d).unwrap();
1101×
        assert_eq!(std::fs::read_link(dir.join("fd")).unwrap(), std::path::Path::new("/proc/self/fd"));
1111×
        assert_eq!(std::fs::read_link(dir.join("stdout")).unwrap(), std::path::Path::new("/proc/self/fd/1"));
112
        // Through the host's /proc, the link works as in a job.
1131×
        assert!(dir.join("fd").join("0").exists() || !std::path::Path::new("/proc/self/fd/0").exists());
1141×
        std::fs::remove_dir_all(&dir).unwrap();
1151×
    }
116
117
    use std::os::unix::fs::PermissionsExt;
118
119
    #[test]
1201×
    fn programs_are_found_in_the_jobs_path() {
1211×
        let dir = std::env::temp_dir().join(format!("flow-init-test-{}", std::process::id()));
1221×
        let (a, b) = (dir.join("a"), dir.join("b"));
1231×
        std::fs::create_dir_all(&a).unwrap();
1241×
        std::fs::create_dir_all(&b).unwrap();
1251×
        std::fs::write(a.join("tool"), "not executable").unwrap();
1261×
        std::fs::write(b.join("tool"), "#!/bin/sh\n").unwrap();
1271×
        std::fs::set_permissions(b.join("tool"), std::fs::Permissions::from_mode(0o755)).unwrap();
1281×
        std::fs::create_dir_all(a.join("dir")).unwrap();
1291×
        let path = format!("{}::{}", a.display(), b.display());
1301×
        assert_eq!(resolve_program("tool", &path), Some(format!("{}/tool", b.display())), "skips a file that is not executable");
1311×
        assert_eq!(resolve_program("dir", &path), None, "and directories");
1321×
        assert_eq!(resolve_program("missing", &path), None);
1331×
        assert_eq!(resolve_program("./x", &path).as_deref(), Some("./x"), "a path is used as given");
1341×
        assert_eq!(resolve_program("", &path), None);
1351×
        std::fs::remove_dir_all(&dir).unwrap();
1361×
    }
137
}