use std::path::{Path, PathBuf};
include!("src/runtime_pins.rs");
include!("src/runtime_file_pins.rs");
include!("src/asset_paths.rs");
include!("src/file_url.rs");
fn main() {
println!("cargo:rerun-if-changed=src/runtime_pins.rs");
println!("cargo:rerun-if-changed=src/runtime_file_pins.rs");
println!("cargo:rerun-if-changed=src/asset_paths.rs");
println!("cargo:rerun-if-changed=src/file_url.rs");
println!("cargo:rerun-if-env-changed=BOXLITE_RUNTIME_URL");
for var in ASSET_ROOT_VARS {
println!("cargo:rerun-if-env-changed={var}");
}
if std::env::var_os("CARGO_FEATURE_EXEC_BOXLITE").is_none() {
return;
}
let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
let Some(archive) = archive_for(&os, &arch) else {
fail(&format!(
"the `exec-boxlite` feature has no pinned sandbox runtime for {os}/{arch}.\n\
Pinned platforms are: {}.\n\
Build without `exec-boxlite` on this host; the ingest and subprocess \
backends are unaffected.",
RUNTIME_ARCHIVES
.iter()
.map(|a| a.target)
.collect::<Vec<_>>()
.join(", ")
));
};
let Some(pinned) = runtime_files_for(archive.target) else {
fail(&format!(
"no extracted-file pins for {target}, though its archive is pinned.\n\
crates/rto-exec/src/runtime_file_pins.rs is out of step with runtime_pins.rs — \
re-derive it:\n\n \
scripts/derive-runtime-file-pins.py\n\n\
Building on with no per-file pins would embed the runtime unverified, which is \
the whole thing this refuses to do.",
target = archive.target
));
};
match std::env::var_os("BOXLITE_RUNTIME_URL") {
Some(url) => verify_the_named_archive(&url.to_string_lossy(), archive),
None => report_the_network_path(
&asset_root().join(RUNTIME_ASSET).join(RUNTIME_FILE),
archive,
),
}
let dir = boxlite_runtime_dir();
match verify_extracted(&dir, pinned) {
Ok(checked) => {
println!(
"cargo:warning=rto-exec: verified {checked} extracted sandbox-runtime file(s) \
against the pins derived from the {target} archive (sha256 {sha})",
target = archive.target,
sha = archive.sha256,
);
println!(
"cargo:rustc-env=ROTEIRO_SANDBOX_RUNTIME_TARGET={}",
archive.target
);
println!(
"cargo:rustc-env=ROTEIRO_SANDBOX_RUNTIME_SHA256={}",
archive.sha256
);
println!("cargo:rustc-env=ROTEIRO_SANDBOX_RUNTIME_VERSION={RUNTIME_VERSION}");
}
Err(why) => fail(&format!(
"the sandbox runtime boxlite produced cannot be verified against the pins.\n\n \
{dir}\n\n\
{why}\n\n\
To take the network out of it entirely, provision the archive and name it — then \
the bytes are checked before boxlite is ever handed them:\n\n \
roteiro security prefetch --analyzer sandbox --allow-download\n \
BOXLITE_RUNTIME_URL=\"{url}\" cargo build --features exec-boxlite",
dir = dir.display(),
url = file_url(&asset_root().join(RUNTIME_ASSET).join(RUNTIME_FILE)),
)),
}
}
fn verify_the_named_archive(url: &str, archive: &PinnedArchive) {
let Some(path) = file_url_path(url) else {
fail(&format!(
"BOXLITE_RUNTIME_URL must be a file:// URL so its bytes can be verified before they \
are built in; got {url:?}.\n\
A remote URL is fetched later by boxlite's build script, which checks nothing — so \
setting one buys nothing over leaving the variable unset, and hides that a \
download happened.\n\n\
Provision it with `roteiro security prefetch --analyzer sandbox --allow-download` \
and point at the local copy, or unset the variable and let the extracted files be \
verified after the fetch."
));
};
match verify(&path, archive) {
Ok(()) => {
println!("cargo:rerun-if-changed={}", path.display());
println!(
"cargo:warning=rto-exec: BOXLITE_RUNTIME_URL names a verified local archive \
({path}); boxlite's fetch stays on disk and no socket is opened",
path = path.display()
);
}
Err(flaw) => fail(&format!(
"the sandbox runtime at {path} is not the pinned artifact: {why}\n\n\
Re-provision it with `roteiro security prefetch --analyzer sandbox \
--allow-download`. If that keeps failing, the published artifact has changed and \
the pin in crates/rto-exec/src/runtime_pins.rs must be re-derived deliberately — \
never widened to make a build pass.",
path = path.display(),
why = flaw.summary(archive),
)),
}
}
fn report_the_network_path(provisioned: &Path, archive: &PinnedArchive) {
println!(
"cargo:warning=rto-exec: BOXLITE_RUNTIME_URL is unset, so boxlite fetched the sandbox \
runtime from {url} — unpinned in transit, verified below against per-file digests \
before anything links",
url = archive.url
);
println!(
"cargo:warning=rto-exec: this embeds third-party executables, GPL-2.0 and \
LGPL-2.0 among them — see crates/rto-exec/NOTICE-boxlite-runtime.md for the \
licence duties that creates"
);
match verify(provisioned, archive) {
Ok(()) => println!(
"cargo:warning=rto-exec: for a build with no network at all, name the verified \
archive you already have: BOXLITE_RUNTIME_URL=\"{url}\"",
url = file_url(provisioned)
),
Err(Flaw::Unreadable(_)) => println!(
"cargo:warning=rto-exec: for a build with no network at all: roteiro security \
prefetch --analyzer sandbox --allow-download, then \
BOXLITE_RUNTIME_URL=\"{url}\"",
url = file_url(provisioned)
),
Err(flaw) => println!(
"cargo:warning=rto-exec: NOTE — the archive at {path} is not the pinned artifact \
({why}). This build did not use it, so this is not fatal; `roteiro security \
prefetch --analyzer sandbox --allow-download` will replace it, and naming it \
with BOXLITE_RUNTIME_URL while it is in this state would stop the build.",
path = provisioned.display(),
why = flaw.summary(archive),
),
}
}
fn boxlite_runtime_dir() -> PathBuf {
println!("cargo:rerun-if-env-changed=DEP_BOXLITE_RUNTIME_DIR");
let Some(dir) = std::env::var_os("DEP_BOXLITE_RUNTIME_DIR") else {
fail(
"cargo did not pass DEP_BOXLITE_RUNTIME_DIR, so there is no way to tell what \
boxlite extracted.\n\
That variable comes from `links = \"boxlite\"` plus a `cargo:runtime_dir=` line in \
boxlite's build script. If either has gone, this check is inert and the runtime \
would be embedded unverified — which is not a thing to shrug at, so the build \
stops. Re-pin boxlite deliberately, or build without `exec-boxlite`.",
);
};
PathBuf::from(dir)
}
fn verify_extracted(dir: &Path, pinned: &PinnedRuntimeFiles) -> Result<usize, String> {
if !dir.is_dir() {
if dir == Path::new("/nonexistent") {
return Err(
"boxlite ran in stub mode (BOXLITE_DEPS_STUB=1) and extracted no runtime, so \
this binary would carry an `exec-boxlite` backend with nothing behind it.\n\
Stub mode exists for `cargo check`-style passes; it cannot produce a working \
sandbox. Unset BOXLITE_DEPS_STUB, or build without `exec-boxlite`."
.to_owned(),
);
}
return Err(format!(
"boxlite reported its runtime directory as {} and there is no directory there. \
Nothing can be verified, and an unverifiable runtime is not a verified one.",
dir.display()
));
}
let entries = std::fs::read_dir(dir).map_err(|e| format!("cannot list it ({e})"))?;
let mut seen: Vec<String> = Vec::new();
let mut problems: Vec<String> = Vec::new();
for entry in entries {
let entry = entry.map_err(|e| format!("cannot read an entry ({e})"))?;
let name = entry.file_name().to_string_lossy().into_owned();
let path = entry.path();
let meta =
std::fs::symlink_metadata(&path).map_err(|e| format!("cannot stat {name} ({e})"))?;
match check_entry(&name, &path, &meta, pinned) {
Entry::Verified => {
println!("cargo:rerun-if-changed={}", path.display());
seen.push(name);
}
Entry::Ignored => {}
Entry::Problem(why) => problems.push(format!(" {why}")),
}
}
for pin in pinned.files {
if !seen.iter().any(|name| name == pin.name)
&& !problems.iter().any(|p| p.contains(pin.name))
{
problems.push(format!(
" {} is missing, and it is pinned for {}",
pin.name, pinned.target
));
}
}
if problems.is_empty() {
return Ok(seen.len());
}
problems.sort();
Err(format!(
"{} of the extracted file(s) did not check out:\n\n{}\n\n\
These are the files `include_bytes!` puts in this binary, so this stops the build \
rather than reporting it. Nothing here is repaired by retrying: either the bytes \
boxlite obtained are not the published artifact, or the published artifact has \
changed and crates/rto-exec/src/runtime_pins.rs must be re-pinned and \
`scripts/derive-runtime-file-pins.py` re-run — deliberately, never widened to make a \
build pass.",
problems.len(),
problems.join("\n")
))
}
enum Entry {
Verified,
Ignored,
Problem(String),
}
fn check_entry(
name: &str,
path: &Path,
meta: &std::fs::Metadata,
pinned: &PinnedRuntimeFiles,
) -> Entry {
if meta.file_type().is_symlink() {
return match std::fs::read_link(path) {
Ok(target) => {
let target = target.to_string_lossy().into_owned();
if pinned.files.iter().any(|f| f.name == target) {
Entry::Ignored
} else {
Entry::Problem(format!(
"{name} is a symlink to {target:?}, which is not one of the pinned files"
))
}
}
Err(e) => Entry::Problem(format!("{name} is a symlink that cannot be read ({e})")),
};
}
if meta.is_dir() {
return Entry::Problem(format!(
"{name} is a directory; the runtime directory is flat and nothing pinned nests"
));
}
if !meta.is_file() {
return Entry::Problem(format!("{name} is neither a regular file nor a symlink"));
}
if name == BOXLITE_FILE_MANIFEST {
return Entry::Ignored;
}
let Some(pin) = pinned.files.iter().find(|f| f.name == name) else {
return Entry::Problem(format!(
"{name} is not pinned, and every regular file here is embedded — so this would be \
built in unverified"
));
};
let bytes = match std::fs::read(path) {
Ok(bytes) => bytes,
Err(e) => return Entry::Problem(format!("{name} cannot be read ({e})")),
};
let actual = bytes.len() as u64;
let digest = sha256_hex(&bytes);
if actual != pin.bytes || digest != pin.sha256 {
return Entry::Problem(format!(
"{name}\n pinned sha256 {} ({} bytes)\n on disk sha256 {digest} \
({actual} bytes)",
pin.sha256, pin.bytes
));
}
if let Some(mode) = setuid_or_setgid(meta) {
return Entry::Problem(format!(
"{name} is set-user-ID/set-group-ID (mode {mode:o}); no pinned runtime file \
carries one"
));
}
Entry::Verified
}
const BOXLITE_FILE_MANIFEST: &str = ".boxlite-runtime-files";
#[cfg(unix)]
fn setuid_or_setgid(meta: &std::fs::Metadata) -> Option<u32> {
use std::os::unix::fs::MetadataExt as _;
let mode = meta.mode();
(mode & 0o6000 != 0).then_some(mode)
}
#[cfg(not(unix))]
fn setuid_or_setgid(_meta: &std::fs::Metadata) -> Option<u32> {
None
}
enum Flaw {
Unreadable(String),
Mismatch { sha256: String, bytes: u64 },
}
impl Flaw {
fn summary(&self, archive: &PinnedArchive) -> String {
match self {
Self::Unreadable(err) => format!("cannot read it ({err})"),
Self::Mismatch { sha256, bytes } => format!(
"expected sha256 {} ({} bytes), found sha256 {sha256} ({bytes} bytes)",
archive.sha256, archive.bytes
),
}
}
}
fn verify(path: &Path, archive: &PinnedArchive) -> Result<(), Flaw> {
let bytes = std::fs::read(path).map_err(|e| Flaw::Unreadable(e.to_string()))?;
let actual_len = bytes.len() as u64;
let digest = sha256_hex(&bytes);
if actual_len != archive.bytes || digest != archive.sha256 {
return Err(Flaw::Mismatch {
sha256: digest,
bytes: actual_len,
});
}
Ok(())
}
fn sha256_hex(bytes: &[u8]) -> String {
use sha2::{Digest, Sha256};
let out = Sha256::digest(bytes);
let mut hex = String::with_capacity(64);
for byte in out {
use std::fmt::Write as _;
let _ = write!(hex, "{byte:02x}");
}
hex
}
fn fail(message: &str) -> ! {
eprintln!("\nerror: rto-exec/build.rs\n\n{message}\n");
panic!(
"{}",
message
.lines()
.next()
.unwrap_or("sandbox runtime is not verified")
);
}