@@ -19,9 +19,10 @@ use microsandbox_utils::term::{self, MULTI_PROGRESS};
1919use microsandbox_utils:: { env, EXTRACTED_LAYER_SUFFIX , LAYERS_SUBDIR , OCI_DB_FILENAME } ;
2020use sqlx:: { Pool , Sqlite } ;
2121#[ cfg( feature = "cli" ) ]
22- use std:: io:: { Read , Result as IoResult } ;
22+ use std:: io:: Result as IoResult ;
2323use std:: path:: { Path , PathBuf } ;
24- #[ cfg( feature = "cli" ) ]
24+ use std:: ffi:: CStr ;
25+ use std:: io:: Read ;
2526use tar:: Archive ;
2627use tempfile:: tempdir;
2728use tokio:: fs;
@@ -99,16 +100,9 @@ const EXTRACT_LAYERS_MSG: &str = "Extracting layers";
99100/// ```
100101pub async fn pull (
101102 name : Reference ,
102- image : bool ,
103+ _image : bool ,
103104 layer_path : Option < PathBuf > ,
104105) -> MicrosandboxResult < ( ) > {
105- // Image must be true
106- if !image {
107- return Err ( MicrosandboxError :: InvalidArgument (
108- "image must be true" . to_string ( ) ,
109- ) ) ;
110- }
111-
112106 // Single image pull mode (default if both flags are false, or if image is true)
113107 let registry = name. to_string ( ) . split ( '/' ) . next ( ) . unwrap_or ( "" ) . to_string ( ) ;
114108 let temp_download_dir = tempdir ( ) ?. into_path ( ) ;
@@ -367,10 +361,223 @@ async fn check_image_layers(
367361 }
368362}
369363
364+ /// Helper function to set xattr with stat information
365+ fn set_stat_xattr (
366+ path : & Path ,
367+ xattr_name : & CStr ,
368+ uid : u64 ,
369+ gid : u64 ,
370+ mode : u32 ,
371+ ) -> Result < ( ) , MicrosandboxError > {
372+ use std:: ffi:: CString ;
373+
374+ let stat_data = format ! ( "{}:{}:0{:o}" , uid, gid, mode) ;
375+ let path_cstring = CString :: new ( path. as_os_str ( ) . as_encoded_bytes ( ) )
376+ . map_err ( |e| MicrosandboxError :: LayerExtraction ( format ! ( "Invalid path: {:?}" , e) ) ) ?;
377+
378+ let result = unsafe {
379+ libc:: setxattr (
380+ path_cstring. as_ptr ( ) ,
381+ xattr_name. as_ptr ( ) ,
382+ stat_data. as_ptr ( ) as * const libc:: c_void ,
383+ stat_data. len ( ) ,
384+ 0 ,
385+ )
386+ } ;
387+
388+ if result != 0 {
389+ let errno = std:: io:: Error :: last_os_error ( ) ;
390+ if errno. raw_os_error ( ) == Some ( libc:: ENOTSUP ) {
391+ tracing:: warn!(
392+ "Filesystem does not support xattrs for {}, continuing without stat shadowing" ,
393+ path. display( )
394+ ) ;
395+ } else {
396+ return Err ( MicrosandboxError :: LayerExtraction ( format ! (
397+ "Failed to set xattr on {}: {}" ,
398+ path. display( ) ,
399+ errno
400+ ) ) ) ;
401+ }
402+ }
403+ Ok ( ( ) )
404+ }
405+
370406/// Extracts a layer from the downloaded tar.gz file into an extracted directory.
371407/// The extracted directory will be named as <layer-name>.extracted
408+ /// Custom extraction function that modifies file ownership during extraction
409+ fn extract_tar_with_ownership_override < R : Read > (
410+ archive : & mut Archive < R > ,
411+ extract_dir : & Path ,
412+ ) -> MicrosandboxResult < ( ) > {
413+ use std:: ffi:: CString ;
414+ use std:: os:: unix:: fs:: PermissionsExt ;
415+
416+ // Cache the xattr name to avoid repeated allocations
417+ let xattr_name = CString :: new ( "user.containers.override_stat" )
418+ . map_err ( |e| MicrosandboxError :: LayerExtraction ( format ! ( "Invalid attr name: {:?}" , e) ) ) ?;
419+
420+ // Structure to store hard link information
421+ struct HardLinkInfo {
422+ link_path : PathBuf ,
423+ target_path : PathBuf ,
424+ uid : u64 ,
425+ gid : u64 ,
426+ mode : u32 ,
427+ }
428+
429+ // Store hard links to process after all regular files are extracted
430+ let mut hard_links = Vec :: new ( ) ;
431+
432+ for entry in archive. entries ( ) ? {
433+ let mut entry =
434+ entry. map_err ( |e| MicrosandboxError :: LayerExtraction ( format ! ( "{:?}" , e) ) ) ?;
435+ let path = entry
436+ . path ( )
437+ . map_err ( |e| MicrosandboxError :: LayerExtraction ( format ! ( "{:?}" , e) ) ) ?;
438+ let full_path = extract_dir. join ( & path) ;
439+
440+ // Get the original metadata from the tar entry
441+ let original_uid = entry. header ( ) . uid ( ) ?;
442+ let original_gid = entry. header ( ) . gid ( ) ?;
443+ let original_mode = entry. header ( ) . mode ( ) ?;
444+
445+ // Check the entry type
446+ let entry_type = entry. header ( ) . entry_type ( ) ;
447+ let is_symlink = entry_type. is_symlink ( ) ;
448+ let is_hard_link = entry_type. is_hard_link ( ) ;
449+
450+ // Handle hard links separately - collect them for processing after all files are extracted
451+ if is_hard_link {
452+ if let Ok ( Some ( link_name) ) = entry. link_name ( ) {
453+ hard_links. push ( HardLinkInfo {
454+ link_path : full_path. clone ( ) ,
455+ target_path : extract_dir. join ( & link_name) ,
456+ uid : original_uid,
457+ gid : original_gid,
458+ mode : original_mode,
459+ } ) ;
460+ }
461+ continue ; // Skip to next entry
462+ }
463+
464+ // Extract the entry (regular files, directories, symlinks)
465+ entry
466+ . unpack ( & full_path)
467+ . map_err ( |e| MicrosandboxError :: LayerExtraction ( format ! ( "{:?}" , e) ) ) ?;
468+
469+ // Skip all operations for symlinks
470+ if is_symlink {
471+ tracing:: trace!(
472+ "Extracted symlink {} with original uid:gid:mode {}:{}:{:o}" ,
473+ full_path. display( ) ,
474+ original_uid,
475+ original_gid,
476+ original_mode
477+ ) ;
478+ continue ;
479+ }
480+
481+ // For regular files and directories, handle permissions and xattrs
482+ let metadata = std:: fs:: metadata ( & full_path) ?;
483+ let is_dir = metadata. is_dir ( ) ;
484+ let current_mode = metadata. permissions ( ) . mode ( ) ;
485+
486+ // Calculate the final desired permissions
487+ let desired_mode = if is_dir {
488+ // For directories, ensure at least u+rwx (0o700)
489+ current_mode | 0o700
490+ } else {
491+ // For files, ensure at least u+rw (0o600)
492+ current_mode | 0o600
493+ } ;
494+
495+ // If we need to modify permissions, do it once
496+ if current_mode != desired_mode {
497+ let mut permissions = metadata. permissions ( ) ;
498+ permissions. set_mode ( desired_mode) ;
499+ std:: fs:: set_permissions ( & full_path, permissions) ?;
500+ }
501+
502+ // Store original uid/gid/mode in xattrs
503+ set_stat_xattr ( & full_path, & xattr_name, original_uid, original_gid, original_mode) ?;
504+
505+ tracing:: trace!(
506+ "Extracted {} with original uid:gid:mode {}:{}:{:o}, stored in xattr" ,
507+ full_path. display( ) ,
508+ original_uid,
509+ original_gid,
510+ original_mode
511+ ) ;
512+ }
513+
514+ // Second pass: process hard links after all regular files are extracted
515+ for link_info in hard_links {
516+ // Create the hard link
517+ match std:: fs:: hard_link ( & link_info. target_path , & link_info. link_path ) {
518+ Ok ( _) => {
519+ // Hard link created successfully, now handle xattrs
520+ // Get metadata and ensure proper permissions
521+ let metadata = match std:: fs:: metadata ( & link_info. link_path ) {
522+ Ok ( m) => m,
523+ Err ( e) => {
524+ tracing:: warn!(
525+ "Failed to get metadata for hard link {}: {}" ,
526+ link_info. link_path. display( ) ,
527+ e
528+ ) ;
529+ continue ;
530+ }
531+ } ;
532+
533+ let current_mode = metadata. permissions ( ) . mode ( ) ;
534+ let desired_mode = current_mode | 0o600 ; // Ensure at least u+rw
535+
536+ // Set permissions if needed
537+ if current_mode != desired_mode {
538+ let mut permissions = metadata. permissions ( ) ;
539+ permissions. set_mode ( desired_mode) ;
540+ if let Err ( e) = std:: fs:: set_permissions ( & link_info. link_path , permissions) {
541+ tracing:: warn!(
542+ "Failed to set permissions for hard link {}: {}" ,
543+ link_info. link_path. display( ) ,
544+ e
545+ ) ;
546+ continue ;
547+ }
548+ }
549+
550+ // Store original uid/gid/mode in xattrs
551+ if let Err ( e) = set_stat_xattr ( & link_info. link_path , & xattr_name, link_info. uid , link_info. gid , link_info. mode ) {
552+ // For hard links, we just warn on xattr errors instead of failing
553+ tracing:: warn!( "Failed to set xattr on hard link {}: {}" , link_info. link_path. display( ) , e) ;
554+ }
555+
556+ tracing:: trace!(
557+ "Created hard link {} -> {} with original uid:gid:mode {}:{}:{:o}" ,
558+ link_info. link_path. display( ) ,
559+ link_info. target_path. display( ) ,
560+ link_info. uid,
561+ link_info. gid,
562+ link_info. mode
563+ ) ;
564+ }
565+ Err ( e) => {
566+ tracing:: warn!(
567+ "Failed to create hard link {} -> {}: {}" ,
568+ link_info. link_path. display( ) ,
569+ link_info. target_path. display( ) ,
570+ e
571+ ) ;
572+ }
573+ }
574+ }
575+
576+ Ok ( ( ) )
577+ }
578+
372579async fn extract_layer (
373- layer_path : impl AsRef < std :: path :: Path > ,
580+ layer_path : impl AsRef < Path > ,
374581 extract_base_dir : impl AsRef < Path > ,
375582) -> MicrosandboxResult < ( ) > {
376583 let layer_path = layer_path. as_ref ( ) ;
@@ -468,7 +675,7 @@ async fn extract_layer(
468675 } ;
469676 let decoder = GzDecoder :: new ( reader) ;
470677 let mut archive = Archive :: new ( decoder) ;
471- archive . unpack ( & extract_dir_clone) ?;
678+ extract_tar_with_ownership_override ( & mut archive , & extract_dir_clone) ?;
472679 Ok ( ( ) )
473680 } )
474681 . await
@@ -480,7 +687,6 @@ async fn extract_layer(
480687 #[ cfg( not( feature = "cli" ) ) ]
481688 {
482689 use flate2:: read:: GzDecoder ;
483- use tar:: Archive ;
484690
485691 let file =
486692 std:: fs:: File :: open ( layer_path) . map_err ( |e| MicrosandboxError :: LayerHandling {
@@ -489,9 +695,7 @@ async fn extract_layer(
489695 } ) ?;
490696 let decoder = GzDecoder :: new ( file) ;
491697 let mut archive = Archive :: new ( decoder) ;
492- archive
493- . unpack ( & extract_dir)
494- . map_err ( |e| MicrosandboxError :: LayerExtraction ( format ! ( "{:?}" , e) ) ) ?;
698+ extract_tar_with_ownership_override ( & mut archive, & extract_dir) ?;
495699 }
496700
497701 tracing:: info!(
0 commit comments