@@ -260,31 +260,48 @@ impl SeccompCowBranch {
260260
261261 /// Execute a file copy synchronously. Used by `ensure_cow_copy` and the
262262 /// async dispatch (via `spawn_blocking`).
263- pub fn execute_copy ( upper : & Path , lower : & Path ) -> Result < ( ) , std:: io:: Error > {
264- match fs:: copy ( lower, upper) {
265- Ok ( _) => {
266- if let Ok ( meta) = lower. metadata ( ) {
267- let _ = fs:: set_permissions ( upper, meta. permissions ( ) ) ;
268- }
269- Ok ( ( ) )
270- }
271- Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: PermissionDenied => {
272- // Can't read the lower file (e.g. root-owned 0640).
273- // Create an empty file in upper so writes can proceed.
263+ pub fn execute_copy (
264+ workdir_root : & Path ,
265+ rel : & str ,
266+ upper : & Path ,
267+ ) -> Result < ( ) , std:: io:: Error > {
268+ use std:: os:: unix:: io:: FromRawFd ;
269+
270+ // Read the lower source confined to the workdir root: a symlink or
271+ // `..` in `rel` cannot escape the tree (issue #112).
272+ let fd = match crate :: sys:: fs:: openat2_in_root (
273+ workdir_root,
274+ rel,
275+ libc:: O_RDONLY | libc:: O_CLOEXEC ,
276+ 0 ,
277+ ) {
278+ Ok ( fd) => fd,
279+ // Unreadable (EACCES) or confined-out / absent (ENOENT): give the
280+ // child an empty COW file so writes proceed, never leaking the
281+ // escape target.
282+ Err ( libc:: EACCES ) | Err ( libc:: ENOENT ) => {
274283 fs:: File :: create ( upper) ?;
275- Ok ( ( ) )
284+ return Ok ( ( ) ) ;
276285 }
277- Err ( e) => Err ( e) ,
286+ Err ( e) => return Err ( std:: io:: Error :: from_raw_os_error ( e) ) ,
287+ } ;
288+
289+ let mut src = unsafe { fs:: File :: from_raw_fd ( fd) } ;
290+ let mut dst = fs:: File :: create ( upper) ?;
291+ std:: io:: copy ( & mut src, & mut dst) ?;
292+ if let Ok ( meta) = src. metadata ( ) {
293+ let _ = fs:: set_permissions ( upper, meta. permissions ( ) ) ;
278294 }
295+ Ok ( ( ) )
279296 }
280297
281298 /// Ensure a COW copy exists in upper (synchronous). Returns the upper path.
282299 /// For callers that don't need async two-phase behavior.
283300 pub fn ensure_cow_copy ( & mut self , rel_path : & str ) -> Result < PathBuf , BranchError > {
284301 match self . prepare_copy ( rel_path) ? {
285302 CowCopyPlan :: Ready ( upper) => Ok ( upper) ,
286- CowCopyPlan :: NeedsCopy { upper, lower, file_size } => {
287- match Self :: execute_copy ( & upper , & lower ) {
303+ CowCopyPlan :: NeedsCopy { upper, lower : _lower , file_size } => {
304+ match Self :: execute_copy ( & self . workdir , rel_path , & upper ) {
288305 Ok ( ( ) ) => Ok ( upper) ,
289306 Err ( e) => {
290307 self . rollback_copy ( file_size) ;
@@ -1464,4 +1481,33 @@ mod tests {
14641481 let path = abs ( & branch, "existing.txt" ) ;
14651482 assert ! ( branch. handle_unlink( & path, false ) . unwrap( ) ) ;
14661483 }
1484+
1485+ #[ test]
1486+ fn copy_up_does_not_follow_symlinked_parent ( ) {
1487+ // workdir/evil -> /etc ; writing evil/group must not copy /etc/group.
1488+ let ( workdir, storage) = setup_workdir ( ) ;
1489+ std:: os:: unix:: fs:: symlink ( "/etc" , workdir. path ( ) . join ( "evil" ) ) . unwrap ( ) ;
1490+ let mut branch =
1491+ SeccompCowBranch :: create ( workdir. path ( ) , Some ( storage. path ( ) ) , 0 ) . unwrap ( ) ;
1492+
1493+ // ensure_cow_copy on a path reached through the symlinked parent.
1494+ let upper = branch. ensure_cow_copy ( "evil/group" ) . unwrap ( ) ;
1495+
1496+ // The upper file must NOT contain the host /etc/group contents.
1497+ let host = std:: fs:: read_to_string ( "/etc/group" ) . unwrap_or_default ( ) ;
1498+ let copied = std:: fs:: read_to_string ( & upper) . unwrap_or_default ( ) ;
1499+ assert ! (
1500+ copied. is_empty( ) || copied != host,
1501+ "copy-up leaked /etc/group into the upper layer"
1502+ ) ;
1503+ }
1504+
1505+ #[ test]
1506+ fn copy_up_copies_legitimate_in_tree_file ( ) {
1507+ let ( workdir, storage) = setup_workdir ( ) ;
1508+ let mut branch =
1509+ SeccompCowBranch :: create ( workdir. path ( ) , Some ( storage. path ( ) ) , 0 ) . unwrap ( ) ;
1510+ let upper = branch. ensure_cow_copy ( "existing.txt" ) . unwrap ( ) ;
1511+ assert_eq ! ( std:: fs:: read_to_string( & upper) . unwrap( ) , "hello" ) ;
1512+ }
14671513}
0 commit comments