Skip to content

Commit 766af10

Browse files
authored
Merge pull request #120 from dzerik/feat/popen-ffi
feat(ffi): C ABI for streaming-stdio popen — sandlock_popen + handle_kill
2 parents 03ef492 + 35c06aa commit 766af10

3 files changed

Lines changed: 651 additions & 23 deletions

File tree

crates/sandlock-ffi/include/sandlock.h

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,42 @@ sandlock_handle_t *sandlock_create_for_run(const sandlock_sandbox_t *policy,
684684
const char *const *argv,
685685
unsigned int argc);
686686

687+
/**
688+
* Spawn a confined process with per-stream stdio wiring and return a live
689+
* handle (the streaming counterpart of `sandlock_create` + `sandlock_start`).
690+
*
691+
* `stdin_mode`/`stdout_mode`/`stderr_mode` are `StdioMode` discriminants
692+
* (0=inherit, 1=piped, 2=null). For each stream set to *piped*, the matching
693+
* `out_*_fd` receives an owned fd the caller must `close()`; for inherit/null
694+
* it receives -1. Pass null for an `out_*_fd` to discard that fd (it is closed
695+
* rather than leaked). Returns null on error (unknown mode, build/fork
696+
* failure) with every `out_*_fd` left -1.
697+
*
698+
* The handle uses a multi-threaded runtime so the seccomp supervisor keeps
699+
* pumping while the caller does blocking IO on the returned fds. Wait for the
700+
* process with `sandlock_handle_wait`, terminate with `sandlock_handle_kill`,
701+
* and release with `sandlock_handle_free`.
702+
*
703+
* Deadlock warning: a piped fd is yours once returned — `close()` a piped
704+
* `out_stdin_fd` *before* `sandlock_handle_wait`, or a child that reads to EOF
705+
* (e.g. `cat`) never exits and the wait blocks forever. Likewise drain a piped
706+
* `out_stdout_fd`/`out_stderr_fd` before waiting: a child that fills the pipe
707+
* buffer blocks on write and never reaches exit.
708+
*
709+
* # Safety
710+
* As `sandlock_create`; `out_*_fd` must each be null or a valid `*mut c_int`.
711+
*/
712+
sandlock_handle_t *sandlock_popen(const sandlock_sandbox_t *policy,
713+
const char *name,
714+
const char *const *argv,
715+
unsigned int argc,
716+
uint32_t stdin_mode,
717+
uint32_t stdout_mode,
718+
uint32_t stderr_mode,
719+
int *out_stdin_fd,
720+
int *out_stdout_fd,
721+
int *out_stderr_fd);
722+
687723
/**
688724
* Release a previously `sandlock_create`d child to execve. Returns 0 on
689725
* success, -1 on error.
@@ -701,11 +737,26 @@ int sandlock_start(sandlock_handle_t *h);
701737
*/
702738
int32_t sandlock_handle_pid(const sandlock_handle_t *h);
703739

740+
/**
741+
* Send SIGKILL to the handle's entire process group. Idempotent: a process
742+
* that already exited is not an error. Returns 0 on success, -1 on error.
743+
* The handle remains valid; call `sandlock_handle_wait` to collect the exit
744+
* status and `sandlock_handle_free` to release it.
745+
*
746+
* # Safety
747+
* `h` must be a valid handle from `sandlock_create` / `sandlock_popen`.
748+
*/
749+
int sandlock_handle_kill(sandlock_handle_t *h);
750+
704751
/**
705752
* Wait for the sandbox to exit. Returns a result handle with stdout/stderr.
706753
*
754+
* For a `sandlock_popen` handle, close a piped `out_stdin_fd` and drain any
755+
* piped `out_stdout_fd`/`out_stderr_fd` *before* calling this, or a child that
756+
* reads to EOF or fills a pipe buffer never exits and this blocks forever.
757+
*
707758
* # Safety
708-
* `h` must be a valid handle from `sandlock_create`.
759+
* `h` must be a valid handle from `sandlock_create` / `sandlock_popen`.
709760
*/
710761
sandlock_result_t *sandlock_handle_wait(sandlock_handle_t *h);
711762

@@ -733,7 +784,7 @@ char *sandlock_handle_port_mappings(const sandlock_handle_t *h);
733784
* Free a sandbox handle. Kills the process if still running.
734785
*
735786
* # Safety
736-
* `h` must be null or a valid handle from `sandlock_create`.
787+
* `h` must be null or a valid handle from `sandlock_create` / `sandlock_popen`.
737788
*/
738789
void sandlock_handle_free(sandlock_handle_t *h);
739790

crates/sandlock-ffi/src/lib.rs

Lines changed: 188 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::time::Duration;
1010

1111
use sandlock_core::pipeline::Stage;
1212
use sandlock_core::sandbox::{BranchAction, ByteSize, SandboxBuilder};
13-
use sandlock_core::{Protection, RunResult, Sandbox};
13+
use sandlock_core::{Protection, RunResult, Sandbox, StdioMode};
1414

1515
pub mod handler;
1616
pub mod notif_repr;
@@ -1021,41 +1021,61 @@ pub struct sandlock_handle_t {
10211021
runtime: tokio::runtime::Runtime,
10221022
}
10231023

1024-
/// Fork the child and install policy; the child is parked between policy
1025-
/// install and execve. Returns a live handle. Call `sandlock_start` to
1026-
/// release the child to execve.
1024+
/// Shared entry-point prologue for the create/popen family: validate the
1025+
/// pointers, parse the optional name and argv, build the requested runtime,
1026+
/// and apply the name to a cloned policy. Returns the owned
1027+
/// `(Sandbox, Runtime, args)` triple, or `None` on any invalid input or
1028+
/// runtime-build failure (callers map `None` to a null handle).
1029+
///
1030+
/// The tail stays with each caller: `sandlock_create*` drive `sb.create()`,
1031+
/// `sandlock_popen` drives `sb.popen()` + fd hand-off. Factoring the prologue
1032+
/// here keeps null/name/argv/runtime handling from drifting between them.
10271033
///
10281034
/// # Safety
10291035
/// `policy` must be a valid policy pointer. `name` may be NULL to
10301036
/// auto-generate a sandbox name, or a valid NUL-terminated string.
10311037
/// `argv` must point to `argc` C strings.
1032-
unsafe fn sandlock_create_with_runtime(
1038+
unsafe fn prepare(
10331039
policy: *const sandlock_sandbox_t,
10341040
name: *const c_char,
10351041
argv: *const *const c_char,
10361042
argc: c_uint,
10371043
build_rt: fn() -> Option<tokio::runtime::Runtime>,
1038-
) -> *mut sandlock_handle_t {
1044+
) -> Option<(Sandbox, tokio::runtime::Runtime, Vec<String>)> {
10391045
if policy.is_null() || argv.is_null() {
1040-
return ptr::null_mut();
1046+
return None;
10411047
}
10421048
let policy = &(*policy)._private;
1043-
let name = match optional_name(name) {
1044-
Ok(name) => name,
1045-
Err(_) => return ptr::null_mut(),
1046-
};
1049+
let name = optional_name(name).ok()?;
10471050
let args = read_argv(argv, argc);
1048-
let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
1049-
1050-
let rt = match build_rt() {
1051-
Some(rt) => rt,
1052-
None => return ptr::null_mut(),
1053-
};
1054-
1055-
let mut sb = match name {
1051+
let rt = build_rt()?;
1052+
let sb = match name {
10561053
Some(ref n) => policy.clone().with_name(n.clone()),
10571054
None => policy.clone(),
10581055
};
1056+
Some((sb, rt, args))
1057+
}
1058+
1059+
/// Fork the child and install policy; the child is parked between policy
1060+
/// install and execve. Returns a live handle. Call `sandlock_start` to
1061+
/// release the child to execve.
1062+
///
1063+
/// # Safety
1064+
/// `policy` must be a valid policy pointer. `name` may be NULL to
1065+
/// auto-generate a sandbox name, or a valid NUL-terminated string.
1066+
/// `argv` must point to `argc` C strings.
1067+
unsafe fn sandlock_create_with_runtime(
1068+
policy: *const sandlock_sandbox_t,
1069+
name: *const c_char,
1070+
argv: *const *const c_char,
1071+
argc: c_uint,
1072+
build_rt: fn() -> Option<tokio::runtime::Runtime>,
1073+
) -> *mut sandlock_handle_t {
1074+
let (mut sb, rt, args) = match prepare(policy, name, argv, argc, build_rt) {
1075+
Some(t) => t,
1076+
None => return ptr::null_mut(),
1077+
};
1078+
let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
10591079

10601080
if !matches!(block_on_runtime(&rt, sb.create(&arg_refs)), Some(Ok(()))) {
10611081
return ptr::null_mut();
@@ -1098,6 +1118,130 @@ pub unsafe extern "C" fn sandlock_create_for_run(
10981118
sandlock_create_with_runtime(policy, name, argv, argc, build_runtime)
10991119
}
11001120

1121+
/// Map a raw `StdioMode` discriminant (0=inherit, 1=piped, 2=null) to the enum.
1122+
/// Returns `None` for any other value so callers can reject it loudly.
1123+
fn stdio_mode_from_raw(v: u32) -> Option<StdioMode> {
1124+
match v {
1125+
0 => Some(StdioMode::Inherit),
1126+
1 => Some(StdioMode::Piped),
1127+
2 => Some(StdioMode::Null),
1128+
_ => None,
1129+
}
1130+
}
1131+
1132+
/// Write `fd` through `out` if non-null; otherwise close it (a piped stream
1133+
/// whose fd the caller did not ask for must not leak).
1134+
unsafe fn write_or_close_fd(out: *mut c_int, fd: c_int) {
1135+
if out.is_null() {
1136+
if fd >= 0 {
1137+
libc::close(fd);
1138+
}
1139+
} else {
1140+
*out = fd;
1141+
}
1142+
}
1143+
1144+
/// Spawn a confined process with per-stream stdio wiring and return a live
1145+
/// handle (the streaming counterpart of `sandlock_create` + `sandlock_start`).
1146+
///
1147+
/// `stdin_mode`/`stdout_mode`/`stderr_mode` are `StdioMode` discriminants
1148+
/// (0=inherit, 1=piped, 2=null). For each stream set to *piped*, the matching
1149+
/// `out_*_fd` receives an owned fd the caller must `close()`; for inherit/null
1150+
/// it receives -1. Pass null for an `out_*_fd` to discard that fd (it is closed
1151+
/// rather than leaked). Returns null on error (unknown mode, build/fork
1152+
/// failure) with every `out_*_fd` left -1.
1153+
///
1154+
/// The handle uses a multi-threaded runtime so the seccomp supervisor keeps
1155+
/// pumping while the caller does blocking IO on the returned fds. Wait for the
1156+
/// process with `sandlock_handle_wait`, terminate with `sandlock_handle_kill`,
1157+
/// and release with `sandlock_handle_free`.
1158+
///
1159+
/// Deadlock warning: a piped fd is yours once returned — `close()` a piped
1160+
/// `out_stdin_fd` *before* `sandlock_handle_wait`, or a child that reads to EOF
1161+
/// (e.g. `cat`) never exits and the wait blocks forever. Likewise drain a piped
1162+
/// `out_stdout_fd`/`out_stderr_fd` before waiting: a child that fills the pipe
1163+
/// buffer blocks on write and never reaches exit.
1164+
///
1165+
/// # Safety
1166+
/// As `sandlock_create`; `out_*_fd` must each be null or a valid `*mut c_int`.
1167+
#[no_mangle]
1168+
pub unsafe extern "C" fn sandlock_popen(
1169+
policy: *const sandlock_sandbox_t,
1170+
name: *const c_char,
1171+
argv: *const *const c_char,
1172+
argc: c_uint,
1173+
stdin_mode: u32,
1174+
stdout_mode: u32,
1175+
stderr_mode: u32,
1176+
out_stdin_fd: *mut c_int,
1177+
out_stdout_fd: *mut c_int,
1178+
out_stderr_fd: *mut c_int,
1179+
) -> *mut sandlock_handle_t {
1180+
use std::os::fd::IntoRawFd;
1181+
1182+
if !out_stdin_fd.is_null() {
1183+
*out_stdin_fd = -1;
1184+
}
1185+
if !out_stdout_fd.is_null() {
1186+
*out_stdout_fd = -1;
1187+
}
1188+
if !out_stderr_fd.is_null() {
1189+
*out_stderr_fd = -1;
1190+
}
1191+
1192+
let (stdin, stdout, stderr) = match (
1193+
stdio_mode_from_raw(stdin_mode),
1194+
stdio_mode_from_raw(stdout_mode),
1195+
stdio_mode_from_raw(stderr_mode),
1196+
) {
1197+
(Some(a), Some(b), Some(c)) => (a, b, c),
1198+
_ => {
1199+
// Fail loud, not into an indistinguishable NULL: name the offending
1200+
// discriminant(s) so the binding author sees why the handle is null.
1201+
eprintln!(
1202+
"sandlock: sandlock_popen rejected an unknown StdioMode \
1203+
(stdin={stdin_mode}, stdout={stdout_mode}, stderr={stderr_mode}); \
1204+
valid values are 0=inherit, 1=piped, 2=null"
1205+
);
1206+
return ptr::null_mut();
1207+
}
1208+
};
1209+
// Shared prologue (null/name/argv/runtime) via `prepare`; popen keeps its own
1210+
// `sb.popen()` + fd-handoff tail. build_live_runtime: the multi-threaded
1211+
// runtime keeps the seccomp supervisor pumping during the caller's blocking IO.
1212+
let (mut sb, rt, args) = match prepare(policy, name, argv, argc, build_live_runtime) {
1213+
Some(t) => t,
1214+
None => return ptr::null_mut(),
1215+
};
1216+
let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
1217+
1218+
// popen returns a Process borrowing `sb`; take the owned fds and let it drop
1219+
// so `sb` can move into the handle. The process keeps running (owned by `sb`).
1220+
// Return OwnedFds (not raw ints) from the future: on the Err/None branch
1221+
// below they drop-close themselves instead of leaking a dangling raw fd.
1222+
let owned = block_on_runtime(&rt, async {
1223+
let mut proc = sb.popen(&arg_refs, stdin, stdout, stderr).await?;
1224+
Ok::<_, sandlock_core::SandlockError>((
1225+
proc.take_stdin(),
1226+
proc.take_stdout(),
1227+
proc.take_stderr(),
1228+
))
1229+
});
1230+
let (in_fd, out_fd, err_fd) = match owned {
1231+
Some(Ok(t)) => t,
1232+
_ => return ptr::null_mut(),
1233+
};
1234+
// Convert to raw only on the infallible side of the match.
1235+
write_or_close_fd(out_stdin_fd, in_fd.map(|f| f.into_raw_fd()).unwrap_or(-1));
1236+
write_or_close_fd(out_stdout_fd, out_fd.map(|f| f.into_raw_fd()).unwrap_or(-1));
1237+
write_or_close_fd(out_stderr_fd, err_fd.map(|f| f.into_raw_fd()).unwrap_or(-1));
1238+
1239+
Box::into_raw(Box::new(sandlock_handle_t {
1240+
sandbox: sb,
1241+
runtime: rt,
1242+
}))
1243+
}
1244+
11011245
/// Release a previously `sandlock_create`d child to execve. Returns 0 on
11021246
/// success, -1 on error.
11031247
///
@@ -1127,10 +1271,33 @@ pub unsafe extern "C" fn sandlock_handle_pid(h: *const sandlock_handle_t) -> i32
11271271
(*h).sandbox.pid().unwrap_or(0)
11281272
}
11291273

1274+
/// Send SIGKILL to the handle's entire process group. Idempotent: a process
1275+
/// that already exited is not an error. Returns 0 on success, -1 on error.
1276+
/// The handle remains valid; call `sandlock_handle_wait` to collect the exit
1277+
/// status and `sandlock_handle_free` to release it.
1278+
///
1279+
/// # Safety
1280+
/// `h` must be a valid handle from `sandlock_create` / `sandlock_popen`.
1281+
#[no_mangle]
1282+
pub unsafe extern "C" fn sandlock_handle_kill(h: *mut sandlock_handle_t) -> c_int {
1283+
if h.is_null() {
1284+
return -1;
1285+
}
1286+
let h = &mut *h;
1287+
if h.sandbox.kill().is_err() {
1288+
return -1;
1289+
}
1290+
0
1291+
}
1292+
11301293
/// Wait for the sandbox to exit. Returns a result handle with stdout/stderr.
11311294
///
1295+
/// For a `sandlock_popen` handle, close a piped `out_stdin_fd` and drain any
1296+
/// piped `out_stdout_fd`/`out_stderr_fd` *before* calling this, or a child that
1297+
/// reads to EOF or fills a pipe buffer never exits and this blocks forever.
1298+
///
11321299
/// # Safety
1133-
/// `h` must be a valid handle from `sandlock_create`.
1300+
/// `h` must be a valid handle from `sandlock_create` / `sandlock_popen`.
11341301
#[no_mangle]
11351302
pub unsafe extern "C" fn sandlock_handle_wait(h: *mut sandlock_handle_t) -> *mut sandlock_result_t {
11361303
if h.is_null() {
@@ -1211,7 +1378,7 @@ pub unsafe extern "C" fn sandlock_handle_port_mappings(h: *const sandlock_handle
12111378
/// Free a sandbox handle. Kills the process if still running.
12121379
///
12131380
/// # Safety
1214-
/// `h` must be null or a valid handle from `sandlock_create`.
1381+
/// `h` must be null or a valid handle from `sandlock_create` / `sandlock_popen`.
12151382
#[no_mangle]
12161383
pub unsafe extern "C" fn sandlock_handle_free(h: *mut sandlock_handle_t) {
12171384
if !h.is_null() {

0 commit comments

Comments
 (0)