@@ -10,7 +10,7 @@ use std::time::Duration;
1010
1111use sandlock_core:: pipeline:: Stage ;
1212use sandlock_core:: sandbox:: { BranchAction , ByteSize , SandboxBuilder } ;
13- use sandlock_core:: { Protection , RunResult , Sandbox , StdioMode } ;
13+ use sandlock_core:: { ExitStatus , Protection , RunResult , Sandbox , StdioMode } ;
1414
1515pub mod handler;
1616pub mod notif_repr;
@@ -1425,6 +1425,43 @@ pub unsafe extern "C" fn sandlock_run_interactive(
14251425// Result accessors
14261426// ----------------------------------------------------------------
14271427
1428+ /// Why a sandboxed process terminated. `EXITED` carries an exit code
1429+ /// (`sandlock_result_exit_code`); `SIGNALED` carries the signal number
1430+ /// (`sandlock_result_signal`). Linux bottoms both a timeout and an OOM kill out
1431+ /// in `SIGKILL`, so there is no distinct OOM reason: a timeout sandlock enforced
1432+ /// is `TIMEOUT`, any other kill is `KILLED`.
1433+ // `#[repr(u32)]` (not `#[repr(C)]`) pins the discriminant to a `uint32_t`,
1434+ // matching the sibling FFI enums and the Go (`uint32`) / Python (`c_uint`)
1435+ // bindings; a bare C enum's width is implementation-defined (`-fshort-enums`).
1436+ #[ allow( non_camel_case_types) ]
1437+ #[ repr( u32 ) ]
1438+ pub enum sandlock_exit_reason_t {
1439+ /// Exited normally with a code (`sandlock_result_exit_code`).
1440+ Exited = 0 ,
1441+ /// Terminated by a signal (`sandlock_result_signal`).
1442+ Signaled = 1 ,
1443+ /// Killed with no recoverable signal number.
1444+ Killed = 2 ,
1445+ /// Killed by sandlock because it exceeded its timeout.
1446+ Timeout = 3 ,
1447+ }
1448+
1449+ fn exit_reason ( status : & ExitStatus ) -> sandlock_exit_reason_t {
1450+ match status {
1451+ ExitStatus :: Code ( _) => sandlock_exit_reason_t:: Exited ,
1452+ ExitStatus :: Signal ( _) => sandlock_exit_reason_t:: Signaled ,
1453+ ExitStatus :: Killed => sandlock_exit_reason_t:: Killed ,
1454+ ExitStatus :: Timeout => sandlock_exit_reason_t:: Timeout ,
1455+ }
1456+ }
1457+
1458+ fn exit_signal ( status : & ExitStatus ) -> c_int {
1459+ match status {
1460+ ExitStatus :: Signal ( n) => * n,
1461+ _ => -1 ,
1462+ }
1463+ }
1464+
14281465/// # Safety
14291466/// `r` must be null or a valid result pointer.
14301467#[ no_mangle]
@@ -1435,6 +1472,35 @@ pub unsafe extern "C" fn sandlock_result_exit_code(r: *const sandlock_result_t)
14351472 ( * r) . _private . code ( ) . unwrap_or ( -1 )
14361473}
14371474
1475+ /// Terminating reason (normal exit / signal / kill / timeout). Pair with
1476+ /// `sandlock_result_exit_code` (for `EXITED`) and `sandlock_result_signal`
1477+ /// (for `SIGNALED`). Returns `KILLED` for a null result.
1478+ ///
1479+ /// # Safety
1480+ /// `r` must be null or a valid result pointer.
1481+ #[ no_mangle]
1482+ pub unsafe extern "C" fn sandlock_result_reason (
1483+ r : * const sandlock_result_t ,
1484+ ) -> sandlock_exit_reason_t {
1485+ if r. is_null ( ) {
1486+ return sandlock_exit_reason_t:: Killed ;
1487+ }
1488+ exit_reason ( & ( * r) . _private . exit_status )
1489+ }
1490+
1491+ /// Signal number for a `SIGNALED` result, or `-1` for any other reason
1492+ /// (including a null result).
1493+ ///
1494+ /// # Safety
1495+ /// `r` must be null or a valid result pointer.
1496+ #[ no_mangle]
1497+ pub unsafe extern "C" fn sandlock_result_signal ( r : * const sandlock_result_t ) -> c_int {
1498+ if r. is_null ( ) {
1499+ return -1 ;
1500+ }
1501+ exit_signal ( & ( * r) . _private . exit_status )
1502+ }
1503+
14381504/// # Safety
14391505/// `r` must be null or a valid result pointer.
14401506#[ no_mangle]
@@ -1615,6 +1681,36 @@ pub unsafe extern "C" fn sandlock_dry_run_result_exit_code(
16151681 ( * r) . _private . run_result . code ( ) . unwrap_or ( -1 ) as c_int
16161682}
16171683
1684+ /// Terminating reason of a dry-run result (parity with
1685+ /// `sandlock_result_reason`). Returns `KILLED` for a null result.
1686+ ///
1687+ /// # Safety
1688+ /// `r` must be null or a valid dry-run result pointer.
1689+ #[ no_mangle]
1690+ pub unsafe extern "C" fn sandlock_dry_run_result_reason (
1691+ r : * const sandlock_dry_run_result_t ,
1692+ ) -> sandlock_exit_reason_t {
1693+ if r. is_null ( ) {
1694+ return sandlock_exit_reason_t:: Killed ;
1695+ }
1696+ exit_reason ( & ( * r) . _private . run_result . exit_status )
1697+ }
1698+
1699+ /// Signal number for a `SIGNALED` dry-run result, or `-1` otherwise (parity
1700+ /// with `sandlock_result_signal`).
1701+ ///
1702+ /// # Safety
1703+ /// `r` must be null or a valid dry-run result pointer.
1704+ #[ no_mangle]
1705+ pub unsafe extern "C" fn sandlock_dry_run_result_signal (
1706+ r : * const sandlock_dry_run_result_t ,
1707+ ) -> c_int {
1708+ if r. is_null ( ) {
1709+ return -1 ;
1710+ }
1711+ exit_signal ( & ( * r) . _private . run_result . exit_status )
1712+ }
1713+
16181714/// Check if the dry-run result indicates success.
16191715///
16201716/// # Safety
@@ -2737,6 +2833,42 @@ mod tests {
27372833 use super :: policy_ret_to_verdict;
27382834 use sandlock_core:: policy_fn:: Verdict ;
27392835
2836+ use super :: {
2837+ exit_reason, exit_signal, sandlock_dry_run_result_reason, sandlock_dry_run_result_signal,
2838+ sandlock_exit_reason_t, sandlock_result_reason, sandlock_result_signal,
2839+ } ;
2840+ use sandlock_core:: ExitStatus ;
2841+
2842+ #[ test]
2843+ fn exit_reason_maps_every_exit_status ( ) {
2844+ assert ! ( matches!( exit_reason( & ExitStatus :: Code ( 0 ) ) , sandlock_exit_reason_t:: Exited ) ) ;
2845+ assert ! ( matches!( exit_reason( & ExitStatus :: Signal ( 15 ) ) , sandlock_exit_reason_t:: Signaled ) ) ;
2846+ // SIGKILL folds into Killed in core, so KILLED is a distinct reason with
2847+ // no recoverable signal — the "systemd-style minus oom-kill" boundary.
2848+ assert ! ( matches!( exit_reason( & ExitStatus :: Killed ) , sandlock_exit_reason_t:: Killed ) ) ;
2849+ assert ! ( matches!( exit_reason( & ExitStatus :: Timeout ) , sandlock_exit_reason_t:: Timeout ) ) ;
2850+ assert_eq ! ( exit_signal( & ExitStatus :: Signal ( 15 ) ) , 15 ) ;
2851+ for s in [ ExitStatus :: Code ( 7 ) , ExitStatus :: Killed , ExitStatus :: Timeout ] {
2852+ assert_eq ! ( exit_signal( & s) , -1 , "only Signal(n) yields a number" ) ;
2853+ }
2854+ }
2855+
2856+ #[ test]
2857+ fn null_result_reason_is_killed_and_signal_minus_one ( ) {
2858+ unsafe {
2859+ assert ! ( matches!(
2860+ sandlock_result_reason( std:: ptr:: null( ) ) ,
2861+ sandlock_exit_reason_t:: Killed
2862+ ) ) ;
2863+ assert_eq ! ( sandlock_result_signal( std:: ptr:: null( ) ) , -1 ) ;
2864+ assert ! ( matches!(
2865+ sandlock_dry_run_result_reason( std:: ptr:: null( ) ) ,
2866+ sandlock_exit_reason_t:: Killed
2867+ ) ) ;
2868+ assert_eq ! ( sandlock_dry_run_result_signal( std:: ptr:: null( ) ) , -1 ) ;
2869+ }
2870+ }
2871+
27402872 #[ test]
27412873 fn policy_ret_maps_documented_values ( ) {
27422874 assert_eq ! ( policy_ret_to_verdict( 0 ) , Verdict :: Allow ) ;
0 commit comments