feat: publish Dever core with project scaffolding and versioned AI skills

This commit is contained in:
Antigravity committed 2026-10-04 13:37:30 +00:00
1 parent 70fa73cdc2
commit ac458ffa7f
960 files changed
+221319 -1163

No files matched your search

+182
View File
@@ -0,0 +1,182 @@
//! Validate the bootstrap dependency closure before executing any asset.
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::io::Read;
use std::path::Path;
use goblin::elf::{Elf, dynamic, header, program_header};
use sha2::{Digest, Sha256};
const MAX_ASSET_BYTES: usize = 32 * 1024 * 1024;
const MAX_ASSET_FILES: usize = 128;
/// The first executable must be independent of every host loader and library.
pub fn validate_bwrap(bytes: &[u8]) -> Result<String, String> {
validate_bwrap_for_target(bytes, host_target())
}
fn host_target() -> &'static str {
if cfg!(target_arch = "aarch64") {
"linux-aarch64"
} else {
"linux-x86_64"
}
}
fn target_abi(target: &str) -> Result<(u16, &'static str, &'static str), String> {
match target {
"linux-x86_64" => Ok((
header::EM_X86_64,
"ld-linux-x86-64.so.2",
"/lib64/ld-linux-x86-64.so.2",
)),
"linux-aarch64" => Ok((
header::EM_AARCH64,
"ld-linux-aarch64.so.1",
"/lib/ld-linux-aarch64.so.1",
)),
_ => Err(format!("unsupported sandbox target '{target}'")),
}
}
pub fn validate_bwrap_for_target(bytes: &[u8], target: &str) -> Result<String, String> {
let elf = Elf::parse(bytes).map_err(|error| format!("invalid sandbox bwrap ELF: {error}"))?;
let (machine, _, _) = target_abi(target)?;
if bytes.len() > MAX_ASSET_BYTES
|| !elf.is_64
|| !elf.little_endian
|| elf.header.e_machine != machine
|| !matches!(elf.header.e_type, header::ET_DYN | header::ET_EXEC)
|| elf
.program_headers
.iter()
.any(|segment| segment.p_type == program_header::PT_INTERP)
|| elf.dynamic.as_ref().is_some_and(|table| {
table
.dyns
.iter()
.any(|entry| entry.d_tag == dynamic::DT_NEEDED)
})
|| !elf.rpaths.is_empty()
|| !elf.runpaths.is_empty()
{
return Err("sandbox bwrap must be a static target ELF without PT_INTERP, DT_NEEDED or library search paths".into());
}
Ok(format!("{:x}", Sha256::digest(bytes)))
}
/// Used both before signing a pack and when launching its verified extraction.
/// Content authentication belongs to the release/bundle owner; this validates
/// that the loader can resolve every dependency without a host-library search.
pub fn validate_assets(files: &[(String, Vec<u8>)]) -> Result<(), String> {
validate_assets_for_target(files, host_target())
}
pub fn validate_assets_for_target(files: &[(String, Vec<u8>)], target: &str) -> Result<(), String> {
let (machine, loader_name, interpreter) = target_abi(target)?;
let total = files
.iter()
.try_fold(0usize, |total, (_, bytes)| total.checked_add(bytes.len()))
.ok_or("sandbox asset size overflow")?;
if files.len() > MAX_ASSET_FILES || total > MAX_ASSET_BYTES {
return Err("sandbox assets exceed their file or byte budget".into());
}
let mut supplied = BTreeMap::new();
for (name, bytes) in files {
let valid = matches!(name.as_str(), "bin/bwrap" | "bin/guard")
|| name.strip_prefix("lib/").is_some_and(|name| {
!name.is_empty() && name != "." && name != ".." && !name.contains(['/', '\\', '\0'])
});
if !valid || supplied.insert(name.as_str(), bytes.as_slice()).is_some() {
return Err("sandbox asset has an invalid or repeated path".into());
}
}
let loader = format!("lib/{loader_name}");
for required in ["bin/bwrap", "bin/guard", &loader] {
if !supplied.contains_key(required) {
return Err(format!("sandbox asset '{required}' is missing"));
}
}
validate_bwrap_for_target(supplied["bin/bwrap"], target)?;
let mut reached = BTreeSet::new();
// The shared OS ABI set also serves packaged language runtimes. Validate
// every supplied library even when the two bootstrap helpers do not use it.
let mut remaining = supplied
.keys()
.map(|name| (*name).to_owned())
.collect::<Vec<_>>();
while let Some(name) = remaining.pop() {
if !reached.insert(name.clone()) {
continue;
}
let bytes = supplied
.get(name.as_str())
.ok_or_else(|| format!("sandbox dependency '{name}' is missing"))?;
let elf =
Elf::parse(bytes).map_err(|error| format!("invalid sandbox ELF asset: {error}"))?;
if !elf.is_64
|| !elf.little_endian
|| elf.header.e_machine != machine
|| !matches!(elf.header.e_type, header::ET_DYN | header::ET_EXEC)
{
return Err("sandbox ELF asset has an incompatible target or object type".into());
}
// Reject paths that can outrank the explicit bootstrap library path.
if !elf.rpaths.is_empty() || !elf.runpaths.is_empty() {
return Err("sandbox bootstrap assets must not declare RPATH or RUNPATH".into());
}
if let Some(actual) = elf.interpreter
&& actual != interpreter
{
return Err("sandbox ELF interpreter differs from the packaged loader".into());
}
for library in elf.libraries {
if library.is_empty() || library.contains(['/', '\\']) || matches!(library, "." | "..")
{
return Err("sandbox dependency is not a plain library name".into());
}
remaining.push(format!("lib/{library}"));
}
}
Ok(())
}
pub(super) fn verify_directory(root: &Path) -> Result<String, String> {
let mut files = Vec::new();
let mut remaining = MAX_ASSET_BYTES;
for directory in ["bin", "lib"] {
let directory_path = super::real_path(&root.join(directory))?;
for entry in fs::read_dir(directory_path).map_err(|error| error.to_string())? {
let entry = entry.map_err(|error| error.to_string())?;
let path = super::real_path(&entry.path())?;
if !entry
.file_type()
.map_err(|error| error.to_string())?
.is_file()
|| files.len() >= MAX_ASSET_FILES
{
return Err("sandbox assets contain a non-regular file or too many files".into());
}
let mut bytes = Vec::new();
fs::File::open(path)
.and_then(|file| file.take(remaining as u64 + 1).read_to_end(&mut bytes))
.map_err(|error| format!("cannot read sandbox asset: {error}"))?;
remaining = remaining
.checked_sub(bytes.len())
.ok_or("sandbox assets exceed their byte budget")?;
let name = entry
.file_name()
.into_string()
.map_err(|_| "sandbox asset name is not UTF-8")?;
files.push((format!("{directory}/{name}"), bytes));
}
}
validate_assets(&files)?;
let bytes = &files
.iter()
.find(|(name, _)| name == "bin/bwrap")
.ok_or("sandbox bwrap is missing")?
.1;
Ok(format!("{:x}", Sha256::digest(bytes)))
}
+322
View File
@@ -0,0 +1,322 @@
//! Fixed allowlist for packaged Linux runtimes. Mounts own file visibility;
//! this filter owns syscall categories that could bypass that visibility.
use super::Capabilities;
use seccompiler::{
BpfProgram, SeccompAction, SeccompCmpArgLen, SeccompCmpOp, SeccompCondition, SeccompFilter,
SeccompRule,
};
use std::collections::BTreeMap;
fn condition(argument: u8, op: SeccompCmpOp, value: u64) -> Result<SeccompCondition, String> {
SeccompCondition::new(argument, SeccompCmpArgLen::Qword, op, value)
.map_err(|error| error.to_string())
}
fn rule(conditions: Vec<SeccompCondition>) -> Result<SeccompRule, String> {
SeccompRule::new(conditions).map_err(|error| error.to_string())
}
fn install(
rules: BTreeMap<i64, Vec<SeccompRule>>,
default: SeccompAction,
matched: SeccompAction,
) -> Result<(), String> {
let architecture = std::env::consts::ARCH
.try_into()
.map_err(|error: seccompiler::BackendError| error.to_string())?;
let filter = SeccompFilter::new(rules, default, matched, architecture)
.map_err(|error| error.to_string())?;
let program: BpfProgram = filter
.try_into()
.map_err(|error: seccompiler::BackendError| error.to_string())?;
seccompiler::apply_filter(&program)
.map_err(|error| format!("cannot enforce Worker syscall policy: {error}"))
}
pub(super) fn apply(capabilities: Capabilities) -> Result<(), String> {
// clone3 carries flags behind a pointer. ENOSYS makes supported libc use
// clone, whose scalar flags can be checked without user-memory races.
install(
BTreeMap::from([(libc::SYS_clone3, vec![])]),
SeccompAction::Allow,
SeccompAction::Errno(libc::ENOSYS as u32),
)?;
let mut rules = BTreeMap::new();
for syscall in [
libc::SYS_read,
libc::SYS_write,
libc::SYS_readv,
libc::SYS_writev,
libc::SYS_pread64,
libc::SYS_pwrite64,
libc::SYS_preadv,
libc::SYS_pwritev,
libc::SYS_close,
libc::SYS_close_range,
libc::SYS_lseek,
libc::SYS_fcntl,
libc::SYS_dup,
libc::SYS_dup3,
libc::SYS_pipe2,
libc::SYS_openat,
libc::SYS_fstat,
libc::SYS_statx,
libc::SYS_faccessat,
libc::SYS_faccessat2,
libc::SYS_readlinkat,
// copy2/copytree inspect extended metadata on their visible source.
// These calls only read mounted files; writes remain independently
// governed by the read-only inputs and explicit writable grants.
libc::SYS_listxattr,
libc::SYS_llistxattr,
libc::SYS_flistxattr,
libc::SYS_getxattr,
libc::SYS_lgetxattr,
libc::SYS_fgetxattr,
libc::SYS_getdents64,
libc::SYS_getcwd,
libc::SYS_chdir,
libc::SYS_fchdir,
libc::SYS_mkdirat,
libc::SYS_unlinkat,
libc::SYS_renameat,
libc::SYS_renameat2,
libc::SYS_linkat,
libc::SYS_symlinkat,
libc::SYS_ftruncate,
libc::SYS_truncate,
libc::SYS_fchmod,
libc::SYS_fchmodat,
libc::SYS_fchown,
libc::SYS_fchownat,
libc::SYS_flock,
libc::SYS_fsync,
libc::SYS_fdatasync,
libc::SYS_sync_file_range,
libc::SYS_fallocate,
libc::SYS_fstatfs,
libc::SYS_statfs,
libc::SYS_umask,
libc::SYS_utimensat,
libc::SYS_sendfile,
libc::SYS_copy_file_range,
libc::SYS_mmap,
libc::SYS_mprotect,
libc::SYS_munmap,
libc::SYS_mremap,
libc::SYS_madvise,
libc::SYS_brk,
libc::SYS_msync,
libc::SYS_rt_sigaction,
libc::SYS_rt_sigprocmask,
libc::SYS_rt_sigreturn,
libc::SYS_rt_sigsuspend,
libc::SYS_rt_sigtimedwait,
libc::SYS_sigaltstack,
libc::SYS_tgkill,
libc::SYS_kill,
libc::SYS_getpid,
libc::SYS_getppid,
libc::SYS_gettid,
libc::SYS_getuid,
libc::SYS_geteuid,
libc::SYS_getgid,
libc::SYS_getegid,
libc::SYS_getgroups,
libc::SYS_getpgid,
libc::SYS_getsid,
libc::SYS_setpgid,
libc::SYS_setsid,
libc::SYS_set_tid_address,
libc::SYS_set_robust_list,
libc::SYS_get_robust_list,
libc::SYS_futex,
libc::SYS_rseq,
libc::SYS_sched_yield,
libc::SYS_sched_getaffinity,
libc::SYS_sched_getparam,
libc::SYS_sched_getscheduler,
libc::SYS_clock_gettime,
libc::SYS_clock_getres,
libc::SYS_clock_nanosleep,
libc::SYS_nanosleep,
libc::SYS_gettimeofday,
libc::SYS_times,
libc::SYS_getrusage,
libc::SYS_getrlimit,
libc::SYS_prlimit64,
libc::SYS_uname,
libc::SYS_sysinfo,
libc::SYS_getrandom,
libc::SYS_epoll_create1,
libc::SYS_epoll_ctl,
libc::SYS_epoll_pwait,
libc::SYS_epoll_pwait2,
libc::SYS_ppoll,
libc::SYS_pselect6,
libc::SYS_eventfd2,
libc::SYS_timerfd_create,
libc::SYS_timerfd_settime,
libc::SYS_timerfd_gettime,
libc::SYS_inotify_init1,
libc::SYS_inotify_add_watch,
libc::SYS_inotify_rm_watch,
libc::SYS_recvfrom,
libc::SYS_sendto,
libc::SYS_recvmsg,
libc::SYS_sendmsg,
libc::SYS_recvmmsg,
libc::SYS_sendmmsg,
libc::SYS_shutdown,
libc::SYS_getsockopt,
libc::SYS_setsockopt,
libc::SYS_getsockname,
libc::SYS_getpeername,
libc::SYS_execve,
libc::SYS_exit,
libc::SYS_exit_group,
libc::SYS_wait4,
libc::SYS_waitid,
libc::SYS_clone3,
] {
rules.insert(syscall, vec![]);
}
#[cfg(target_arch = "x86_64")]
for syscall in [
libc::SYS_arch_prctl,
libc::SYS_open,
libc::SYS_stat,
libc::SYS_lstat,
libc::SYS_newfstatat,
libc::SYS_access,
libc::SYS_readlink,
libc::SYS_pipe,
libc::SYS_dup2,
libc::SYS_poll,
libc::SYS_select,
libc::SYS_epoll_wait,
libc::SYS_epoll_create,
libc::SYS_mkdir,
libc::SYS_rmdir,
libc::SYS_unlink,
libc::SYS_rename,
libc::SYS_link,
libc::SYS_symlink,
libc::SYS_chmod,
libc::SYS_chown,
libc::SYS_time,
libc::SYS_alarm,
libc::SYS_getpgrp,
] {
rules.insert(syscall, vec![]);
}
#[cfg(target_arch = "aarch64")]
{
rules.insert(libc::SYS_newfstatat, vec![]);
}
// New namespaces and CLONE_PARENT could escape the owned PID lifecycle.
let forbidden_clone = (libc::CLONE_NEWCGROUP
| libc::CLONE_NEWIPC
| libc::CLONE_NEWNET
| libc::CLONE_NEWNS
| libc::CLONE_NEWPID
| libc::CLONE_NEWUSER
| libc::CLONE_NEWUTS
| libc::CLONE_PARENT
| libc::CLONE_UNTRACED) as u64;
let mut clone = vec![condition(0, SeccompCmpOp::MaskedEq(forbidden_clone), 0)?];
if !capabilities.process {
let required = (libc::CLONE_THREAD | libc::CLONE_VM | libc::CLONE_SIGHAND) as u64;
clone.push(condition(
0,
SeccompCmpOp::MaskedEq(required | 255),
required,
)?);
}
rules.insert(libc::SYS_clone, vec![rule(clone)?]);
if capabilities.process {
// POSIX_SPAWN_RESETIDS restores the caller's real identity before exec.
// Permit that exact reset only; real/saved IDs and group membership
// cannot change. Linux uid_t/gid_t consume the low 32 argument bits.
for (syscall, identity) in [
(libc::SYS_setresuid, rustix::process::getuid().as_raw()),
(libc::SYS_setresgid, rustix::process::getgid().as_raw()),
] {
let conditions = [u32::MAX, identity, u32::MAX]
.into_iter()
.enumerate()
.map(|(argument, value)| {
SeccompCondition::new(
argument as u8,
SeccompCmpArgLen::Dword,
SeccompCmpOp::Eq,
u64::from(value),
)
.map_err(|error| error.to_string())
})
.collect::<Result<Vec<_>, _>>()?;
rules.insert(syscall, vec![rule(conditions)?]);
}
}
#[cfg(target_arch = "x86_64")]
if capabilities.process {
rules.insert(libc::SYS_fork, vec![]);
rules.insert(libc::SYS_vfork, vec![]);
}
// Anonymous socket pairs cannot reach another process. They are needed by
// language runtime event loops even without the network capability.
rules.insert(
libc::SYS_socketpair,
vec![rule(vec![condition(
0,
SeccompCmpOp::Eq,
libc::AF_UNIX as u64,
)?])?],
);
if capabilities.network {
let mut sockets = Vec::new();
for domain in [libc::AF_INET, libc::AF_INET6] {
for kind in [libc::SOCK_STREAM, libc::SOCK_DGRAM] {
sockets.push(rule(vec![
condition(0, SeccompCmpOp::Eq, domain as u64)?,
condition(
1,
SeccompCmpOp::MaskedEq(
!(libc::SOCK_NONBLOCK | libc::SOCK_CLOEXEC) as u32 as u64,
),
kind as u64,
)?,
])?);
}
}
rules.insert(libc::SYS_socket, sockets);
for syscall in [
libc::SYS_connect,
libc::SYS_bind,
libc::SYS_listen,
libc::SYS_accept,
libc::SYS_accept4,
] {
rules.insert(syscall, vec![]);
}
}
// ioctl truncates request to unsigned int. Compare that same width so a
// caller cannot hide a forbidden request behind nonzero upper bits.
let terminal_requests = [libc::TIOCSTI, libc::TIOCLINUX]
.into_iter()
.map(|request| {
SeccompCondition::new(1, SeccompCmpArgLen::Dword, SeccompCmpOp::Ne, request)
.map_err(|error| error.to_string())
})
.collect::<Result<Vec<_>, _>>()?;
rules.insert(libc::SYS_ioctl, vec![rule(terminal_requests)?]);
// The allowlist excludes namespace, mount, ptrace/process_vm, io_uring,
// device creation, kernel administration, BPF and all x32 syscall numbers.
install(
rules,
SeccompAction::Errno(libc::EPERM as u32),
SeccompAction::Allow,
)
}
+321
View File
@@ -0,0 +1,321 @@
//! The fixed Linux execution boundary shared by Lib runtime and build owners.
//! Callers verify signed assets and own every child; this crate never discovers tools.
use std::collections::BTreeSet;
use std::ffi::OsString;
use std::path::{Component, Path, PathBuf};
use std::process::Command;
mod assets;
mod mounts;
mod observation;
mod trusted_helper;
pub use assets::{
validate_assets, validate_assets_for_target, validate_bwrap, validate_bwrap_for_target,
};
pub use observation::CommandNamespace;
pub use trusted_helper::{APPARMOR_PROFILE, TRUSTED_HELPER_ROOT, validate_host_policy};
/// Executable modes are part of the bootstrap resource contract. The private
/// loader is needed by the guard and packaged language runtimes. Authoring and
/// embedding can run on a different host; target validation belongs to assets.
pub fn asset_executable(path: &str) -> bool {
matches!(
path,
"bin/bwrap" | "bin/guard" | "lib/ld-linux-x86-64.so.2" | "lib/ld-linux-aarch64.so.1"
)
}
#[cfg(target_os = "linux")]
mod filter;
#[derive(Clone, Copy, Debug, Default)]
pub struct Capabilities {
pub network: bool,
pub process: bool,
}
/// The source is a parent-validated deployment grant, never a Worker request.
#[derive(Debug)]
pub struct Grant {
pub source: PathBuf,
pub destination: PathBuf,
pub writable: bool,
}
pub struct Launch<'a> {
pub assets: &'a Path,
pub worker: &'a Path,
pub executable: &'a Path,
pub arguments: &'a [OsString],
pub working_directory: &'a Path,
pub capabilities: Capabilities,
pub grants: &'a [Grant],
}
/// Guard exec confirmation and PID-namespace lifetime, separate from user I/O.
pub struct CommandObservation {
pub startup: std::fs::File,
pub namespace: std::fs::File,
pub release: std::fs::File,
}
/// A private ELF closure cannot honor additional host preload libraries.
/// Applies to compiler launches as well as the sandbox bootstrap.
pub fn require_no_loader_preload() -> Result<(), String> {
match std::fs::symlink_metadata("/etc/ld.so.preload") {
Ok(_) => {
Err("private executable does not support host loader preload configuration".into())
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(format!(
"cannot inspect host loader preload configuration: {error}"
)),
}
}
impl Launch<'_> {
pub fn command(&self) -> Result<Command, String> {
self.command_with_tools(None, None)
}
#[cfg(target_os = "linux")]
pub fn observed_command(&self) -> Result<(Command, CommandObservation), String> {
use std::os::fd::OwnedFd;
let (startup, startup_writer) = std::io::pipe().map_err(|error| error.to_string())?;
let (namespace, namespace_writer) = std::io::pipe().map_err(|error| error.to_string())?;
let (blocked, release) = std::io::pipe().map_err(|error| error.to_string())?;
let command = self.command_with_tools(
None,
Some([
std::fs::File::from(OwnedFd::from(startup_writer)),
std::fs::File::from(OwnedFd::from(namespace_writer)),
std::fs::File::from(OwnedFd::from(blocked)),
]),
)?;
Ok((
command,
CommandObservation {
startup: OwnedFd::from(startup).into(),
namespace: OwnedFd::from(namespace).into(),
release: OwnedFd::from(release).into(),
},
))
}
#[cfg(not(target_os = "linux"))]
pub fn observed_command(&self) -> Result<(Command, CommandObservation), String> {
Err("external command sandbox is not supported on this platform".into())
}
/// Only the explicit package-build owner may supply this verified signed
/// root. Ordinary Worker launches never acquire a compiler or /usr mount.
pub fn command_for_build(&self, tool_root: &Path) -> Result<Command, String> {
if self.capabilities.network || !self.capabilities.process {
return Err(
"package builds require isolated network and child-process capability".into(),
);
}
self.command_with_tools(Some(tool_root), None)
}
fn command_with_tools(
&self,
tool_root: Option<&Path>,
observation: Option<[std::fs::File; 3]>,
) -> Result<Command, String> {
if !cfg!(all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
)) {
return Err("external Worker sandbox is not supported on this platform".into());
}
let assets = real_path(self.assets)?;
let bwrap_hash = assets::verify_directory(&assets)?;
require_no_loader_preload()?;
let worker = real_path(self.worker)?;
let tool_root = tool_root.map(real_path).transpose()?;
let executable = mapped_path(&worker, self.executable)?;
let working_directory = mapped_path(&worker, self.working_directory)?;
let loader = assets.join("lib").join(loader_name());
for file in [
&loader,
&assets.join("bin/bwrap"),
&assets.join("bin/guard"),
] {
if !real_path(file)?.is_file() {
return Err("sandbox executable asset is not a regular file".into());
}
}
// A direct static exec is required for the trusted AppArmor attachment.
let bwrap = trusted_helper::executable(&assets, &bwrap_hash)?;
let mut command = Command::new(bwrap);
command.env_clear().args([
"--unshare-all",
"--die-with-parent",
"--new-session",
"--cap-drop",
"ALL",
]);
if self.capabilities.network {
command.arg("--share-net");
}
let mut mounts = mounts::Mounts::default();
let startup_fd = if let Some([startup, namespace, blocked]) = observation {
let startup = mounts.pass(startup)?;
let namespace = mounts.pass(namespace)?;
let blocked = mounts.pass(blocked)?;
command
.arg("--info-fd")
.arg(namespace.to_string())
.arg("--block-fd")
.arg(blocked.to_string());
Some(startup)
} else {
None
};
mounts.bind(&mut command, &assets, Path::new("/sandbox"), false)?;
mounts.bind(&mut command, &assets.join("lib"), Path::new("/lib"), false)?;
command.args(["--symlink", "lib", "/lib64"]);
mounts.bind(&mut command, &worker, Path::new("/worker"), false)?;
if let Some(root) = &tool_root {
mounts.bind(&mut command, &root.join("usr"), Path::new("/usr"), false)?;
command.args(["--symlink", "usr/bin", "/bin"]);
}
command.args([
"--proc", "/proc", "--dev", "/dev", "--tmpfs", "/tmp", "--dir", "/data",
]);
if self.capabilities.network {
// DNS is an OS service input, never application configuration.
// Expose only these bounded resolver files, not the host's /etc.
for name in ["resolv.conf", "hosts"] {
let source = Path::new("/etc")
.join(name)
.canonicalize()
.map_err(|error| format!("cannot resolve network resolver input: {error}"))?;
let source = real_path(&source)?;
let metadata = std::fs::metadata(&source).map_err(|error| error.to_string())?;
if !metadata.is_file() || metadata.len() > 65_536 {
return Err("network resolver input is not a bounded regular file".into());
}
mounts.bind(&mut command, &source, &Path::new("/etc").join(name), false)?;
}
}
let mut destinations = BTreeSet::new();
for grant in self.grants {
let source = real_path(&grant.source)?;
if grant.writable
&& [&assets, &worker]
.into_iter()
.chain(tool_root.iter())
.any(|protected| {
source.starts_with(protected) || protected.starts_with(&source)
})
{
return Err("sandbox writable grant overlaps executable inputs".into());
}
let destination = &grant.destination;
normalized_absolute(destination)?;
if !destination.starts_with("/data") || destination == Path::new("/data") {
return Err("sandbox file grant must have a path below /data".into());
}
if destinations.iter().any(|prior: &&Path| {
prior.starts_with(destination) || destination.starts_with(prior)
}) {
return Err("sandbox file grants overlap".into());
}
destinations.insert(destination.as_path());
mounts.bind(&mut command, &source, destination, grant.writable)?;
}
let flags =
u8::from(self.capabilities.network) | (u8::from(self.capabilities.process) << 1);
command
.arg("--chdir")
.arg(working_directory)
.args(["--", "/sandbox/bin/guard"]);
if let Some(fd) = startup_fd {
command.arg("--observe").arg(fd.to_string());
}
command.arg(flags.to_string()).arg(executable);
for argument in self.arguments {
// Only resource arguments are absolute; fixed flags stay literal.
// Other absolute arguments cannot silently expose a host pathname.
let path = Path::new(argument);
command.arg(if path.is_absolute() {
self.argument_path(path)?.into_os_string()
} else {
argument.clone()
});
}
mounts.attach(&mut command)?;
Ok(command)
}
/// Build manifests need the same names as command arguments. Output files
/// may not exist yet; their owning mount was already validated at launch.
pub fn argument_path(&self, path: &Path) -> Result<PathBuf, String> {
normalized_absolute(path)?;
for grant in self.grants {
if let Ok(relative) = path.strip_prefix(&grant.source) {
return Ok(grant.destination.join(relative));
}
}
let relative = path
.strip_prefix(self.worker)
.map_err(|_| "sandbox argument escapes its owned mounts")?;
Ok(Path::new("/worker").join(relative))
}
}
pub fn loader_name() -> &'static str {
if cfg!(target_arch = "aarch64") {
"ld-linux-aarch64.so.1"
} else {
"ld-linux-x86-64.so.2"
}
}
fn normalized_absolute(path: &Path) -> Result<(), String> {
if !path.is_absolute()
|| path
.components()
.any(|part| !matches!(part, Component::RootDir | Component::Normal(_)))
{
return Err("sandbox path must be absolute and normalized".into());
}
Ok(())
}
fn real_path(path: &Path) -> Result<PathBuf, String> {
normalized_absolute(path)?;
for ancestor in path.ancestors() {
let metadata = std::fs::symlink_metadata(ancestor)
.map_err(|error| format!("cannot inspect sandbox input: {error}"))?;
if metadata.file_type().is_symlink() {
return Err("sandbox input must not contain symbolic links".into());
}
}
Ok(path.to_owned())
}
fn mapped_path(worker: &Path, path: &Path) -> Result<PathBuf, String> {
let path = real_path(path)?;
let relative = path
.strip_prefix(worker)
.map_err(|_| "sandbox execution path escapes the Worker tree")?;
Ok(Path::new("/worker").join(relative))
}
/// Called only by the single-threaded guard immediately before exec. The filter
/// is inherited by exec and every subsequently created thread or child.
pub fn restrict(capabilities: Capabilities) -> Result<(), String> {
#[cfg(target_os = "linux")]
{
filter::apply(capabilities)
}
#[cfg(not(target_os = "linux"))]
{
let _ = capabilities;
Err("sandbox is not supported on this platform".into())
}
}
+79
View File
@@ -0,0 +1,79 @@
//! Private signed helper. It accepts a fixed capability mask, never filter code.
#[cfg(target_os = "linux")]
fn execute() -> Result<(), String> {
use std::io::Write;
use std::os::unix::process::CommandExt;
let mut arguments = std::env::args_os().skip(1);
let first = arguments.next();
let mut observation = if first.as_deref() == Some(std::ffi::OsStr::new("--observe")) {
let fd = arguments
.next()
.and_then(|value| value.to_str().and_then(|value| value.parse::<i32>().ok()))
.filter(|fd| *fd >= 3)
.ok_or("invalid sandbox observation descriptor")?;
// Opening proc creates a safe owned CLOEXEC duplicate. Close the
// inherited raw descriptor before exec so neither channel leaks.
let file = std::fs::OpenOptions::new()
.write(true)
.open(format!("/proc/self/fd/{fd}"))
.map_err(|error| format!("cannot open sandbox observation: {error}"))?;
nix::unistd::close(fd)
.map_err(|error| format!("cannot close inherited observation: {error}"))?;
Some(file)
} else {
None
};
let first = if observation.is_some() {
arguments.next()
} else {
first
};
let result = (|| {
let flags = first
.and_then(|value| value.to_str().and_then(|value| value.parse::<u8>().ok()))
.filter(|value| *value <= 3)
.ok_or("invalid sandbox capability mask")?;
let executable = arguments.next().ok_or("missing sandbox executable")?;
if !std::path::Path::new(&executable).starts_with("/worker") {
return Err("sandbox executable must be inside its Worker tree".into());
}
let mut command = std::process::Command::new(executable);
command.args(arguments).env_clear();
dever_sandbox::restrict(dever_sandbox::Capabilities {
network: flags & 1 != 0,
process: flags & 2 != 0,
})?;
if let Some(observation) = &mut observation {
observation
.write_all(b"R")
.map_err(|error| format!("cannot confirm sandbox guard: {error}"))?;
}
Err(format!(
"cannot execute sandboxed Worker: {}",
command.exec()
))
})();
if let (Some(observation), Err(error)) = (&mut observation, &result) {
observation
.write_all(b"E")
.and_then(|_| observation.write_all(error.as_bytes()))
.map_err(|write| format!("{error}; cannot report sandbox startup: {write}"))?;
}
result
}
#[cfg(not(target_os = "linux"))]
fn execute() -> Result<(), String> {
Err("sandbox is not supported on this platform".into())
}
fn main() -> std::process::ExitCode {
match execute() {
Ok(()) => std::process::ExitCode::SUCCESS,
Err(error) => {
eprintln!("{error}");
std::process::ExitCode::FAILURE
}
}
}
+87
View File
@@ -0,0 +1,87 @@
//! Pin deployment grants until bwrap has mounted the exact checked inode.
use std::path::Path;
use std::process::Command;
#[derive(Default)]
pub(super) struct Mounts {
#[cfg(target_os = "linux")]
descriptors: Vec<command_fds::FdMapping>,
}
impl Mounts {
#[cfg(target_os = "linux")]
pub(super) fn pass(&mut self, file: std::fs::File) -> Result<i32, String> {
let child_fd =
i32::try_from(self.descriptors.len()).map_err(|_| "too many sandbox descriptors")? + 3;
self.descriptors.push(command_fds::FdMapping {
parent_fd: file.into(),
child_fd,
});
Ok(child_fd)
}
#[cfg(not(target_os = "linux"))]
pub(super) fn pass(&mut self, _: std::fs::File) -> Result<i32, String> {
Err("sandbox descriptors are not supported on this platform".into())
}
#[cfg(target_os = "linux")]
pub(super) fn bind(
&mut self,
command: &mut Command,
source: &Path,
destination: &Path,
writable: bool,
) -> Result<(), String> {
use rustix::fs::{Mode, OFlags, ResolveFlags, openat2};
let descriptor = openat2(
rustix::fs::CWD,
source,
OFlags::PATH | OFlags::CLOEXEC,
Mode::empty(),
ResolveFlags::NO_SYMLINKS | ResolveFlags::NO_MAGICLINKS,
)
.map_err(|error| format!("cannot pin sandbox mount without following links: {error}"))?;
let child_fd =
i32::try_from(self.descriptors.len()).map_err(|_| "too many sandbox mounts")? + 3;
command
.arg(if writable {
"--bind-fd"
} else {
"--ro-bind-fd"
})
.arg(child_fd.to_string())
.arg(destination);
self.descriptors.push(command_fds::FdMapping {
parent_fd: descriptor,
child_fd,
});
Ok(())
}
#[cfg(target_os = "linux")]
pub(super) fn attach(self, command: &mut Command) -> Result<(), String> {
use command_fds::CommandFdExt;
command
.fd_mappings(self.descriptors)
.map_err(|error| format!("cannot pass sandbox mount descriptors: {error}"))?;
Ok(())
}
#[cfg(not(target_os = "linux"))]
pub(super) fn bind(
&mut self,
_: &mut Command,
_: &Path,
_: &Path,
_: bool,
) -> Result<(), String> {
Err("sandbox mounts are not supported on this platform".into())
}
#[cfg(not(target_os = "linux"))]
pub(super) fn attach(self, _: &mut Command) -> Result<(), String> {
Err("sandbox mounts are not supported on this platform".into())
}
}
+48
View File
@@ -0,0 +1,48 @@
//! A pinned namespace init is the completion boundary for all descendants.
#[cfg(target_os = "linux")]
use std::os::fd::{AsRawFd, RawFd};
pub struct CommandNamespace {
#[cfg(target_os = "linux")]
descriptor: std::fs::File,
}
impl CommandNamespace {
#[cfg(target_os = "linux")]
pub fn pin(pid: i32, wrapper: u32) -> Result<Self, String> {
use rustix::process::{Pid, PidfdFlags, pidfd_open};
let pid = Pid::from_raw(pid).ok_or("invalid sandbox namespace PID")?;
let descriptor = pidfd_open(pid, PidfdFlags::NONBLOCK)
.map_err(|error| format!("cannot pin sandbox namespace: {error}"))?;
// --block-fd holds the namespace before guard/target execution. If
// setup failed and its PID was reused, do not acquire authority over
// that process: only this bwrap's direct child belongs to this launch.
let status = std::fs::read_to_string(format!("/proc/{}/status", pid.as_raw_pid()))
.map_err(|error| format!("cannot verify sandbox namespace parent: {error}"))?;
let parent = status
.lines()
.find_map(|line| line.strip_prefix("PPid:"))
.and_then(|value| value.trim().parse::<u32>().ok());
if parent != Some(wrapper) {
return Err("sandbox namespace no longer belongs to its launcher".into());
}
Ok(Self {
descriptor: descriptor.into(),
})
}
#[cfg(target_os = "linux")]
pub fn terminate(&self) -> Result<(), String> {
match rustix::process::pidfd_send_signal(&self.descriptor, rustix::process::Signal::KILL) {
Ok(()) | Err(rustix::io::Errno::SRCH) => Ok(()),
Err(error) => Err(format!("cannot stop owned command namespace: {error}")),
}
}
}
#[cfg(target_os = "linux")]
impl AsRawFd for CommandNamespace {
fn as_raw_fd(&self) -> RawFd {
self.descriptor.as_raw_fd()
}
}
+130
View File
@@ -0,0 +1,130 @@
//! The only host integration is a signed, administrator-installed static helper.
use std::path::{Path, PathBuf};
pub const TRUSTED_HELPER_ROOT: &str = "/opt/dever/sandbox";
pub const APPARMOR_PROFILE: &str = include_str!("../../../sdk/apparmor/dever-sandbox");
#[cfg(all(test, target_os = "linux"))]
#[path = "../../../test/dever-sandbox-tests/trusted_helper.rs"]
mod tests;
pub(super) fn executable(assets: &Path, expected: &str) -> Result<PathBuf, String> {
#[cfg(target_os = "linux")]
if rustix::process::geteuid().as_raw() != 0 && validate_host_policy()? {
let path = Path::new(TRUSTED_HELPER_ROOT).join(expected).join("bwrap");
verify_helper(&path, expected)?;
require_profiles()?;
return Ok(path);
}
let _ = expected;
Ok(assets.join("bin/bwrap"))
}
/// Validate the host prerequisite shared by non-root launch and real-host
/// policy installation. This only reads kernel state; administrators own it.
pub fn validate_host_policy() -> Result<bool, String> {
#[cfg(target_os = "linux")]
{
host_policy(|path| std::fs::read_to_string(path))
}
#[cfg(not(target_os = "linux"))]
{
Ok(false)
}
}
#[cfg(target_os = "linux")]
fn host_policy(read: impl Fn(&str) -> std::io::Result<String>) -> Result<bool, String> {
let restricted = policy_flag(&read, "apparmor_restrict_unprivileged_userns")?.unwrap_or(false);
if !restricted {
return Ok(false);
}
if policy_flag(&read, "apparmor_restrict_unprivileged_unconfined")? != Some(true) {
return Err("Dever AppArmor deployment requires kernel.apparmor_restrict_unprivileged_unconfined=1 while user namespace restriction is enabled; an administrator must approve strict profile transitions before installing or loading the Dever policy".into());
}
Ok(true)
}
#[cfg(target_os = "linux")]
fn policy_flag(
read: &impl Fn(&str) -> std::io::Result<String>,
name: &str,
) -> Result<Option<bool>, String> {
match read(&format!("/proc/sys/kernel/{name}")) {
Ok(value) => match value.trim() {
"0" => Ok(Some(false)),
"1" => Ok(Some(true)),
_ => Err(format!(
"invalid AppArmor kernel flag '{name}': expected 0 or 1"
)),
},
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(format!(
"cannot inspect AppArmor kernel flag '{name}': {error}"
)),
}
}
#[cfg(target_os = "linux")]
fn verify_helper(path: &Path, expected: &str) -> Result<(), String> {
use std::io::Read;
use std::os::unix::fs::MetadataExt;
for (index, ancestor) in path.ancestors().enumerate() {
let metadata = std::fs::symlink_metadata(ancestor).map_err(|error| format!(
"trusted sandbox helper is missing or inaccessible at '{}': {error}; install the matching signed Dever sandbox assets", path.display()))?;
if metadata.file_type().is_symlink()
|| metadata.uid() != 0
|| metadata.mode() & 0o022 != 0
|| (index == 0
&& (!metadata.is_file()
|| metadata.mode() & 0o6111 != 0o111
|| metadata.nlink() != 1))
|| (index != 0 && !metadata.is_dir())
{
return Err("trusted sandbox helper requires real root-owned paths without shared writes, setuid or setgid".into());
}
}
match rustix::fs::lgetxattr(path, "security.capability", &mut [0u8; 64]) {
Err(rustix::io::Errno::NODATA | rustix::io::Errno::OPNOTSUPP) => {}
Ok(_) => return Err("trusted sandbox helper must not have file capabilities".into()),
Err(error) => {
return Err(format!(
"cannot inspect sandbox helper capabilities: {error}"
));
}
}
let mut bytes = Vec::new();
std::fs::File::open(path)
.and_then(|file| file.take(32 * 1024 * 1024 + 1).read_to_end(&mut bytes))
.map_err(|error| format!("cannot read trusted sandbox helper: {error}"))?;
if super::validate_bwrap(&bytes)? != expected {
return Err("trusted sandbox helper differs from the embedded signed asset".into());
}
Ok(())
}
#[cfg(target_os = "linux")]
fn require_profiles() -> Result<(), String> {
// The profiles list requires policy-admin privilege; the policy filesystem
// exposes each profile's name/mode to ordinary users without changing state.
let entries = std::fs::read_dir("/sys/kernel/security/apparmor/policy/profiles")
.map_err(|error| format!("cannot verify the Dever AppArmor profiles: {error}; an administrator must load /etc/apparmor.d/dever-sandbox"))?;
let mut required = std::collections::BTreeSet::from(["dever-bwrap", "dever-bwrap-child"]);
for entry in entries.take(4096) {
let entry = entry.map_err(|error| format!("cannot inspect AppArmor profiles: {error}"))?;
let name = std::fs::read_to_string(entry.path().join("name"))
.map_err(|error| format!("cannot read AppArmor profile name: {error}"))?;
if required.contains(name.trim()) {
let mode = std::fs::read_to_string(entry.path().join("mode"))
.map_err(|error| format!("cannot read AppArmor profile mode: {error}"))?;
if mode.trim() != "enforce" {
return Err("Dever AppArmor profiles must be loaded in enforce mode".into());
}
required.remove(name.trim());
}
if required.is_empty() {
return Ok(());
}
}
Err("Dever AppArmor profile is not loaded; an administrator must load /etc/apparmor.d/dever-sandbox".into())
}