255 lines
9.3 KiB
Rust
255 lines
9.3 KiB
Rust
//! Offline native command trees, including their explicitly supplied libraries.
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::fs::{self, File};
|
|
use std::io::Read;
|
|
use std::path::Path;
|
|
|
|
use dever_core::hir::ExternalWorkerContract;
|
|
use dever_core::native::EmbeddedResource;
|
|
use goblin::elf::{dynamic, header};
|
|
use serde_json::json;
|
|
|
|
use super::{MAX_TREE_BYTES, MAX_TREE_FILES, Tree, digest, elf, resource, runtime, valid_path};
|
|
use crate::toolchain::BuildTarget;
|
|
|
|
pub(super) fn prepare(
|
|
root: &Path,
|
|
contract: &ExternalWorkerContract,
|
|
package_files: &BTreeMap<String, Vec<u8>>,
|
|
supplied: &BTreeMap<&str, &EmbeddedResource>,
|
|
target: BuildTarget,
|
|
) -> Result<Vec<EmbeddedResource>, String> {
|
|
let worker = contract.ecosystem == "exec";
|
|
let tree_kind = if worker { "workers" } else { "commands" };
|
|
let binary_name = if worker { "worker" } else { "tool" };
|
|
let prefix = format!(
|
|
"{tree_kind}/{}",
|
|
digest(&serde_json::to_vec(&contract.sdk_manifest()).map_err(|error| error.to_string())?)
|
|
);
|
|
let mut tree = Tree::new(prefix.clone());
|
|
let entry = if worker {
|
|
let entry = supplied
|
|
.get(contract.entry.as_str())
|
|
.ok_or("exec Worker is missing from verified resources")?;
|
|
if !entry.executable || entry.bytes.is_empty() {
|
|
return Err("exec Worker entry must be a nonempty executable resource".into());
|
|
}
|
|
Some(entry.bytes.as_slice())
|
|
} else {
|
|
None
|
|
};
|
|
let source = command_files(root, &contract.entry, package_files, entry)?;
|
|
let binary = &source["program/tool"];
|
|
runtime::validate_binary(&contract.entry, binary, target.platform())?;
|
|
let machine = match target {
|
|
BuildTarget::LinuxX86_64 => header::EM_X86_64,
|
|
BuildTarget::LinuxAarch64 => header::EM_AARCH64,
|
|
};
|
|
let executable = elf::object("program/tool", binary, machine)?;
|
|
let interpreter = executable.interpreter.map(str::to_owned);
|
|
if interpreter.is_none() && !executable.libraries.is_empty() {
|
|
return Err("command executable has dynamic dependencies without an interpreter".into());
|
|
}
|
|
let mut files = source
|
|
.iter()
|
|
.map(|(path, bytes)| (path.clone(), bytes.as_slice()))
|
|
.collect::<BTreeMap<_, _>>();
|
|
files.extend(
|
|
supplied
|
|
.iter()
|
|
.filter(|(path, _)| path.starts_with("process-runtime/lib/"))
|
|
.map(|(path, entry)| ((*path).to_owned(), entry.bytes.as_slice())),
|
|
);
|
|
let closure = elf::Closure {
|
|
files: &files,
|
|
machine,
|
|
directories: vec!["program/lib".into(), "process-runtime/lib".into()],
|
|
};
|
|
for (path, bytes) in &source {
|
|
let object = elf::object(path, bytes, machine)?;
|
|
reject_loader_hooks(&object)?;
|
|
for fields in [&object.rpaths, &object.runpaths] {
|
|
for directory in elf::origin_directories(path, fields)? {
|
|
if directory != "program" && !directory.starts_with("program/") {
|
|
return Err("command library search path escapes its packaged tree".into());
|
|
}
|
|
}
|
|
}
|
|
if let Some(name) = path.strip_prefix("program/lib/") {
|
|
if supplied.contains_key(format!("process-runtime/lib/{name}").as_str()) {
|
|
return Err(format!(
|
|
"command library '{name}' overrides a signed process_runtime library"
|
|
));
|
|
}
|
|
if object.header.e_type != header::ET_DYN
|
|
|| object.interpreter.is_some()
|
|
|| object.soname.is_some_and(|soname| soname != name)
|
|
{
|
|
return Err(format!(
|
|
"command library '{name}' is not a matching SONAME shared object"
|
|
));
|
|
}
|
|
}
|
|
closure.follow(path, Vec::new(), &BTreeMap::new())?;
|
|
}
|
|
for (path, bytes) in source {
|
|
let name = path.strip_prefix("program/").expect("owned command tree");
|
|
tree.add(
|
|
if name == "tool" { binary_name } else { name },
|
|
bytes,
|
|
name == "tool",
|
|
)?;
|
|
}
|
|
if worker {
|
|
tree.add(
|
|
"contract.json",
|
|
serde_json::to_vec(&contract.sdk_manifest()).map_err(|error| error.to_string())?,
|
|
false,
|
|
)?;
|
|
}
|
|
if let Some(interpreter) = interpreter {
|
|
let name = interpreter
|
|
.rsplit('/')
|
|
.next()
|
|
.expect("validated GNU loader");
|
|
let loader = supplied
|
|
.get(format!("process-runtime/lib/{name}").as_str())
|
|
.ok_or("command requires the signed target GNU loader")?;
|
|
runtime::validate_binary(name, &loader.bytes, target.platform())?;
|
|
}
|
|
let suffix = if worker { "worker" } else { "command" };
|
|
let manifest = json!({
|
|
"format": format!("dever-{suffix}-launch-v1"), "ecosystem": contract.ecosystem,
|
|
"entry": contract.entry, "executable": format!("{prefix}/{binary_name}"),
|
|
"arguments": [], "working_directory": prefix,
|
|
});
|
|
let mut output = tree.finish();
|
|
output.push(resource(
|
|
format!("{}.dever-{suffix}.json", contract.entry),
|
|
serde_json::to_vec(&manifest).map_err(|error| error.to_string())?,
|
|
false,
|
|
));
|
|
Ok(output)
|
|
}
|
|
|
|
fn reject_loader_hooks(elf: &goblin::elf::Elf<'_>) -> Result<(), String> {
|
|
if elf.dynamic.as_ref().is_some_and(|table| {
|
|
table.dyns.iter().any(|entry| {
|
|
matches!(
|
|
entry.d_tag,
|
|
dynamic::DT_CONFIG
|
|
| dynamic::DT_AUDIT
|
|
| dynamic::DT_DEPAUDIT
|
|
| 0x7fff_fffd
|
|
| 0x7fff_ffff
|
|
)
|
|
})
|
|
}) {
|
|
return Err("command ELF contains unsupported dynamic loader hooks".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn command_files(
|
|
root: &Path,
|
|
entry: &str,
|
|
package_files: &BTreeMap<String, Vec<u8>>,
|
|
verified: Option<&[u8]>,
|
|
) -> Result<BTreeMap<String, Vec<u8>>, String> {
|
|
valid_path(entry)?;
|
|
let parent = entry
|
|
.rsplit_once('/')
|
|
.map(|(parent, _)| parent)
|
|
.ok_or("command entry must belong to a domain")?;
|
|
let library_prefix = format!("{parent}/lib/");
|
|
let packaged = crate::packages::owns_worker(root, entry)?;
|
|
let binary = if let Some(bytes) = verified {
|
|
bytes.to_vec()
|
|
} else if packaged {
|
|
package_files
|
|
.get(entry)
|
|
.cloned()
|
|
.ok_or("Package command executable is missing")?
|
|
} else {
|
|
read_file(root, entry)?
|
|
};
|
|
let mut files = BTreeMap::from([("program/tool".to_owned(), binary)]);
|
|
let mut bytes = files["program/tool"].len();
|
|
if bytes == 0 || bytes > MAX_TREE_BYTES {
|
|
return Err("command executable is empty or exceeds the byte budget".into());
|
|
}
|
|
let names = if packaged {
|
|
package_files
|
|
.keys()
|
|
.filter_map(|path| path.strip_prefix(&library_prefix))
|
|
.map(str::to_owned)
|
|
.collect::<Vec<_>>()
|
|
} else {
|
|
library_names(&root.join(&library_prefix))?
|
|
};
|
|
for name in names {
|
|
valid_path(&name)?;
|
|
if name.contains('/') || files.len() >= MAX_TREE_FILES {
|
|
return Err("command lib directory must contain bounded regular library files".into());
|
|
}
|
|
let path = format!("{library_prefix}{name}");
|
|
let library = if packaged {
|
|
package_files[&path].clone()
|
|
} else {
|
|
read_file(root, &path)?
|
|
};
|
|
bytes = bytes
|
|
.checked_add(library.len())
|
|
.ok_or("command library size overflow")?;
|
|
if bytes > MAX_TREE_BYTES {
|
|
return Err("command libraries exceed the byte budget".into());
|
|
}
|
|
files.insert(format!("program/lib/{name}"), library);
|
|
}
|
|
Ok(files)
|
|
}
|
|
|
|
fn library_names(directory: &Path) -> Result<Vec<String>, String> {
|
|
let metadata = match fs::symlink_metadata(directory) {
|
|
Ok(metadata) => metadata,
|
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
|
|
Err(error) => return Err(format!("cannot inspect command lib directory: {error}")),
|
|
};
|
|
if !metadata.is_dir() || metadata.file_type().is_symlink() {
|
|
return Err("command lib directory must be a real directory".into());
|
|
}
|
|
let mut names = Vec::new();
|
|
for entry in fs::read_dir(directory).map_err(|error| error.to_string())? {
|
|
let entry = entry.map_err(|error| error.to_string())?;
|
|
if names.len() >= MAX_TREE_FILES
|
|
|| !entry
|
|
.file_type()
|
|
.map_err(|error| error.to_string())?
|
|
.is_file()
|
|
{
|
|
return Err("command lib directory must contain bounded regular library files".into());
|
|
}
|
|
names.push(
|
|
entry
|
|
.file_name()
|
|
.into_string()
|
|
.map_err(|_| "command library name is not UTF-8")?,
|
|
);
|
|
}
|
|
names.sort();
|
|
Ok(names)
|
|
}
|
|
|
|
fn read_file(root: &Path, entry: &str) -> Result<Vec<u8>, String> {
|
|
let path = crate::libs::owned_worker(root, entry)?;
|
|
let mut bytes = Vec::new();
|
|
File::open(path)
|
|
.and_then(|file| file.take(MAX_TREE_BYTES as u64 + 1).read_to_end(&mut bytes))
|
|
.map_err(|error| format!("cannot read command resource '{entry}': {error}"))?;
|
|
if bytes.len() > MAX_TREE_BYTES {
|
|
return Err("command resource exceeds the byte budget".into());
|
|
}
|
|
Ok(bytes)
|
|
}
|