@@ -938,6 +938,76 @@ def _dispatch(self, notif: SeccompNotif) -> None:
938938 self ._respond_continue (notif .id )
939939 return
940940
941+ # --- COW: execve / execveat path redirection ---
942+ if self ._cow_handler is not None :
943+ nr_execve = _SYSCALL_NR .get ("execve" )
944+ nr_execveat = _SYSCALL_NR .get ("execveat" )
945+
946+ if nr in (nr_execve , nr_execveat ):
947+ try :
948+ if nr == nr_execve :
949+ # execve(pathname, argv, envp)
950+ pathname_addr = notif .data .args [0 ]
951+ path = resolve_openat_path (pid , - 100 , pathname_addr )
952+ else :
953+ # execveat(dirfd, pathname, argv, envp, flags)
954+ dirfd = ctypes .c_int32 (notif .data .args [0 ] & 0xFFFFFFFF ).value
955+ exec_flags = notif .data .args [4 ]
956+ if exec_flags & 0x1000 : # AT_EMPTY_PATH — fd-based, no path
957+ self ._respond_continue (notif .id )
958+ return
959+ pathname_addr = notif .data .args [1 ]
960+ path = resolve_openat_path (pid , dirfd , pathname_addr )
961+ except OSError :
962+ self ._respond_continue (notif .id )
963+ return
964+
965+ if not self ._id_valid (notif .id ):
966+ return
967+
968+ if not self ._cow_handler .matches (path ):
969+ self ._respond_continue (notif .id )
970+ return
971+
972+ # Resolve through COW layer
973+ real_path = self ._cow_handler .handle_stat (path )
974+ if real_path is None :
975+ # File deleted in COW
976+ self ._respond_errno (notif .id , errno .ENOENT )
977+ return
978+
979+ # If unchanged (real_path == path), let kernel handle it
980+ if real_path == path :
981+ self ._respond_continue (notif .id )
982+ return
983+
984+ # File is in upper layer — inject fd then rewrite path
985+ try :
986+ src_fd = os .open (real_path , os .O_RDONLY | os .O_CLOEXEC )
987+ except OSError :
988+ self ._respond_continue (notif .id )
989+ return
990+
991+ try :
992+ child_fd = self ._inject_fd (notif .id , src_fd , cloexec = False )
993+ finally :
994+ os .close (src_fd )
995+
996+ if child_fd < 0 :
997+ self ._respond_continue (notif .id )
998+ return
999+
1000+ # Overwrite the pathname in child memory with /proc/self/fd/N
1001+ proc_path = f"/proc/self/fd/{ child_fd } \0 " .encode ()
1002+ try :
1003+ write_bytes (pid , pathname_addr , proc_path )
1004+ except OSError :
1005+ self ._respond_continue (notif .id )
1006+ return
1007+
1008+ self ._respond_continue (notif .id )
1009+ return
1010+
9411011 # --- Filesystem: open / openat virtualization + COW ---
9421012 nr_openat = _SYSCALL_NR .get ("openat" )
9431013 nr_open = _SYSCALL_NR .get ("open" )
@@ -1050,6 +1120,26 @@ def _respond_addfd(self, notif_id: int, src_fd: int) -> None:
10501120 ctypes .byref (resp ),
10511121 )
10521122
1123+ def _inject_fd (self , notif_id : int , src_fd : int ,
1124+ cloexec : bool = False ) -> int :
1125+ """Inject an fd into the child without completing the notification.
1126+
1127+ Returns the fd number in the child's table, or -1 on failure.
1128+ """
1129+ addfd = SeccompNotifAddfd ()
1130+ addfd .id = notif_id
1131+ addfd .flags = 0 # Don't auto-send response
1132+ addfd .srcfd = src_fd
1133+ addfd .newfd = 0
1134+ addfd .newfd_flags = os .O_CLOEXEC if cloexec else 0
1135+
1136+ ret = _libc .ioctl (
1137+ ctypes .c_int (self ._notify_fd ),
1138+ ctypes .c_ulong (SECCOMP_IOCTL_NOTIF_ADDFD ),
1139+ ctypes .byref (addfd ),
1140+ )
1141+ return ret
1142+
10531143 # Network, memory, and fork handlers moved to _network.py and _resource.py
10541144
10551145 def _handle_port_remap (self , notif : SeccompNotif , nr : int ) -> None :
0 commit comments