@@ -2,11 +2,19 @@ use crate::context::RuntimeContext;
22use crate :: error:: { ExecutorError , Result } ;
33use crate :: ExecutionRequest ;
44use nix:: fcntl:: { Flock , FlockArg } ;
5- use serde:: Deserialize ;
5+ use serde:: { Deserialize , Serialize } ;
66use std:: path:: { Path , PathBuf } ;
77use std:: process:: Stdio ;
88use tokio:: process:: Command as TokioCommand ;
99
10+ const EXCLUDED_ROOTFS_DIRS : & [ & str ] = & [ "dev" , "proc" , "tmp" ] ;
11+
12+ #[ derive( Debug , Serialize , Deserialize ) ]
13+ struct OverlayCapabilityCache {
14+ tmp_overlay_supported : bool ,
15+ checked_at : String ,
16+ }
17+
1018pub struct BwrapRuntime ;
1119
1220const EXCLUDED_HOST_DIRS : & [ & str ] = & [ "dev" , "proc" , "sys" , "nix" ] ;
@@ -212,7 +220,20 @@ impl BwrapRuntime {
212220 Ok ( union_dir)
213221 }
214222
215- pub async fn check_overlay_support ( ctx : & RuntimeContext < ' _ > , lower_dir : & Path ) -> bool {
223+ pub async fn check_overlay_support ( ctx : & RuntimeContext < ' _ > , _lower_dir : & Path ) -> bool {
224+ let cache_dir = ctx. get_capabilities_cache_dir ( ) ;
225+ let cache_file = cache_dir. join ( "overlay_support.json" ) ;
226+
227+ if let Ok ( content) = tokio:: fs:: read_to_string ( & cache_file) . await {
228+ if let Ok ( cached) = serde_json:: from_str :: < OverlayCapabilityCache > ( & content) {
229+ return cached. tmp_overlay_supported ;
230+ }
231+ }
232+
233+ Self :: run_overlay_check ( ctx) . await
234+ }
235+
236+ async fn run_overlay_check ( ctx : & RuntimeContext < ' _ > ) -> bool {
216237 let bwrap_path = match ctx. get_host_tool_path ( "bwrap" ) {
217238 Ok ( p) => p,
218239 Err ( _) => return false ,
@@ -234,10 +255,12 @@ impl BwrapRuntime {
234255 let upper = start_path. join ( "upper" ) ;
235256 let work = start_path. join ( "work" ) ;
236257 let merged = start_path. join ( "merged" ) ;
258+ let lower = start_path. join ( "lower" ) ;
237259
238260 if tokio:: fs:: create_dir ( & upper) . await . is_err ( )
239261 || tokio:: fs:: create_dir ( & work) . await . is_err ( )
240262 || tokio:: fs:: create_dir ( & merged) . await . is_err ( )
263+ || tokio:: fs:: create_dir ( & lower) . await . is_err ( )
241264 {
242265 return false ;
243266 }
@@ -249,7 +272,7 @@ impl BwrapRuntime {
249272 . arg ( "/" )
250273 . arg ( "/" )
251274 . arg ( "--overlay-src" )
252- . arg ( lower_dir )
275+ . arg ( & lower )
253276 . arg ( "--overlay" )
254277 . arg ( & upper)
255278 . arg ( & work)
@@ -265,6 +288,103 @@ impl BwrapRuntime {
265288 }
266289 }
267290
291+ pub async fn check_tmp_overlay_support ( ctx : & RuntimeContext < ' _ > , rootfs_path : & Path ) -> bool {
292+ let cache_dir = ctx. get_capabilities_cache_dir ( ) ;
293+ let cache_file = cache_dir. join ( "overlay_support.json" ) ;
294+
295+ if let Ok ( content) = tokio:: fs:: read_to_string ( & cache_file) . await {
296+ if let Ok ( cached) = serde_json:: from_str :: < OverlayCapabilityCache > ( & content) {
297+ tracing:: debug!(
298+ "Using cached overlay support result: supported={}" ,
299+ cached. tmp_overlay_supported
300+ ) ;
301+ return cached. tmp_overlay_supported ;
302+ }
303+ }
304+
305+ let supported = Self :: run_tmp_overlay_check ( ctx, rootfs_path) . await ;
306+
307+ if let Err ( e) = tokio:: fs:: create_dir_all ( & cache_dir) . await {
308+ tracing:: debug!( "Failed to create capabilities cache dir: {}" , e) ;
309+ } else {
310+ let cache_entry = OverlayCapabilityCache {
311+ tmp_overlay_supported : supported,
312+ checked_at : chrono:: Utc :: now ( ) . to_rfc3339 ( ) ,
313+ } ;
314+ if let Ok ( json) = serde_json:: to_string_pretty ( & cache_entry) {
315+ if let Err ( e) = tokio:: fs:: write ( & cache_file, json) . await {
316+ tracing:: debug!( "Failed to write overlay capability cache: {}" , e) ;
317+ }
318+ }
319+ }
320+
321+ supported
322+ }
323+
324+ async fn run_tmp_overlay_check ( ctx : & RuntimeContext < ' _ > , rootfs_path : & Path ) -> bool {
325+ let bwrap_path = match ctx. get_host_tool_path ( "bwrap" ) {
326+ Ok ( p) => p,
327+ Err ( _) => return false ,
328+ } ;
329+
330+ let temp_base = ctx. get_temp_path ( ) ;
331+ let temp_dir = match tempfile:: Builder :: new ( )
332+ . prefix ( ".repx-tmp-overlay-check-" )
333+ . tempdir_in ( & temp_base)
334+ {
335+ Ok ( t) => t,
336+ Err ( e) => {
337+ tracing:: debug!( "Failed to create temp dir for tmp-overlay check: {}" , e) ;
338+ return false ;
339+ }
340+ } ;
341+
342+ let test_lower = if rootfs_path. join ( "bin" ) . exists ( ) {
343+ rootfs_path. join ( "bin" )
344+ } else if rootfs_path. join ( "etc" ) . exists ( ) {
345+ rootfs_path. join ( "etc" )
346+ } else {
347+ rootfs_path. to_path_buf ( )
348+ } ;
349+
350+ let test_mount_point = temp_dir. path ( ) . join ( "test" ) ;
351+ if tokio:: fs:: create_dir ( & test_mount_point) . await . is_err ( ) {
352+ return false ;
353+ }
354+
355+ let mut cmd = TokioCommand :: new ( & bwrap_path) ;
356+
357+ cmd. arg ( "--unshare-user" )
358+ . arg ( "--dev-bind" )
359+ . arg ( "/" )
360+ . arg ( "/" )
361+ . arg ( "--overlay-src" )
362+ . arg ( & test_lower)
363+ . arg ( "--tmp-overlay" )
364+ . arg ( & test_mount_point)
365+ . arg ( "true" ) ;
366+
367+ ctx. restrict_command_environment ( & mut cmd, & [ ] ) ;
368+ cmd. stdout ( Stdio :: null ( ) ) . stderr ( Stdio :: null ( ) ) ;
369+
370+ match cmd. status ( ) . await {
371+ Ok ( status) => {
372+ let supported = status. success ( ) ;
373+ if !supported {
374+ tracing:: debug!(
375+ "tmp-overlay check failed for {:?} - kernel may not support userxattr overlay" ,
376+ test_lower
377+ ) ;
378+ }
379+ supported
380+ }
381+ Err ( e) => {
382+ tracing:: debug!( "tmp-overlay check command failed: {}" , e) ;
383+ false
384+ }
385+ }
386+ }
387+
268388 pub async fn build_command (
269389 ctx : & RuntimeContext < ' _ > ,
270390 rootfs_path : & Path ,
@@ -290,12 +410,25 @@ impl BwrapRuntime {
290410 } else {
291411 cmd. arg ( "--unshare-all" )
292412 . arg ( "--hostname" )
293- . arg ( "repx-container" )
294- . arg ( "--overlay-src" )
295- . arg ( rootfs_path)
296- . arg ( "--tmp-overlay" )
297- . arg ( "/" )
298- . arg ( "--dev" )
413+ . arg ( "repx-container" ) ;
414+
415+ let overlay_supported = Self :: check_tmp_overlay_support ( ctx, rootfs_path) . await ;
416+
417+ if overlay_supported {
418+ cmd. arg ( "--overlay-src" )
419+ . arg ( rootfs_path)
420+ . arg ( "--tmp-overlay" )
421+ . arg ( "/" ) ;
422+ } else {
423+ tracing:: info!(
424+ "Overlay filesystem not supported on target (kernel may lack userxattr support). \
425+ Using read-only bind mounts for rootfs."
426+ ) ;
427+
428+ Self :: configure_readonly_rootfs_mounts ( & mut cmd, rootfs_path) . await ?;
429+ }
430+
431+ cmd. arg ( "--dev" )
299432 . arg ( "/dev" )
300433 . arg ( "--proc" )
301434 . arg ( "/proc" )
@@ -497,4 +630,35 @@ impl BwrapRuntime {
497630
498631 Ok ( ( ) )
499632 }
633+
634+ async fn configure_readonly_rootfs_mounts (
635+ cmd : & mut TokioCommand ,
636+ rootfs_path : & Path ,
637+ ) -> Result < ( ) > {
638+ let entries = match std:: fs:: read_dir ( rootfs_path) {
639+ Ok ( e) => e,
640+ Err ( e) => {
641+ return Err ( ExecutorError :: Io ( std:: io:: Error :: new (
642+ std:: io:: ErrorKind :: NotFound ,
643+ format ! ( "Failed to read rootfs directory {:?}: {}" , rootfs_path, e) ,
644+ ) ) ) ;
645+ }
646+ } ;
647+
648+ for entry in entries. flatten ( ) {
649+ let file_name = entry. file_name ( ) ;
650+ let file_name_str = file_name. to_string_lossy ( ) ;
651+
652+ if EXCLUDED_ROOTFS_DIRS . contains ( & file_name_str. as_ref ( ) ) {
653+ continue ;
654+ }
655+
656+ let source_path = entry. path ( ) ;
657+ let target_path = format ! ( "/{}" , file_name_str) ;
658+
659+ cmd. arg ( "--ro-bind" ) . arg ( & source_path) . arg ( & target_path) ;
660+ }
661+
662+ Ok ( ( ) )
663+ }
500664}
0 commit comments