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
8 changes: 6 additions & 2 deletions crates/sandlock-ffi/include/sandlock.h
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,9 @@ sandlock_builder_t *sandlock_sandbox_builder_fs_mount_ro(sandlock_builder_t *b,

/**
* Set the COW branch action on successful exit.
* `action`: 0 = Commit, 1 = Abort, 2 = Keep, 3 = Defer.
* `action`: 0 = Commit, 1 = Abort, 2 = Keep, 3 = Defer. Any other value
* frees the builder and returns null, so the eventual build fails rather
* than guessing an action that may write into the workdir.
*
* # Safety
* `b` must be a valid builder pointer.
Expand All @@ -464,7 +466,9 @@ sandlock_builder_t *sandlock_sandbox_builder_on_exit(sandlock_builder_t *b, uint

/**
* Set the COW branch action on error exit.
* `action`: 0 = Commit, 1 = Abort, 2 = Keep, 3 = Defer.
* `action`: 0 = Commit, 1 = Abort, 2 = Keep, 3 = Defer. Any other value
* frees the builder and returns null, so the eventual build fails rather
* than guessing an action that may write into the workdir.
*
* # Safety
* `b` must be a valid builder pointer.
Expand Down
33 changes: 21 additions & 12 deletions crates/sandlock-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,9 @@ pub unsafe extern "C" fn sandlock_sandbox_builder_fs_mount_ro(
}

/// Set the COW branch action on successful exit.
/// `action`: 0 = Commit, 1 = Abort, 2 = Keep, 3 = Defer.
/// `action`: 0 = Commit, 1 = Abort, 2 = Keep, 3 = Defer. Any other value
/// frees the builder and returns null, so the eventual build fails rather
/// than guessing an action that may write into the workdir.
///
/// # Safety
/// `b` must be a valid builder pointer.
Expand All @@ -317,12 +319,16 @@ pub unsafe extern "C" fn sandlock_sandbox_builder_on_exit(
return b;
}
let builder = *Box::from_raw(b);
let action = branch_action(action);
Box::into_raw(Box::new(builder.on_exit(action)))
match try_branch_action_from_raw(action) {
Some(action) => Box::into_raw(Box::new(builder.on_exit(action))),
None => ptr::null_mut(),
}
}

/// Set the COW branch action on error exit.
/// `action`: 0 = Commit, 1 = Abort, 2 = Keep, 3 = Defer.
/// `action`: 0 = Commit, 1 = Abort, 2 = Keep, 3 = Defer. Any other value
/// frees the builder and returns null, so the eventual build fails rather
/// than guessing an action that may write into the workdir.
///
/// # Safety
/// `b` must be a valid builder pointer.
Expand All @@ -335,16 +341,19 @@ pub unsafe extern "C" fn sandlock_sandbox_builder_on_error(
return b;
}
let builder = *Box::from_raw(b);
let action = branch_action(action);
Box::into_raw(Box::new(builder.on_error(action)))
match try_branch_action_from_raw(action) {
Some(action) => Box::into_raw(Box::new(builder.on_error(action))),
None => ptr::null_mut(),
}
}

fn branch_action(discriminant: u8) -> BranchAction {
match discriminant {
1 => BranchAction::Abort,
2 => BranchAction::Keep,
3 => BranchAction::Defer,
_ => BranchAction::Commit,
fn try_branch_action_from_raw(raw: u8) -> Option<BranchAction> {
match raw {
0 => Some(BranchAction::Commit),
1 => Some(BranchAction::Abort),
2 => Some(BranchAction::Keep),
3 => Some(BranchAction::Defer),
_ => None,
}
}

Expand Down
56 changes: 56 additions & 0 deletions crates/sandlock-ffi/tests/branch_action.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! C ABI `BranchAction` setters: discriminants map onto the Rust enum, and
//! any other value fails the build instead of picking an action.

use std::ptr;

use sandlock_core::sandbox::{BranchAction, SandboxBuilder};
use sandlock_core::Sandbox;
use sandlock_ffi::{
sandlock_sandbox_build, sandlock_sandbox_builder_new, sandlock_sandbox_builder_on_error,
sandlock_sandbox_builder_on_exit,
};

type Setter = unsafe extern "C" fn(*mut SandboxBuilder, u8) -> *mut SandboxBuilder;

const DISCRIMINANTS: [(u8, BranchAction); 4] = [
(0, BranchAction::Commit),
(1, BranchAction::Abort),
(2, BranchAction::Keep),
(3, BranchAction::Defer),
];

fn build_with(setter: Setter, raw: u8) -> Sandbox {
let b = unsafe { setter(sandlock_sandbox_builder_new(), raw) };
assert!(!b.is_null(), "valid discriminant {raw} rejected");
unsafe { *Box::from_raw(b) }.build().expect("build failed")
}

#[test]
fn on_exit_discriminants_match_rust_enum() {
for (raw, action) in DISCRIMINANTS {
assert_eq!(build_with(sandlock_sandbox_builder_on_exit, raw).on_exit, action);
}
}

#[test]
fn on_error_discriminants_match_rust_enum() {
for (raw, action) in DISCRIMINANTS {
assert_eq!(build_with(sandlock_sandbox_builder_on_error, raw).on_error, action);
}
}

#[test]
fn unrecognized_discriminant_fails_the_build() {
let setters: [Setter; 2] = [sandlock_sandbox_builder_on_exit, sandlock_sandbox_builder_on_error];
for setter in setters {
for raw in [4u8, 99, u8::MAX] {
let b = unsafe { setter(sandlock_sandbox_builder_new(), raw) };
assert!(b.is_null(), "discriminant {raw} accepted");

let mut err = 0;
let policy = unsafe { sandlock_sandbox_build(b, &mut err, ptr::null_mut()) };
assert!(policy.is_null());
assert_eq!(err, -1);
}
}
}
5 changes: 5 additions & 0 deletions go/sandlock_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@ func (s *Sandbox) buildPolicy() (*C.sandlock_sandbox_t, error) {
if err := s.validateStrings(); err != nil {
return nil, err
}
for _, a := range []BranchAction{s.OnExit, s.OnError} {
if a > BranchActionDefer {
return nil, fmt.Errorf("sandlock: invalid BranchAction %d", a)
}
}

b := C.sandlock_sandbox_builder_new()

Expand Down
11 changes: 11 additions & 0 deletions go/sandlock_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,17 @@ func TestRunNULRejected(t *testing.T) {
}
}

func TestRunUnknownBranchActionRejected(t *testing.T) {
for _, sb := range []*sandlock.Sandbox{
{OnExit: sandlock.BranchActionDefer + 1},
{OnError: sandlock.BranchAction(255)},
} {
if _, err := sb.Run(context.Background(), "true"); err == nil || !strings.Contains(err.Error(), "invalid BranchAction") {
t.Fatalf("err = %v, want invalid BranchAction", err)
}
}
}

func TestSyscallEventArgvContains(t *testing.T) {
ev := sandlock.SyscallEvent{Argv: []string{"python3", "-c", "print(1)"}}
if !ev.ArgvContains("python") {
Expand Down
10 changes: 5 additions & 5 deletions python/src/sandlock/_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from pathlib import Path
from typing import Any, NamedTuple, Sequence

from .sandbox import Change, Entry, Image, ImageConfig, Sandbox as PolicyDataclass
from .sandbox import BranchAction, Change, Entry, Image, ImageConfig, Sandbox as PolicyDataclass

# ----------------------------------------------------------------
# Load the shared library
Expand Down Expand Up @@ -1201,13 +1201,13 @@ def _build_from_policy(policy: PolicyDataclass):

# COW branch actions (0=Commit, 1=Abort, 2=Keep, 3=Defer)
_action_map = {"commit": 0, "abort": 1, "keep": 2, "defer": 3}
on_exit_val = policy.on_exit.value if hasattr(policy.on_exit, 'value') else str(policy.on_exit)
on_error_val = policy.on_error.value if hasattr(policy.on_error, 'value') else str(policy.on_error)
on_exit_val = BranchAction(policy.on_exit).value
on_error_val = BranchAction(policy.on_error).value
# An image's writes are always discarded, and the native build rejects
# any other exit action, so leave the COMMIT default unsent for it.
if policy.image is None or on_exit_val != "commit":
b = _b_on_exit(b, _action_map.get(on_exit_val, 0))
b = _b_on_error(b, _action_map.get(on_error_val, 1))
b = _b_on_exit(b, _action_map[on_exit_val])
b = _b_on_error(b, _action_map[on_error_val])

if policy.max_memory is not None:
if isinstance(policy.max_memory, str):
Expand Down
2 changes: 2 additions & 0 deletions python/src/sandlock/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,8 @@ def __post_init__(self):
raise ValueError("sandbox name must not contain '/'")
if self.name in (".", ".."):
raise ValueError("sandbox name must not be '.' or '..'")
self.on_exit = BranchAction(self.on_exit)
self.on_error = BranchAction(self.on_error)
# Runtime state — not dataclass fields, not serialized
self._native = None # _NativePolicy created lazily on first use
self._handle = None # live sandbox handle during start()/run()
Expand Down
9 changes: 9 additions & 0 deletions python/tests/test_sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,15 @@ def test_resume_not_running_raises(self):
class TestBranchAction:
"""Every run reports its changes; DEFER hands the disposition to the caller."""

def test_string_action_is_coerced(self):
p = Sandbox(on_exit="abort", on_error="keep")
assert (p.on_exit, p.on_error) == (BranchAction.ABORT, BranchAction.KEEP)

@pytest.mark.parametrize("field", ["on_exit", "on_error"])
def test_unknown_action_is_rejected(self, field):
with pytest.raises(ValueError):
Sandbox(**{field: "Abort"})

def test_abort_reports_added_file_without_creating_it(self, tmp_path):
workdir = tmp_path / "add"
workdir.mkdir()
Expand Down
Loading