| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
use anyhow::{bail, Context, Result}; |
| 6 |
use flow_core::{Job, MountSource}; |
| 7 |
use std::path::{Path, PathBuf}; |
| 8 |
|
| 9 |
|
| 10 |
pub const MAX_ARTIFACT_BYTES: u64 = 512 << 20; |
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
|
| 15 |
pub fn locate(job: &Job, workspace: &Path, scratch: &Path, path: &str) -> (PathBuf, String) { |
| 16 |
locate_in(job, workspace, path, |i| scratch.join(format!("mount-{i}.ext4"))) |
| 17 |
} |
| 18 |
|
| 19 |
|
| 20 |
|
| 21 |
pub fn locate_dir(job: &Job, workspace: &Path, scratch: &Path, path: &str) -> (PathBuf, String) { |
| 22 |
locate_in(job, workspace, path, |i| crate::syscall::disk_dir(scratch, i)) |
| 23 |
} |
| 24 |
|
| 25 |
fn locate_in(job: &Job, workspace: &Path, path: &str, disk: impl Fn(usize) -> PathBuf) -> (PathBuf, String) { |
| 26 |
for (i, m) in job.mounts.iter().filter(|m| m.source == MountSource::Disk).enumerate() { |
| 27 |
if let Some(rest) = path.strip_prefix(&format!("{}/", m.target)) { |
| 28 |
return (disk(i), format!("/{rest}")); |
| 29 |
} |
| 30 |
} |
| 31 |
(workspace.to_path_buf(), path.strip_prefix("/src").unwrap_or(path).to_string()) |
| 32 |
} |
| 33 |
|
| 34 |
|
| 35 |
|
| 36 |
|
| 37 |
|
| 38 |
pub fn read_dir_file(base: &Path, inner: &str) -> Result<Vec<u8>> { |
| 39 |
use std::ffi::CString; |
| 40 |
use std::io::Read; |
| 41 |
use std::os::fd::FromRawFd; |
| 42 |
let parts: Vec<&str> = inner.split('/').filter(|p| !p.is_empty()).collect(); |
| 43 |
if parts.is_empty() || parts.iter().any(|p| *p == "." || *p == "..") { |
| 44 |
bail!("{inner} is not a clean path"); |
| 45 |
} |
| 46 |
let c = |s: &str| CString::new(s).context("a path holds NUL"); |
| 47 |
let mut dir = unsafe { libc::open(c(&base.to_string_lossy())?.as_ptr(), libc::O_DIRECTORY | libc::O_RDONLY | libc::O_CLOEXEC) }; |
| 48 |
if dir < 0 { |
| 49 |
bail!("open {}: {}", base.display(), std::io::Error::last_os_error()); |
| 50 |
} |
| 51 |
let (last, dirs) = parts.split_last().unwrap(); |
| 52 |
for d in dirs { |
| 53 |
let next = unsafe { libc::openat(dir, c(d)?.as_ptr(), libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_RDONLY | libc::O_CLOEXEC) }; |
| 54 |
let err = std::io::Error::last_os_error(); |
| 55 |
unsafe { libc::close(dir) }; |
| 56 |
if next < 0 { |
| 57 |
bail!("{inner}: {d} is not a directory in the job's files ({err})"); |
| 58 |
} |
| 59 |
dir = next; |
| 60 |
} |
| 61 |
let fd = unsafe { libc::openat(dir, c(last)?.as_ptr(), libc::O_NOFOLLOW | libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NONBLOCK) }; |
| 62 |
let err = std::io::Error::last_os_error(); |
| 63 |
unsafe { libc::close(dir) }; |
| 64 |
if fd < 0 { |
| 65 |
bail!("{inner} is not a regular file in the job's files ({err})"); |
| 66 |
} |
| 67 |
let file = unsafe { std::fs::File::from_raw_fd(fd) }; |
| 68 |
let meta = file.metadata()?; |
| 69 |
if !meta.is_file() { |
| 70 |
bail!("{inner} is not a regular file in the job's files (it may be missing or a link)"); |
| 71 |
} |
| 72 |
if meta.len() > MAX_ARTIFACT_BYTES { |
| 73 |
bail!("{inner} is {} MiB; artifacts are at most {} MiB", meta.len() >> 20, MAX_ARTIFACT_BYTES >> 20); |
| 74 |
} |
| 75 |
let mut bytes = Vec::with_capacity(meta.len() as usize); |
| 76 |
file.take(MAX_ARTIFACT_BYTES + 1).read_to_end(&mut bytes)?; |
| 77 |
if bytes.len() as u64 > MAX_ARTIFACT_BYTES { |
| 78 |
bail!("{inner} grew past {} MiB while it was read", MAX_ARTIFACT_BYTES >> 20); |
| 79 |
} |
| 80 |
Ok(bytes) |
| 81 |
} |
| 82 |
|
| 83 |
fn debugfs(image: &Path, request: &str) -> Result<String> { |
| 84 |
let out = std::process::Command::new("debugfs").arg("-R").arg(request).arg(image).output().context("run debugfs (e2fsprogs)")?; |
| 85 |
if !out.status.success() { |
| 86 |
bail!("debugfs {request:?}: {}", String::from_utf8_lossy(&out.stderr).trim()); |
| 87 |
} |
| 88 |
Ok(String::from_utf8_lossy(&out.stdout).into_owned()) |
| 89 |
} |
| 90 |
|
| 91 |
|
| 92 |
|
| 93 |
|
| 94 |
pub fn read(image: &Path, inner: &str, scratch: &Path) -> Result<Vec<u8>> { |
| 95 |
let stat = debugfs(image, &format!("stat \"{inner}\""))?; |
| 96 |
if !stat.contains("Type: regular") { |
| 97 |
bail!("{inner} is not a regular file in the job's disk (it may be missing or a link)"); |
| 98 |
} |
| 99 |
let size: u64 = stat |
| 100 |
.split("Size: ") |
| 101 |
.nth(1) |
| 102 |
.and_then(|s| s.split_whitespace().next()) |
| 103 |
.and_then(|s| s.parse().ok()) |
| 104 |
.context("debugfs gave no size")?; |
| 105 |
if size > MAX_ARTIFACT_BYTES { |
| 106 |
bail!("{inner} is {} MiB; artifacts are at most {} MiB", size >> 20, MAX_ARTIFACT_BYTES >> 20); |
| 107 |
} |
| 108 |
let dest = scratch.join("artifact.out"); |
| 109 |
let _ = std::fs::remove_file(&dest); |
| 110 |
debugfs(image, &format!("dump \"{inner}\" \"{}\"", dest.display()))?; |
| 111 |
let bytes = std::fs::read(&dest).with_context(|| format!("{inner}: debugfs wrote nothing"))?; |
| 112 |
let _ = std::fs::remove_file(&dest); |
| 113 |
if bytes.len() as u64 != size { |
| 114 |
bail!("{inner}: read {} bytes of {size}", bytes.len()); |
| 115 |
} |
| 116 |
Ok(bytes) |
| 117 |
} |
| 118 |
|
| 119 |
#[cfg(test)] |
| 120 |
mod tests { |
| 121 |
use super::*; |
| 122 |
use flow_core::Mount; |
| 123 |
|
| 124 |
fn job() -> Job { |
| 125 |
Job { |
| 126 |
name: "build".into(), |
| 127 |
sandbox: "rust".into(), |
| 128 |
scope: vec![], |
| 129 |
depends_on: vec![], |
| 130 |
env: Default::default(), |
| 131 |
exec: vec!["make".into()], |
| 132 |
secrets: vec![], |
| 133 |
identities: vec![], |
| 134 |
mounts: vec![ |
| 135 |
Mount { source: MountSource::Tmpfs, target: "/src/cache".into(), size_mib: 64 }, |
| 136 |
Mount { source: MountSource::Disk, target: "/src/target".into(), size_mib: 1024 }, |
| 137 |
Mount { source: MountSource::Disk, target: "/src/work".into(), size_mib: 1024 }, |
| 138 |
], |
| 139 |
outputs: vec![], |
| 140 |
runs_on: vec![], |
| 141 |
timeout_s: None, |
| 142 |
on: None, |
| 143 |
} |
| 144 |
} |
| 145 |
|
| 146 |
#[test] |
| 147 |
fn paths_are_found_on_the_disk_that_holds_them() { |
| 148 |
let (w, s) = (Path::new("/w.ext4"), Path::new("/scratch")); |
| 149 |
assert_eq!(locate(&job(), w, s, "/src/target/release/oxy"), (s.join("mount-0.ext4"), "/release/oxy".into())); |
| 150 |
assert_eq!(locate(&job(), w, s, "/src/work/a"), (s.join("mount-1.ext4"), "/a".into())); |
| 151 |
assert_eq!(locate(&job(), w, s, "/src/docs/a.1"), (w.to_path_buf(), "/docs/a.1".into())); |
| 152 |
assert_eq!(locate(&job(), w, s, "/src/targets/x"), (w.to_path_buf(), "/targets/x".into()), "a prefix is not a parent"); |
| 153 |
} |
| 154 |
|
| 155 |
#[test] |
| 156 |
fn files_are_read_from_directories_without_following_links() { |
| 157 |
let dir = tempfile::tempdir().unwrap(); |
| 158 |
let tree = dir.path().join("tree"); |
| 159 |
std::fs::create_dir_all(tree.join("release")).unwrap(); |
| 160 |
std::fs::write(tree.join("release/tool"), b"built").unwrap(); |
| 161 |
std::fs::write(dir.path().join("secret"), b"runner key").unwrap(); |
| 162 |
std::os::unix::fs::symlink(dir.path().join("secret"), tree.join("release/link")).unwrap(); |
| 163 |
std::os::unix::fs::symlink(dir.path(), tree.join("up")).unwrap(); |
| 164 |
assert_eq!(read_dir_file(&tree, "/release/tool").unwrap(), b"built"); |
| 165 |
assert!(read_dir_file(&tree, "/release/link").is_err(), "a link to a host file"); |
| 166 |
assert!(read_dir_file(&tree, "/up/secret").is_err(), "a linked directory"); |
| 167 |
assert!(read_dir_file(&tree, "/release/../release/tool").is_err()); |
| 168 |
assert!(read_dir_file(&tree, "/release").is_err(), "a directory"); |
| 169 |
let s = Path::new("/scratch"); |
| 170 |
assert_eq!(locate_dir(&job(), &tree, s, "/src/target/a"), (s.join("mount-0"), "/a".into())); |
| 171 |
} |
| 172 |
|
| 173 |
#[test] |
| 174 |
fn files_are_read_back_from_an_ext4_image() { |
| 175 |
let dir = tempfile::tempdir().unwrap(); |
| 176 |
let tree = dir.path().join("tree"); |
| 177 |
std::fs::create_dir_all(tree.join("release")).unwrap(); |
| 178 |
let body: Vec<u8> = (0..200_000u32).map(|i| (i % 251) as u8).collect(); |
| 179 |
std::fs::write(tree.join("release/tool"), &body).unwrap(); |
| 180 |
std::os::unix::fs::symlink("tool", tree.join("release/link")).unwrap(); |
| 181 |
let image = dir.path().join("disk.ext4"); |
| 182 |
crate::workspace::image(&tree, &image).unwrap(); |
| 183 |
assert_eq!(read(&image, "/release/tool", dir.path()).unwrap(), body); |
| 184 |
assert!(read(&image, "/release/missing", dir.path()).is_err()); |
| 185 |
assert!(read(&image, "/release/link", dir.path()).unwrap_err().to_string().contains("not a regular file")); |
| 186 |
} |
| 187 |
} |