@@ -1120,6 +1120,62 @@ pub fn write_child_mem(
11201120 Ok ( ( ) )
11211121}
11221122
1123+ /// Write bytes to a child, forcing past read-only page protections.
1124+ ///
1125+ /// [`write_child_mem`] uses `process_vm_writev`, which honors the target VMA's
1126+ /// protection bits and so returns `EFAULT` when the destination page is
1127+ /// read-only — e.g. a `.rodata` path literal a program hands to
1128+ /// `chdir`/`execve`. This variant writes through `/proc/<pid>/mem`, whose writes
1129+ /// use `FOLL_FORCE` and therefore copy-on-write past a read-only mapping (the
1130+ /// same mechanism a debugger uses to plant a breakpoint in read-only `.text`).
1131+ /// It handles writable and read-only destinations through one path, so the
1132+ /// argument-rewrite sites need no `EFAULT` fallback.
1133+ ///
1134+ /// Use ONLY for rewriting an *input* path argument the child owns that the
1135+ /// kernel re-reads under `SECCOMP_USER_NOTIF_FLAG_CONTINUE` (the `chdir`/`execve`
1136+ /// path rewrites). Do NOT use it for syscall *output* buffers (`stat`,
1137+ /// `getdents`, `getcwd`, `readlink`, the `getsockname`/`recvfrom` source
1138+ /// address): if a child supplies a read-only output buffer the real syscall
1139+ /// would `EFAULT`, and faithful emulation must too — keep those on
1140+ /// [`write_child_mem`]. (`bind`/`connect` need nothing here: they are emulated
1141+ /// on-behalf on a duped fd and never rewrite the child's sockaddr.)
1142+ ///
1143+ /// Opening `/proc/<pid>/mem` for write needs `PTRACE_MODE_ATTACH_FSCREDS` over
1144+ /// the child (no actual attach or stop): satisfied by the supervisor as the
1145+ /// child's same-uid parent under the common Yama scopes. Returns `Err` (so the
1146+ /// caller can still surface `EFAULT`) if the `mem` file can't be opened or
1147+ /// written — it never silently no-ops. TOCTOU-safe: brackets the write with
1148+ /// `id_valid` like [`write_child_mem`].
1149+ pub fn write_child_mem_force (
1150+ notif_fd : RawFd ,
1151+ id : u64 ,
1152+ pid : u32 ,
1153+ addr : u64 ,
1154+ data : & [ u8 ] ,
1155+ ) -> Result < ( ) , NotifError > {
1156+ id_valid ( notif_fd, id) . map_err ( NotifError :: Ioctl ) ?;
1157+ write_child_mem_proc ( pid, addr, data) ?;
1158+ id_valid ( notif_fd, id) . map_err ( NotifError :: Ioctl ) ?;
1159+ Ok ( ( ) )
1160+ }
1161+
1162+ /// Write bytes to a process's memory via `/proc/<pid>/mem` (open + `pwrite`).
1163+ ///
1164+ /// Inner helper for [`write_child_mem_force`]; the file offset is the target
1165+ /// virtual address. Writes here use `FOLL_FORCE`, so they copy-on-write past a
1166+ /// read-only destination page where [`write_child_mem_vm`] (`process_vm_writev`)
1167+ /// returns `EFAULT`.
1168+ fn write_child_mem_proc ( pid : u32 , addr : u64 , data : & [ u8 ] ) -> Result < ( ) , NotifError > {
1169+ use std:: os:: unix:: fs:: FileExt ;
1170+ let mem = std:: fs:: OpenOptions :: new ( )
1171+ . write ( true )
1172+ . open ( format ! ( "/proc/{}/mem" , pid) )
1173+ . map_err ( NotifError :: ChildMemoryWrite ) ?;
1174+ mem. write_all_at ( data, addr)
1175+ . map_err ( NotifError :: ChildMemoryWrite ) ?;
1176+ Ok ( ( ) )
1177+ }
1178+
11231179// ============================================================
11241180// Response dispatch
11251181// ============================================================
@@ -2267,6 +2323,57 @@ mod tests {
22672323 assert_eq ! ( data, 0x1234567890ABCDEF ) ;
22682324 }
22692325
2326+ /// The force-write path (`/proc/<pid>/mem`, FOLL_FORCE) must overwrite a
2327+ /// read-only page where `process_vm_writev` (`write_child_mem_vm`) refuses.
2328+ /// A read-only private page stands in for the `.rodata` path literal a child
2329+ /// hands to chdir/execve — the exact case the chroot/cow rewrite sites hit.
2330+ #[ test]
2331+ fn write_child_mem_proc_forces_past_readonly_page ( ) {
2332+ const PAGE : usize = 4096 ;
2333+ let orig = b"/some/rodata/path\0 " ;
2334+ let newb = b"/proc/self/fd/7\0 \0 " ; // fits within the page, near orig's len
2335+
2336+ let addr = unsafe {
2337+ libc:: mmap (
2338+ std:: ptr:: null_mut ( ) ,
2339+ PAGE ,
2340+ libc:: PROT_READ | libc:: PROT_WRITE ,
2341+ libc:: MAP_PRIVATE | libc:: MAP_ANONYMOUS ,
2342+ -1 ,
2343+ 0 ,
2344+ )
2345+ } ;
2346+ assert_ne ! ( addr, libc:: MAP_FAILED , "mmap failed" ) ;
2347+ unsafe { std:: ptr:: copy_nonoverlapping ( orig. as_ptr ( ) , addr as * mut u8 , orig. len ( ) ) } ;
2348+
2349+ // Flip the page read-only: a normal store would now SIGSEGV, and
2350+ // process_vm_writev honors the protection and fails.
2351+ assert_eq ! (
2352+ unsafe { libc:: mprotect( addr, PAGE , libc:: PROT_READ ) } ,
2353+ 0 ,
2354+ "mprotect PROT_READ failed"
2355+ ) ;
2356+
2357+ let pid = std:: process:: id ( ) ;
2358+ let uaddr = addr as u64 ;
2359+
2360+ assert ! (
2361+ write_child_mem_vm( pid, uaddr, newb) . is_err( ) ,
2362+ "process_vm_writev must fail on a read-only page"
2363+ ) ;
2364+
2365+ // FOLL_FORCE via /proc/<pid>/mem copies-on-write past the RO page.
2366+ write_child_mem_proc ( pid, uaddr, newb)
2367+ . expect ( "force-write through /proc/pid/mem must succeed on a read-only page" ) ;
2368+
2369+ // The write landed at the target address (page is still RO-mapped, so a
2370+ // plain read is fine and reflects the COW'd contents).
2371+ let got = unsafe { std:: slice:: from_raw_parts ( addr as * const u8 , newb. len ( ) ) } ;
2372+ assert_eq ! ( got, newb, "forced write must be visible at the target address" ) ;
2373+
2374+ unsafe { libc:: munmap ( addr, PAGE ) } ;
2375+ }
2376+
22702377 #[ test]
22712378 fn denylist_blocks_matching_cidr_allows_rest ( ) {
22722379 use crate :: network:: IpCidr ;
0 commit comments