You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Question: should the path-string syscall wrappers fail with EFAULT on an unreadable path pointer, and if so centrally in the path resolver or per wrapper? #9368
A bad-pointer sweep over the libkernel and libkernel_sys exports (983 functions, 1771 calls; each pointer parameter replaced in turn by nullptr, 1 and 0xfffffffffffff000, one child process per call, Ubuntu 24.04 WSL2 GCC 13.3 and Windows 10 MinGW GCC 15.2, main9c437e8) shows that nullptr already fails with EFAULT almost everywhere, but a non-null pointer into unmapped memory crashes the host (SIGSEGV on Linux, access violation on Windows) in 207 to 208 functions. Many of them are pthread_* and similar user-space functions where FreeBSD does not validate pointers either; 84 syscall-like functions crash on both hosts.
For the path arguments, 22 functions crash on Windows (24 on Linux) when the const char* is unreadable: open, creat, mkdir, rmdir, unlink, chmod, fchmodat, stat, lstat, link, symlink, renameat, utimes, lutimes, pathconf, shm_open, shm_unlink and the sceKernelStat, sceKernelMkdir, sceKernelRmdir, sceKernelUnlink, sceKernelRename, sceKernelUtimes, sceKernelCheckReachability forms.
FreeBSD 11.4 copies the path with copyinstr (sys/kern/vfs_lookup.c:328) and returns EFAULT.
Question
Do you want that behaviour, and where?
A readability check for the C string inside the path resolver (ResolvePathError_nid_no_patch), one place, but it touches the code every file call goes through; or
a small check in each wrapper (PathError in Stdio.cpp covers 12 of them; the sceKernel* forms have their own entry checks).
I tried (2) locally for PathError only: it removes about 20 of the roughly 700 crashing cases on Linux, because the sceKernel* forms and the resolver itself dereference the string before or without it. I have not opened a PR for the path strings, since #8680 was closed for touching Windows path resolution, and will wait for an answer.
The first family I have ready is the output structs (stat/fstat/fstatfs): the wrappers fill a local struct and copy it out through a checked write (GuestCopyOut, process_vm_writev on Linux and a VirtualQuery walk on Windows), returning EFAULT as FreeBSD's copyout does. A third question: is a shared helper like that acceptable, or would you rather have each wrapper use the existing guest-memory checks? I will wait for an answer before opening that PR as well.
I would keep the copy primitive central, but invoke it per wrapper. A readability probe in ResolvePathError_nid_no_patch is not sufficient and would still leave host crashes:
openat, mkdirat, fstatat and unlinkat build a native path directly for relative names, without calling the resolver.
symlink must copy both strings, but its target is intentionally never resolved.
Several wrappers read *path, call strlen, construct string_view(path) or log the pointer before they reach ResolvePathError_nid_no_patch.
ResolvePath_nid_no_patch also has trusted internal callers ("/app0/...", generated std::string::c_str() values and library paths). Making that low-level resolver classify every input as guest memory mixes two contracts.
Checking readability and then continuing to use the original pointer leaves a check/use race. FreeBSD copyinstr instead gives the syscall an owned snapshot.
The reusable part should therefore be something equivalent to:
It should use the same checked host-specific read mechanism as the existing ReadGuest/CopyIovecs work, copy through the first NUL into owned storage, inspect at most MAXPATHLEN (1024) bytes, return EFAULT when any required byte is unreadable, and ENAMETOOLONG when no NUL occurs in those 1024 bytes. Reading in page-bounded chunks avoids 1024 system calls while never reading beyond the page containing a terminator. The resolver can then take the copied string/string_view; no wrapper should dereference the original pointer afterward.
Each syscall wrapper still needs to call that helper where FreeBSD performs its copyinstr, because validation precedence is part of the ABI. For two-path operations, copy in the kernel order and stop after the first copy error. The same applies to the proposed output-struct work: a shared CopyGuestOut primitive is preferable to repeating process_vm_writev / VirtualQuery / Mach code, but wrappers should decide when it runs and should fill a local object first.
A focused regression test should cover, on all three hosts:
an unreadable first byte;
a readable prefix whose NUL lies in an unreadable next page;
a NUL in the last readable byte before a guard page (success);
1023 bytes plus NUL (success) and 1024 bytes without NUL (ENAMETOOLONG);
both operands and precedence for link, symlink and rename;
relative *at paths, proving the non-resolver paths are covered;
no filesystem side effect after a copy failure.
So my recommendation is option 2 at the wrapper boundary, backed by one shared checked C-string copy helper rather than repeated ad hoc pointer checks. This also separates the path topic cleanly from the broader 207-function bad-pointer sweep.
I found one path consumer that still appears outside #9423: access_nid_postfix in core/libs/prx/libc/src/Filesystem.cpp checks null and then evaluates *path before calling ResolvePath_nid_no_patch(path). The current #9474 symlink-target change also leaves that entry path untouched. Unless access is intentionally outside this syscall issue, an unreadable non-null pointer can still fault in the host before it returns guest EFAULT; it may be worth listing as remaining scope or covering in a follow-up. I have not tested this case locally.
Activity
AliZedan1A commentedon Oct 21, 2026
I would keep the copy primitive central, but invoke it per wrapper. A readability probe in
ResolvePathError_nid_no_patchis not sufficient and would still leave host crashes:openat,mkdirat,fstatatandunlinkatbuild a native path directly for relative names, without calling the resolver.symlinkmust copy both strings, but its target is intentionally never resolved.*path, callstrlen, constructstring_view(path)or log the pointer before they reachResolvePathError_nid_no_patch.ResolvePath_nid_no_patchalso has trusted internal callers ("/app0/...", generatedstd::string::c_str()values and library paths). Making that low-level resolver classify every input as guest memory mixes two contracts.copyinstrinstead gives the syscall an owned snapshot.The reusable part should therefore be something equivalent to:
It should use the same checked host-specific read mechanism as the existing
ReadGuest/CopyIovecswork, copy through the first NUL into owned storage, inspect at mostMAXPATHLEN(1024) bytes, returnEFAULTwhen any required byte is unreadable, andENAMETOOLONGwhen no NUL occurs in those 1024 bytes. Reading in page-bounded chunks avoids 1024 system calls while never reading beyond the page containing a terminator. The resolver can then take the copiedstring/string_view; no wrapper should dereference the original pointer afterward.Each syscall wrapper still needs to call that helper where FreeBSD performs its
copyinstr, because validation precedence is part of the ABI. For two-path operations, copy in the kernel order and stop after the first copy error. The same applies to the proposed output-struct work: a sharedCopyGuestOutprimitive is preferable to repeatingprocess_vm_writev/VirtualQuery/ Mach code, but wrappers should decide when it runs and should fill a local object first.A focused regression test should cover, on all three hosts:
ENAMETOOLONG);link,symlinkand rename;*atpaths, proving the non-resolver paths are covered;So my recommendation is option 2 at the wrapper boundary, backed by one shared checked C-string copy helper rather than repeated ad hoc pointer checks. This also separates the path topic cleanly from the broader 207-function bad-pointer sweep.
MarcZX10 commentedon Oct 21, 2026
I found one path consumer that still appears outside #9423:
access_nid_postfixincore/libs/prx/libc/src/Filesystem.cppchecks null and then evaluates*pathbefore callingResolvePath_nid_no_patch(path). The current #9474 symlink-target change also leaves that entry path untouched. Unlessaccessis intentionally outside this syscall issue, an unreadable non-null pointer can still fault in the host before it returns guestEFAULT; it may be worth listing as remaining scope or covering in a follow-up. I have not tested this case locally.