@@ -24,6 +24,8 @@ pub(crate) struct ContainerResult {
2424 pub ( crate ) filesystem_diff : Vec < String > ,
2525 pub ( crate ) timed_out : bool ,
2626 pub ( crate ) killed_by_monitor : bool ,
27+ /// Which runtime was used: "podman-gvisor", "podman"
28+ pub ( crate ) runtime_used : String ,
2729}
2830
2931/// A security alert from Falco runtime monitoring.
@@ -80,6 +82,25 @@ pub(crate) fn falco_log_path() -> Option<String> {
8082 None
8183}
8284
85+ /// Detect gVisor's runsc binary. Checks SCURL_RUNSC_PATH env var first,
86+ /// then common installation paths. Returns the absolute path if found.
87+ pub ( crate ) fn detect_runsc ( ) -> Option < String > {
88+ // Environment override
89+ if let Ok ( path) = std:: env:: var ( "SCURL_RUNSC_PATH" ) {
90+ let p = Path :: new ( & path) ;
91+ if p. is_file ( ) {
92+ return Some ( path) ;
93+ }
94+ }
95+ // Common install locations
96+ for candidate in & [ "/usr/local/bin/runsc" , "/usr/bin/runsc" ] {
97+ if Path :: new ( candidate) . is_file ( ) {
98+ return Some ( candidate. to_string ( ) ) ;
99+ }
100+ }
101+ None
102+ }
103+
83104/// Classify a Falco alert into a severity bucket for snipe decisions.
84105pub ( crate ) fn classify_alert ( alert : & FalcoAlert ) -> AlertSeverity {
85106 let rule_lower = alert. rule . to_lowercase ( ) ;
@@ -236,14 +257,66 @@ pub(crate) async fn monitor_falco(
236257 let _ = child. kill ( ) . await ;
237258}
238259
260+ /// Configure common Podman run arguments on an async command.
261+ /// When `runsc_path` is Some, adds `--runtime=<path>` for gVisor isolation.
262+ fn configure_podman_run (
263+ cmd : & mut tokio:: process:: Command ,
264+ container_name : & str ,
265+ host_path : & str ,
266+ runsc_path : Option < & str > ,
267+ ) {
268+ cmd. arg ( "run" )
269+ . arg ( "--name" )
270+ . arg ( container_name) ;
271+
272+ if let Some ( path) = runsc_path {
273+ cmd. arg ( format ! ( "--runtime={}" , path) ) ;
274+ }
275+
276+ cmd. arg ( "--network=none" )
277+ . arg ( "--read-only" )
278+ . arg ( "--tmpfs" )
279+ . arg ( "/tmp:rw,noexec,nosuid,size=64m" )
280+ . arg ( "--cap-drop=ALL" )
281+ . arg ( "--security-opt=no-new-privileges" )
282+ . arg ( "--memory=256m" )
283+ . arg ( "--pids-limit=256" )
284+ . arg ( "-v" )
285+ . arg ( format ! ( "{}:/install.sh:ro" , host_path) )
286+ . arg ( "docker.io/library/alpine:latest" )
287+ . arg ( "/bin/sh" )
288+ . arg ( "/install.sh" ) ;
289+ }
290+
291+ /// Warn if running rootless Podman with gVisor (known compatibility issues).
292+ fn warn_rootless_gvisor ( ) {
293+ let is_root = std:: process:: Command :: new ( "id" )
294+ . arg ( "-u" )
295+ . output ( )
296+ . ok ( )
297+ . and_then ( |o| String :: from_utf8 ( o. stdout ) . ok ( ) )
298+ . map ( |s| s. trim ( ) == "0" )
299+ . unwrap_or ( false ) ;
300+
301+ if !is_root {
302+ eprintln ! (
303+ " {} Rootless Podman + gVisor may have issues (cgroups, network namespaces).\n \
304+ Consider running with sudo for reliable gVisor isolation.\n \
305+ See: https://github.com/google/gvisor/issues/311",
306+ "⚠" . yellow( )
307+ ) ;
308+ }
309+ }
310+
239311/// Execute a script in a Podman container with optional Falco monitoring.
240- /// Returns the container result, any Falco alerts collected, and whether
241- /// the container was killed by the monitor .
312+ /// When `runsc_path` is Some, uses gVisor (runsc) as the OCI runtime.
313+ /// Returns the container result and any Falco alerts collected .
242314pub ( crate ) async fn execute_in_container (
243315 script : & str ,
244316 timeout_secs : u64 ,
245317 monitor_level : & MonitorLevel ,
246318 enable_monitor : bool ,
319+ runsc_path : Option < & str > ,
247320) -> Result < ( ContainerResult , Vec < FalcoAlert > ) > {
248321 use tokio:: process:: Command as AsyncCommand ;
249322
@@ -254,6 +327,10 @@ pub(crate) async fn execute_in_container(
254327 ) ;
255328 }
256329
330+ if runsc_path. is_some ( ) {
331+ warn_rootless_gvisor ( ) ;
332+ }
333+
257334 let start = std:: time:: Instant :: now ( ) ;
258335 let run_id = SystemTime :: now ( )
259336 . duration_since ( SystemTime :: UNIX_EPOCH )
@@ -297,23 +374,9 @@ pub(crate) async fn execute_in_container(
297374 let ( timed_out, killed_by_monitor, stdout, stderr, exit_code, alerts) = if falco_available {
298375 use tokio:: io:: AsyncReadExt ;
299376
300- let mut child = AsyncCommand :: new ( "podman" )
301- . arg ( "run" )
302- . arg ( "--name" )
303- . arg ( & container_name)
304- . arg ( "--network=none" )
305- . arg ( "--read-only" )
306- . arg ( "--tmpfs" )
307- . arg ( "/tmp:rw,noexec,nosuid,size=64m" )
308- . arg ( "--cap-drop=ALL" )
309- . arg ( "--security-opt=no-new-privileges" )
310- . arg ( "--memory=256m" )
311- . arg ( "--pids-limit=256" )
312- . arg ( "-v" )
313- . arg ( format ! ( "{}:/install.sh:ro" , host_path) )
314- . arg ( "docker.io/library/alpine:latest" )
315- . arg ( "/bin/sh" )
316- . arg ( "/install.sh" )
377+ let mut child_cmd = AsyncCommand :: new ( "podman" ) ;
378+ configure_podman_run ( & mut child_cmd, & container_name, host_path, runsc_path) ;
379+ let mut child = child_cmd
317380 . stdout ( std:: process:: Stdio :: piped ( ) )
318381 . stderr ( std:: process:: Stdio :: piped ( ) )
319382 . spawn ( ) ?;
@@ -425,27 +488,12 @@ pub(crate) async fn execute_in_container(
425488
426489 ( timed, sniped, so, se, code, collected)
427490 } else {
428- // ── Unmonitored path (simple output, same as Day 1) ──────────────
491+ // ── Unmonitored path (simple output) ────────────────────────────
492+ let mut run_cmd = AsyncCommand :: new ( "podman" ) ;
493+ configure_podman_run ( & mut run_cmd, & container_name, host_path, runsc_path) ;
429494 let run_result = tokio:: time:: timeout (
430495 Duration :: from_secs ( timeout_secs) ,
431- AsyncCommand :: new ( "podman" )
432- . arg ( "run" )
433- . arg ( "--name" )
434- . arg ( & container_name)
435- . arg ( "--network=none" )
436- . arg ( "--read-only" )
437- . arg ( "--tmpfs" )
438- . arg ( "/tmp:rw,noexec,nosuid,size=64m" )
439- . arg ( "--cap-drop=ALL" )
440- . arg ( "--security-opt=no-new-privileges" )
441- . arg ( "--memory=256m" )
442- . arg ( "--pids-limit=256" )
443- . arg ( "-v" )
444- . arg ( format ! ( "{}:/install.sh:ro" , host_path) )
445- . arg ( "docker.io/library/alpine:latest" )
446- . arg ( "/bin/sh" )
447- . arg ( "/install.sh" )
448- . output ( ) ,
496+ run_cmd. output ( ) ,
449497 )
450498 . await ;
451499
@@ -530,6 +578,12 @@ pub(crate) async fn execute_in_container(
530578 // Cleanup
531579 cleanup_container ( & container_name) . await ;
532580
581+ let runtime_used = if runsc_path. is_some ( ) {
582+ "podman-gvisor" . to_string ( )
583+ } else {
584+ "podman" . to_string ( )
585+ } ;
586+
533587 let result = ContainerResult {
534588 container_id,
535589 exit_code,
@@ -539,6 +593,7 @@ pub(crate) async fn execute_in_container(
539593 filesystem_diff,
540594 timed_out,
541595 killed_by_monitor,
596+ runtime_used,
542597 } ;
543598
544599 Ok ( ( result, alerts) )
@@ -557,6 +612,7 @@ pub(crate) fn display_container_result(result: &ContainerResult, alerts: &[Falco
557612 println ! ( "{}" , "─" . repeat( 50 ) ) ;
558613
559614 println ! ( "{} {}" , "Container ID:" . bold( ) , result. container_id) ;
615+ println ! ( "{} {}" , "Runtime:" . bold( ) , result. runtime_used) ;
560616 println ! ( "{} {}ms" , "Duration:" . bold( ) , result. duration_ms) ;
561617
562618 if result. killed_by_monitor {
@@ -668,6 +724,7 @@ mod tests {
668724 ] ,
669725 timed_out,
670726 killed_by_monitor,
727+ runtime_used : "podman" . to_string ( ) ,
671728 }
672729 }
673730
@@ -726,6 +783,7 @@ mod tests {
726783 10 ,
727784 & MonitorLevel :: Medium ,
728785 false ,
786+ None ,
729787 )
730788 . await ;
731789 assert ! ( result. is_err( ) ) ;
@@ -957,4 +1015,36 @@ mod tests {
9571015 }
9581016 }
9591017 }
1018+
1019+ // ── gVisor / runsc detection tests ──
1020+
1021+ #[ test]
1022+ fn test_detect_runsc_returns_option ( ) {
1023+ // Just verify it doesn't panic; actual result depends on system
1024+ let _result = detect_runsc ( ) ;
1025+ }
1026+
1027+ #[ test]
1028+ fn test_detect_runsc_respects_env ( ) {
1029+ // With a nonexistent path in env, should return None
1030+ std:: env:: set_var ( "SCURL_RUNSC_PATH" , "/nonexistent/runsc" ) ;
1031+ let result = detect_runsc ( ) ;
1032+ assert ! ( result. is_none( ) ) ;
1033+ std:: env:: remove_var ( "SCURL_RUNSC_PATH" ) ;
1034+ }
1035+
1036+ #[ test]
1037+ fn test_container_result_runtime_used_field ( ) {
1038+ let result = make_container_result ( false , false ) ;
1039+ assert_eq ! ( result. runtime_used, "podman" ) ;
1040+ }
1041+
1042+ #[ test]
1043+ fn test_display_container_result_shows_runtime ( ) {
1044+ let mut result = make_container_result ( false , false ) ;
1045+ result. runtime_used = "podman-gvisor" . to_string ( ) ;
1046+ // Should not panic; verify runtime field is present
1047+ display_container_result ( & result, & [ ] ) ;
1048+ assert_eq ! ( result. runtime_used, "podman-gvisor" ) ;
1049+ }
9601050}
0 commit comments