Skip to content

feat(libkernel_sys): implement chown, lchown, fchownat and linkat - #9103

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
HQasper:claude/project-thread-w0s18y-chown
Oct 23, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
HQasper:claude/project-thread-w0s18y-chown

Conversation

@HQasper

@HQasper HQasper commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

What

chown, lchown, fchownat and linkat in libkernel_sys were throwing stubs. This implements them on top of the calls libkernel_sys already has.

  • chown, lchown and fchownat follow fchown: ownership is not modelled, so owner and group 0 and the unchanged value (-1) are accepted, and any other owner or group throws through NotImplemented_nid_no_patch, naming the call. fchown is unchanged.
  • The path is looked up first, with stat (chown), lstat (lchown) or fstatat (fchownat), and the owner is checked afterwards, as in FreeBSD 11's kern_fchownat. So a null, empty, missing or not-a-directory path or a bad descriptor fails as those calls do, also with a foreign owner, and nothing is changed on the host. fchownat flags other than AT_SYMLINK_NOFOLLOW (0x200) return EINVAL, as in FreeBSD 11's kern_fchownat.
  • linkat passes AT_FDCWD (-100) and absolute paths to link, and flags other than AT_SYMLINK_FOLLOW (0x400) return EINVAL, as in FreeBSD 11's sys_linkat. A relative path with a directory descriptor throws. A symbolic link as the source without AT_SYMLINK_FOLLOW also throws: FreeBSD then links the symbolic link itself, while link follows it. On Windows the source is checked for FILE_ATTRIBUTE_REPARSE_POINT, as fchmodat does, since libstdc++ there reports a symbolic link as a regular file. With AT_SYMLINK_FOLLOW a symbolic link source goes to link, which follows it on every host, on Windows since fix(libkernel_sys): follow a symbolic link in link on Windows #9336.
  • The debt file gets entries for both. The patched libkernel_sys.prx exports the four NIDs and imports stat and lstat from libkernel.prx, which defines them.

Tested

Linux (Ubuntu 24.04.5 container, GCC 13.3, Release, BUILD_TESTING=ON):

  • guest_kernel_sys with a new ownership and link case: passed. It checks owner and group 0 and -1 for all three ownership calls, fchownat through AT_FDCWD and a directory descriptor, with and without AT_SYMLINK_NOFOLLOW, ENOENT, ENOTDIR, EFAULT and EINVAL, and the throws with their messages. A foreign owner with a missing path, a null path, a not-a-directory path or a bad descriptor returns the lookup error, and fchownat with a bad flag and a foreign owner returns EINVAL. A dangling symbolic link fails chown with ENOENT and passes lchown. For linkat it checks that a hard link is made (link count 2), EEXIST, ENOENT, EINVAL for AT_SYMLINK_NOFOLLOW, EFAULT, and the two throws. With AT_SYMLINK_FOLLOW on a symbolic link it checks that the new name is a hard link to the target (link count 3), and that it is not a symbolic link (on Windows: no reparse point attribute). The symbolic link part was checked to run by breaking one of its expectations once.
  • patched_libraries: passed, and the patched exports and imports were checked with nm -D.
  • python3 tools/check_conventions.py --base upstream/main: passed.
  • On 705f6e1 (main 0a0ed72): guest_kernel_sys, guest_kernel_sys_thread_defaults, guest_kernel_sys_sdk_version, guest_kernel_sys_barrier, guest_kernel_sys_module_internal and patched_libraries passed.
  • MinGW-w64 GCC 13 -fsyntax-only on Export.cpp and GuestKernelSys.cpp at 705f6e1: passed.
    Windows 11 Pro 10.0.26300 (MinGW-w64 GCC 15.2.0 WinLibs x86_64-ucrt-posix-seh r7, Release, BUILD_TESTING=ON, symbolic links allowed through Developer Mode, so the symbolic-link block runs):
  • On 705f6e1 (2026-10-23): guest_kernel_sys, guest_kernel_sys_thread_defaults, guest_kernel_sys_sdk_version, guest_kernel_sys_barrier and guest_kernel_sys_module_internal passed, with the symbolic-link block running. This covers the changed Windows case of AT_SYMLINK_FOLLOW on a symbolic link, which now goes through link as fix(libkernel_sys): follow a symbolic link in link on Windows #9336 does.
  • Before the rebase, guest_kernel_sys passed on 2c5d550. On 3c7427d it fails at line 136 (guest errno 2), as in a reviewer's Windows run; that run found the two issues fixed in 2c5d550.
  • Not tested: any title or homebrew.

Note: #9008 and #9200 have merged and their neighbouring-stub conflicts are resolved in 705f6e1. Since #9336 merged, linkat on Windows passes a symbolic link source with AT_SYMLINK_FOLLOW to link, which now follows it, instead of throwing.

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

Duplicate and overlap check

Checked on 2026-10-23 against upstream main 0a0ed72 and the heads of all 1240 open PRs.

@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 9161 core/libs/prx/libkernel_sys/Export.Discovery.cpp L84-91
PR 9442 core/libs/prx/libkernel_sys/Export.Discovery.cpp L269-279
PR 10016 core/libs/tests/GuestKernelSys.cpp L71

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 6044 core/libs/tests/GuestKernelSys.cpp
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 9097 core/libs/prx/libkernel_sys/Export.Discovery.cpp
core/libs/prx/libkernel_sys/Export.cpp
core/libs/tests/GuestKernelSys.cpp
PR 9284 core/libs/prx/libkernel_sys/Export.Discovery.cpp
PR 9293 core/libs/prx/libkernel_sys/Export.Discovery.cpp
PR 9297 core/libs/prx/libkernel_sys/Export.Discovery.cpp
docs/dev/debt/libkernel_sys.md
PR 9302 core/libs/prx/libkernel_sys/Export.Discovery.cpp
docs/dev/debt/libkernel_sys.md
PR 9423 core/libs/prx/libkernel_sys/Export.cpp
core/libs/tests/GuestKernelSys.cpp
PR 9693 core/libs/prx/libkernel_sys/Export.Discovery.cpp
core/libs/prx/libkernel_sys/Export.cpp
core/libs/tests/GuestKernelSys.cpp
PR 9826 core/libs/prx/libkernel_sys/Export.cpp

@xXPBSXx

xXPBSXx commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Independent Windows run of this PR (data only).

Environment: Windows 10 Pro 10.0.19045, Ryzen 7 5800X3D, WinLibs GCC 15.2.0 r7 (MinGW-w64 UCRT), Release, BUILD_TESTING=ON. Merge-tree of the PR head against main is clean. I built only guest_kernel_sys_tests; ctest -N -R guest_kernel_sys lists exactly one test.

PR head tested: 3c7427dd7128ce09e59a03e1fd3453f80943dca5.

Results (3 runs each):

  • PR head: guest_kernel_sys passes 3/3.
  • Baseline: I kept the PR's test and replaced only these production files: core/libs/prx/libkernel_sys/Export.Discovery.cpp, core/libs/prx/libkernel_sys/Export.cpp with the version from the PR's merge base (158d8ad34c), then rebuilt. The test fails 3/3 (exit 0xc0000409). I restored the PR's files afterwards.

GPU: the run output has no Selected GPU line; no GPU claim is made.

Not run: other platforms, the other tests in the same family, the full repository CTest, and any title or console run.

AI-assisted (Claude Code).

@ycbuzi

ycbuzi commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Windows run of 3c7427d merged onto main 745b394 (a2f1954c38, clean), the platform the description lists as not tested. Windows 11 Home 10.0.26200, MinGW-w64 GCC 15.2.0 (ucrt-posix-seh r7), Release, BUILD_TESTING=ON. The session can create symbolic links, so the new symbolic-link block runs. Without that privilege symlink_nid_postfix fails and the block is skipped, which fits the 3/3 pass in the Windows run above.

guest_kernel_sys fails, while main's passes twice on the same host:

Kernel sys check failed at line 136 (guest errno 2)

Both causes are the same MinGW limitation: libstdc++ in MinGW-w64 GCC 15.2 does not see Windows symbolic links. A probe built with the same compiler shows a CreateSymbolicLinkW link with FILE_ATTRIBUTE_REPARSE_POINT set, but std::filesystem::symlink_status reports it as a regular file (is_symlink 0, is_regular_file 1). std::filesystem::create_symlink there fails with "Function not implemented".

  1. The new check in linkat_nid_postfix uses symlink_status, so it never sees the link and does not throw. With the check done the way fchmodat_nid_postfix in the same file does it on Windows, line 136 passes:
#ifdef _WIN32
        const auto attributes = ResolvePathError_nid_no_patch(path1, source) == 0 ? GetFileAttributesW(source.wstring().c_str()) : INVALID_FILE_ATTRIBUTES;
        if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
#else
        if (ResolvePathError_nid_no_patch(path1, source) == 0 && std::filesystem::is_symlink(std::filesystem::symlink_status(source, error)))
#endif
  1. The test then fails at line 138: linkat with AT_SYMLINK_FOLLOW returns 0, but owned.txt keeps one link. That part is main's link_nid_postfix, not this PR's code. 7cba4ce decides whether to follow with std::filesystem::is_symlink, which has the same blind spot, and its test is under #ifndef _WIN32. A probe on main 80565c5: after link("owned.link", "other.txt") returns 0, other.txt is a reparse point, owned.link has 2 links and owned.txt 1. So on Windows link makes a hard link to the symbolic link itself, and this PR's new case is the first test that reaches that path there.

Not run: a fix for link_nid_postfix on Windows, Linux, a title.

@verusferro

Copy link
Copy Markdown
Contributor

The owner check runs before the path lookup, so a lookup error turns into a throw when the owner or group is not 0 or -1. FreeBSD 11.4 does it the other way round: sys_fchownat checks the flag, then kern_fchownat returns any namei error (L2626-2627) before setfown looks at the owner (L2629). chown and lchown go through the same function.

Probe on 3c7427d, Linux (Ubuntu 24.04, GCC 13.3, Release): temporary lines in OwnershipAndLinkTests, run through guest_kernel_sys:

Call This PR FreeBSD order
chown(missing, 1000, 0) throws chown: an owner … -1, ENOENT
chown(nullptr, 1000, 0) throws -1, EFAULT
lchown("owned.txt/inner", 0, 1000) throws -1, ENOTDIR
fchownat(4321, "x", 1000, 0, 0) throws -1, EBADF
chown(missing, 0, 0) -1, ENOENT -1, ENOENT

Calling RequireGuestOwner after the lookup gives the FreeBSD order, with the fchownat flag check staying first:

int APS5_VABI chown_nid_postfix(const char* path, std::uint32_t uid, std::uint32_t gid) {
    FileStat status{};
    if (stat_nid_postfix(path, &status) != 0) return -1;
    RequireGuestOwner("chown", uid, gid);
    return 0;
}

The same goes for lchown with lstat and fchownat with fstatat. A check such as Require(chown_nid_postfix(missing.c_str(), 1000, 0) == -1 && Error() == 2); keeps the order.

Not run: Windows, a title.

AI-assisted (Claude Code).

@HQasper

HQasper commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the Windows run and the two probes. Both are fixed in 2c5d550:

  1. linkat now detects a symbolic link source on Windows with GetFileAttributesW and FILE_ATTRIBUTE_REPARSE_POINT, as fchmodat does, so the symbolic-link check in the test now throws as intended. Since link on Windows links the reparse point itself instead of its target (your second finding, in main's link), linkat with AT_SYMLINK_FOLLOW on such a source now throws on Windows instead of quietly making the wrong link, and the test expects that throw there and checks that nothing was created. Making link follow symbolic links on Windows is left for a separate PR; the debt entry records the gap.
  2. chown, lchown and fchownat now look the path up first and check the owner afterwards, with the fchownat flag check still first. The test now covers your table: chown(missing, 1000, 0) gives ENOENT, chown(nullptr, 1000, 0) EFAULT, lchown("owned.txt/inner", 0, 1000) ENOTDIR and fchownat(4321, ...) EBADF, and fchownat with AT_SYMLINK_FOLLOW and a foreign owner gives EINVAL.

Tested on Windows 11 Pro 10.0.26300 with MinGW-w64 GCC 15.2.0 (WinLibs ucrt-posix-seh r7), Release, BUILD_TESTING=ON, in a session that can create symbolic links (Developer Mode), so the symbolic-link block runs: on 3c7427d guest_kernel_sys fails at line 136 (guest errno 2), as in your run, and on 2c5d550 it passes. On Linux (Ubuntu 24.04, GCC 13.3, Release) guest_kernel_sys and patched_libraries pass, and so does check_conventions.

@ycbuzi

ycbuzi commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Thanks for both fixes. The separate PR for link exists: #9336 makes link follow a symbolic link on Windows (a GetFileAttributesW reparse check, then CreateFileW and GetFinalPathNameByHandleW; a dangling link gives ENOENT, a loop ELOOP, a directory EPERM). Since linkat passes absolute paths to link, whichever of the two lands second can drop the Windows AT_SYMLINK_FOLLOW throw and its debt entry. Both also change Export.cpp and GuestKernelSys.cpp around link, so the second one needs a rebase there.

@HQasper

HQasper commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for clarifying. I'll wait to see which PR lands first and adjust mine accordingly to avoid redundant changes.

@MarcZX10 MarcZX10 mentioned this pull request Oct 22, 2026
12 tasks done
@MarcZX10

Copy link
Copy Markdown
Contributor

The current overlap scan flags the #9442 libkernel_sys/Export.cpp insertion around fstatat/unlink. Your chown/lchown/fchownat additions share that insertion area with its lchflags wrapper. Please coordinate with #9442’s author on rebase/order so both sets of exports remain.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 22, 2026
@Gruzzberry

Copy link
Copy Markdown

#9442 adds lchflags in libkernel_sys/Export.cpp next to the unlink declaration where this PR adds chown/lchown, and new cases in GuestKernelSys.cpp; text only, whichever lands second rebases.

@Gruzzberry Gruzzberry mentioned this pull request Oct 22, 2026
12 tasks done
@HQasper
HQasper force-pushed the claude/project-thread-w0s18y-chown branch from 2c5d550 to 6650443 Compare October 22, 2026 10:18
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 22, 2026
@verusferro verusferro mentioned this pull request Oct 22, 2026
12 tasks done
@oneandonlydean oneandonlydean self-assigned this Oct 22, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 23, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

@HQasper this PR now conflicts with main in core/libs/prx/libkernel_sys/Export.Discovery.cpp. main has since removed the fstat and lchmod stubs there and added sceKernelGetCompiledSdkVersion, which sit next to the four stubs your PR replaces. The resolution is to delete those four stubs (chown_nid_postfix, fchownat_nid_postfix, lchown_nid_postfix, linkat_nid_postfix) and keep the rest of that file as it is on main. Nothing else conflicts.

Could you merge current main into your branch and resolve it that way? I tried to push the merge to the PR branch myself, but the fork's repository rules reject updates to that branch ("Cannot update this protected ref"), even though the PR allows edits from maintainers.

If that is easier for you, I have the finished merge ready (parents: your head fef39386c7 and main a8a34cbb75; the only change to the conflicting file is the 23-line deletion above) and can attach it here as a patch for you to apply. Or you can let maintainers push to that branch by adding a bypass for it in the fork's rules. Tell me which you prefer.

All four exports threw. The ownership calls follow fchown: owner and
group 0 and the unchanged value are accepted and any other value
throws. The path is looked up first with stat, lstat or fstatat, as
FreeBSD 11's kern_fchownat does, so a lookup error is returned before
the owner is checked. fchownat rejects flags other than
AT_SYMLINK_NOFOLLOW with EINVAL. linkat passes AT_FDCWD and absolute
paths to link and rejects flags other than AT_SYMLINK_FOLLOW with
EINVAL; a relative path with a directory descriptor and a symbolic
link source without AT_SYMLINK_FOLLOW throw. On Windows a symbolic
link source is detected with the file attributes, since libstdc++
there reports it as a regular file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@HQasper
HQasper force-pushed the claude/project-thread-w0s18y-chown branch from fef3938 to 705f6e1 Compare October 23, 2026 11:53
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 23, 2026
@HQasper

HQasper commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

@MarcZX10
Thanks. This is already handled. Since fef3938, and still in the current head 705f6e1, chown, lchown and fchownat sit before fstatat, right after the stat/lstat declarations, so they no longer share #9442's insertion point after fstatat. I checked it with git merge-tree: #9442's Export.cpp change merges cleanly with this branch in either order, and once this PR is on main, #9442 has no conflict in Export.cpp. Its remaining conflicts with main (Export.Discovery.cpp after #9200, and GuestKernelSys.cpp) are there without this PR too. In Export.Discovery.cpp this PR only removes the lchown and linkat stubs next to its lchflags stub, so its rebase there stays the same small stub deletion.

@Gruzzberry
Agreed, and both sets of exports stay in either order. Export.cpp and GuestKernelSys.cpp merge cleanly between the two PRs: the ownership and link tests sit after ChrootThrows and are called right after DirectoryRenameTests, away from #9442's cases. Only Export.Discovery.cpp needs a small rebase for whichever lands second, because both delete neighbouring stubs.

@oneandonlydean
Thanks for preparing the merge, and sorry about the protected branch. The conflict is already resolved. Earlier today I rebased the branch onto main 0a0ed72, after #9008, #9200 and #9336 merged, and force-pushed 705f6e1 with that same resolution: the four stubs deleted and the rest of Export.Discovery.cpp as on main. GitHub shows no conflict now, all checks pass, and the head also merges cleanly with current main 7e9af67, where guest_kernel_sys, the other guest_kernel_sys_* tests and patched_libraries pass. So the patch isn't needed.

Since #9336 makes link follow symbolic links on Windows, the rebase also drops linkat's Windows-only throw for AT_SYMLINK_FOLLOW on a symbolic link. That path now runs on Windows 11 (MinGW-w64 GCC 15.2) with symbolic links enabled, and guest_kernel_sys passes there.

I've also turned off the fork's branch rules, so maintainers can push to this branch from now on.

@MarcZX10 MarcZX10 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.

Reviewed the implementation, test changes, debt notes, and current checks. The ownership calls perform path or descriptor lookup before validating unsupported owner values, preserving the intended lookup-error precedence; fchownat also validates its supported flag mask before forwarding. The linkat implementation clearly bounds its supported cases and throws for the two unsupported cases instead of silently claiming success. The added cases cover null and empty paths, lookup errors, flag errors, unchanged and guest ownership, symlinks, descriptor-relative paths, and hard-link results on both reported platforms. The PR reports focused Linux and Windows test runs; current hosted product test jobs are skipped, while conventions, intake, and labels pass. I found no blocking issue in the reviewed changes.

@oneandonlydean
oneandonlydean merged commit c675a41 into boykopovar:main Oct 23, 2026
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.

8 participants