@@ -436,26 +436,149 @@ pub(crate) async fn handle_chroot_open(
436436 }
437437 }
438438
439- // Open directly via openat2(RESOLVE_IN_ROOT) — single atomic open
440- // confined to the chroot root (or mount target), no resolve-then-reopen TOCTOU gap.
441- let vp_str = virtual_path. to_string_lossy ( ) ;
442- let mode = if is_write { 0o666 } else { 0 } ;
443- let ( resolve_root, resolve_path) = if let Some ( ( mt, sub) ) = ctx. mount_target ( & virtual_path) {
444- ( mt. to_path_buf ( ) , sub)
445- } else {
446- ( ctx. root . to_path_buf ( ) , vp_str. to_string ( ) )
447- } ;
448- let fd = match openat2_in_root ( & resolve_root, & resolve_path, flags as i32 , mode) {
449- Ok ( fd) => fd,
450- Err ( errno) => return NotifAction :: Errno ( errno) ,
451- } ;
439+ // Resolve the path to an fd to hand the child: either a freshly opened tree
440+ // file or a dup of one of the child's own fds (a magic link). See
441+ // `open_in_namespace`.
442+ //
443+ // openat2 rejects a non-zero mode unless O_CREAT/O_TMPFILE is set (stricter
444+ // than openat), so only supply a creation mode when the child asks to create.
445+ // O_TMPFILE is a composite (__O_TMPFILE | O_DIRECTORY), so it must be matched
446+ // as a full mask, not with a bitwise-and that any O_DIRECTORY open would trip.
447+ let flags_i = flags as i32 ;
448+ let creates = flags_i & libc:: O_CREAT != 0 || flags_i & libc:: O_TMPFILE == libc:: O_TMPFILE ;
449+ let mode = if creates { 0o666 } else { 0 } ;
452450 let newfd_flags = if flags & libc:: O_CLOEXEC as u64 != 0 {
453451 libc:: O_CLOEXEC as u32
454452 } else {
455453 0
456454 } ;
457- let owned = unsafe { OwnedFd :: from_raw_fd ( fd) } ;
458- NotifAction :: InjectFdSend { srcfd : owned, newfd_flags }
455+ match open_in_namespace ( ctx, notif. pid , & virtual_path, flags as i32 , mode) {
456+ Ok ( srcfd) => NotifAction :: InjectFdSend { srcfd, newfd_flags } ,
457+ Err ( errno) => NotifAction :: Errno ( errno) ,
458+ }
459+ }
460+
461+ /// Open `virtual_path` within the child's virtual namespace, returning an fd to
462+ /// inject into the child.
463+ ///
464+ /// A path names one of two things:
465+ ///
466+ /// 1. A **tree object** reachable by walking the rootfs (or a mount target).
467+ /// `openat2(RESOLVE_IN_ROOT)` opens these atomically and confined, with no
468+ /// resolve-then-reopen TOCTOU gap. This is the overwhelmingly common case.
469+ ///
470+ /// 2. An **fd reference** such as `/proc/self/fd/N` (named directly, or reached
471+ /// through a symlink chain like `/dev/stderr -> /proc/self/fd/N` that
472+ /// container images use for logging). This is not a filesystem object at
473+ /// all; it names an entry in the child's *own* fd table, which the child
474+ /// already holds. `openat2(RESOLVE_IN_ROOT)` rightly declines to fabricate
475+ /// it (a magic link points outside the root by definition), so we serve it
476+ /// by dup'ing the child's fd. No new access: the open was already authorized
477+ /// by the caller and the child already owns the fd.
478+ ///
479+ /// openat2 success proves category 1 (it refuses magic links), so we only look
480+ /// for category 2 when it can't complete: EXDEV when the magic link sits in the
481+ /// same root, ENOENT/ENOTDIR when it points into another mount (e.g.
482+ /// `/dev/stderr -> /proc/...` resolved within the `/dev` mount). The fd walk is
483+ /// authoritative and returns None for genuine misses, so non-magic paths
484+ /// propagate their original errno unchanged.
485+ fn open_in_namespace (
486+ ctx : & ChrootCtx < ' _ > ,
487+ child_pid : u32 ,
488+ virtual_path : & Path ,
489+ flags : i32 ,
490+ mode : u32 ,
491+ ) -> Result < OwnedFd , i32 > {
492+ let vp_str = virtual_path. to_string_lossy ( ) ;
493+
494+ // Category 2, named directly: skip the open and dup straight away.
495+ if let Some ( child_fd) = magic_self_fd ( & vp_str, child_pid) {
496+ return crate :: seccomp:: notif:: dup_fd_from_pid ( child_pid, child_fd)
497+ . map_err ( |_| libc:: EBADF ) ;
498+ }
499+
500+ let ( root, sub) = match ctx. mount_target ( virtual_path) {
501+ Some ( ( mt, sub) ) => ( mt. to_path_buf ( ) , sub) ,
502+ None => ( ctx. root . to_path_buf ( ) , vp_str. to_string ( ) ) ,
503+ } ;
504+ match openat2_in_root ( & root, & sub, flags, mode) {
505+ // Category 1.
506+ Ok ( fd) => Ok ( unsafe { OwnedFd :: from_raw_fd ( fd) } ) ,
507+ // Category 2, reached through symlinks.
508+ Err ( errno) if matches ! ( errno, libc:: EXDEV | libc:: ENOENT | libc:: ENOTDIR ) => {
509+ match resolve_self_fd_magic ( ctx, child_pid, & vp_str) {
510+ Some ( child_fd) => crate :: seccomp:: notif:: dup_fd_from_pid ( child_pid, child_fd)
511+ . map_err ( |_| errno) ,
512+ None => Err ( errno) ,
513+ }
514+ }
515+ Err ( errno) => Err ( errno) ,
516+ }
517+ }
518+
519+ /// If `path` is a self-referential `/proc/.../fd/N` magic link for the calling
520+ /// child (`/proc/self`, `/proc/thread-self`, or `/proc/<child_pid>`), return the
521+ /// child fd `N`. The fd component must be a bare number (no trailing path).
522+ fn magic_self_fd ( path : & str , child_pid : u32 ) -> Option < i32 > {
523+ let ( who, fd_part) = path. strip_prefix ( "/proc/" ) ?. split_once ( "/fd/" ) ?;
524+ let is_self =
525+ who == "self" || who == "thread-self" || who. parse :: < u32 > ( ) . ok ( ) == Some ( child_pid) ;
526+ if !is_self || fd_part. contains ( '/' ) {
527+ return None ;
528+ }
529+ fd_part. parse :: < i32 > ( ) . ok ( )
530+ }
531+
532+ /// Read the symlink target at `virtual_path`, resolved within the chroot/mounts,
533+ /// without following the final component. Returns None if it is not a symlink.
534+ fn read_symlink_in_root ( ctx : & ChrootCtx < ' _ > , virtual_path : & str ) -> Option < String > {
535+ let confined = crate :: chroot:: resolve:: confine ( virtual_path) ;
536+ let ( root, sub) = if let Some ( ( mt, sub) ) = ctx. mount_target ( & confined) {
537+ ( mt. to_path_buf ( ) , sub)
538+ } else {
539+ ( ctx. root . to_path_buf ( ) , confined. to_string_lossy ( ) . into_owned ( ) )
540+ } ;
541+ let fd = openat2_in_root (
542+ & root,
543+ & sub,
544+ libc:: O_PATH | libc:: O_NOFOLLOW | libc:: O_CLOEXEC ,
545+ 0 ,
546+ )
547+ . ok ( ) ?;
548+ let mut buf = [ 0u8 ; 4096 ] ;
549+ let n = unsafe {
550+ libc:: readlinkat (
551+ fd,
552+ c"" . as_ptr ( ) ,
553+ buf. as_mut_ptr ( ) as * mut libc:: c_char ,
554+ buf. len ( ) ,
555+ )
556+ } ;
557+ unsafe { libc:: close ( fd) } ;
558+ if n <= 0 {
559+ return None ; // EINVAL (not a symlink) or read error
560+ }
561+ Some ( String :: from_utf8_lossy ( & buf[ ..n as usize ] ) . into_owned ( ) )
562+ }
563+
564+ /// Walk the symlink chain at `virtual_path` (confined to the chroot/mounts)
565+ /// looking for a terminating self-fd magic link; return the child fd it names.
566+ fn resolve_self_fd_magic ( ctx : & ChrootCtx < ' _ > , child_pid : u32 , virtual_path : & str ) -> Option < i32 > {
567+ let mut cur = virtual_path. to_string ( ) ;
568+ for _ in 0 ..40 {
569+ if let Some ( fd) = magic_self_fd ( & cur, child_pid) {
570+ return Some ( fd) ;
571+ }
572+ let target = read_symlink_in_root ( ctx, & cur) ?;
573+ let next = if Path :: new ( & target) . is_absolute ( ) {
574+ target
575+ } else {
576+ let parent = Path :: new ( & cur) . parent ( ) . unwrap_or_else ( || Path :: new ( "/" ) ) ;
577+ parent. join ( & target) . to_string_lossy ( ) . into_owned ( )
578+ } ;
579+ cur = crate :: chroot:: resolve:: confine ( & next) . to_string_lossy ( ) . into_owned ( ) ;
580+ }
581+ None
459582}
460583
461584// ============================================================
0 commit comments