use std::io::Write as _;
use std::path::Path;
const MAX_JOB: u64 = 4 << 20;
fn fail(code: i32, why: &str) -> ! {
eprintln!("holger-scan-worker: {why}");
std::process::exit(code)
}
fn inside(fill: usize) -> serde_json::Value {
use std::time::Duration;
let connect = |addr: &str| -> String {
match addr.parse::<std::net::SocketAddr>() {
Ok(a) => match std::net::TcpStream::connect_timeout(&a, Duration::from_secs(2)) {
Ok(_) => "connected".into(),
Err(e) => format!("refused: {e}"),
},
Err(e) => format!("bad address: {e}"),
}
};
unsafe {
libc::signal(libc::SIGXFSZ, libc::SIG_IGN);
}
let write = |p: &str, n: usize| -> String {
let piece = vec![0u8; n.min(1 << 20)];
let r = std::fs::File::create(p).and_then(|mut f| {
let mut left = n;
while left > 0 {
let k = left.min(piece.len());
f.write_all(&piece[..k])?;
left -= k;
}
Ok(())
});
match r {
Ok(()) => "written".into(),
Err(e) => format!("refused: {e}"),
}
};
let scratch = std::env::var("TMPDIR")
.ok()
.filter(|t| !t.is_empty())
.unwrap_or_else(|| "/scratch".into());
let write_scratch = write(&format!("{scratch}/holger-probe"), 1024);
let write_past_scratch_cap = write(&format!("{scratch}/holger-fill"), fill);
let _ = std::fs::remove_file(format!("{scratch}/holger-fill"));
let status = std::fs::read_to_string("/proc/self/status").unwrap_or_default();
let field = |k: &str| {
status
.lines()
.find_map(|l| l.strip_prefix(k))
.map(|v| v.trim().to_string())
.unwrap_or_default()
};
let v = serde_json::json!({
"connect_public": connect("1.1.1.1:443"),
"connect_gateway": connect("10.0.2.2:80"),
"resolve": format!("{:?}", std::net::ToSocketAddrs::to_socket_addrs(&("pypi.org", 443)).map(|a| a.count())),
"write_root": write("/holger-probe", 1),
"write_etc": write("/etc/holger-probe", 1),
"write_in": write("/in/holger-probe", 1),
"write_traversal": write("/scratch/../etc/holger-probe", 1),
"write_scratch": write_scratch,
"write_past_scratch_cap": write_past_scratch_cap,
"uid": field("Uid:"),
"cap_eff": field("CapEff:"),
"cap_bnd": field("CapBnd:"),
"no_new_privs": field("NoNewPrivs:"),
"seccomp": field("Seccomp:"),
});
v
}
fn probe(args: &[String]) -> ! {
use std::time::Duration;
let num = |f: &str| {
args.iter()
.position(|a| a == f)
.and_then(|i| args.get(i + 1))
.and_then(|s| s.parse::<u64>().ok())
};
if let Some(s) = num("--sleep") {
std::thread::sleep(Duration::from_secs(s));
}
if let Some(mib) = num("--alloc") {
let mut held: Vec<Vec<u8>> = Vec::new();
for _ in 0..mib {
held.push(vec![1u8; 1 << 20]);
}
eprintln!("allocated {} MiB", held.len());
}
if let Some(mib) = num("--spam") {
let line = vec![b'x'; 1 << 20];
let mut so = std::io::stdout().lock();
for _ in 0..mib {
let _ = so.write_all(&line);
}
std::process::exit(0)
}
println!("{}", inside(1 << 30));
std::process::exit(0)
}
fn probe_job(
w: &server_lib::job_runner::holger_job_runner::wire::WireJob,
dirs: &server_lib::job_runner::holger_job_runner::wire::Dirs,
) -> server_lib::job_runner::holger_job_runner::wire::Answer {
use server_lib::job_runner::holger_job_runner::wire::Answer;
let fill = w.params["fill_mib"]
.as_u64()
.map_or(1 << 30, |m| (m.min(4096) << 20) as usize);
let v = inside(fill);
match std::fs::write(
dirs.out.join(server_lib::sandbox::PROBE_FILE),
v.to_string(),
) {
Ok(()) => Answer::accepted(vec![server_lib::sandbox::PROBE_FILE.into()]),
Err(e) => Answer::refused(
server_lib::errcode::SANDBOX_REPORT_INVALID.code,
server_lib::errcode::SANDBOX_REPORT_INVALID.tag(format!("the probe's answer: {e}")),
),
}
}
fn serve(args: &[String]) -> ! {
let me = std::env::current_exe()
.unwrap_or_else(|e| fail(2, &format!("this program's own path: {e}")));
let mut cfg = holger_sandbox::remote::ServeConfig::defaults(me);
let mut it = args.iter();
while let Some(flag) = it.next() {
let mut val = || {
it.next()
.cloned()
.unwrap_or_else(|| fail(2, &format!("{flag} needs a value")))
};
let num = |v: String| {
v.parse::<u64>()
.unwrap_or_else(|_| fail(2, &format!("{flag} {v:?} is not a number")))
};
match flag.as_str() {
"--socket" => cfg.socket = val().into(),
"--share" => cfg.share = val().into(),
"--scratch" => cfg.scratch = val().into(),
"--max-jobs" => cfg.max_jobs = num(val()).max(1) as usize,
"--memory-mib" => cfg.ceiling.memory_mib = num(val()),
"--timeout-secs" => cfg.ceiling.timeout_secs = num(val()),
"--pod-pids-limit" => cfg.pod_pids_limit = Some(num(val())).filter(|n| *n > 0),
other => fail(2, &format!("serve: unknown flag {other:?}")),
}
}
let listener = holger_sandbox::remote::bind(&cfg)
.unwrap_or_else(|e| fail(2, &format!("binding {}: {e}", cfg.socket.display())));
let socket = cfg.socket.clone();
if let Err(e) = ctrlc::set_handler(move || {
let _ = std::fs::remove_file(&socket);
std::process::exit(0)
}) {
fail(2, &format!("the signal handler: {e}"));
}
eprintln!(
"holger-scan-worker: the scan sidecar listens on {} over {} ({} job slots, at most {} MiB and {} s a job)",
cfg.socket.display(),
cfg.share.display(),
cfg.max_jobs,
cfg.ceiling.memory_mib,
cfg.ceiling.timeout_secs
);
match holger_sandbox::remote::serve(listener, cfg) {
Ok(()) => std::process::exit(0),
Err(e) => fail(2, &format!("the socket: {e}")),
}
}
fn rebase(job: &mut server_lib::sandbox::wire::Job, dir: &Path) {
if dir == Path::new("/in") {
return;
}
if let Some(r) = job.inspect.yara.as_mut().and_then(|y| y.rules.as_mut()) {
if let Some(rest) = r.strip_prefix("/in/") {
*r = dir.join(rest).display().to_string();
}
}
}
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.get(1).map(String::as_str) == Some("probe") {
probe(&args[2..]);
}
if args.get(1).map(String::as_str) == Some("serve") {
serve(&args[2..]);
}
let dir = args.get(1).cloned().unwrap_or_else(|| "/in".to_string());
let dir = Path::new(&dir);
let job_path = dir.join("job.json");
let meta = std::fs::metadata(&job_path)
.unwrap_or_else(|e| fail(2, &format!("{}: {e}", job_path.display())));
if meta.len() > MAX_JOB {
fail(2, "the job is larger than its cap");
}
let raw = std::fs::read(&job_path).unwrap_or_else(|e| fail(2, &format!("the job: {e}")));
if let Ok(w) =
serde_json::from_slice::<server_lib::job_runner::holger_job_runner::wire::WireJob>(&raw)
{
let dirs = server_lib::job_runner::holger_job_runner::wire::Dirs::under(dir, w.reads);
let answer = if w.program() == server_lib::sandbox::PROBE_PROGRAM {
probe_job(&w, &dirs)
} else {
server_lib::job_runner::programs::run(&w, &dirs)
};
let out = server_lib::job_runner::holger_job_runner::wire::deliver(&w, &dirs.out, &answer)
.unwrap_or_else(|e| fail(2, &format!("the answer: {e}")));
if let Some(out) = out {
let mut so = std::io::stdout().lock();
if so.write_all(&out).and_then(|_| so.flush()).is_err() {
fail(2, "stdout closed");
}
}
std::process::exit(0);
}
let mut job: server_lib::sandbox::wire::Job =
serde_json::from_slice(&raw).unwrap_or_else(|e| fail(2, &format!("the job: {e}")));
if job.schema != server_lib::sandbox::wire::JOB_SCHEMA {
fail(2, &format!("job schema {:?}", job.schema));
}
rebase(&mut job, dir);
let f = std::fs::File::open(dir.join("artifact"))
.unwrap_or_else(|e| fail(2, &format!("the artifact: {e}")));
let len = f.metadata().map(|m| m.len()).unwrap_or(0);
let map;
let bytes: &[u8] = if len == 0 {
&[]
} else {
map = unsafe { memmap2::Mmap::map(&f) }
.unwrap_or_else(|e| fail(2, &format!("mapping the artifact: {e}")));
&map[..]
};
let report = server_lib::sandbox::worker::run(&job, bytes, &dir.join("m"));
if report.sha256 != job.sha256 {
fail(3, "the artifact is not the bytes the job names");
}
let out = serde_json::to_vec(&report).unwrap_or_else(|e| fail(2, &format!("the report: {e}")));
let mut so = std::io::stdout().lock();
if so.write_all(&out).and_then(|_| so.flush()).is_err() {
fail(2, "stdout closed");
}
}
#[cfg(test)]
mod tests {
use super::*;
fn job_with_rules(rules: &str) -> server_lib::sandbox::wire::Job {
let mut v = serde_json::json!({
"schema": server_lib::sandbox::wire::JOB_SCHEMA,
"sha256": "00", "file_name": "f", "ecosystem": "raw", "format_name": "raw",
"inspect": {}, "kinds": [],
});
let mut job: server_lib::sandbox::wire::Job = serde_json::from_value(v.take()).unwrap();
job.inspect.yara = Some(Default::default());
job.inspect.yara.as_mut().unwrap().rules = Some(rules.into());
job
}
#[test]
fn a_sidecar_job_reads_its_rules_from_its_own_directory() {
let mut j = job_with_rules("/in/d-yara");
rebase(&mut j, Path::new("/run/holger-scan/holger-scan-abc"));
assert_eq!(
j.inspect.yara.unwrap().rules.as_deref(),
Some("/run/holger-scan/holger-scan-abc/d-yara")
);
let mut k = job_with_rules("/in/d-yara");
rebase(&mut k, Path::new("/in"));
assert_eq!(
k.inspect.yara.unwrap().rules.as_deref(),
Some("/in/d-yara"),
"in a container the paths stay"
);
let mut o = job_with_rules("/etc/rules");
rebase(&mut o, Path::new("/x"));
assert_eq!(
o.inspect.yara.unwrap().rules.as_deref(),
Some("/etc/rules"),
"a path outside /in is not touched"
);
}
}