Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
shared-key: ci-clippy
- uses: taiki-e/install-action@v2
with:
tool: actionlint@1.7.7
- name: Install actionlint
run: |
go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.7
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
- run: actionlint
- run: just fmt-rust --check
- run: just lint-rust
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ hyper-util = { version = "0.1", default-features = false }
ipnet = "2"
jj-lib = "0.43.0"
libc = "0.2"
libloading = "0.8"
libkrun = "=1.19.3"
log = "0.4"
persisting-control = { path = "crates/persisting-control" }
Expand Down Expand Up @@ -116,6 +117,12 @@ strip = "symbols"
codegen-units = 8
panic = "abort"

# Build scripts and proc macros do not need release stripping. Keeping their
# symbols avoids invoking a host rust-objcopy that may be unavailable when
# cross-compiling from macOS, while the final application remains stripped.
[profile.release.build-override]
strip = "none"

[patch.crates-io]
fuser = { path = "vendor/fuser" }
libkrun = { path = "vendor/libkrun" }
Expand Down
4 changes: 2 additions & 2 deletions crates/persisting-overlay-core/src/sys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ fn cvt(rc: libc::c_int) -> io::Result<()> {
fn timespec(time: SystemTime) -> libc::timespec {
match time.duration_since(UNIX_EPOCH) {
Ok(value) => libc::timespec {
tv_sec: value.as_secs() as libc::time_t,
tv_sec: value.as_secs() as i64 as _,
tv_nsec: value.subsec_nanos() as libc::c_long,
},
Err(value) => {
let value = value.duration();
libc::timespec {
tv_sec: -(value.as_secs() as libc::time_t) - 1,
tv_sec: (-(value.as_secs() as i64) - 1) as _,
tv_nsec: 1_000_000_000 - value.subsec_nanos() as libc::c_long,
}
}
Expand Down
8 changes: 7 additions & 1 deletion crates/persisting-pvisor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ authors.workspace = true
license.workspace = true
description = "pVisor foreground Agent Run manager and portable execution runtime"
readme = "README.md"
build = "build.rs"

[features]
default = []
Expand All @@ -21,7 +22,6 @@ flate2.workspace = true
fs2.workspace = true
globset.workspace = true
libc.workspace = true
libkrun = { workspace = true, features = ["net"] }
persisting-control.workspace = true
persisting-gateway = { workspace = true, default-features = false }
persisting-overlaynet.workspace = true
Expand All @@ -42,9 +42,15 @@ uuid = { workspace = true, features = ["v4"] }
zstd.workspace = true
reqwest = { workspace = true, features = ["blocking", "json", "rustls-tls"] }

[target.'cfg(not(all(target_os = "macos", target_arch = "x86_64")))'.dependencies]
libkrun = { workspace = true, features = ["net"] }

[[bin]]
name = "pvisor"
path = "src/bin/pvisor.rs"

[dev-dependencies]
proptest.workspace = true

[build-dependencies]
libloading.workspace = true
113 changes: 113 additions & 0 deletions crates/persisting-pvisor/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
use std::env;
use std::fs;
use std::path::PathBuf;

fn main() {
println!("cargo:rerun-if-env-changed=PERSISTING_KRUNFW_PATH");
println!("cargo:rerun-if-env-changed=PERSISTING_KRUNFW_KERNEL_BUNDLE");

if env::var("TARGET").as_deref() != Ok("x86_64-unknown-linux-musl") {
return;
}

let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("Cargo did not set OUT_DIR"));
let bundle_dir = env::var_os("PERSISTING_KRUNFW_KERNEL_BUNDLE").map(PathBuf::from);
let (kernel, guest_addr, entry_addr) = if let Some(directory) = bundle_dir {
println!(
"cargo:rerun-if-changed={}",
directory.join("kernel.bin").display()
);
println!(
"cargo:rerun-if-changed={}",
directory.join("kernel.json").display()
);
let kernel = fs::read(directory.join("kernel.bin")).unwrap_or_else(|error| {
panic!(
"read embedded libkrun kernel bundle {}: {error}",
directory.join("kernel.bin").display()
)
});
let metadata = fs::read_to_string(directory.join("kernel.json")).unwrap_or_else(|error| {
panic!(
"read embedded libkrun kernel metadata {}: {error}",
directory.join("kernel.json").display()
)
});
let guest_addr = metadata_value(&metadata, "guest_addr");
let entry_addr = metadata_value(&metadata, "entry_addr");
(kernel, guest_addr, entry_addr)
} else {
let path = env::var_os("PERSISTING_KRUNFW_PATH").unwrap_or_else(|| {
panic!(
"building x86_64 Linux musl requires \
PERSISTING_KRUNFW_PATH pointing to libkrunfw.so.5, or \
PERSISTING_KRUNFW_KERNEL_BUNDLE pointing to a directory containing \
kernel.bin and kernel.json"
)
});
println!("cargo:rerun-if-changed={}", PathBuf::from(&path).display());
extract_kernel_bundle(&PathBuf::from(path))
};

assert!(!kernel.is_empty(), "libkrun kernel bundle is empty");
let kernel_path = out_dir.join("embedded-libkrun-kernel.bin");
fs::write(&kernel_path, &kernel).unwrap_or_else(|error| {
panic!(
"write embedded libkrun kernel {}: {error}",
kernel_path.display()
)
});
let generated = out_dir.join("embedded_kernel.rs");
let kernel_literal = format!("{:?}", kernel_path.to_string_lossy());
let source = format!(
"pub static KERNEL: &[u8] = include_bytes!({kernel_literal});\n\
pub const GUEST_ADDR: u64 = {guest_addr};\n\
pub const ENTRY_ADDR: u64 = {entry_addr};\n"
);
fs::write(&generated, source)
.unwrap_or_else(|error| panic!("write generated kernel module: {error}"));
}

fn extract_kernel_bundle(path: &std::path::Path) -> (Vec<u8>, u64, u64) {
type GetKernel = unsafe extern "C" fn(*mut u64, *mut u64, *mut usize) -> *mut std::ffi::c_char;

let library = unsafe { libloading::Library::new(path) }.unwrap_or_else(|error| {
panic!(
"load libkrunfw {} while extracting kernel bundle: {error}",
path.display()
)
});
let get_kernel = unsafe { library.get::<GetKernel>(b"krunfw_get_kernel\0") }
.unwrap_or_else(|error| panic!("resolve krunfw_get_kernel: {error}"));
let mut guest_addr = 0;
let mut entry_addr = 0;
let mut size = 0;
let pointer = unsafe { get_kernel(&mut guest_addr, &mut entry_addr, &mut size) };
if pointer.is_null() || size == 0 {
panic!("krunfw_get_kernel returned an empty kernel bundle");
}
let kernel = unsafe { std::slice::from_raw_parts(pointer.cast::<u8>(), size) }.to_vec();
(kernel, guest_addr, entry_addr)
}

fn metadata_value(metadata: &str, key: &str) -> u64 {
let marker = format!("\"{key}\"");
let token = metadata
.split_once(&marker)
.and_then(|(_, rest)| rest.split_once(':'))
.and_then(|(_, rest)| {
let value = rest
.chars()
.skip_while(|character| !character.is_ascii_hexdigit() && *character != 'x')
.take_while(|character| character.is_ascii_hexdigit() || *character == 'x')
.collect::<String>();
(!value.is_empty()).then_some(value)
})
.unwrap_or_else(|| panic!("kernel metadata is missing numeric {key}"));
if let Some(hex) = token.strip_prefix("0x") {
u64::from_str_radix(hex, 16)
} else {
token.parse()
}
.unwrap_or_else(|error| panic!("kernel metadata {key} is not a valid integer: {error}"))
}
50 changes: 36 additions & 14 deletions crates/persisting-pvisor/src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,10 @@ impl RunBundle {
let network_non_bypassable = !sandbox_setup_failed
&& enforcement
.is_some_and(|evidence| evidence.is_enforced(CapabilityDimension::Network));
let filesystem_read_non_bypassable = filesystem.is_some()
&& !sandbox_setup_failed
let filesystem_read_non_bypassable = !sandbox_setup_failed
&& enforcement
.is_some_and(|evidence| evidence.is_enforced(CapabilityDimension::FilesystemRead));
let filesystem_write_non_bypassable = filesystem.is_some()
&& !sandbox_setup_failed
let filesystem_write_non_bypassable = !sandbox_setup_failed
&& enforcement
.is_some_and(|evidence| evidence.is_enforced(CapabilityDimension::FilesystemWrite));
let filesystem_non_bypassable =
Expand All @@ -236,16 +234,35 @@ impl RunBundle {
);
}
if rootless_process && !sandbox_setup_failed {
safety_warnings.push(
"filesystem access and process-tree cleanup are kernel-enforced; the host kernel and syscall surface remain shared"
.into(),
);
if enforcement.is_some_and(|evidence| {
evidence.is_enforced(CapabilityDimension::FilesystemRead)
|| evidence.is_enforced(CapabilityDimension::FilesystemWrite)
}) {
safety_warnings.push(
"filesystem access and process-tree cleanup are kernel-enforced; the host kernel and syscall surface remain shared"
.into(),
);
} else {
safety_warnings.push(
"process-tree cleanup and selected network boundaries are kernel-enforced; filesystem access remains host-visible"
.into(),
);
}
}
if seatbelt_process && !sandbox_setup_failed {
safety_warnings.push(
"filesystem writes are Seatbelt-enforced; reads, the host PID namespace, syscall surface, and resource limits remain shared"
.into(),
);
if enforcement.is_some_and(|evidence| {
evidence.is_enforced(CapabilityDimension::FilesystemWrite)
}) {
safety_warnings.push(
"filesystem writes are Seatbelt-enforced; reads, the host PID namespace, syscall surface, and resource limits remain shared"
.into(),
);
} else {
safety_warnings.push(
"selected network boundaries are Seatbelt-enforced; filesystem writes remain host-visible"
.into(),
);
}
}
if virtual_machine {
safety_warnings.push(
Expand Down Expand Up @@ -415,9 +432,9 @@ fn resource_summary(record: &RunRecord, result: &RunResult) -> ResourceSummary {

#[cfg(unix)]
fn effective_native_limits(requested: &ResourceLimits) -> ResourceLimits {
#[cfg(target_os = "linux")]
#[cfg(all(target_os = "linux", not(target_env = "musl")))]
type RlimitResource = libc::__rlimit_resource_t;
#[cfg(not(target_os = "linux"))]
#[cfg(any(not(target_os = "linux"), target_env = "musl"))]
type RlimitResource = libc::c_int;

fn clamp(resource: RlimitResource, requested: Option<u64>) -> Option<u64> {
Expand Down Expand Up @@ -642,6 +659,11 @@ mod tests {
let intercepted_vm = RunBundle::capture(&record, &result, agentctl.clone(), true).unwrap();
assert!(intercepted_vm.safety.filesystem_non_bypassable);
assert!(intercepted_vm.safety.network_non_bypassable);
let mut vm_without_stage = record.clone();
vm_without_stage.overlay = None;
let vm_without_stage =
RunBundle::capture(&vm_without_stage, &result, agentctl.clone(), true).unwrap();
assert!(vm_without_stage.safety.filesystem_non_bypassable);

record.executor = Some(ExecutorDescriptor {
name: "local-rootless-v1".into(),
Expand Down
17 changes: 9 additions & 8 deletions crates/persisting-pvisor/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ use clap::{Parser, Subcommand};

#[cfg(target_os = "linux")]
const ROOT_ABOUT: &str =
"Foreground Agent Run manager with rootless Linux sandboxing and reviewable workspaces";
"Foreground Agent Run manager with independent filesystem, network, and staging policies";
#[cfg(target_os = "linux")]
const ROOT_LONG_ABOUT: &str = "Foreground Agent Run manager: execute, control, Gateway, and OverlayFS.\n\nOn Linux, host runs use safe-best-effort rootless isolation when supported: user and mount namespaces, a minimal synthetic root with chroot, Landlock, no_new_privs, and dropped capabilities. Add `--overlaynet-deny-all` to isolate direct network sockets in a private network namespace.";
const ROOT_LONG_ABOUT: &str = "Foreground Agent Run manager: execute, control, Gateway, and OverlayFS.\n\nHost execution preserves the host filesystem view by default. Use `--filesystem sandbox` for synthetic-root/Landlock restrictions, `--stage` for independent workspace staging, and `--overlaynet` for network policy. On Linux, `--overlaynet-deny-all` uses a private network namespace without enabling filesystem restrictions.";

#[cfg(target_os = "macos")]
const ROOT_ABOUT: &str =
"Foreground Agent Run manager with Seatbelt isolation and reviewable workspaces";
"Foreground Agent Run manager with independent filesystem, network, and staging policies";
#[cfg(target_os = "macos")]
const ROOT_LONG_ABOUT: &str = "Foreground Agent Run manager: execute, control, Gateway, and OverlayFS.\n\nOn macOS, host runs use safe-best-effort macFUSE workspace views and Seatbelt confinement when supported. Full-disk reads remain available for local toolchain compatibility. `--overlaynet-deny-all` also blocks non-loopback IP and ambient host Unix sockets while retaining loopback proxy access and Run-local IPC.";
const ROOT_LONG_ABOUT: &str = "Foreground Agent Run manager: execute, control, Gateway, and OverlayFS.\n\nHost execution preserves the host filesystem view by default. Use `--filesystem sandbox` for Seatbelt filesystem restrictions, `--stage` for independent workspace staging (macFUSE may be required), and `--overlaynet` for network policy. Full-disk reads remain available and ambient unless filesystem sandboxing is requested. `--overlaynet-deny-all` blocks non-loopback IP and ambient host Unix sockets while retaining loopback proxy access and Run-local IPC.";

#[cfg(not(any(target_os = "linux", target_os = "macos")))]
const ROOT_ABOUT: &str = "Foreground Agent Run manager with staged, reviewable workspaces";
Expand Down Expand Up @@ -268,10 +268,11 @@ mod tests {

#[cfg(target_os = "linux")]
{
assert!(help.contains("safe-best-effort"));
assert!(help.contains("rootless isolation"));
assert!(help.contains("namespace"));
assert!(help.contains("Landlock"));
let normalized = help.split_whitespace().collect::<Vec<_>>().join(" ");
assert!(normalized.contains("host filesystem view by default"));
assert!(normalized.contains("--filesystem sandbox"));
assert!(normalized.contains("namespace"));
assert!(normalized.contains("Landlock"));
}
#[cfg(target_os = "macos")]
{
Expand Down
10 changes: 8 additions & 2 deletions crates/persisting-pvisor/src/cli/product.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,17 @@ pub fn review(args: ReviewArgs) -> anyhow::Result<()> {
.as_ref()
.map(|executor| executor.isolation)
{
Some(persisting_control::IsolationKind::RootlessProcess)
if bundle.safety.filesystem_non_bypassable =>
"rootless user namespace + Landlock",
Some(persisting_control::IsolationKind::RootlessProcess) => {
"rootless user namespace + Landlock"
"rootless user namespace + network namespace"
}
Some(persisting_control::IsolationKind::SandboxedProcess)
if bundle.safety.filesystem_write_non_bypassable =>
"macOS Seatbelt filesystem policy",
Some(persisting_control::IsolationKind::SandboxedProcess) => {
"macOS Seatbelt process sandbox"
"macOS Seatbelt network policy"
}
Some(persisting_control::IsolationKind::Container) => {
"OCI container with injected pVisor"
Expand Down
Loading
Loading