Skip to content
Open
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
20 changes: 17 additions & 3 deletions src/boxlite/src/jailer/builder.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! JailerBuilder for constructing a [`Jailer`](super::Jailer).

use super::Jailer;
use super::sandbox::{PlatformSandbox, Sandbox};
use super::sandbox::{PlatformSandbox, Sandbox, SandboxBindMount};
use crate::runtime::advanced_options::{ResourceLimits, SecurityOptions};
use crate::runtime::layout::BoxFilesystemLayout;
use crate::runtime::options::VolumeSpec;
Expand All @@ -26,6 +26,7 @@ use std::path::PathBuf;
pub struct JailerBuilder {
security: SecurityOptions,
volumes: Vec<VolumeSpec>,
bind_mounts: Vec<SandboxBindMount>,
box_id: Option<String>,
layout: Option<BoxFilesystemLayout>,
preserved_fds: Vec<(RawFd, i32)>,
Expand All @@ -44,6 +45,7 @@ impl JailerBuilder {
Self {
security: SecurityOptions::default(),
volumes: Vec::new(),
bind_mounts: Vec::new(),
box_id: None,
layout: None,
preserved_fds: Vec::new(),
Expand Down Expand Up @@ -84,6 +86,16 @@ impl JailerBuilder {
self
}

/// Set bind mounts that should be installed by namespace sandboxes.
///
/// These are distinct from `volumes`: `volumes` describe user intent and
/// path-access policy, while sandbox bind mounts name exact source/target
/// pairs that bwrap should mount inside its private namespace.
pub fn with_bind_mounts(mut self, bind_mounts: Vec<SandboxBindMount>) -> Self {
self.bind_mounts = bind_mounts;
self
}

/// Enable or disable jailer isolation.
pub fn with_jailer_enabled(mut self, enabled: bool) -> Self {
self.security.jailer_enabled = enabled;
Expand Down Expand Up @@ -324,6 +336,7 @@ impl JailerBuilder {
sandbox,
security: self.security,
volumes: self.volumes,
bind_mounts: self.bind_mounts,
box_id,
layout,
preserved_fds: self.preserved_fds,
Expand Down Expand Up @@ -571,15 +584,16 @@ mod tests {
.expect("should build dev");
assert!(!dev.security().jailer_enabled);

// Maximum → strict: jailer + seccomp on, namespaces on.
// Maximum → strict profile. Seccomp is Linux-only, matching
// SecurityOptions::default().
let max = JailerBuilder::new()
.with_box_id("test-box")
.with_layout(test_layout("/tmp/box"))
.with_security_enabled()
.build()
.expect("should build max");
assert!(max.security().jailer_enabled);
assert!(max.security().seccomp_enabled);
assert_eq!(max.security().seccomp_enabled, cfg!(target_os = "linux"));

// Standard is the recommended default; just confirm it
// overrides whatever the chain set before.
Expand Down
57 changes: 52 additions & 5 deletions src/boxlite/src/jailer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ pub use crate::runtime::advanced_options::{ResourceLimits, SecurityOptions};
pub use builder::JailerBuilder;
pub use error::{ConfigError, IsolationError, JailerError, SystemError};
pub use sandbox::{
CompositeSandbox, NoopSandbox, PathAccess, PlatformSandbox, Sandbox, SandboxContext,
CompositeSandbox, NoopSandbox, PathAccess, PlatformSandbox, Sandbox, SandboxBindMount,
SandboxContext,
};

// ============================================================================
Expand Down Expand Up @@ -321,9 +322,9 @@ fn build_path_access(layout: &BoxFilesystemLayout, volumes: &[VolumeSpec]) -> Ve
}
}

// User volumes. Directories are shared directly, so grant the VMM access.
// Single files are staged under shared_dir (granted above), so they need no
// grant here — this also keeps the file's host siblings out of the sandbox.
// Fallback user volumes. Direct directory virtio-fs shares need source
// access here. Volumes handled by SandboxBindMount skip this path, so the
// original host source is not also visible at its original path.
for vol in volumes {
let p = PathBuf::from(&vol.host_path);
if let Some(VolumeShare::Dir(dir)) = classify_volume_share(&p) {
Expand Down Expand Up @@ -378,6 +379,8 @@ pub struct Jailer<S: Sandbox> {
pub(crate) security: SecurityOptions,
/// Volume mounts (for sandbox path restrictions).
pub(crate) volumes: Vec<VolumeSpec>,
/// Bind mounts installed inside namespace sandboxes.
pub(crate) bind_mounts: Vec<SandboxBindMount>,
/// Unique box identifier.
pub(crate) box_id: String,
/// Box filesystem layout (provides typed path accessors).
Expand Down Expand Up @@ -532,10 +535,16 @@ impl<S: Sandbox> Jailer<S> {
///
/// Delegates to [`build_path_access`] for granular filesystem rules.
fn context(&self) -> SandboxContext<'_> {
let paths = build_path_access(&self.layout, &self.volumes);
let path_volumes: &[VolumeSpec] = if self.bind_mounts.is_empty() {
self.volumes.as_slice()
} else {
&[]
};
let paths = build_path_access(&self.layout, path_volumes);
tracing::debug!(
box_id = %self.box_id,
path_count = paths.len(),
bind_mount_count = self.bind_mounts.len(),
paths = ?paths,
"Built sandbox path access list"
);
Expand All @@ -546,6 +555,7 @@ impl<S: Sandbox> Jailer<S> {
SandboxContext {
id: &self.box_id,
paths,
bind_mounts: self.bind_mounts.clone(),
resource_limits: &self.security.resource_limits,
network_enabled: self.security.network_enabled,
sandbox_profile: self.security.sandbox_profile.as_deref(),
Expand Down Expand Up @@ -843,6 +853,43 @@ mod tests {
assert!(rw_vol.writable, "RW volume should be writable");
}

#[test]
fn test_context_with_sandbox_bind_mounts_does_not_expose_volume_source_path() {
let dir = tempdir().unwrap();
let layout = test_layout(dir.path().join("box"));

let host_volume = dir.path().join("host-data");
std::fs::create_dir_all(&host_volume).unwrap();
let sandbox_target = layout.shared_dir().join("containers/main/volumes/uservol0");

let jail = JailerBuilder::new()
.with_box_id("test-box")
.with_layout(layout)
.with_volumes(vec![VolumeSpec {
host_path: host_volume.to_string_lossy().to_string(),
guest_path: "/data".to_string(),
read_only: false,
}])
.with_bind_mounts(vec![SandboxBindMount::new(
host_volume.clone(),
sandbox_target.clone(),
true,
)])
.build()
.expect("jailer should build");

let ctx = jail.context();

assert!(
ctx.paths.iter().all(|p| p.path != host_volume),
"sandbox bind-backed volumes should not also expose the host source at its original path"
);
assert_eq!(ctx.bind_mounts.len(), 1);
assert_eq!(ctx.bind_mounts[0].source, host_volume);
assert_eq!(ctx.bind_mounts[0].target, sandbox_target);
assert!(ctx.bind_mounts[0].writable);
}

#[test]
fn test_build_path_access_nonexistent_volume_skipped() {
let dir = tempdir().unwrap();
Expand Down
69 changes: 69 additions & 0 deletions src/boxlite/src/jailer/sandbox/bwrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,23 @@ impl Sandbox for BwrapSandbox {
bwrap_cmd.ro_bind(&pa.path, &pa.path);
tracing::debug!(path = %pa.path.display(), "bwrap: ro-bind");
}
for mount in &ctx.bind_mounts {
if mount.writable {
bwrap_cmd.bind(&mount.source, &mount.target);
tracing::debug!(
source = %mount.source.display(),
target = %mount.target.display(),
"bwrap: bind user volume"
);
} else {
bwrap_cmd.ro_bind(&mount.source, &mount.target);
tracing::debug!(
source = %mount.source.display(),
target = %mount.target.display(),
"bwrap: ro-bind user volume"
);
}
}

// =====================================================================
// Environment sanitization
Expand Down Expand Up @@ -193,6 +210,7 @@ mod tests {
let ctx = SandboxContext {
id: "test-box",
paths: vec![],
bind_mounts: vec![],
resource_limits: limits,
network_enabled: false,
sandbox_profile: None,
Expand Down Expand Up @@ -237,6 +255,7 @@ mod tests {
let ctx = SandboxContext {
id: "test-box",
paths: vec![],
bind_mounts: vec![],
resource_limits: limits,
network_enabled: false,
sandbox_profile: None,
Expand All @@ -256,4 +275,54 @@ mod tests {
"detached box must not get --die-with-parent or it is killed when run -d returns"
);
}

#[test]
fn apply_emits_namespace_bind_mounts() {
if !bwrap::is_available() {
eprintln!("skipping apply_emits_namespace_bind_mounts: bwrap not available");
return;
}

let limits = Box::leak(Box::new(ResourceLimits::default()));
let ctx = SandboxContext {
id: "test-box",
paths: vec![],
bind_mounts: vec![
crate::jailer::SandboxBindMount::new(
"/host/rw",
"/box/shared/containers/main/volumes/rw",
true,
),
crate::jailer::SandboxBindMount::new(
"/host/ro",
"/box/shared/containers/main/volumes/ro",
false,
),
],
resource_limits: limits,
network_enabled: false,
sandbox_profile: None,
detached: false,
};

let mut cmd = Command::new("/var/lib/boxlite/boxes/abc/bin/boxlite-shim");
BwrapSandbox::new().apply(&ctx, &mut cmd);
let args: Vec<String> = cmd
.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect();

assert!(
args.windows(3).any(|w| w[0] == "--bind"
&& w[1] == "/host/rw"
&& w[2] == "/box/shared/containers/main/volumes/rw"),
"writable sandbox bind mount must be emitted"
);
assert!(
args.windows(3).any(|w| w[0] == "--ro-bind"
&& w[1] == "/host/ro"
&& w[2] == "/box/shared/containers/main/volumes/ro"),
"read-only sandbox bind mount must be emitted"
);
}
}
1 change: 1 addition & 0 deletions src/boxlite/src/jailer/sandbox/composite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ mod tests {
SandboxContext {
id: "test",
paths: vec![],
bind_mounts: vec![],
resource_limits: limits,
network_enabled: false,
sandbox_profile: None,
Expand Down
1 change: 1 addition & 0 deletions src/boxlite/src/jailer/sandbox/landlock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ mod tests {
SandboxContext {
id: "test",
paths: vec![],
bind_mounts: vec![],
resource_limits: limits,
network_enabled: false,
sandbox_profile: None,
Expand Down
24 changes: 24 additions & 0 deletions src/boxlite/src/jailer/sandbox/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,28 @@ pub struct PathAccess {
pub writable: bool,
}

/// A bind mount performed inside the sandbox mount namespace.
///
/// Unlike [`PathAccess`], source and target are intentionally distinct. The
/// bwrap sandbox uses this for user volumes: host source paths are mounted onto
/// the existing box shared tree that libkrun exports to the guest.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SandboxBindMount {
pub source: PathBuf,
pub target: PathBuf,
pub writable: bool,
}

impl SandboxBindMount {
pub fn new(source: impl Into<PathBuf>, target: impl Into<PathBuf>, writable: bool) -> Self {
Self {
source: source.into(),
target: target.into(),
writable,
}
}
}

// ============================================================================
// SandboxContext
// ============================================================================
Expand All @@ -119,6 +141,8 @@ pub struct SandboxContext<'a> {
pub id: &'a str,
/// Pre-computed filesystem path access rules.
pub paths: Vec<PathAccess>,
/// Bind mounts to perform inside namespace sandboxes.
pub bind_mounts: Vec<SandboxBindMount>,
/// Resource limits (for cgroup configuration).
pub resource_limits: &'a ResourceLimits,
/// Whether network access is enabled.
Expand Down
Loading
Loading