Skip to content

fix(libc): remove read-only files on Windows - #2342

Merged
oneandonlydean merged 2 commits into
boykopovar:mainfrom
reyani-dev:fix/remove-readonly
Oct 10, 2026
Merged

oneandonlydean merged 2 commits into
boykopovar:mainfrom
reyani-dev:fix/remove-readonly

Conversation

@reyani-dev

@reyani-dev reyani-dev commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

What

On FreeBSD, unlink (and so remove) ignores the file's own permission bits: what matters is write access to the directory. A guest that does chmod(path, 0400) and later remove(path) therefore succeeds on the console.

On Windows, the guest chmod (_wchmod) sets FILE_ATTRIBUTE_READONLY when the write bit is cleared. DeleteFileW then fails with ERROR_ACCESS_DENIED, so remove returned -1 with EACCES, and the file stayed.

When the entry carries FILE_ATTRIBUTE_READONLY (the attributes are already read), remove now clears it before the single delete. If the delete fails (e.g. the file is open without delete sharing), it restores the original attributes and reports the delete's error, so a failed remove leaves the file as it was. Linux is unchanged.

Tested

Windows 11, WinLibs GCC 15.2.0 r7, Release, -DBUILD_TESTING=ON:

sceKernelUnlink/unlink in libkernel likely behave the same way on Windows. They are left for a separate PR, because #2289 changes that function.

A Linux and Windows full build will be posted here once it has run.

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 in TechnicalDebt
  • 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
  • AI-assisted: yes

🤖 Generated with Claude Code

FreeBSD's unlink ignores the file's own permission bits, so remove()
succeeds on a file the guest made read-only with chmod(0400). On Windows
the guest chmod sets FILE_ATTRIBUTE_READONLY, and DeleteFileW then fails
with ERROR_ACCESS_DENIED, which remove() reported as EACCES. Clear the
attribute and retry, restoring it if the retry still fails.

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

Copy link
Copy Markdown
Contributor Author

Full Linux and Windows builds and tests for this head (58e18f08), using the project's Build workflow. I can't start the upstream manual run, so I ran the same build.yml in my fork through a mirror PR. Like the upstream run, the jobs check out the PR head merged into current main (d70b8998):

Job Result
linux success; full build, ctest: 516/516 passed
windows success; full build, ctest: 507/507 passed
relinker (Linux, Windows, macOS) success

Run: https://github.com/AchrafReyani/AnyPS5/actions/runs/37955618179

@oneandonlydean

Copy link
Copy Markdown
Collaborator

remove_nid_postfix deletes, and on ERROR_ACCESS_DENIED clears the read-only attribute and deletes again (Filesystem.cpp:121-136). Retrying after an error isn't allowed here; attributes is already read, so please clear FILE_ATTRIBUTE_READONLY before the first delete when it's set (restoring it if that delete fails), and drop the second attempt.

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

Copy link
Copy Markdown
Contributor Author

Done in e8d4c02: remove now clears FILE_ATTRIBUTE_READONLY before the single delete when it is set, and restores the original attributes if that delete fails. The second attempt is gone.

I rechecked on Windows:

  • guest_filesystem passes;
  • a read-only file held open by another handle still fails with EBUSY, stays in place and keeps its read-only attribute;
  • ctest -E "^agc_|video_out" is 249/249, and check_conventions.py passes.

The Linux and Windows full build for the new head is running and will be posted here.

@reyani-dev

Copy link
Copy Markdown
Contributor Author

Full Linux and Windows builds and tests for this head (e8d4c022), using the project's Build workflow. I can't start the upstream manual run, so I ran the same build.yml in my fork through a mirror PR. Like the upstream run, the jobs check out the PR head merged into current main (d70b8998):

Job Result
linux success; full build, ctest: 516/516 passed
windows success; full build, ctest: 507/507 passed
relinker (Linux, Windows, macOS) success

Run: https://github.com/AchrafReyani/AnyPS5/actions/runs/37986819849

@oneandonlydean
oneandonlydean merged commit 79d1220 into boykopovar:main Oct 10, 2026
8 of 9 checks passed
SleglJan pushed a commit to SleglJan/AnyPS5 that referenced this pull request Oct 10, 2026
fix(libc): remove read-only files on Windows
MarcZX10 pushed a commit to MarcZX10/AnyPS5 that referenced this pull request Oct 10, 2026
FreeBSD's unlink ignores the file's own permission bits, so it succeeds
on a file the guest made read-only with chmod(0400). On Windows the guest
chmod sets the read-only attribute and _wunlink then failed with EACCES,
which sceKernelUnlink and unlink reported. When the path is a regular file
without write permission, make it writable before the single _wunlink and
restore the read-only attribute if the unlink fails, as remove() does
since boykopovar#2342.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit fbf0ec0)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants