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
53 changes: 35 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,31 +41,48 @@ jobs:
timeout-minutes: 20
run: cargo test --release --workspace --no-fail-fast -- --test-threads=1

rust-minimal:
name: Rust without optional features
rust-features:
name: Rust features (${{ matrix.feature }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
feature: [minimal, http, checkpoint, image, profiles, cow]
steps:
- uses: actions/checkout@v4

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Rust cache
uses: Swatinem/rust-cache@v2

- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
key: features-${{ matrix.feature }}
- name: Allow unprivileged user namespaces
run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0

- name: Minimal build pulls in no HTTP stack
- name: Check production dependency graph
env:
FEATURE: ${{ matrix.feature }}
run: |
if cargo tree --workspace --no-default-features -e normal --prefix none \
| grep -E '^(hyper|rustls|rcgen|bollard) '; then
exit 1
fi

- name: Run all tests
args=(--no-default-features)
if [ "$FEATURE" != minimal ]; then args+=(--features "$FEATURE"); fi
cargo tree --workspace "${args[@]}" -e normal --prefix none > /tmp/dependencies.txt
case "$FEATURE" in
minimal) forbidden='bincode|toml|tar|flate2|ruzstd|base64|walkdir|pathdiff|ring|hyper|rustls|rcgen|bollard' ;;
http) forbidden='bincode|toml|tar|flate2|ruzstd|walkdir|pathdiff|bollard' ;;
checkpoint) forbidden='toml|tar|flate2|ruzstd|walkdir|pathdiff|ring|hyper|rustls|rcgen|bollard' ;;
image) forbidden='bincode|toml|rcgen' ;;
profiles) forbidden='bincode|tar|flate2|ruzstd|walkdir|pathdiff|ring|hyper|rustls|rcgen|bollard' ;;
cow) forbidden='bincode|toml|tar|flate2|ruzstd|hyper|rustls|rcgen|bollard' ;;
esac
if grep -E "^($forbidden) " /tmp/dependencies.txt; then exit 1; fi
- name: Test selected features
timeout-minutes: 20
run: cargo test --release --workspace --no-default-features --no-fail-fast -- --test-threads=1
env:
FEATURE: ${{ matrix.feature }}
run: |
case "$FEATURE" in
minimal) args=(--no-default-features) ;;
*) args=(--no-default-features --features "$FEATURE") ;;
esac
cargo test --release --workspace "${args[@]}" --no-fail-fast -- --test-threads=1


rust-low-abi:
name: Rust low-ABI (ubuntu-22.04)
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,19 @@ cd python && pip install -e .
cargo install --path crates/sandlock-cli
```

### Optional features

All five features (`http`, `checkpoint`, `image`, `profiles`, `cow`) are enabled
by default. Build with none or select the ones you need:

```bash
cargo build --release --no-default-features
cargo build --release --no-default-features --features http,profiles
```

See [Building Sandlock](docs/building.md) for feature dependencies and Rust
library configuration.

## Quick Start

### CLI
Expand Down Expand Up @@ -181,6 +194,7 @@ confine(&confinement)?;

| Document | Contents |
|---|---|
| [`docs/building.md`](docs/building.md) | Build commands, optional features, and minimal Rust embedding |
| [`docs/cli.md`](docs/cli.md) | Every `sandlock run` option by example, profiles, `ps` / `inspect` / `kill` |
| [`docs/sandbox-reference.md`](docs/sandbox-reference.md) | Every `Sandbox` field, default, and TOML key; credentials; protection opt-out |
| [`docs/network.md`](docs/network.md) | Endpoint grammar, protocol gating, HTTP interception, bind rules, port virtualization |
Expand Down
8 changes: 6 additions & 2 deletions crates/sandlock-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,19 @@ name = "sandlock"
path = "src/main.rs"

[features]
default = ["http"]
cow = ["sandlock-core/cow"]
profiles = ["sandlock-core/profiles", "dep:toml"]
image = ["cow", "sandlock-core/image"]
checkpoint = ["sandlock-core/checkpoint"]
default = ["http", "checkpoint", "image", "profiles", "cow"]
http = ["sandlock-core/http"]

[dependencies]
sandlock-core = { version = "0.8.9", path = "../sandlock-core", default-features = false, features = ["cli"] }
clap = { version = "4", features = ["derive"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
anyhow = "1"
toml = "0.8"
toml = { version = "0.8", optional = true }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
libc = "0.2"
Expand Down
14 changes: 14 additions & 0 deletions crates/sandlock-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use sandlock_core::profile;
use anyhow::{Result, anyhow};
use std::path::PathBuf;

#[cfg(all(feature = "profiles", feature = "cow"))]
mod learn;
#[derive(Parser)]
#[command(name = "sandlock", about = "Lightweight process sandbox", version)]
Expand Down Expand Up @@ -321,12 +322,17 @@ async fn main() -> Result<()> {
Ok(resp) if resp.ok => {
if let Some(data) = resp.data {
if toml {
#[cfg(not(feature = "profiles"))]
return Err(anyhow!("TOML output requires the profiles feature"));
#[cfg(feature = "profiles")]
{
// Round-trip through ProfileInput -> TOML.
let profile: sandlock_core::ProfileInput =
serde_json::from_value(data)
.map_err(|e| anyhow!("parse config response: {}", e))?;
let toml_str = toml::to_string_pretty(&profile)?;
println!("{}", toml_str);
}
} else {
let json_str = serde_json::to_string_pretty(&data)?;
println!("{}", json_str);
Expand Down Expand Up @@ -413,7 +419,10 @@ async fn main() -> Result<()> {
}

Command::Learn(args) => {
#[cfg(all(feature = "profiles", feature = "cow"))]
learn::run(args).await?;
#[cfg(not(all(feature = "profiles", feature = "cow")))]
return Err(anyhow!("learning profiles requires the profiles and cow features"));
}

Command::Profile { action } => {
Expand Down Expand Up @@ -645,9 +654,14 @@ async fn run_command(args: RunArgs) -> Result<i32> {

let image_cmd: Option<Vec<String>>;
if let Some(ref img) = args.image {
#[cfg(not(feature = "image"))]
return Err(anyhow!("image support is disabled; enable the image feature"));
#[cfg(feature = "image")]
{
let image = sandlock_core::image::pull(img, None).await?;
builder = builder.image(&image);
image_cmd = args.cmd.is_empty().then(|| image.config.default_cmd());
}
} else {
image_cmd = None;
}
Expand Down
5 changes: 5 additions & 0 deletions crates/sandlock-cli/tests/cli_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ fn test_run_hostname_virtualized() {
}

#[test]
#[cfg(feature = "profiles")]
fn test_profile_list_empty() {
let output = sandlock_bin()
.args(["profile", "list"])
Expand Down Expand Up @@ -277,6 +278,7 @@ fn test_no_supervisor_exit_code() {
/// stayed orphaned in `/tmp/sandlock-cow-*/upper/` and never appeared
/// in the workdir, even though the default `on_exit` is `commit`.
#[test]
#[cfg(feature = "cow")]
fn test_cow_commit_runs_on_cli_exit() {
let workdir = tempfile::tempdir().expect("tempdir");
let sentinel = workdir.path().join("sentinel.txt");
Expand Down Expand Up @@ -366,6 +368,7 @@ fn test_uid_mapping_arbitrary_uid() {
/// `sandlock run --profile-file` prints a hint to stderr suggesting
/// `sandlock learn --merge` when the sandboxed process exits non-zero.
#[test]
#[cfg(all(feature = "profiles", feature = "cow"))]
fn test_run_denial_hint_on_nonzero_exit() {
let profile = tempfile::NamedTempFile::new().expect("tempfile");
let profile_path = profile.path().to_str().unwrap().to_owned();
Expand Down Expand Up @@ -585,6 +588,7 @@ fn test_inspect_returns_json_policy() {
}

#[test]
#[cfg(feature = "profiles")]
fn test_inspect_toml_flag_produces_toml() {
let name = format!("test-config-toml-cli-{}", std::process::id());
let mut child = spawn_sandbox(&name);
Expand Down Expand Up @@ -696,6 +700,7 @@ fn test_help_shows_ps_and_inspect() {
/// `--dry-run` is a run with both branch actions forced to `abort`: the
/// change list is reported on stderr and nothing lands in the workdir.
#[test]
#[cfg(feature = "cow")]
fn test_dry_run_reports_changes_and_writes_nothing() {
let workdir = tempfile::tempdir().expect("tempdir");
let sentinel = workdir.path().join("planned.txt");
Expand Down
2 changes: 2 additions & 0 deletions crates/sandlock-cli/tests/learn_integration.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![cfg(all(feature = "profiles", feature = "cow"))]

// Each test here spawns a full `sandlock` process.
// Running all tests in parallel exhausts kernel resources on most hosts.
// Limit parallelism when running this suite:
Expand Down
2 changes: 2 additions & 0 deletions crates/sandlock-cli/tests/learn_test.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![cfg(all(feature = "profiles", feature = "cow"))]

// Each test here spawns a full `sandlock` process.
// Running all tests in parallel exhausts kernel resources on most hosts.
// Limit parallelism when running this suite:
Expand Down
2 changes: 2 additions & 0 deletions crates/sandlock-cli/tests/profile_integration.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![cfg(feature = "profiles")]

use std::process::Command;

fn sandlock_bin() -> Command {
Expand Down
27 changes: 16 additions & 11 deletions crates/sandlock-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,37 +19,42 @@ rand = "0.8"
rand_chacha = "0.3"
goblin = "0.9"
uuid = { version = "1", features = ["v4"] }
bincode = "1"
bincode = { version = "1", optional = true }
serde_json = "1"
walkdir = "2"
ring = "0.17"
toml = "0.8"
walkdir = { version = "2", optional = true }
ring = { version = "0.17", optional = true }
toml = { version = "0.8", optional = true }
jiff = "0.2"
pathdiff = "0.2"
pathdiff = { version = "0.2", optional = true }
tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"], optional = true }
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"], optional = true }
rcgen = { version = "0.13", features = ["pem", "x509-parser"], optional = true }
hyper = { version = "1", features = ["server", "client", "http1"], optional = true }
hyper-util = { version = "0.1", features = ["client-legacy", "http1", "tokio"], optional = true }
http-body-util = { version = "0.1", optional = true }
hyper-rustls = { version = "0.27", default-features = false, features = ["ring", "native-tokio", "http1", "tls12", "logging"], optional = true }
tar = "0.4"
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
ruzstd = "0.9"
base64 = "0.22"
tar = { version = "0.4", optional = true }
flate2 = { version = "1", default-features = false, features = ["rust_backend"], optional = true }
ruzstd = { version = "0.9", optional = true }
base64 = { version = "0.22", optional = true }
clap = { version = "4", features = ["derive"], optional = true }
bollard = { version = "0.21", optional = true }
futures-util = { version = "0.3", optional = true }

[features]
default = ["http"]
cow = ["dep:walkdir", "dep:pathdiff", "dep:ring"]
profiles = ["dep:toml"]
image = ["cow", "dep:tar", "dep:flate2", "dep:ruzstd", "dep:base64", "dep:bollard", "dep:futures-util"]
checkpoint = ["dep:bincode"]
default = ["http", "checkpoint", "image", "profiles", "cow"]
cli = ["dep:clap"]
http = [
"dep:hyper", "dep:hyper-util", "dep:http-body-util", "dep:hyper-rustls",
"dep:tokio-rustls", "dep:rustls", "dep:rcgen", "dep:bollard", "dep:futures-util",
"dep:tokio-rustls", "dep:rustls", "dep:rcgen",
]

[dev-dependencies]
toml = "0.8"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "test-util"] }
tempfile = "3"
rustls-pemfile = "2"
Expand Down
Loading
Loading