OXYGENHUB
# 337afb8f6043 oxygenflow / crates / flow-init / src / job.rs
5.9 KiB · Rust · last changed The syscall tier: jobs without KVM 1 month ago source blame history raw
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.
11
pub fn resolve_program(program: &str, path: &str) -> Option<String> {
12
    use std::os::unix::fs::PermissionsExt;
13
    if program.is_empty() {
14
        return None;
15
    }
16
    if program.contains('/') {
17
        return Some(program.to_string());
18
    }
19
    path.split(':')
20
        .filter(|d| !d.is_empty())
21
        .map(|d| format!("{d}/{program}"))
22
        .find(|p| std::fs::metadata(p).is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0))
23
}
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`.
29
pub fn job_env(image: Option<&str>, job: &[(String, String)]) -> std::collections::BTreeMap<String, String> {
30
    let mut env = std::collections::BTreeMap::new();
31
    env.insert("PATH".to_string(), PATH.to_string());
32
    env.insert("HOME".to_string(), "/tmp".to_string());
33
    for line in image.unwrap_or("").lines() {
34
        if let Some((k, v)) = line.split_once('=') {
35
            if !k.is_empty() {
36
                env.insert(k.to_string(), v.to_string());
37
            }
38
        }
39
    }
40
    for (k, v) in job {
41
        env.insert(k.clone(), v.clone());
42
    }
43
    env
44
}
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.
49
pub fn dev_links(dev: &str) -> Result<(), String> {
50
    for (name, target) in DEV_LINKS {
51
        let link = format!("{dev}/{name}");
52
        if std::fs::symlink_metadata(&link).is_err() {
53
            std::os::unix::fs::symlink(target, &link).map_err(|e| format!("link {link}: {e}"))?;
54
        }
55
    }
56
    Ok(())
57
}
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.
63
pub fn loopback_up() -> Result<(), String> {
64
    let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM | libc::SOCK_CLOEXEC, 0) };
65
    if fd < 0 {
66
        return Err(format!("socket for lo: {}", std::io::Error::last_os_error()));
67
    }
68
    let mut req: libc::ifreq = unsafe { std::mem::zeroed() };
69
    for (i, b) in b"lo".iter().enumerate() {
70
        req.ifr_name[i] = *b as libc::c_char;
71
    }
72
    let ok = unsafe {
73
        libc::ioctl(fd, libc::SIOCGIFFLAGS, &mut req) == 0 && {
74
            req.ifr_ifru.ifru_flags |= libc::IFF_UP as libc::c_short;
75
            libc::ioctl(fd, libc::SIOCSIFFLAGS, &req) == 0
76
        }
77
    };
78
    let err = std::io::Error::last_os_error();
79
    unsafe { libc::close(fd) };
80
    if !ok {
81
        return Err(format!("bring up lo: {err}"));
82
    }
83
    Ok(())
84
}
85
86
#[cfg(test)]
87
mod tests {
88
    use super::{dev_links, job_env, resolve_program};
89
90
    #[test]
91
    fn the_image_environment_sits_under_the_jobs() {
92
        let image = "PATH=/opt/llvm-mos/bin:/usr/bin:/bin\nX16EMU=/opt/x16emu\nnot a line\n";
93
        let env = job_env(Some(image), &[("X16EMU".into(), "/job".into()), ("LANG".into(), "C".into())]);
94
        assert_eq!(env["PATH"], "/opt/llvm-mos/bin:/usr/bin:/bin", "the image's PATH replaces the default");
95
        assert_eq!(env["X16EMU"], "/job", "the job's value wins");
96
        assert_eq!(env["HOME"], "/tmp");
97
        assert_eq!(env["LANG"], "C");
98
        assert_eq!(job_env(None, &[])["PATH"], super::PATH);
99
    }
100
101
    #[test]
102
    fn dev_gets_the_usual_links_once() {
103
        let dir = std::env::temp_dir().join(format!("flow-init-dev-{}", std::process::id()));
104
        let _ = std::fs::remove_dir_all(&dir);
105
        std::fs::create_dir_all(&dir).unwrap();
106
        let d = dir.to_str().unwrap();
107
        dev_links(d).unwrap();
108
        dev_links(d).unwrap();
109
        assert_eq!(std::fs::read_link(dir.join("fd")).unwrap(), std::path::Path::new("/proc/self/fd"));
110
        assert_eq!(std::fs::read_link(dir.join("stdout")).unwrap(), std::path::Path::new("/proc/self/fd/1"));
111
        // Through the host's /proc, the link works as in a job.
112
        assert!(dir.join("fd").join("0").exists() || !std::path::Path::new("/proc/self/fd/0").exists());
113
        std::fs::remove_dir_all(&dir).unwrap();
114
    }
115
116
    use std::os::unix::fs::PermissionsExt;
117
118
    #[test]
119
    fn programs_are_found_in_the_jobs_path() {
120
        let dir = std::env::temp_dir().join(format!("flow-init-test-{}", std::process::id()));
121
        let (a, b) = (dir.join("a"), dir.join("b"));
122
        std::fs::create_dir_all(&a).unwrap();
123
        std::fs::create_dir_all(&b).unwrap();
124
        std::fs::write(a.join("tool"), "not executable").unwrap();
125
        std::fs::write(b.join("tool"), "#!/bin/sh\n").unwrap();
126
        std::fs::set_permissions(b.join("tool"), std::fs::Permissions::from_mode(0o755)).unwrap();
127
        std::fs::create_dir_all(a.join("dir")).unwrap();
128
        let path = format!("{}::{}", a.display(), b.display());
129
        assert_eq!(resolve_program("tool", &path), Some(format!("{}/tool", b.display())), "skips a file that is not executable");
130
        assert_eq!(resolve_program("dir", &path), None, "and directories");
131
        assert_eq!(resolve_program("missing", &path), None);
132
        assert_eq!(resolve_program("./x", &path).as_deref(), Some("./x"), "a path is used as given");
133
        assert_eq!(resolve_program("", &path), None);
134
        std::fs::remove_dir_all(&dir).unwrap();
135
    }
136
}