Skip to content

fix(libkernel): append O_APPEND writes atomically on Windows - #5831

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
ahmad3a4:fix/libkernel-append-atomic-windows
Oct 16, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
ahmad3a4:fix/libkernel-append-atomic-windows

Conversation

@ahmad3a4

@ahmad3a4 ahmad3a4 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

What

On Windows, NativeOpen hands the file to the C runtime with _O_APPEND. The runtime appends by seeking to the end and then writing, two separate steps. When two descriptors append to the same file at once, both seek to the same end and the second write overwrites the first. FreeBSD's vn_write sets IO_APPEND for an O_APPEND file and writes at the end under the vnode lock, so appends never collide there.

A standalone Windows program shows the size of the loss: 4 threads each append 2,000 lines through their own descriptor. With the C runtime's _O_APPEND, 6,999 of the 8,000 lines arrive. With WriteFile at offset 0xFFFFFFFFFFFFFFFF, all 8,000 arrive. That offset is Windows' atomic write to end of file, "functionally equivalent" to FILE_APPEND_DATA, and it leaves the file pointer after the data, as FreeBSD leaves the offset at the end.

The change:

  • sceKernelOpen already records the guest open flags with File::RememberDescriptor, and those flags are shared across dup, dup2 and F_DUPFD. The new File::Appends(fd) reads its O_APPEND bit.
  • On Windows, NativeWrite (sceKernelWrite, write, _write) and NativeTransfer (writev, sceKernelWritev) send writes on such a descriptor through the new File::WriteAtEnd. It calls WriteFile at the end-of-file offset, only for disk files. Pipes and devices keep the existing path.
  • writev on an O_APPEND descriptor gathers its buffers into one write, because FreeBSD appends the whole vector at once. Until now, each buffer was written separately, so other writers could land between them. This is also in the Windows code: Linux calls the host's writev, which already appends the whole vector at once.
  • pwrite is unchanged.

guest_append_writes covers four concurrent writers on one file:

  • sceKernelWrite on its own descriptor;
  • write on another descriptor;
  • sceKernelWrite on a dup of the first descriptor;
  • a two-part writev.

Each writes 1,500 fixed-length lines. The test checks that the file holds exactly 6,000 intact lines, each exactly once. It then checks that a write after lseek(0, SEEK_SET) lands at the end and leaves the offset after it. On main it fails at the size check in 10 of 10 runs; with the change it passes.

Overlap with #5833

#5833 holds the descriptor's position lock across a whole vectored transfer and splits NativeTransfer into a lock-free NativeTransferBuffer and a locking wrapper. The two changes don't overlap in purpose: #5833 keeps the aliases of one descriptor from interleaving, and this PR makes O_APPEND writes atomic across independent opens. Agreed with @Ondra97 on #5833: #5833 lands first, and this PR is then rebased onto it with two adjustments:

With main 84b01f9, #5833 and this PR resolved that way, guest_append_writes, guest_file_vector (with #5833's concurrent records) and guest_kernel_fcntl pass 5 of 5 runs.

Tested

Windows 11 Pro (26200), WinLibs GCC 15.2.0 r7, Release, BUILD_TESTING=ON:

  • On main 9a3e2ac, the full build and full ctest pass: 1172 tests, 10 of them skipped by their own environment checks.
  • The NID patch of libkernel.prx succeeds.
  • After rebasing onto main 8ffbb49, whose new commits don't touch these files, guest_append_writes, guest_kernel_fcntl, guest_file_vector and guest_system_configuration pass (3 runs).

Java 25 LAN / DNS (PPSA99869, #5501) was run with #5563, #5643 and #5666 stacked, which take it past its remaining missing imports (#5502 and #5187 are merged), and relinked with --trap-sdk-syscall-stubs. It keeps a trace in /download0/java25-vm-trace.log, which its threads append to through separate O_APPEND descriptors. During the DNS stage (two threads), without this change 86 lines lose their start and 99 lines lose their newline and run into the next. With it, no line is cut and no newline is lost. The 174 lines that still join are each followed by an empty line, because the title writes a line and its newline separately.

Linux not tested; nothing changes there.

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: fix(libkernel): serialize Windows vectored file transfers #5833
  • Unblocks: PPSA99869 Java 25 LAN / DNS (PPSA99869) #5501
  • GPUs tested: n/a
  • AI-assisted: yes

@github-actions

github-actions Bot commented Oct 15, 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 6228 core/libs/prx/libkernel/File/src/Stdio.cpp L1088

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 4972 core/libs/prx/libkernel/CMakeLists.txt
PR 5021 core/libs/prx/libkernel/CMakeLists.txt
core/libs/prx/libkernel/File/src/Open.cpp
core/libs/prx/libkernel/File/src/Stdio.cpp
PR 6044 core/libs/prx/libkernel/CMakeLists.txt
PR 6222 core/libs/prx/libkernel/CMakeLists.txt
core/libs/prx/libkernel/File/src/Stdio.cpp
PR 6484 core/libs/prx/libkernel/File/src/Stdio.cpp
PR 6879 core/libs/prx/libkernel/File/src/Stdio.cpp

@screamm

screamm commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Windows cross-check of head 17d69f14a, merged onto main d92a8dad3: clean merge, no conflicts.

Windows 11 Pro 26220, Ryzen 7 5700X, RTX 4070 (driver 596.21), WinLibs MinGW-w64 GCC 15.2.0 r7, Ninja, Release.

  • Build: full default build and libs pass. No warnings in the files this PR touches; the only warnings are the ones main also has.
  • guest_append_writes, --repeat until-fail:3: 3 of 3 pass.
  • Negative control: this head's guest_append_writes_tests.exe with main's libkernel.prx/libc.prx first on PATH fails 11 of 13 runs with append check failed at line 88 (the size check). The file from the last failing run holds 154,690 of 192,000 bytes and 4,067 of 6,000 intact lines. With this PR's libraries it passes 13 of 13.
  • 32 libkernel/libc file and descriptor tests (guest_kernel_fcntl, guest_file_vector, guest_positioned_race, guest_dup, guest_aio*, guest_sendfile, guest_file_lock_*, guest_host_streams, file_streams, and others), --repeat until-fail:3: 31 pass 3 of 3. guest_file_lock_process is flaky on this host on main too: it gives each of its 24 child processes 3 s, and main's own test binary with main's libraries also exits 0xC0000409 3 of 3 here. It never writes.
  • ctest -j8 --timeout 120 -E '^agc_driver_|^video_out_flip': 807/815 under load. Six of the eight failures pass when rerun alone. guest_file_lock_process is the flaky test above, and hardware_oracle_compare (python, 20 s limit) needs 39.9 s on this loaded host.
  • tools/check_conventions.py --base origin/main and git diff --check: pass.

Titles, 60 s each, relinked once with main's relinker and run with main's or this PR's libraries:

Title main main + #5831
PPSA99002 ProsperoLight (stdout/stderr go to /data/prosperolight/logs/prosperolight-launcher.log, opened O_WRONLY | O_APPEND) ProsperoLight | FPS: 8.63 (357), log 2,194 lines, intact ProsperoLight | FPS: 6.50 (353), log 2,176 lines, intact
PPSA99420 PS5MC (no HOME) PS5MC (Kodi 22) | FPS: 9.77 (439), kodi.log 314 lines, intact PS5MC (Kodi 22) | FPS: 12.10 (574), kodi.log 314 lines, intact
PPSA99082 OpenNOW OpenNOW PS5 - Prototype | FPS: 9.08 (656) OpenNOW PS5 - Prototype | FPS: 16.91 (839)

There is no stop line in any run. FPS varied with other load on the machine.

One observation outside this PR's path: PS5MC's download0/ps5-opengl.log (its stdout/stderr) has lines whose start was overwritten on both builds (5 on main, 4 with the PR), for example [debug:0] [kodi[s2c/Open.cpp:215 sceKernelOpen] ... followed by 026-10-15 10:12:51.555 T:100001 .... That file is not opened through sceKernelOpen. libc's fopen/freopen/fdopen append streams still append through the C runtime (FileAndHeap.cpp), so streams opened that way keep the old behaviour. A possible follow-up, not a reason to widen this PR.

Verdict: confirmed on Windows. Concurrent appends through separate descriptors no longer lose data, and there are no test or title regressions.

AI-assisted (Claude Code).

@ahmad3a4

Copy link
Copy Markdown
Contributor Author

Thanks @screamm for the cross-check, and for the negative control with main's libraries.

On the overlap with #6228 flagged above: it's textual only. Both PRs change neighbouring lines in TransferIovecs. #6228 replaces the DirectoryDescriptorPath check with IsDirectoryDescriptor, and this PR adds the O_APPEND gathering right below it, so whichever lands second keeps both. #6228 doesn't touch the open-flag registry that File::Appends reads. I merged this head onto #6228 (head as of today) with that resolution. It builds, and these pass with --repeat until-fail:3: guest_append_writes, guest_kernel_sys, guest_kernel_fcntl, guest_file_vector, guest_directory, guest_directory_buffer, guest_directory_rewind, guest_directory_close, guest_kernel_rename, guest_positioned_race, guest_dup, guest_host_streams, file_streams, guest_working_directory and guest_file_lock_duplicate (15 of 15). @Ondra97 nothing needed on your side. The agreed order with #5833 stays.

On the libc streams: agreed, that's a separate path. fopen, freopen and fdopen in an append mode give the C runtime a descriptor with _O_APPEND, so stream flushes still seek to the end and then write. I'll take it as a follow-up PR.

This was referenced Oct 15, 2026

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

Linux run (Ubuntu 26.04, Intel i7-8700, GCC 15.2, full build, non-GPU tests), head 17d69f14a merged onto main ce991f1ec. The description says Linux was not tested.

  • Builds; guest_append_writes, which is registered on every platform, passes 30 of 30 repeated runs. guest_file_vector, guest_kernel_fcntl, guest_filesystem and guest_file_timestamps pass too.
  • With the five libkernel files from main (the PR's test kept), guest_append_writes also passes 30 of 30. That matches the description: the host write/writev on Linux already append atomically, so the new test is safe on the Linux CI and only guards the Windows path.

Non-GPU suite (ctest -E "^agc"): same result as main; only file_writer_binary and file_writer_text fail, and they fail on main too on this machine (Ubuntu 26.04 coreutils).

Written with AI assistance (Claude Code). I ran the builds and tests above on my machine.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 15, 2026
On Windows the C runtime appends by seeking to the end and then writing,
so concurrent appends through different descriptors overwrite each other
and lose data. Write and writev on an O_APPEND descriptor now go through
WriteFile at the end-of-file offset, which Windows appends atomically,
and writev gathers its buffers into one append as FreeBSD writes the
whole vector at the end.
@adriadam10
adriadam10 force-pushed the fix/libkernel-append-atomic-windows branch from 17d69f1 to e8c97bf Compare October 16, 2026 18:28
@adriadam10

Copy link
Copy Markdown
Collaborator

Rebased onto main after #5833: the File::Appends branch now lives in NativeTransferBuffer, and the writev gathering calls NativeTransferBuffer (TransferIovecs already holds the position lock, so NativeTransfer would lock it twice). Scoped build and ctest on Linux: 176/176 pass. WriteAtEnd is Windows-only; the negative control is @screamm's (main's libraries fail guest_append_writes 11 of 13 runs, this head passes), and the Windows job of the full Build covers the rebase before merge.

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 16, 2026
@adriadam10
adriadam10 merged commit 1391ea3 into boykopovar:main Oct 16, 2026
8 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