@@ -283,6 +283,9 @@ impl SeccompCowBranch {
283283 fs:: File :: create ( upper) ?;
284284 return Ok ( ( ) ) ;
285285 }
286+ // On a kernel without openat2 (ENOSYS) the copy fails and the caller
287+ // rolls back / returns Continue; the child then hits Landlock, which
288+ // is the backstop. Do not "fix" this with an unconfined fs::copy.
286289 Err ( e) => return Err ( std:: io:: Error :: from_raw_os_error ( e) ) ,
287290 } ;
288291
@@ -795,6 +798,21 @@ impl SeccompCowBranch {
795798 if entry. file_type ( ) . is_dir ( ) {
796799 fs:: create_dir_all ( & dest)
797800 . map_err ( |e| BranchError :: Operation ( format ! ( "mkdir: {}" , e) ) ) ?;
801+ } else if entry. file_type ( ) . is_symlink ( ) {
802+ // Recreate the symlink verbatim. fs::copy would follow it and
803+ // read the target outside any root or Landlock (issue #112),
804+ // and dereferencing would also lose the link in the workdir.
805+ if let Some ( parent) = dest. parent ( ) {
806+ let _ = fs:: create_dir_all ( parent) ;
807+ }
808+ let target = fs:: read_link ( entry. path ( ) )
809+ . map_err ( |e| BranchError :: Operation ( format ! ( "readlink: {}" , e) ) ) ?;
810+ if dest. exists ( ) || dest. is_symlink ( ) {
811+ let _ = fs:: remove_file ( & dest) ;
812+ }
813+ std:: os:: unix:: fs:: symlink ( & target, & dest)
814+ . map_err ( |e| BranchError :: Operation ( format ! ( "symlink: {}" , e) ) ) ?;
815+ synced_dirs. insert ( dest. parent ( ) . unwrap ( ) . to_path_buf ( ) ) ;
798816 } else {
799817 if let Some ( parent) = dest. parent ( ) {
800818 let _ = fs:: create_dir_all ( parent) ;
@@ -1510,4 +1528,30 @@ mod tests {
15101528 let upper = branch. ensure_cow_copy ( "existing.txt" ) . unwrap ( ) ;
15111529 assert_eq ! ( std:: fs:: read_to_string( & upper) . unwrap( ) , "hello" ) ;
15121530 }
1531+
1532+ #[ test]
1533+ fn commit_does_not_dereference_escaping_symlink ( ) {
1534+ // A pre-existing workdir symlink with an absolute target gets copied
1535+ // verbatim into upper by prepare_copy; commit() must recreate it as a
1536+ // symlink, never read the target's content (issue #112, commit path).
1537+ let ( workdir, storage) = setup_workdir ( ) ;
1538+ std:: os:: unix:: fs:: symlink ( "/etc/group" , workdir. path ( ) . join ( "secret" ) ) . unwrap ( ) ;
1539+ let mut branch =
1540+ SeccompCowBranch :: create ( workdir. path ( ) , Some ( storage. path ( ) ) , 0 ) . unwrap ( ) ;
1541+ // Trigger copy-up of the symlink into upper.
1542+ let upper = branch. ensure_cow_copy ( "secret" ) . unwrap ( ) ;
1543+ assert ! ( upper. is_symlink( ) , "precondition: upper holds a verbatim symlink" ) ;
1544+
1545+ branch. commit ( ) . unwrap ( ) ;
1546+
1547+ let committed = workdir. path ( ) . join ( "secret" ) ;
1548+ assert ! (
1549+ committed. is_symlink( ) ,
1550+ "commit dereferenced the symlink instead of recreating it"
1551+ ) ;
1552+ assert_eq ! ( std:: fs:: read_link( & committed) . unwrap( ) , std:: path:: Path :: new( "/etc/group" ) ) ;
1553+ // The workdir entry must not have become a regular file holding the host content.
1554+ let meta = std:: fs:: symlink_metadata ( & committed) . unwrap ( ) ;
1555+ assert ! ( meta. file_type( ) . is_symlink( ) ) ;
1556+ }
15131557}
0 commit comments