Skip to content

fix(libkernel_sys): follow a symbolic link in link on Windows - #9336

Merged
oneandonlydean merged 5 commits into
boykopovar:mainfrom
ycbuzi:fix/libkernel-sys-link-follow-windows
Oct 22, 2026
Merged

oneandonlydean merged 5 commits into
boykopovar:mainfrom
ycbuzi:fix/libkernel-sys-link-follow-windows

Conversation

@ycbuzi

@ycbuzi ycbuzi commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

What

link follows a symbolic link source, as FreeBSD's does (7cba4ce). It decided that with std::filesystem::is_symlink, and MinGW-w64's libstdc++ never reports a Windows symbolic link as one: symlink_status of a CreateSymbolicLinkW link with FILE_ATTRIBUTE_REPARSE_POINT set returns a regular file. So on Windows link made a hard link to the symbolic link itself. The test for following is under #ifndef _WIN32, so nothing showed it.

On Windows link now checks the source for FILE_ATTRIBUTE_REPARSE_POINT, as fchmodat in the same file does. It opens a reparse point source with CreateFileW without FILE_FLAG_OPEN_REPARSE_POINT and links to GetFinalPathNameByHandleW of that handle. A dangling link returns ENOENT and a loop (ERROR_CANT_RESOLVE_FILENAME) ELOOP, instead of linking the reparse point. A source that resolves to a directory returns EPERM, as before. Linux and macOS are unchanged.

The link-through-a-link case of guest_kernel_sys now runs on Windows too, when symbolic links can be created. It uses dot-link for its link to ., because the Windows part of the test already makes a directory-link earlier. On Windows it checks that the new name is not a reparse point, since is_symlink cannot tell there.

Tested

Windows 11 Home 10.0.26200, MinGW-w64 GCC 15.2.0 (ucrt-posix-seh r7), Release, BUILD_TESTING=ON, on main 950388b:

Found while running #9103's new linkat test on Windows; its AT_SYMLINK_FOLLOW case goes through this link.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Fixes: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes

🤖 Generated with Claude Code

link_nid_postfix decided whether to follow the source with
std::filesystem::is_symlink, which MinGW-w64's libstdc++ never reports for
a Windows symbolic link, so on Windows link made a hard link to the link
itself. Follow a reparse-point source to its final path with
GetFinalPathNameByHandleW instead, and run the link-through-a-link case of
guest_kernel_sys on Windows too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 21, 2026
@cadenasynyster

Copy link
Copy Markdown
Contributor

Independent Windows run of this PR (head a293868).

Environment: Windows 11 10.0.26300, Ryzen 7 9800X3D, WinLibs GCC 15.2.0 r7 (MinGW), Release, BUILD_TESTING=ON. PowerShell not elevated, Developer Mode on.

Results:

  • PR head: guest_kernel_sys passes 3/3 (--repeat until-fail:3).
  • Baseline: I kept the PR's test and replaced only core/libs/prx/libkernel_sys/Export.cpp with the merge-base version. guest_kernel_sys fails with "Kernel sys check failed at line 692 (guest errno 2)", the same as the description's negative control. I restored the PR's file afterwards.

On the unelevated note in the description: my run also printed "symbolic links unavailable: the link cases not tested", but the new link-through-a-link cases still ran. The base failure at line 692 is inside that block, and that block's own skip message ("the symlink success cases not tested") was not printed. The "link cases not tested" message comes from the earlier readlink block, which creates its link with std::filesystem::create_symlink, and that call failed here. The new block depends on the guest symlink instead, which passes SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE, and on this machine it succeeded without elevation. So whether the new cases ran is shown by the absence of "the symlink success cases not tested", not by the "link cases not tested" line. I did not check why create_symlink fails here.

Not run: a symbolic link loop or a chain of links, Linux/macOS, the full repository CTest, and any title.

AI-assisted: yes

Resolves the conflict with boykopovar#8522 in link_nid_postfix: keep its ENOTDIR
return for a destination below a regular file and the Windows
symbolic-link follow after it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ycbuzi

ycbuzi commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

@cadenasynyster thanks for the run and for the correction. You're right: the link cases not tested comes from the earlier block, whose std::filesystem::create_symlink cannot succeed on Windows with MinGW's libstdc++ (it fails with "Function not implemented" here even elevated). The skip line for the new block is the symlink success cases not tested. My unelevated run had Developer Mode off, so the guest symlink failed and that line was printed too. I've corrected the description.

Since #8522 landed in the meantime, I merged main into the branch (339d5c1). The conflict was its ENOTDIR return on the destination line just above this change; both are kept, and guest_kernel_sys passes 3 of 3 on the merge.

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 21, 2026
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 9573 docs/dev/debt/libkernel_sys.md L14

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 5021 core/libs/prx/libkernel_sys/Export.cpp
PR 5528 core/libs/prx/libkernel_sys/Export.cpp
core/libs/tests/GuestKernelSys.cpp
PR 6044 core/libs/tests/GuestKernelSys.cpp
PR 6525 core/libs/prx/libkernel_sys/Export.cpp
core/libs/tests/GuestKernelSys.cpp
docs/dev/debt/libkernel_sys.md
PR 7201 core/libs/prx/libkernel_sys/Export.cpp
PR 8457 core/libs/prx/libkernel_sys/Export.cpp
core/libs/tests/GuestKernelSys.cpp
docs/dev/debt/libkernel_sys.md
PR 8701 core/libs/prx/libkernel_sys/Export.cpp
core/libs/tests/GuestKernelSys.cpp
docs/dev/debt/libkernel_sys.md
PR 9019 core/libs/prx/libkernel_sys/Export.cpp
docs/dev/debt/libkernel_sys.md
PR 9382 core/libs/prx/libkernel_sys/Export.cpp
docs/dev/debt/libkernel_sys.md
PR 9693 core/libs/prx/libkernel_sys/Export.cpp
core/libs/tests/GuestKernelSys.cpp
PR 9826 core/libs/prx/libkernel_sys/Export.cpp

@harisibrar1 harisibrar1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked against main 8b3e398 and the Windows SDK 10.0.26100 UCRT source (ucrt/filesystem/stat.cpp):

  • This merges cleanly with current main, and main still uses is_symlink here, so the fix is still needed.
  • I think the loop → ELOOP case can't be reached. link_nid_postfix runs std::filesystem::status(source) and returns ENOENT/ENOTDIR when it doesn't exist (Export.cpp:374-375), before FollowLinkPath (:381). On MinGW that status is UCRT _wstat64, and when CreateFileW fails on a path that isn't a drive root, UCRT stat reports ERROR_FILE_NOT_FOUND (stat.cpp:378-388). So a loop comes back as ENOENT before FollowLinkPath runs. Example: guest symlink("b","a"), symlink("a","b"), link("a","x") should give 62 (ELOOP) as on FreeBSD, but would give 2. Could FollowLinkPath run before the existence check on Windows, with a loop case added to guest_kernel_sys? I couldn't run this locally (Smart App Control blocked my probe), so a run on your side would confirm it.
  • For the same reason, the ERROR_FILE_NOT_FOUND → ENOENT branch only matters in a race. Dangling links still return ENOENT correctly through the earlier check.
  • Minor: Windows follows up to 63 reparse hops and FreeBSD stops at 32 (MAXSYMLINKS), so a 33-63 link chain succeeds here where the console returns ELOOP. Fine to leave for the debt file if you prefer.

Reviewed with AI assistance (Claude Code); the checks above were run locally on my machine.

ycbuzi and others added 2 commits October 22, 2026 07:54
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…indows

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ycbuzi

ycbuzi commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

@harisibrar1 thanks for reading it against the UCRT source. I ran your case here (Windows 11 10.0.26200, WinLibs GCC 15.2.0, elevated so the guest can create symbolic links): after symlink("b","a") and symlink("a","b"), link("a","x") returns -1 with errno 62. On this machine _wstat64 succeeds on the loop (it returns 0), so std::filesystem::status reports a regular file, the existence check lets it through, and FollowLinkPath's CreateFileW fails with ERROR_CANT_RESOLVE_FILENAME (1921), which it maps to ELOOP. I added that loop to guest_kernel_sys on Windows (37ba44c); it passes 2 of 2.

The chain limit is real: a chain of 40 links is linked here, where FreeBSD stops at 32. I recorded it in the debt file (e9e345f).

@github-actions github-actions Bot added the docs label Oct 22, 2026

@harisibrar1 harisibrar1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked at e9e345f:

  • Thanks for running it. I was wrong about the loop: I read UCRT's _wstat64 error path but didn't run it, and on a real loop it succeeds, so the existence check lets it through and FollowLinkPath returns ELOOP as intended. Sorry for the noise.
  • The new Windows loop case in guest_kernel_sys (37ba44c) pins that, and the debt line (e9e345f) records that longer chains link here, where FreeBSD stops at MAXSYMLINKS (32). Nothing left from my side.

Reviewed with AI assistance (Claude Code).

@oneandonlydean oneandonlydean self-assigned this Oct 22, 2026
@oneandonlydean
oneandonlydean merged commit 287d539 into boykopovar:main Oct 22, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants