std::filesystem::status does not follow a Windows symbolic link under
MinGW, so the lookup chflags makes saw a link whose target is missing as
an existing file and returned 0. A reparse point in the last component
is now looked up through the target with the guest stat, as FreeBSD 11.4's
namei follows it: a missing target gives ENOENT, and a trailing slash
after a link to a file gives ENOTDIR.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's #9618 has lstat fill a local FileStat and copy it out to the guest.
The merge keeps that and this branch's module lookup in the same call, so an
original .prx name still resolves to its relinked .guest.prx.
On Windows, rename converted ENOENT or ENOTDIR to ELOOP when either parent directory was below a symbolic link loop. A missing source, or a source below a non-directory, paired with a destination through a loop then reported ELOOP. FreeBSD 11.4 kern_renameat returns the failed source lookup before it examines the destination. A loop below the source still gives ELOOP, and a loop below the destination gives ELOOP only when the source lookup succeeded.
guest_filesystem gains the missing-source and non-directory-source-prefix cases paired with a looping destination, for rename and sceKernelRename.
FreeBSD 11.4's vn_open_cred does not follow the last component when
O_EXCL is set, so O_CREAT|O_EXCL on a symbolic link fails with EEXIST
whatever it points to. On Windows the host open followed a dangling link
and created its target. A reparse point in the last component now fails
O_CREAT|O_EXCL with EEXIST before the host open.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
_wchmod changes the read-only attribute of the path it is given, so on
Windows chmod through a symbolic link changed the link and left the file
it names unchanged, and a dangling link succeeded. A reparse point is now
opened through its target and the target's path is changed, as FreeBSD
11.4's sys_chmod passes FOLLOW: a missing target fails with ENOENT and a
link loop with ELOOP. The path lookup of a handle moves out of
WindowsDescriptorPath_nid_no_patch so both use it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
FreeBSD resolves the descriptor with getvnode in sys_fchflags
(vfs_syscalls.c:2403), which fails with EINVAL for a descriptor that has no
vnode. fchflags threw for a kqueue descriptor instead.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
FreeBSD 11.4's lookup() (sys/kern/vfs_lookup.c) fails with ENOTDIR when
a path with a trailing slash ends at something that is not a directory.
On Windows a mount alias dropped the slash, and the host calls do not
report ENOTDIR for "file\", so unlink, rename and open acted on the file
or failed with EINVAL. Guest path resolution now rejects an existing
non-directory named with a trailing slash with ENOTDIR on every host, and
a mount alias keeps the slash on Windows as it does on Linux and macOS.
fchmod and sceKernelFchmod on either end of a pipe answered ENOENT on
Windows, where the pipe has no path, and changed the pipe's mode on Linux.
FreeBSD 11's pipe_chmod sends an unnamed pipe to invfo_chmod, which
returns EINVAL, as sockets already get here.
FreeBSD 11.4's vn_seek fails SEEK_CUR and SEEK_END with EOVERFLOW when the base offset plus a positive offset does not fit in an off_t, and with EINVAL only when the result is negative. The host lseek reports EINVAL for the overflow, so the guest saw EINVAL. When the host call fails with EINVAL for a positive offset on SEEK_CUR or SEEK_END, the base position is read back and the call fails with EOVERFLOW if the sum would overflow; the file position is left unchanged.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
FreeBSD 11 fopen opens the path with _open, and vn_open_vnode refuses a
directory only for FWRITE or O_TRUNC, so fopen of a directory in a read
mode succeeds; Linux hosts already open it. On Windows CreateFileW
without FILE_FLAG_BACKUP_SEMANTICS refused it with EACCES.
fopen now opens such a path as a libkernel directory descriptor, which
libkernel registers in libc when it loads, and fclose and freopen remove
it from libkernel's table. Reads on the stream fail with EISDIR and set
the error indicator, as on a Linux host and FreeBSD's tmpfs: the Windows
CRT fails them already but reports EINVAL. Writes fail with EBADF, and
fseek, ftell and ungetc work, as on any read-only stream.
The two Windows cases, a doubled separator after the drive root and after a
folder name, come from MarcZX10's follow-up d68de63527 to the root review on
this PR.
Co-Authored-By: MarcZX10 <[email protected]>
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Trimming every trailing separator turned the Windows drive root "C:\" into
the drive-relative "C:". Trim only while the parent is longer than its
root_path(), and cover "/", "C:\" and "C:/" in guest_filesystem.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
RenameDotError_nid_no_patch resolved the parent with the separator kept
("file/"). On Linux std::filesystem::exists("file/") is false for a regular
file, so unlink, rmdir and rename of "file/." answered ENOENT instead of
ENOTDIR. Trim the trailing separators first (a lone "/" stays) and cover
rename of "file/." and "file/.." in guest_filesystem.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
FreeBSD lookup() refuses "." and ".." as the last component of a DELETE
lookup with EINVAL (sys/kern/vfs_lookup.c:750-754 and 823-826), before the
unlink and rmdir checks run. unlink and sceKernelUnlink answered EPERM,
rmdir and sceKernelRmdir answered ENOTEMPTY for "..", and unlinkat without
AT_REMOVEDIR answered EPERM. Reuse the RenameDot check added for rename, so a
missing or non-directory parent still reports ENOENT or ENOTDIR first.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Resolve() checks a "." only as the last component, so "file/./child" was
already ENOTDIR from the host and did not exercise the change.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Both fixtures required FSCTL_SET_REPARSE_POINT to succeed; a user without the right would have
aborted guest_filesystem and guest_kernel_sys. They now print that the checks are skipped and carry
on, as the symbolic link checks do without Developer Mode. On this PC the control succeeds at
medium integrity, so the checks ran both elevated and unelevated.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
FreeBSD looks the "." up in the directory before it, so "file/." fails with
ENOTDIR and "missing/." with ENOENT (sys/kern/vfs_cache.c vfs_cache_lookup,
`dvp->v_type != VDIR`). Resolve() dropped a trailing "." and handed the
host "file/", which stat, access, open and mkdir answered with ENOENT,
EINVAL or EEXIST. The ".." branch already checks the directory first;
do the same for a path whose last component is ".".
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A null buffer, or for pread and pwrite a negative offset, was answered with
EFAULT or EINVAL before the descriptor was looked at, so a descriptor that is not
open (EBADF) or a pipe (ESPIPE) was hidden. FreeBSD 11.4 sys_generic.c looks
the descriptor up first (kern_readv, kern_preadv: fget, then the seekable
check, then the offset) and only reaches the buffer when it copies. The check
runs only when the buffer or offset is invalid, so a normal transfer costs nothing.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A path through a self-referencing symbolic link returned ENOTDIR, EIO or EINVAL on Windows; FreeBSD returns ELOOP. The failure paths now use the loop check that sceKernelStat uses.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GtXavMkgCTCdmRpweviHcQ
CreateFileW fails a symbolic link that resolves to itself with
ERROR_CANT_RESOLVE_FILENAME (1921). WindowsOpenError_nid_no_patch had no
case for it, so sceKernelOpen, open and fopen reported EINVAL where
FreeBSD reports ELOOP.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A path below a self-referencing symbolic link returned ENOTDIR on Windows; FreeBSD returns ELOOP. A failed native open now uses the loop check that sceKernelStat uses.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GtXavMkgCTCdmRpweviHcQ
lstat and readlink threw NotImplemented for every Windows reparse point
other than a symbolic link or a drive junction. A tag without the name
surrogate bit (cloud files, deduplication) does not stand for another path,
so lstat now answers as stat does and readlink returns EINVAL, as FreeBSD
does for a file that is not a link and as CPython's os.lstat and os.readlink
do (bpo-37834). A name surrogate that is neither a symbolic link nor a
junction (a mounted volume, a WSL link) is reported as the reparse point
itself with the mode of its attributes, and readlink returns EINVAL.
The tests set reparse tags with FSCTL_SET_REPARSE_POINT, one without the
name surrogate bit and one with it. A compact /exe (WOF) file cannot serve
as the fixture: on Windows 11 24H2 the WOF filter hides the reparse point
from GetFileAttributesW and FileAttributeTagInfo alike. Windows refuses
every open of an unhandled tag without FILE_FLAG_OPEN_REPARSE_POINT, so the
test requires lstat to agree with stat rather than to succeed.
mashingaan's boykopovar/AnyPS5#6447 returned EINVAL for other reparse
points without the tag check.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Fable 5.1 <[email protected]>