From ef5416afdfa959c81e67a8a44018cc32ec841f2c Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Mon, 1 Jun 2026 17:00:41 -0700 Subject: [PATCH 1/2] ffi: generate sandlock.h with cbindgen Signed-off-by: Cong Wang --- .github/workflows/ci.yml | 23 + crates/sandlock-ffi/build.rs | 6 +- crates/sandlock-ffi/cbindgen.toml | 75 ++ crates/sandlock-ffi/include/sandlock.h | 1560 +++++++++++++++++++----- crates/sandlock-ffi/src/handler/abi.rs | 18 +- 5 files changed, 1367 insertions(+), 315 deletions(-) create mode 100644 crates/sandlock-ffi/cbindgen.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9104b997..52e953c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,6 +58,29 @@ jobs: CC_riscv64gc_unknown_linux_gnu: riscv64-linux-gnu-gcc run: cargo build --release --target riscv64gc-unknown-linux-gnu + cbindgen-header: + name: cbindgen header up to date + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Install cbindgen + run: cargo install cbindgen --version 0.29.2 --locked + + # Regenerate include/sandlock.h in place and fail if it differs from the + # committed copy, so the C ABI header can never drift from the crate's + # #[no_mangle] definitions. + - name: Regenerate header and check it is up to date + run: | + cbindgen --config crates/sandlock-ffi/cbindgen.toml \ + --crate sandlock-ffi \ + --output crates/sandlock-ffi/include/sandlock.h + git diff --exit-code crates/sandlock-ffi/include/sandlock.h \ + || { echo "::error::sandlock.h is stale; regenerate it with cbindgen (see crates/sandlock-ffi/cbindgen.toml)"; exit 1; } + python: name: Python tests (${{ matrix.runner }}, py${{ matrix.python-version }}) runs-on: ${{ matrix.runner }} diff --git a/crates/sandlock-ffi/build.rs b/crates/sandlock-ffi/build.rs index 776dc80c..89d67877 100644 --- a/crates/sandlock-ffi/build.rs +++ b/crates/sandlock-ffi/build.rs @@ -1,4 +1,8 @@ fn main() { - // Header is hand-maintained — no auto-generation. + // include/sandlock.h is generated from this crate's #[no_mangle] exports + // by cbindgen, not produced here: the build does not run cbindgen so a + // plain `cargo build` needs no extra tooling. Regenerate the header with + // the command in cbindgen.toml after changing the C ABI; CI checks that the + // committed header matches a fresh generation. // Run `cargo build -p sandlock-ffi` then find the .so in target/release/. } diff --git a/crates/sandlock-ffi/cbindgen.toml b/crates/sandlock-ffi/cbindgen.toml new file mode 100644 index 00000000..6cefada4 --- /dev/null +++ b/crates/sandlock-ffi/cbindgen.toml @@ -0,0 +1,75 @@ +# Configuration for generating include/sandlock.h with cbindgen. +# +# Regenerate the header with: +# cbindgen --config crates/sandlock-ffi/cbindgen.toml \ +# --crate sandlock-ffi --output crates/sandlock-ffi/include/sandlock.h +# +# CI verifies the committed header matches a fresh generation, so the C ABI +# contract can never drift from the #[no_mangle] definitions in src/. + +language = "C" +style = "type" +include_guard = "SANDLOCK_H" +cpp_compat = true +documentation = true +documentation_style = "doxy" + +no_includes = true +sys_includes = ["stdint.h", "stddef.h", "stdbool.h"] + +header = """/* + * sandlock C API — opaque handle bindings to sandlock-core. + * + * GENERATED BY cbindgen. DO NOT EDIT THIS FILE BY HAND. + * See crates/sandlock-ffi/cbindgen.toml for the regeneration command. + * + * All pointer types are opaque handles; free each with its _free(). + * Builder functions consume and return the builder (move semantics). + */""" + +# Opaque handle types forward-declared by hand: +# - sandlock_builder_t / sandlock_t (renamed below from the sandlock-core +# types SandboxBuilder / Sandbox) are foreign; cbindgen does not parse the +# dependency crate (parse_deps = false), so it cannot emit them itself, yet +# they appear behind pointers in this ABI. +# - sandlock_sandbox_t / sandlock_result_t / sandlock_handler_t are #[repr(C)] +# wrappers around non-C inner types; they are opaque to C (only ever used +# behind pointers), so their bodies are excluded below and forward-declared +# here instead. +after_includes = """ + +typedef struct sandlock_builder_t sandlock_builder_t; +typedef struct sandlock_t sandlock_t; +typedef struct sandlock_sandbox_t sandlock_sandbox_t; +typedef struct sandlock_result_t sandlock_result_t; +typedef struct sandlock_handler_t sandlock_handler_t; +""" + +[parse] +parse_deps = false + +[export] +# Force-emit the two action/exception enums: they are referenced from Rust only +# via `... as u32` casts, never as a typed field or parameter, so cbindgen would +# otherwise drop them as unreferenced even though their constants are public ABI. +include = ["sandlock_action_kind_t", "sandlock_exception_policy_t"] +exclude = [ + "SandboxBuilder", + "Sandbox", + "sandlock_sandbox_t", + "sandlock_result_t", + "sandlock_handler_t", +] + +[export.rename] +"SandboxBuilder" = "sandlock_builder_t" +"Sandbox" = "sandlock_t" +# Rename the enum *types* so prefix_with_name yields the established C constant +# names (SANDLOCK_EXCEPTION_KILL, SANDLOCK_ACTION_INJECT_FD_SEND, ...) rather +# than the verbose SANDLOCK_EXCEPTION_POLICY_T_* / SANDLOCK_ACTION_KIND_T_*. +"sandlock_exception_policy_t" = "sandlock_exception" +"sandlock_action_kind_t" = "sandlock_action" + +[enum] +rename_variants = "ScreamingSnakeCase" +prefix_with_name = true diff --git a/crates/sandlock-ffi/include/sandlock.h b/crates/sandlock-ffi/include/sandlock.h index 43a2b011..86385d23 100644 --- a/crates/sandlock-ffi/include/sandlock.h +++ b/crates/sandlock-ffi/include/sandlock.h @@ -1,7 +1,10 @@ -/** +/* * sandlock C API — opaque handle bindings to sandlock-core. * - * All pointer types are opaque handles. Free with the corresponding _free(). + * GENERATED BY cbindgen. DO NOT EDIT THIS FILE BY HAND. + * See crates/sandlock-ffi/cbindgen.toml for the regeneration command. + * + * All pointer types are opaque handles; free each with its _free(). * Builder functions consume and return the builder (move semantics). */ @@ -9,397 +12,1330 @@ #define SANDLOCK_H #include +#include #include +typedef struct sandlock_builder_t sandlock_builder_t; +typedef struct sandlock_t sandlock_t; +typedef struct sandlock_sandbox_t sandlock_sandbox_t; +typedef struct sandlock_result_t sandlock_result_t; +typedef struct sandlock_handler_t sandlock_handler_t; + + +/** + * `flags` bit for [`sandlock_action_set_inject_bytes`]: leave the injected + * memfd writable (do not seal). Default (bit clear) seals it read-only. + */ +#define SANDLOCK_INJECT_WRITABLE (1 << 0) + +/** + * `flags` bit for [`sandlock_action_set_inject_bytes`]: do not set + * `O_CLOEXEC` on the child-side fd. Default (bit clear) sets `O_CLOEXEC`. + */ +#define SANDLOCK_INJECT_NO_CLOEXEC (1 << 1) + +/** + * Tag distinguishing payload variants of `sandlock_action_out_t`. + */ +enum sandlock_action #ifdef __cplusplus -extern "C" { -#endif + : uint32_t +#endif // __cplusplus + { + /** + * No action set yet; the supervisor treats this as "fall through to + * the handler's on_exception policy" (see `exception_action` in + * `FfiHandler`). + */ + SANDLOCK_ACTION_UNSET = 0, + SANDLOCK_ACTION_CONTINUE = 1, + SANDLOCK_ACTION_ERRNO = 2, + SANDLOCK_ACTION_RETURN_VALUE = 3, + SANDLOCK_ACTION_INJECT_FD_SEND = 4, + SANDLOCK_ACTION_INJECT_FD_SEND_TRACKED = 5, + SANDLOCK_ACTION_HOLD = 6, + SANDLOCK_ACTION_KILL = 7, +}; +#ifndef __cplusplus +typedef uint32_t sandlock_action; +#endif // __cplusplus + +/** + * Exception policy applied when the handler callback fails to set a + * valid action (returns non-zero rc, leaves `kind == Unset`, or panics + * across the FFI boundary). + */ +enum sandlock_exception +#ifdef __cplusplus + : uint32_t +#endif // __cplusplus + { + /** + * Treat the failure as `NotifAction::Kill { sig: SIGKILL, pgid: child_pgid }`. + * Default; "fail-closed" — the safe option. + */ + SANDLOCK_EXCEPTION_KILL = 0, + /** + * Treat the failure as `NotifAction::Errno(EPERM)`. Useful for + * audit-style handlers where the syscall is what failed rather than + * the supervisor. + */ + SANDLOCK_EXCEPTION_DENY_EPERM = 1, + /** + * Treat the failure as `NotifAction::Continue`. Explicit fail-open; + * only safe when the syscall is *also* allowed by the BPF filter and + * Landlock layer (e.g. observability handlers). + */ + SANDLOCK_EXCEPTION_CONTINUE = 2, + /** + * Treat the failure as `NotifAction::Errno(EIO)`. Idiomatic for + * audit-only handlers: EIO propagates to the caller as a plain + * `OSError` rather than `PermissionError`, which is closer to what + * callers expect from a failed syscall. + */ + SANDLOCK_EXCEPTION_DENY_EIO = 3, +}; +#ifndef __cplusplus +typedef uint32_t sandlock_exception; +#endif // __cplusplus + +/** + * Opaque handle wrapping a [`Checkpoint`]. + */ +typedef struct sandlock_checkpoint_t sandlock_checkpoint_t; + +/** + * C-compatible policy context handle. + */ +typedef struct sandlock_ctx_t sandlock_ctx_t; + +/** + * Opaque dry-run result. + */ +typedef struct sandlock_dry_run_result_t sandlock_dry_run_result_t; + +/** + * Opaque handle for fork result (holds clone handles with pipes). + */ +typedef struct sandlock_fork_result_t sandlock_fork_result_t; + +typedef struct sandlock_gather_t sandlock_gather_t; + +/** + * Opaque handle for a live sandbox. + * + * Owns both the Sandbox and a small Tokio runtime that drives its + * supervisor. Live handles need a runtime whose spawned tasks keep + * progressing after `sandlock_start` returns. + */ +typedef struct sandlock_handle_t sandlock_handle_t; + +/** + * Opaque handle wrapping a [`Pipeline`]. + */ +typedef struct sandlock_pipeline_t sandlock_pipeline_t; + +/** + * C-compatible syscall event passed to the policy callback. + * + * Path strings are intentionally absent (issue #27); use static Landlock + * rules for path-based access control. argv is exposed for execve only + * and is TOCTOU-safe via sibling-thread freeze before Continue. + */ +typedef struct { + const char *syscall; + /** + * Category: 0=File, 1=Network, 2=Process, 3=Memory + */ + uint8_t category; + uint32_t pid; + uint32_t parent_pid; + const char *host; + uint16_t port; + bool denied; + const char *const *argv; + uint32_t argc; +} sandlock_event_t; + +/** + * C callback type for policy_fn. + * Return value: 0 = allow, -1 = deny (EPERM), -2 = audit (allow + flag), + * positive = deny with that errno (e.g. 13 = EACCES). + */ +typedef int32_t (*sandlock_policy_fn_t)(const sandlock_event_t *event, sandlock_ctx_t *ctx); + +/** + * C callback types for fork init and work functions. + */ +typedef void (*sandlock_init_fn_t)(void); + +typedef void (*sandlock_work_fn_t)(uint32_t clone_id); + +/** + * Opaque child-memory accessor handed to a C handler callback. + * + * Constructed on the stack inside the Rust adapter just before the + * callback fires, invalidated when the callback returns. C handlers + * must not store the pointer beyond the callback's return. + */ +typedef struct { + int notif_fd; + uint64_t notif_id; + uint32_t pid; +} sandlock_mem_handle_t; + +typedef struct { + /** + * Owned by the C caller; ownership transfers to the supervisor on + * successful invocation of the corresponding setter. + */ + int32_t srcfd; + uint32_t newfd_flags; +} sandlock_action_inject_t; + +/** + * Token reserved for a future tracker-aware inject variant. Currently + * unimplemented — kept as a type alias so the ABI of the + * `sandlock_action_inject_tracked_t` payload stays stable across the + * future release that wires the tracker callback. + */ +typedef uint64_t sandlock_inject_tracker_t; + +typedef struct { + int32_t srcfd; + uint32_t newfd_flags; + sandlock_inject_tracker_t tracker; +} sandlock_action_inject_tracked_t; + +typedef struct { + int32_t sig; + int32_t pgid; +} sandlock_action_kill_t; + +typedef union { + uint64_t none; + /** + * `errno_value` rather than `errno` to mirror the C header field + * (the C side avoids the name `errno` because `` macros + * it). Keeping both languages in sync removes a documentation + * hazard for callers that grep across Rust and C sources. + */ + int32_t errno_value; + int64_t return_value; + sandlock_action_inject_t inject_send; + sandlock_action_inject_tracked_t inject_send_tracked; + sandlock_action_kill_t kill; +} sandlock_action_payload_t; + +typedef struct { + uint32_t kind; + sandlock_action_payload_t payload; +} sandlock_action_out_t; + +/** + * Stable wire-layout snapshot of a seccomp notification. + * + * Field order, types, and padding must match `sandlock.h` exactly. The + * size assertion in `tests/handler_smoke.rs` guards against accidental + * drift; if a new field is added, bump the documented size and update + * the C header in the same commit. + */ +typedef struct { + uint64_t id; + uint32_t pid; + uint32_t flags; + int32_t syscall_nr; + uint32_t arch; + uint64_t instruction_pointer; + uint64_t args[6]; +} sandlock_notif_data_t; -/* Opaque handle types */ -typedef void sandlock_builder_t; -typedef void sandlock_sandbox_t; -typedef void sandlock_result_t; -typedef void sandlock_pipeline_t; +/** + * C-side pair of `(syscall_nr, handler*)` consumed by + * `sandlock_run_with_handlers`. Ownership of `handler` transfers into + * the run on success; the supervisor frees the container. + */ +typedef struct { + int64_t syscall_nr; + sandlock_handler_t *handler; +} sandlock_handler_registration_t; -/* ---------------------------------------------------------------- - * Sandbox Builder - * ---------------------------------------------------------------- */ +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus sandlock_builder_t *sandlock_sandbox_builder_new(void); -/* Filesystem */ +/** + * # Safety + * `b` and `path` must be valid pointers. + */ sandlock_builder_t *sandlock_sandbox_builder_fs_read(sandlock_builder_t *b, const char *path); + +/** + * # Safety + * `b` and `path` must be valid pointers. + */ sandlock_builder_t *sandlock_sandbox_builder_fs_write(sandlock_builder_t *b, const char *path); + +/** + * # Safety + * `b` and `path` must be valid pointers. + */ sandlock_builder_t *sandlock_sandbox_builder_fs_deny(sandlock_builder_t *b, const char *path); + +/** + * # Safety + * `b` and `path` must be valid pointers. + */ +sandlock_builder_t *sandlock_sandbox_builder_fs_storage(sandlock_builder_t *b, const char *path); + +/** + * # Safety + * `b` must be a valid pointer. `devices` must point to `len` u32 values (or be null when len == 0). + */ +sandlock_builder_t *sandlock_sandbox_builder_gpu_devices(sandlock_builder_t *b, + const uint32_t *devices, + uint32_t len); + +/** + * # Safety + * `b` and `path` must be valid pointers. + */ sandlock_builder_t *sandlock_sandbox_builder_workdir(sandlock_builder_t *b, const char *path); + +/** + * # Safety + * `b` and `path` must be valid pointers. + */ +sandlock_builder_t *sandlock_sandbox_builder_cwd(sandlock_builder_t *b, const char *path); + +/** + * # Safety + * `b` and `path` must be valid pointers. + */ sandlock_builder_t *sandlock_sandbox_builder_chroot(sandlock_builder_t *b, const char *path); -/* Resource limits */ +/** + * Add a filesystem mount mapping (virtual_path -> host_path). + * + * # Safety + * `b`, `virtual_path`, and `host_path` must be valid pointers. + */ +sandlock_builder_t *sandlock_sandbox_builder_fs_mount(sandlock_builder_t *b, + const char *virtual_path, + const char *host_path); + +/** + * Set the COW branch action on successful exit. + * `action`: 0 = Commit, 1 = Abort, 2 = Keep. + * + * # Safety + * `b` must be a valid builder pointer. + */ +sandlock_builder_t *sandlock_sandbox_builder_on_exit(sandlock_builder_t *b, uint8_t action); + +/** + * Set the COW branch action on error exit. + * `action`: 0 = Commit, 1 = Abort, 2 = Keep. + * + * # Safety + * `b` must be a valid builder pointer. + */ +sandlock_builder_t *sandlock_sandbox_builder_on_error(sandlock_builder_t *b, uint8_t action); + +/** + * # Safety + * `b` must be a valid builder pointer. + */ sandlock_builder_t *sandlock_sandbox_builder_max_memory(sandlock_builder_t *b, uint64_t bytes); + +/** + * # Safety + * `b` must be a valid builder pointer. + */ +sandlock_builder_t *sandlock_sandbox_builder_max_disk(sandlock_builder_t *b, uint64_t bytes); + +/** + * # Safety + * `b` must be a valid builder pointer. + */ sandlock_builder_t *sandlock_sandbox_builder_max_processes(sandlock_builder_t *b, uint32_t n); + +/** + * # Safety + * `b` must be a valid builder pointer. + */ sandlock_builder_t *sandlock_sandbox_builder_max_cpu(sandlock_builder_t *b, uint8_t pct); + +/** + * # Safety + * `b` must be a valid builder pointer. + */ sandlock_builder_t *sandlock_sandbox_builder_num_cpus(sandlock_builder_t *b, uint32_t n); -/* Network */ -/* `spec` is `host:port[,port,...]` (IP-restricted) or `:port` / `*:port` - * (any IP). Validated when the sandbox is built. */ +/** + * # Safety + * `b` must be a valid builder pointer. `cores` must point to `len` u32 values. + */ +sandlock_builder_t *sandlock_sandbox_builder_cpu_cores(sandlock_builder_t *b, + const uint32_t *cores, + uint32_t len); + +/** + * Append a `--net-allow` endpoint rule. `spec` is `host:port[,port,...]`, + * `:port`, or `*:port`. Spec is validated when the policy is built; + * invalid specs surface as a build error. + * + * # Safety + * `b` and `spec` must be valid pointers. + */ sandlock_builder_t *sandlock_sandbox_builder_net_allow(sandlock_builder_t *b, const char *spec); + +/** + * # Safety + * `b` must be a valid builder pointer. + */ sandlock_builder_t *sandlock_sandbox_builder_net_bind_port(sandlock_builder_t *b, uint16_t port); + +/** + * # Safety + * `b` must be a valid builder pointer. + */ sandlock_builder_t *sandlock_sandbox_builder_port_remap(sandlock_builder_t *b, bool v); -/* Protocol gating (UDP, ICMP) is expressed via net_allow rule schemes - * (`udp://`, `icmp://`) — there are no separate boolean setters. */ -/* Isolation & determinism */ +/** + * # Safety + * `b` must be a valid builder pointer. + */ +sandlock_builder_t *sandlock_sandbox_builder_uid(sandlock_builder_t *b, uint32_t id); + +/** + * # Safety + * `b` and `rule` must be valid pointers. + */ +sandlock_builder_t *sandlock_sandbox_builder_http_allow(sandlock_builder_t *b, const char *rule); + +/** + * # Safety + * `b` and `rule` must be valid pointers. + */ +sandlock_builder_t *sandlock_sandbox_builder_http_deny(sandlock_builder_t *b, const char *rule); + +/** + * # Safety + * `b` must be a valid pointer. + */ +sandlock_builder_t *sandlock_sandbox_builder_http_port(sandlock_builder_t *b, uint16_t port); + +/** + * # Safety + * `b` and `path` must be valid pointers. + */ +sandlock_builder_t *sandlock_sandbox_builder_http_ca(sandlock_builder_t *b, const char *path); + +/** + * # Safety + * `b` and `path` must be valid pointers. + */ +sandlock_builder_t *sandlock_sandbox_builder_http_key(sandlock_builder_t *b, const char *path); + +/** + * # Safety + * `b` must be a valid builder pointer. + */ sandlock_builder_t *sandlock_sandbox_builder_random_seed(sandlock_builder_t *b, uint64_t seed); + +/** + * # Safety + * `b` must be a valid builder pointer. + */ sandlock_builder_t *sandlock_sandbox_builder_clean_env(sandlock_builder_t *b, bool v); -sandlock_builder_t *sandlock_sandbox_builder_env_var(sandlock_builder_t *b, const char *key, const char *value); + +/** + * # Safety + * `b`, `key`, and `value` must be valid pointers. + */ +sandlock_builder_t *sandlock_sandbox_builder_env_var(sandlock_builder_t *b, + const char *key, + const char *value); + +/** + * # Safety + * `b` must be a valid builder pointer. `epoch_secs` is seconds since UNIX epoch. + */ +sandlock_builder_t *sandlock_sandbox_builder_time_start(sandlock_builder_t *b, uint64_t epoch_secs); + +/** + * # Safety + * `b` must be a valid builder pointer. `names` is a comma-separated NUL-terminated string. + */ +sandlock_builder_t *sandlock_sandbox_builder_extra_deny_syscalls(sandlock_builder_t *b, + const char *names); + +/** + * # Safety + * `b` must be a valid builder pointer. `names` is a comma-separated NUL-terminated string. + */ +sandlock_builder_t *sandlock_sandbox_builder_extra_allow_syscalls(sandlock_builder_t *b, + const char *names); + +/** + * Resolve a syscall name (e.g. `"openat"`) to its kernel syscall + * number for the host architecture. + * + * Intended for filling a `sandlock_handler_registration_t`'s + * `syscall_nr` without hard-coding architecture-specific numbers. + * + * Returns the syscall number on success, or `-1` if `name` is NULL, + * is not valid UTF-8, or names a syscall sandlock does not know. The + * resolvable set covers the syscalls sandlock filters or supervises; + * syscalls outside that set (e.g. `getpid`) return `-1` and must be + * registered by raw number. + * + * # Safety + * `name` must be NULL or a valid NUL-terminated C string. + */ +int64_t sandlock_syscall_nr(const char *name); + +/** + * # Safety + * `b` must be a valid builder pointer. + */ +sandlock_builder_t *sandlock_sandbox_builder_max_open_files(sandlock_builder_t *b, unsigned int n); + +/** + * # Safety + * `b` must be a valid builder pointer. + */ sandlock_builder_t *sandlock_sandbox_builder_no_randomize_memory(sandlock_builder_t *b, bool v); + +/** + * # Safety + * `b` must be a valid builder pointer. + */ sandlock_builder_t *sandlock_sandbox_builder_no_huge_pages(sandlock_builder_t *b, bool v); -/* Build & free */ -/* On failure, *err is set to -1 and *err_msg (if non-null) is set to a - * heap-allocated C string with the error description. Caller frees it - * via sandlock_string_free. Pass NULL for err_msg to discard. */ -sandlock_sandbox_t *sandlock_sandbox_build(sandlock_builder_t *b, - int *err, - char **err_msg); +/** + * # Safety + * `b` must be a valid builder pointer. + */ +sandlock_builder_t *sandlock_sandbox_builder_no_coredump(sandlock_builder_t *b, bool v); + +/** + * # Safety + * `b` must be a valid builder pointer. + */ +sandlock_builder_t *sandlock_sandbox_builder_deterministic_dirs(sandlock_builder_t *b, bool v); + +/** + * Consume the builder and produce a policy. + * On success, `*err` is 0 and a non-null policy pointer is returned. + * On failure, `*err` is -1, null is returned, and `*err_msg` (if + * non-null) is set to a heap-allocated C string describing the + * error. The caller must release that string with + * [`sandlock_string_free`]. Pass `null` for `err_msg` to discard. + * + * # Safety + * `b` must be a valid builder pointer. `err` and `err_msg` may both + * be null. When `err_msg` is non-null, it must point to writable + * storage for one `*mut c_char`. + */ +sandlock_sandbox_t *sandlock_sandbox_build(sandlock_builder_t *b, int *err, char **err_msg); + +/** + * # Safety + * `p` must be null or a valid pointer from `sandlock_sandbox_build`. + */ void sandlock_sandbox_free(sandlock_sandbox_t *p); -/* sandlock_string_free is declared further down — used for any - * heap-allocated C string the FFI returns to the caller. */ -/* ---------------------------------------------------------------- - * Run - * ---------------------------------------------------------------- */ +/** + * Confine the calling process with Landlock filesystem rules. + * This is irreversible. Returns 0 on success, -1 on error. + * + * # Safety + * `policy` must be a valid policy pointer. + */ +int sandlock_confine(const sandlock_sandbox_t *policy); -/** Run with captured stdout/stderr. Returns result handle (NULL on failure). */ -/* name may be NULL to auto-generate as "sandbox-{pid}". */ +/** + * Run a command with captured stdout/stderr. Returns a result handle. + * + * # Safety + * `policy` must be a valid policy pointer. `name` may be NULL to + * auto-generate a sandbox name, or a valid NUL-terminated string. + * `argv` must point to `argc` C strings. + */ sandlock_result_t *sandlock_run(const sandlock_sandbox_t *policy, const char *name, - const char *const *argv, unsigned int argc); + const char *const *argv, + unsigned int argc); + +sandlock_handle_t *sandlock_create(const sandlock_sandbox_t *policy, + const char *name, + const char *const *argv, + unsigned int argc); + +/** + * Create a sandbox handle for immediate start+wait use on the calling + * FFI thread. Unlike `sandlock_create`, this uses the thread-local + * `current_thread` runtime and does not create Tokio worker threads. + * + * This is intended for blocking one-shot wrappers that call + * `sandlock_start` and `sandlock_handle_wait*` immediately from the + * same thread. Long-lived handles should use `sandlock_create` so the + * supervisor keeps progressing between FFI calls. + * + * # Safety + * Same constraints as `sandlock_create`. + */ +sandlock_handle_t *sandlock_create_for_run(const sandlock_sandbox_t *policy, + const char *name, + const char *const *argv, + unsigned int argc); + +/** + * Release a previously `sandlock_create`d child to execve. Returns 0 on + * success, -1 on error. + * + * # Safety + * `h` must be a valid handle from `sandlock_create`. + */ +int sandlock_start(sandlock_handle_t *h); + +/** + * Get the child PID. Returns 0 if not available. + * + * # Safety + * `h` must be a valid handle from `sandlock_create`. + */ +int32_t sandlock_handle_pid(const sandlock_handle_t *h); + +/** + * Wait for the sandbox to exit. Returns a result handle with stdout/stderr. + * + * # Safety + * `h` must be a valid handle from `sandlock_create`. + */ +sandlock_result_t *sandlock_handle_wait(sandlock_handle_t *h); + +/** + * Wait for the sandbox to exit with a timeout in milliseconds. + * Returns a result handle, or null on error. On timeout the sandbox is + * killed and a result with `ExitStatus::Timeout` is returned. + * + * # Safety + * `h` must be a valid handle from `sandlock_create`. + */ +sandlock_result_t *sandlock_handle_wait_timeout(sandlock_handle_t *h, uint64_t timeout_ms); + +/** + * Get port mappings as a JSON string (e.g. `{"80":9001,"443":9002}`). + * Returns a C string that must be freed with `sandlock_string_free`. + * Returns null if port_remap is not active or no ports are mapped. + * + * # Safety + * `h` must be a valid handle from `sandlock_create`. + */ +char *sandlock_handle_port_mappings(const sandlock_handle_t *h); + +/** + * Free a sandbox handle. Kills the process if still running. + * + * # Safety + * `h` must be null or a valid handle from `sandlock_create`. + */ +void sandlock_handle_free(sandlock_handle_t *h); -/** Run with inherited stdio. Returns exit code (-1 on failure). */ -/* name may be NULL to auto-generate as "sandbox-{pid}". */ +/** + * Run a command with inherited stdio (interactive). Returns exit code. + * + * # Safety + * `policy` must be a valid policy pointer. `name` may be NULL to + * auto-generate a sandbox name, or a valid NUL-terminated string. + * `argv` must point to `argc` C strings. + */ int sandlock_run_interactive(const sandlock_sandbox_t *policy, const char *name, - const char *const *argv, unsigned int argc); - -/* ---------------------------------------------------------------- - * Result - * ---------------------------------------------------------------- */ + const char *const *argv, + unsigned int argc); +/** + * # Safety + * `r` must be null or a valid result pointer. + */ int sandlock_result_exit_code(const sandlock_result_t *r); + +/** + * # Safety + * `r` must be null or a valid result pointer. + */ bool sandlock_result_success(const sandlock_result_t *r); -/** Get stdout as C string. Caller must free with sandlock_string_free(). */ +/** + * Get captured stdout. Returns a malloc'd NUL-terminated string. + * Caller must free with `sandlock_string_free`. Returns null if no capture. + * + * # Safety + * `r` must be null or a valid result pointer. + */ char *sandlock_result_stdout(const sandlock_result_t *r); -/** Get stderr as C string. Caller must free with sandlock_string_free(). */ + +/** + * Get captured stderr. Same semantics as `sandlock_result_stdout`. + * + * # Safety + * `r` must be null or a valid result pointer. + */ char *sandlock_result_stderr(const sandlock_result_t *r); -/** Get stdout as raw bytes. Returns pointer valid until result is freed. */ -const uint8_t *sandlock_result_stdout_bytes(const sandlock_result_t *r, size_t *len); -/** Get stderr as raw bytes. */ -const uint8_t *sandlock_result_stderr_bytes(const sandlock_result_t *r, size_t *len); +/** + * Get captured stdout as raw bytes. Writes length to `*len`. + * Returns pointer to internal buffer (valid until result is freed). Null if no capture. + * + * # Safety + * `r` must be a valid result pointer. `len` must be a valid pointer. + */ +const uint8_t *sandlock_result_stdout_bytes(const sandlock_result_t *r, uintptr_t *len); + +/** + * Get captured stderr as raw bytes. + * + * # Safety + * `r` must be a valid result pointer. `len` must be a valid pointer. + */ +const uint8_t *sandlock_result_stderr_bytes(const sandlock_result_t *r, uintptr_t *len); +/** + * # Safety + * `r` must be null or a valid pointer from `sandlock_run`. + */ void sandlock_result_free(sandlock_result_t *r); + +/** + * Free a string returned by `sandlock_result_stdout` or `sandlock_result_stderr`. + * + * # Safety + * `s` must be null or a pointer from a `sandlock_result_std*` function. + */ void sandlock_string_free(char *s); -/* ---------------------------------------------------------------- - * Pipeline - * ---------------------------------------------------------------- */ +/** + * Run a command in dry-run mode with captured stdout/stderr. + * + * # Safety + * `policy` must be a valid policy pointer. `name` may be NULL to + * auto-generate a sandbox name, or a valid NUL-terminated string. + * `argv` must point to `argc` C strings. + */ +sandlock_dry_run_result_t *sandlock_dry_run(const sandlock_sandbox_t *policy, + const char *name, + const char *const *argv, + unsigned int argc); + +/** + * Get the exit code from a dry-run result. + * + * # Safety + * `r` must be a valid dry-run result pointer. + */ +int sandlock_dry_run_result_exit_code(const sandlock_dry_run_result_t *r); + +/** + * Check if the dry-run result indicates success. + * + * # Safety + * `r` must be a valid dry-run result pointer. + */ +bool sandlock_dry_run_result_success(const sandlock_dry_run_result_t *r); + +/** + * Get captured stdout bytes from a dry-run result. + * + * # Safety + * `r` must be a valid dry-run result pointer. `len` must be a valid pointer. + */ +const uint8_t *sandlock_dry_run_result_stdout_bytes(const sandlock_dry_run_result_t *r, + uintptr_t *len); + +/** + * Get captured stderr bytes from a dry-run result. + * + * # Safety + * `r` must be a valid dry-run result pointer. `len` must be a valid pointer. + */ +const uint8_t *sandlock_dry_run_result_stderr_bytes(const sandlock_dry_run_result_t *r, + uintptr_t *len); + +/** + * Get the number of filesystem changes in a dry-run result. + * + * # Safety + * `r` must be a valid dry-run result pointer. + */ +uintptr_t sandlock_dry_run_result_changes_len(const sandlock_dry_run_result_t *r); + +/** + * Get the kind of the i-th change: 'A' (added), 'M' (modified), 'D' (deleted). + * + * # Safety + * `r` must be a valid dry-run result pointer. `i` must be < changes_len. + */ +char sandlock_dry_run_result_change_kind(const sandlock_dry_run_result_t *r, uintptr_t i); + +/** + * Get the path of the i-th change as a C string. Caller must free with `sandlock_string_free`. + * + * # Safety + * `r` must be a valid dry-run result pointer. `i` must be < changes_len. + */ +char *sandlock_dry_run_result_change_path(const sandlock_dry_run_result_t *r, uintptr_t i); + +/** + * Free a dry-run result. + * + * # Safety + * `r` must be null or a valid dry-run result pointer. + */ +void sandlock_dry_run_result_free(sandlock_dry_run_result_t *r); +/** + * Create a new empty pipeline. + */ sandlock_pipeline_t *sandlock_pipeline_new(void); +/** + * Add a stage to the pipeline. The policy is cloned; the caller retains ownership. + * + * # Safety + * `pipe` must be a valid pipeline pointer. `policy` must be a valid policy pointer. + * `argv` must point to `argc` C strings. + */ void sandlock_pipeline_add_stage(sandlock_pipeline_t *pipe, const sandlock_sandbox_t *policy, - const char *const *argv, unsigned int argc); + const char *const *argv, + unsigned int argc); -/** Run pipeline (consumes pipe). timeout_ms=0 means no timeout. */ +/** + * Run the pipeline. Returns a result handle (last stage's output). + * `timeout_ms` is the total timeout in milliseconds (0 = no timeout). + * + * # Safety + * `pipe` is consumed and freed by this call. Do not use it after. + */ sandlock_result_t *sandlock_pipeline_run(sandlock_pipeline_t *pipe, uint64_t timeout_ms); +/** + * Free a pipeline without running it. + * + * # Safety + * `pipe` must be null or a valid pipeline pointer. + */ void sandlock_pipeline_free(sandlock_pipeline_t *pipe); -/* ---------------------------------------------------------------- - * Handler ABI — extension handlers for seccomp-notif syscalls. - * ---------------------------------------------------------------- */ - -/** Snapshot of a kernel seccomp notification. Field layout must stay - * in lock-step with `sandlock_ffi::notif_repr::sandlock_notif_data_t`. */ -typedef struct sandlock_notif_data_t { - uint64_t id; - uint32_t pid; - uint32_t flags; - int32_t syscall_nr; - uint32_t arch; - uint64_t instruction_pointer; - uint64_t args[6]; -} sandlock_notif_data_t; +/** + * Create a new empty gather. + */ +sandlock_gather_t *sandlock_gather_new(void); -/** Opaque child-memory accessor (lifetime: single callback invocation). */ -typedef struct sandlock_mem_handle_t sandlock_mem_handle_t; +/** + * Add a named source to the gather. + * + * # Safety + * All pointers must be valid. `name` must be a NUL-terminated C string. + */ +void sandlock_gather_add_source(sandlock_gather_t *g, + const char *name, + const sandlock_sandbox_t *policy, + const char *const *argv, + unsigned int argc); -/** Read a NUL-terminated string. Returns 0 on success, -1 on failure. - * On success the buffer is NUL-terminated and `*out_len` holds the byte - * count copied (excluding NUL); `max_len` must be at least 1 to fit the - * NUL. */ -int sandlock_mem_read_cstr(const sandlock_mem_handle_t *handle, - uint64_t addr, - uint8_t *buf, size_t max_len, - size_t *out_len); - -/** Raw memory read. Returns 0/-1; `*out_len` holds actual bytes copied. */ -int sandlock_mem_read(const sandlock_mem_handle_t *handle, - uint64_t addr, - uint8_t *buf, size_t len, - size_t *out_len); - -/** Raw memory write. Returns 0/-1. */ -int sandlock_mem_write(const sandlock_mem_handle_t *handle, - uint64_t addr, - const uint8_t *buf, size_t len); - -typedef enum sandlock_action_kind { - SANDLOCK_ACTION_UNSET = 0, - SANDLOCK_ACTION_CONTINUE = 1, - SANDLOCK_ACTION_ERRNO = 2, - SANDLOCK_ACTION_RETURN_VALUE = 3, - SANDLOCK_ACTION_INJECT_FD_SEND = 4, - SANDLOCK_ACTION_INJECT_FD_SEND_TRACKED = 5, - SANDLOCK_ACTION_HOLD = 6, - SANDLOCK_ACTION_KILL = 7, -} sandlock_action_kind_t; - -typedef struct { int32_t sig; int32_t pgid; } sandlock_action_kill_t; +/** + * Set the consumer stage for the gather. + * + * # Safety + * All pointers must be valid. + */ +void sandlock_gather_set_consumer(sandlock_gather_t *g, + const sandlock_sandbox_t *policy, + const char *const *argv, + unsigned int argc); -typedef struct { - int32_t srcfd; - uint32_t newfd_flags; -} sandlock_action_inject_t; +/** + * Run the gather. Consumes the gather handle. + * + * # Safety + * `g` must be a valid gather pointer. + */ +sandlock_result_t *sandlock_gather_run(sandlock_gather_t *g, uint64_t timeout_ms); -typedef uint64_t sandlock_inject_tracker_t; +/** + * Free a gather without running it. + * + * # Safety + * `g` must be null or a valid gather pointer. + */ +void sandlock_gather_free(sandlock_gather_t *g); -typedef struct { - int32_t srcfd; - uint32_t newfd_flags; - sandlock_inject_tracker_t tracker; -} sandlock_action_inject_tracked_t; +/** + * Set a policy callback on the builder. + * + * # Safety + * `b` must be a valid builder pointer. `cb` must be a valid function pointer + * that remains valid for the lifetime of the sandbox. + */ +sandlock_builder_t *sandlock_sandbox_builder_policy_fn(sandlock_builder_t *b, + sandlock_policy_fn_t cb); -typedef union { - uint64_t none; - int32_t errno_value; - int64_t return_value; - sandlock_action_inject_t inject_send; - sandlock_action_inject_tracked_t inject_send_tracked; - sandlock_action_kill_t kill; -} sandlock_action_payload_t; +/** + * Restrict network to the given IPs. Permanent — cannot grant back. + * + * # Safety + * `ctx` must be a valid context pointer from inside a policy callback. + * `ips` must point to `count` valid C strings. + */ +void sandlock_ctx_restrict_network(sandlock_ctx_t *ctx, const char *const *ips, uint32_t count); -typedef struct sandlock_action_out_t { - uint32_t kind; /* sandlock_action_kind_t */ - sandlock_action_payload_t payload; -} sandlock_action_out_t; +/** + * Grant network IPs (within ceiling). Fails silently if restricted. + * + * # Safety + * Same as `sandlock_ctx_restrict_network`. + */ +void sandlock_ctx_grant_network(sandlock_ctx_t *ctx, const char *const *ips, uint32_t count); + +/** + * Restrict max memory. Permanent. + * + * # Safety + * `ctx` must be a valid context pointer. + */ +void sandlock_ctx_restrict_max_memory(sandlock_ctx_t *ctx, uint64_t bytes); + +/** + * Restrict max processes. Permanent. + * + * # Safety + * `ctx` must be a valid context pointer. + */ +void sandlock_ctx_restrict_max_processes(sandlock_ctx_t *ctx, uint32_t n); + +/** + * Restrict network for a specific PID. + * + * # Safety + * `ctx` must be a valid context pointer. `ips` must point to `count` C strings. + */ +void sandlock_ctx_restrict_pid_network(sandlock_ctx_t *ctx, + uint32_t pid, + const char *const *ips, + uint32_t count); + +/** + * Deny access to a path dynamically (checked on openat). + * + * # Safety + * `ctx` must be a valid context pointer. `path` must be a valid C string. + */ +void sandlock_ctx_deny_path(sandlock_ctx_t *ctx, const char *path); + +/** + * Remove a previously denied path. + * + * # Safety + * `ctx` must be a valid context pointer. `path` must be a valid C string. + */ +void sandlock_ctx_allow_path(sandlock_ctx_t *ctx, const char *path); + +/** + * Create a sandbox with init/work functions for COW forking. + * + * # Safety + * `policy` must be valid. `name` may be NULL to auto-generate a sandbox + * name, or a valid NUL-terminated string. `init_fn` and `work_fn` must + * be valid function pointers. + */ +sandlock_t *sandlock_new_with_fns(const sandlock_sandbox_t *policy, + const char *name, + sandlock_init_fn_t init_fn, + sandlock_work_fn_t work_fn); + +/** + * Fork N COW clones. Returns a fork result handle (NULL on error). + * + * # Safety + * `sb` must be a valid sandbox pointer from `sandlock_new_with_fns`. + */ +sandlock_fork_result_t *sandlock_fork(sandlock_t *sb, uint32_t n); + +/** + * Get the number of clones. + */ +uint32_t sandlock_fork_result_count(const sandlock_fork_result_t *r); + +/** + * Get a clone's PID. + */ +int32_t sandlock_fork_result_pid(const sandlock_fork_result_t *r, uint32_t index); + +/** + * Reduce: read all clone stdout pipes, feed to reducer stdin, return result. + * + * # Safety + * `fork_result` is consumed. `policy` and `argv` must be valid. `name` + * may be NULL to auto-generate a sandbox name, or a valid + * NUL-terminated string. + */ +sandlock_result_t *sandlock_reduce(sandlock_fork_result_t *fork_result, + const sandlock_sandbox_t *policy, + const char *name, + const char *const *argv, + unsigned int argc); + +/** + * Free a fork result without reducing. + */ +void sandlock_fork_result_free(sandlock_fork_result_t *r); + +/** + * Wait for the sandbox template to exit. Returns exit code. + * + * # Safety + * `sb` must be a valid sandbox pointer. + */ +int sandlock_wait(sandlock_t *sb); + +/** + * Free a Sandbox handle created by `sandlock_new_with_fns`. + * + * # Safety + * `sb` must be null or a valid Sandbox pointer. + */ +void sandlock_sandbox_fns_free(sandlock_t *sb); + +/** + * Capture a checkpoint from a live sandbox handle. + * The sandbox is frozen (SIGSTOP + fork-hold), state is captured via ptrace, + * then thawed. Returns NULL on error. + * + * # Safety + * `h` must be a valid handle from `sandlock_create`. + */ +sandlock_checkpoint_t *sandlock_handle_checkpoint(sandlock_handle_t *h); + +/** + * Save a checkpoint to a directory (human-readable JSON + binary layout). + * Returns 0 on success, -1 on error. + * + * # Safety + * `cp` must be a valid checkpoint pointer. `dir` must be a valid C string path. + */ +int sandlock_checkpoint_save(const sandlock_checkpoint_t *cp, const char *dir); + +/** + * Load a checkpoint from a directory. + * Returns NULL on error. + * + * # Safety + * `dir` must be a valid C string path to a checkpoint directory. + */ +sandlock_checkpoint_t *sandlock_checkpoint_load(const char *dir); + +/** + * Set the checkpoint name. + * + * # Safety + * `cp` must be a valid checkpoint pointer. `name` must be a valid C string. + */ +void sandlock_checkpoint_set_name(sandlock_checkpoint_t *cp, const char *name); + +/** + * Get the checkpoint name. Returns a malloc'd C string; free with `sandlock_string_free`. + * + * # Safety + * `cp` must be a valid checkpoint pointer. + */ +char *sandlock_checkpoint_name(const sandlock_checkpoint_t *cp); + +/** + * Set optional app-level state bytes on the checkpoint. + * + * # Safety + * `cp` must be valid. `data` must point to `len` bytes, or be NULL to clear. + */ +void sandlock_checkpoint_set_app_state(sandlock_checkpoint_t *cp, + const uint8_t *data, + uintptr_t len); + +/** + * Get app-level state bytes. Returns pointer to internal buffer (valid until + * checkpoint is freed). Writes length to `*len`. Returns NULL if no app state. + * + * # Safety + * `cp` must be a valid checkpoint pointer. `len` must be a valid pointer. + */ +const uint8_t *sandlock_checkpoint_app_state(const sandlock_checkpoint_t *cp, uintptr_t *len); + +/** + * Free a checkpoint handle. + * + * # Safety + * `cp` must be null or a valid checkpoint pointer. + */ +void sandlock_checkpoint_free(sandlock_checkpoint_t *cp); + +/** + * Query the Landlock ABI version supported by the running kernel. + * Returns the ABI version (>= 1), or -1 if Landlock is unavailable. + */ +int sandlock_landlock_abi_version(void); + +/** + * Return the minimum Landlock ABI version required by this build of sandlock. + */ +int sandlock_min_landlock_abi(void); + +/** + * Read up to `max_len-1` bytes of a NUL-terminated string at `addr` from the + * traced child. On success the destination buffer is NUL-terminated and + * `*out_len` holds the byte count copied (excluding the NUL); returns 0. + * On failure returns -1 and leaves `*out_len` untouched. `max_len` must be + * at least 1 to fit the NUL terminator. + * + * # Safety + * `handle` must point to a live `sandlock_mem_handle_t` provided by the + * supervisor; `buf` must be writable for `max_len` bytes; `out_len` must + * be a valid `size_t*`. + */ +int32_t sandlock_mem_read_cstr(const sandlock_mem_handle_t *handle, + uint64_t addr, + uint8_t *buf, + uintptr_t max_len, + uintptr_t *out_len); + +/** + * Raw byte read at `addr` of exactly `len` bytes. Writes byte count + * actually read to `*out_len`. Returns 0 on success, -1 on failure. + * + * # Safety + * Same constraints as `sandlock_mem_read_cstr`. + */ +int32_t sandlock_mem_read(const sandlock_mem_handle_t *handle, + uint64_t addr, + uint8_t *buf, + uintptr_t len, + uintptr_t *out_len); + +/** + * Write `len` bytes from `buf` into the child at `addr`. Returns 0 on + * success, -1 on failure. + * + * # Safety + * Same constraints as `sandlock_mem_read_cstr`; `buf` must be readable + * for `len` bytes. + */ +int32_t sandlock_mem_write(const sandlock_mem_handle_t *handle, + uint64_t addr, + const uint8_t *buf, + uintptr_t len); -/* Setters — exactly one tag is written; the payload is filled in - * accordingly. Calling a setter overwrites any prior setting. */ +/** + * Mark the action as `Continue` (let the syscall proceed unchanged). + * + * # Safety + * `out` must be a valid pointer to a `sandlock_action_out_t` writable + * for the duration of the call, or null (in which case the call is a + * no-op). + */ void sandlock_action_set_continue(sandlock_action_out_t *out); + +/** + * Fail the syscall with `errno`. + * + * # Safety + * Same constraints as `sandlock_action_set_continue`. + */ void sandlock_action_set_errno(sandlock_action_out_t *out, int32_t errno_value); + +/** + * Return a specific value from the syscall without entering the kernel. + * + * # Safety + * Same constraints as `sandlock_action_set_continue`. + */ void sandlock_action_set_return_value(sandlock_action_out_t *out, int64_t value); -/** Ownership of `srcfd` transfers from the caller to the supervisor - * only when the resulting action is actually dispatched. If the - * caller subsequently calls a different setter on the same - * `sandlock_action_out_t` (overwriting the kind tag before the - * supervisor reads it), `srcfd` is NOT closed and leaks. Pick one - * setter per action. */ + +/** + * Inject the supervisor-side fd `srcfd` into the traced child as a new + * fd (number chosen by the kernel via `SECCOMP_IOCTL_NOTIF_ADDFD`). + * + * Note: ownership of `srcfd` transfers from the C caller to the + * supervisor only when the resulting action is actually dispatched. + * If the C caller subsequently calls a different setter on the same + * `sandlock_action_out_t` (overwriting the kind tag before the + * supervisor reads it), `srcfd` is NOT closed and leaks. Pick one + * setter per action. + * + * # Safety + * Same constraints as `sandlock_action_set_continue`; `srcfd` must be + * a valid open fd in the supervisor process at the moment of the + * supervisor's dispatch. + */ void sandlock_action_set_inject_fd_send(sandlock_action_out_t *out, - int32_t srcfd, uint32_t newfd_flags); - -/* Flags for sandlock_action_set_inject_bytes(). flags == 0 is the safe - * default: the injected file is sealed read-only and the child-side fd is - * O_CLOEXEC. */ -#define SANDLOCK_INJECT_WRITABLE (1u << 0) /* leave the memfd writable (do not seal) */ -#define SANDLOCK_INJECT_NO_CLOEXEC (1u << 1) /* clear O_CLOEXEC on the child-side fd */ - -/** Inject `len` bytes from `data` into the child as the syscall's returned - * fd, backed by an in-memory file the supervisor creates. The bytes are - * copied during the call, so `data` need not outlive it, and the caller - * owns no fd (the supervisor creates, populates, and closes the memfd). - * Use this instead of sandlock_action_set_inject_fd_send() when injecting - * synthetic content (secrets, virtual files, fetched objects) rather than - * an fd you already hold. On allocation failure the action becomes - * Errno(EIO). `data` may be NULL when `len == 0` (injects an empty file). */ + int srcfd, + uint32_t newfd_flags); + +/** + * Inject `len` bytes from `data` into the child as the syscall's returned + * fd, backed by an in-memory file created by the supervisor. The bytes are + * copied immediately, so `data` need not outlive the call. + * + * `flags` is a bitmask of `SANDLOCK_INJECT_*`. With `flags == 0` the memfd is + * sealed read-only and the child-side fd is `O_CLOEXEC` — the safe default + * for synthetic file content (secrets, virtual files, fetched objects). On + * allocation failure the action is set to `Errno(EIO)`, so the callback's + * one-setter contract still holds and the child gets a definite response. + * + * Unlike [`sandlock_action_set_inject_fd_send`], the caller owns no fd here: + * the supervisor creates, populates, and (on dispatch) closes the memfd. + * + * # Safety + * `out` follows the same constraints as `sandlock_action_set_continue`. + * `data` must point to at least `len` readable bytes, or be null when + * `len == 0`. + */ void sandlock_action_set_inject_bytes(sandlock_action_out_t *out, - const uint8_t *data, size_t len, + const uint8_t *data, + uintptr_t len, uint32_t flags); -/* NOTE: `SANDLOCK_ACTION_INJECT_FD_SEND_TRACKED` (= 5) and - * `sandlock_action_inject_tracked_t` are reserved for a future - * tracker-aware inject variant. No setter is exposed in this release; - * actions left with that kind tag are treated as `UNSET` and routed - * through the handler's exception policy. */ -void sandlock_action_set_hold(sandlock_action_out_t *out); -/** Kill action setter. `pgid == 0` is a sentinel: the supervisor - * substitutes the child process group id (resolved via getpgid(pid) - * on the notification's pid). To target a specific group, pass an - * explicit non-zero pgid. - * - * If the supervisor cannot resolve a safe process group id for the - * child, the Kill action is refused and the notification is routed - * through the handler's exception policy instead. This happens when - * the notification pid is <= 0, when getpgid() fails, or when the - * resolved pgid collides with the supervisor's own group (all - * reachable in nested PID namespaces, e.g. Kubernetes pod-in-pod). - * An explicit non-zero pgid is likewise refused if it matches the - * supervisor's own process group. */ -void sandlock_action_set_kill(sandlock_action_out_t *out, int32_t sig, int32_t pgid); - -/** Policy applied when a handler callback fails to set a valid action: - * it returns non-zero, leaves the action UNSET, or panics across the - * FFI boundary (Rust handlers only). */ -typedef enum sandlock_exception_policy { - /** Kill the child process group with SIGKILL. Fail-closed, and the - * default. If no safe process group id is available for the child - * (see `sandlock_action_set_kill`), this degrades to failing the - * syscall with EPERM rather than risk signalling the supervisor's - * own process group. */ - SANDLOCK_EXCEPTION_KILL = 0, - /** Fail the syscall with EPERM. */ - SANDLOCK_EXCEPTION_DENY_EPERM = 1, - /** Let the syscall continue unchanged (explicit fail-open). */ - SANDLOCK_EXCEPTION_CONTINUE = 2, - /** Fail the syscall with EIO. Idiomatic for audit-only handlers that - * propagate the failure as a plain OSError rather than - * PermissionError. */ - SANDLOCK_EXCEPTION_DENY_EIO = 3, -} sandlock_exception_policy_t; - -/** Opaque handler container. - * - * Ownership: allocated by `sandlock_handler_new` and freed by either - * `sandlock_handler_free` (if never registered) or by the supervisor - * after a successful or failed `sandlock_run_with_handlers` call. - * - * Thread safety: the supervisor MAY invoke the handler callback from - * multiple worker threads concurrently across different notifications - * (today's dispatch loop is largely serial; the public ABI makes no - * concurrency guarantee, so a future dispatcher could parallelise - * without breaking compatibility). The caller MUST ensure their `ud` - * pointer is thread-safe — either immutable, or guarded by their own - * synchronization primitives (atomics, mutex, etc.). Rust provides no - * synchronization for an opaque `void*`. */ -typedef struct sandlock_handler_t sandlock_handler_t; -/** C handler signature. Return 0 on success; a non-zero return triggers - * the handler's exception policy. The callee MUST call exactly one - * sandlock_action_set_*() on `out` before returning 0. +/** + * Hold the syscall pending until the supervisor explicitly releases it. * - * Thread safety: see `sandlock_handler_t` — this function may be - * invoked concurrently from multiple worker threads. Any state - * reachable through `ud` must be thread-safe. */ -typedef int (*sandlock_handler_fn_t)(void *ud, - const sandlock_notif_data_t *notif, - sandlock_mem_handle_t *mem, - sandlock_action_out_t *out); + * # Safety + * Same constraints as `sandlock_action_set_continue`. + */ +void sandlock_action_set_hold(sandlock_action_out_t *out); -typedef void (*sandlock_handler_ud_drop_t)(void *ud); +/** + * Kill the target with signal `sig`. Pass `pgid > 0` to target an + * explicit process group; `pgid == 0` is a sentinel — the supervisor + * substitutes the child process group id resolved via `getpgid(pid)` + * on the notification's pid. + * + * # Safety + * Same constraints as `sandlock_action_set_continue`. + */ +void sandlock_action_set_kill(sandlock_action_out_t *out, int32_t sig, int32_t pgid); -/** Allocate a handler container. Returns NULL when `handler_fn` is NULL - * or when `on_exception` is not one of the documented `SANDLOCK_EXCEPTION_*` - * values. +/** + * Allocate a handler container. `handler_fn` must be non-null; passing + * `ud_drop = None` is legal when `ud` does not require cleanup. * - * `ud` must be thread-safe to access — see `sandlock_handler_t` for - * the concurrency contract. `ud_drop`, if non-NULL, is invoked exactly - * once when the container is freed. */ -sandlock_handler_t *sandlock_handler_new(sandlock_handler_fn_t handler_fn, + * # Safety + * `ud` is opaque to Rust — the caller guarantees that the pointer + * remains valid until either (a) `sandlock_handler_free` is called or + * (b) the supervisor takes ownership via `sandlock_run_with_handlers` + * and the run completes. + * If `on_exception` does not match a defined `sandlock_exception_policy_t` + * discriminant (0, 1, 2, or 3), the call returns null and no allocation occurs. + */ +sandlock_handler_t *sandlock_handler_new(int32_t (*handler_fn)(void *ud, + const sandlock_notif_data_t *notif, + sandlock_mem_handle_t *mem, + sandlock_action_out_t *out), void *ud, - sandlock_handler_ud_drop_t ud_drop, - sandlock_exception_policy_t on_exception); - -/** Free a handler container that has not been handed to the supervisor. */ -void sandlock_handler_free(sandlock_handler_t *h); + void (*ud_drop)(void *ud), + uint32_t on_exception); -/** Mark a handler as deferred. The supervisor then runs its callback off the - * notification loop, so a slow callback (network round-trip, blocking - * syscall) does not stall every other trapped syscall. Call before - * registering the handler with a run. - * - * A deferred handler makes a *terminal* decision: deferral short-circuits - * the handler chain, so if it would continue and other user handlers are - * registered on the same syscall, those later handlers do not run. - * Builtins always run first regardless. Deferral is refused at dispatch - * time for syscalls whose continue path needs the execve argv-safety freeze - * (`execve`/`execveat`) or fork creation-tracking; such a call is denied - * with EPERM. If more than an internal cap of deferrals are in flight at - * once, further ones are failed with EAGAIN rather than queued. */ +/** + * Mark a handler as deferred: the supervisor will run its callback off the + * notification loop so a slow callback does not block other trapped + * syscalls. Must be called before the handler is registered with a run. + * + * A deferred handler makes a *terminal* decision: deferral short-circuits + * the handler chain, so if a deferred handler would `Continue` and other + * user handlers are registered on the same syscall, those later handlers do + * not run. Builtins always run first regardless. Deferral is refused at + * dispatch time for syscalls whose Continue path needs the execve + * argv-safety freeze (`execve`/`execveat`) or fork creation-tracking. + * + * # Safety + * `h` must be a non-null pointer returned by `sandlock_handler_new` that has + * not yet been registered with a run or freed. + */ void sandlock_handler_set_deferred(sandlock_handler_t *h, bool deferred); -typedef struct sandlock_handler_registration_t { - int64_t syscall_nr; - sandlock_handler_t *handler; /* ownership transferred on a successful run */ -} sandlock_handler_registration_t; +/** + * Free a handler container that has *not* been registered with a + * sandbox. After successful registration the supervisor owns the + * handler; calling this on a registered handler is undefined behaviour + * (the supervisor's later free would double-free). + * + * The ABI is `extern "C-unwind"` rather than plain `extern "C"` so a + * panic propagated from a Rust-side `ud_drop` (declared as + * [`sandlock_handler_ud_drop_t`], itself `extern "C-unwind"`) unwinds + * the caller rather than aborting the process. Pure-C callers see no + * difference (C has no unwinding). + * + * # Safety + * `h` must be either null or a pointer previously returned by + * `sandlock_handler_new` that has not yet been registered with the + * supervisor and has not already been freed. + */ +void sandlock_handler_free(sandlock_handler_t *h); -/** Run the policy with extra C handlers. Returns NULL on failure. - * - * `name` may be NULL to auto-generate as `sandbox-{pid}`, mirroring the - * convention used by `sandlock_run`. - * - * Must not be called from a thread already running a Tokio runtime. - * This function builds and drives its own runtime internally; calling - * it from within an existing runtime panics, and the panic unwinds - * across the FFI boundary via this function's `extern "C-unwind"` ABI. - * - * Ownership of every `registrations[i].handler` pointer transfers into - * the call on entry. After this function returns, the caller MUST NOT - * call `sandlock_handler_free` on any handler pointer that was passed - * in — successful or not, the supervisor is responsible for freeing - * the containers (which also invokes the registered `ud_drop`). - * - * Null handler pointers in the array are treated as a validation error - * and the call returns NULL; non-null entries in the same array are - * still freed by the supervisor (the array is consumed as a whole). */ -sandlock_result_t *sandlock_run_with_handlers( - const sandlock_sandbox_t *policy, - const char *name, - const char *const *argv, unsigned int argc, - const sandlock_handler_registration_t *registrations, - size_t nregistrations); - -/** Interactive-stdio variant of `sandlock_run_with_handlers`. Returns - * NULL on failure. - * - * `name` may be NULL to auto-generate as `sandbox-{pid}`, mirroring the - * convention used by `sandlock_run_interactive`. - * - * Must not be called from a thread already running a Tokio runtime. - * This function builds and drives its own runtime internally; calling - * it from within an existing runtime panics, and the panic unwinds - * across the FFI boundary via this function's `extern "C-unwind"` ABI. - * - * Ownership of every `registrations[i].handler` pointer transfers into - * the call on entry. After this function returns, the caller MUST NOT - * call `sandlock_handler_free` on any handler pointer that was passed - * in — successful or not, the supervisor is responsible for freeing - * the containers (which also invokes the registered `ud_drop`). - * - * Null handler pointers in the array are treated as a validation error - * and the call returns NULL; non-null entries in the same array are - * still freed by the supervisor (the array is consumed as a whole). */ -sandlock_result_t *sandlock_run_interactive_with_handlers( - const sandlock_sandbox_t *policy, - const char *name, - const char *const *argv, unsigned int argc, - const sandlock_handler_registration_t *registrations, - size_t nregistrations); - -/** Resolve a syscall name (e.g. "openat") to its kernel syscall number - * for the host architecture, for use as a `sandlock_handler_registration_t` - * `syscall_nr`. Saves callers from hard-coding architecture-specific - * numbers. - * - * Returns -1 if `name` is NULL, is not valid UTF-8, or names a syscall - * sandlock does not know. The resolvable set covers the syscalls - * sandlock filters or supervises; syscalls outside that set (e.g. - * `getpid`) return -1 and must be registered by raw number. */ -int64_t sandlock_syscall_nr(const char *name); +/** + * Run the policy with C handlers. Returns NULL on failure. + * + * `name` may be NULL to auto-generate `sandbox-{pid}`, or a valid + * NUL-terminated UTF-8 C string; the placement mirrors the existing + * `sandlock_run` entry point in `lib.rs`. + * + * Declared `extern "C-unwind"` because the handler containers reach + * this entry point as part of the registration array and their + * user-supplied `ud_drop` may panic when the supervisor frees them + * (either during a normal Box-drop or on the early-return cleanup in + * `release_registrations`). Unwinding across an `extern "C"` boundary + * is undefined behaviour and aborts the process under modern + * rustc — `extern "C-unwind"` is the only legal way to let such a + * panic propagate to the caller, who can then decide whether to + * catch it. + * + * # Safety + * All pointer arguments must be valid for their documented lifetimes: + * `policy` must come from `sandlock_sandbox_build`, `argv` must be a + * readable array of `argc` NUL-terminated strings, and each handler + * pointer must come from `sandlock_handler_new` and must not be reused + * after this call (ownership transfers in). + */ +sandlock_result_t *sandlock_run_with_handlers(const sandlock_sandbox_t *policy, + const char *name, + const char *const *argv, + uint32_t argc, + const sandlock_handler_registration_t *registrations, + uintptr_t nregistrations); + +/** + * Interactive-stdio variant of `sandlock_run_with_handlers`. + * + * `name` follows the same convention as `sandlock_run_with_handlers`. + * The `extern "C-unwind"` declaration carries the same rationale: a + * panicking `ud_drop` must be able to unwind out of this entry point + * without process abort. + * + * # Safety + * Same constraints as `sandlock_run_with_handlers`. + */ +sandlock_result_t *sandlock_run_interactive_with_handlers(const sandlock_sandbox_t *policy, + const char *name, + const char *const *argv, + uint32_t argc, + const sandlock_handler_registration_t *registrations, + uintptr_t nregistrations); #ifdef __cplusplus -} -#endif +} // extern "C" +#endif // __cplusplus -#endif /* SANDLOCK_H */ +#endif /* SANDLOCK_H */ diff --git a/crates/sandlock-ffi/src/handler/abi.rs b/crates/sandlock-ffi/src/handler/abi.rs index 3e2f3e2e..892d26d7 100644 --- a/crates/sandlock-ffi/src/handler/abi.rs +++ b/crates/sandlock-ffi/src/handler/abi.rs @@ -491,11 +491,25 @@ impl Drop for sandlock_handler_t { /// and the run completes. /// If `on_exception` does not match a defined `sandlock_exception_policy_t` /// discriminant (0, 1, 2, or 3), the call returns null and no allocation occurs. +// The two callback parameters are spelled inline rather than via the +// `sandlock_handler_fn_t` / `sandlock_handler_ud_drop_t` aliases. A type alias +// is the same type to Rust, so this is ABI-identical and the body is +// unchanged, but cbindgen only flattens an `Option` into a nullable C +// function pointer when the `fn` is written inline; `Option` is +// emitted as an (uncallable) opaque by-value struct. The aliases remain in use +// by the struct fields and the Rust tests. #[no_mangle] pub unsafe extern "C" fn sandlock_handler_new( - handler_fn: Option, + handler_fn: Option< + extern "C-unwind" fn( + ud: *mut std::ffi::c_void, + notif: *const crate::notif_repr::sandlock_notif_data_t, + mem: *mut sandlock_mem_handle_t, + out: *mut sandlock_action_out_t, + ) -> i32, + >, ud: *mut std::ffi::c_void, - ud_drop: Option, + ud_drop: Option, on_exception: u32, ) -> *mut sandlock_handler_t { if handler_fn.is_none() { From b95dd0f2403e8384d6958e22522af216cbb21455 Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Mon, 1 Jun 2026 17:03:25 -0700 Subject: [PATCH 2/2] gitignore: keep tracking the generated sandlock.h header Signed-off-by: Cong Wang --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6e33698a..b82ada31 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /target/ -crates/sandlock-ffi/include/ +crates/sandlock-ffi/include/* +!crates/sandlock-ffi/include/sandlock.h __pycache__/ *.pyc *.pyo