Skip to content
Closed
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
16 changes: 7 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,15 +385,13 @@ positive int = deny with errno, `"audit"`/`-2` = allow + flag.
> belongs in static Landlock rules (`fs_readable` / `fs_writable` /
> `fs_denied`) — kernel-enforced and TOCTOU-immune. Use
> `ctx.deny_path()` for runtime additions.
> - **`event.argv` is exposed and TOCTOU-safe.** Before exposing
> `argv` to `policy_fn` or returning `Continue` for an
> `execve`, the supervisor freezes every task in `ProcessIndex`,
> including peer processes that may alias argv through shared memory.
> With `policy_fn` active, fork-like syscalls are traced for one
> ptrace creation event, so children are registered in `ProcessIndex`
> before they can run user code. If the freeze or creation tracking
> cannot be established (e.g., YAMA blocks ptrace), the syscall is
> denied with `EPERM`; the safety invariant is never silently relaxed.
> - **`event.argv` is exposed and TOCTOU-safe.** The supervisor reads
> `argv` once and, on `Continue`, redirects the `execve` to a small
> static relay program delivered in a sealed memfd at an fd number the
> sandbox cannot repopulate. The relay execs the target with exactly the
> argv the policy saw, so a sibling thread or `CLONE_VM` peer rewriting
> the original memory changes nothing. No task is stopped and no ptrace
> is involved; the one visible cost is an extra `execve` per spawn.

**Context methods:**
- `ctx.restrict_network(ips)` / `ctx.grant_network(ips)` — network control
Expand Down
32 changes: 32 additions & 0 deletions crates/sandlock-core/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,38 @@ fn main() {
// Emit the path every run (rustc-env is not cached across build-script runs),
// whether or not the binary was just (re)built.
println!("cargo:rustc-env=RESTORE_STUB_PATH={}", stub_bin.display());

// exec-relay: the supervisor execs it in place of every policy-checked
// execve, so unlike the restore stub it is embedded into the crate and
// must build for every target; a missing compiler is a hard error.
let relay_src = manifest_dir.join("src/exec_relay/relay.c");
let relay_bin = out_dir.join("exec-relay");
let relay_ccs: &[&str] = if is_riscv64 && !host.starts_with("riscv64") {
&["riscv64-linux-gnu-gcc", "riscv64-unknown-linux-gnu-gcc"]
} else if target.starts_with("aarch64") && !host.starts_with("aarch64") {
&["aarch64-linux-gnu-gcc"]
} else {
&["cc"]
};
if !build_static(
&relay_src,
&relay_bin,
relay_ccs,
&[
"-static",
"-nostdlib",
"-no-pie",
"-fPIE",
"-O2",
"-ffreestanding",
"-fno-builtin",
"-fno-tree-loop-distribute-patterns",
"-fno-stack-protector",
],
) {
panic!("failed to compile exec-relay for {target}: no working C compiler");
}
println!("cargo:rustc-env=EXEC_RELAY_PATH={}", relay_bin.display());
}

/// Compile `src` to `bin` with the first working compiler in `ccs`, skipping the
Expand Down
1 change: 1 addition & 0 deletions crates/sandlock-core/src/arch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ legacy_syscall!(sys_chown, "chown");
legacy_syscall!(sys_lchown, "lchown");
legacy_syscall!(sys_vfork, "vfork");
legacy_syscall!(sys_fork, "fork");
legacy_syscall!(sys_dup2, "dup2");

/// `renameat` syscall number on this architecture, or `None` where the ABI
/// omits it. Unlike the legacy syscalls above it survived into the generic
Expand Down
7 changes: 5 additions & 2 deletions crates/sandlock-core/src/context/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,18 @@ fn test_notif_syscalls_always_has_clone() {
}
}

/// Bare fork(2) stays out of the filter even under policy_fn: the exec
/// relay needs no fork-time child registration, so hot fork loops keep
/// bypassing the supervisor.
#[test]
fn test_notif_syscalls_fork_gated_on_policy_fn() {
fn test_notif_syscalls_fork_not_intercepted_under_policy_fn() {
let Some(fork) = arch::sys_fork() else { return };
let policy = Sandbox::builder()
.policy_fn(|_event, _ctx| crate::policy_fn::Verdict::Allow)
.build()
.unwrap();
let nrs = notif_syscalls(&policy, None);
assert!(nrs.contains(&(fork as u32)));
assert!(!nrs.contains(&(fork as u32)));
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion crates/sandlock-core/src/cow/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ fn resolve_at_path_with_virtual(
}
}

fn map_cow_upper_path(cow: &SeccompCowBranch, path: &str) -> String {
pub(crate) fn map_cow_upper_path(cow: &SeccompCowBranch, path: &str) -> String {
let path = PathBuf::from(path);
if let Ok(rel) = path.strip_prefix(cow.upper_dir()) {
return normalize_path(cow.workdir().join(rel)).to_string_lossy().into_owned();
Expand Down
Loading
Loading