Skip to content

fix(libkernel): read and write guest sockets - #6

Merged
SleglJan merged 132 commits into
mainfrom
feat/libkernel-socket-read-write
Oct 10, 2026
Merged

SleglJan merged 132 commits into
mainfrom
feat/libkernel-socket-read-write

Conversation

@SleglJan

@SleglJan SleglJan commented Oct 8, 2026

Copy link
Copy Markdown
Owner

What

read, write, _read and _write on a guest socket descriptor (0x10000000 and up) went to the host as if the number were a host descriptor, so sceKernelRead and sceKernelWrite threw (sceKernelWrite: write failed, fd=…, errno=9) on the first byte a title moved through a socket that way. On the console both are the socket's recv and send without flags (soo_read/soo_write).

  • GuestSockets::Read and GuestSockets::Write in the socket runtime forward to recv and send with flags 0, so the socket layer's errno applies: EAGAIN (35) on an empty non-blocking socket, EPIPE (32) after the peer closed, EBADF (9) on a closed descriptor. A zero-length read returns 0 without touching the socket, as dofileread does, and a read longer than INT_MAX is clamped.
  • read, write, _read and _write route descriptors at or above GuestSockets::FirstDescriptor to them; sceKernelRead and sceKernelWrite do the same, before their null-buffer check, and return the kernel error (0x80020023 for EAGAIN, 0x80020009 for EBADF) instead of throwing.
  • Recorded in TechnicalDebt: EPIPE raises the host's SIGPIPE on Linux, which the guest can neither ignore nor suppress, and nothing on Windows (the console raises it unless SO_NOSIGPIPE); a write above INT_MAX bytes is EMSGSIZE; readv, writev, pread, pwrite, their sceKernel forms and AIO on a guest socket still go to the host.

The 15 catalog titles whose OpenSSL QUIC notifier creates a socketpair (boykopovar#1711, open) signal it with write and drain it with read (rio_notifier.c through writesocket/readsocket); the same applies to any TCP socket a title reads with read.

Overlap: boykopovar#1307 rewrites the four wrappers in Stdio.cpp (the socket branch has to stay first); boykopovar#1711 and boykopovar#1708 insert a TechnicalDebt line at the same place; boykopovar#1091 and boykopovar#1738 add declarations to SocketRuntime.hpp, and boykopovar#1091 code to Udp.cpp near the table, where this adds two declarations and two functions at the end. Whichever merges second rebases. The pr-overlap report's __error_nid_postfix rows (boykopovar#1613, boykopovar#1616, boykopovar#1627, boykopovar#1675) are the shared extern "C" declaration of the errno accessor that Open.cpp now adds, not an export.

Tested

  • Linux (CachyOS, kernel 7.2.9, GCC 16.2.1), full build with BUILD_TESTING=ON and ANYPS5_ENABLE_SPIRV_TOOLS=ON on this branch (based on main b83e849): ctest --repeat until-fail:3 467/467; tools/check_test_registration.py and tools/check_conventions.py --base upstream/main pass.
  • New guest_socket_read_write test over a TCP loopback pair: write/read, _write/_read and sceKernelWrite/sceKernelRead carrying data both ways; EAGAIN and 0x80020023 on an empty non-blocking socket; zero-length reads returning 0; 0 from both forms after the peer closed; (Linux) EPIPE on write after the peer closed, with the host's SIGPIPE ignored by the test since a title cannot; EBADF and 0x80020009 from every entry point on a closed descriptor. Each receive waits with poll first, accepting POLLHUP after the close. Written before the change: on main it aborts with the sceKernelWrite throw above; it passes with the change.
  • Not tested on Windows; the routing is platform-neutral and the test excludes only the EPIPE case there, like guest_tcp.

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 (Claude Code)

🤖 Generated with Claude Code

MathiasDevelopes and others added 30 commits October 8, 2026 05:46
Read the module layout from the PE image that holds the address. A
relinked image reports its .elf sections as segments and the
eh_frame_hdr recorded in .ehmeta; any other image reports its sections
with memory access and its .ehfram section. The eh_frame_hdr and
eh_frame helpers are shared with the Linux path, and the guest module
info test now also runs on Windows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…alls

sceKernelGetModuleInfoForUnwind and sceKernelGetModuleInfoFromAddr each
parsed .ehmeta and the eh_frame_hdr it names. Both now read them through
Module/EhFrame.hpp, so a bad table fails the same way in both: an
eh_frame_hdr version other than 1, an indirect or non-absptr/pcrel
encoding, a 64-bit record length or anything outside the image throws.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkiyF6p7oDAaTtVe8ci5zP
Add sigemptyset, sigfillset, sigaddset, sigdelset and sigismember with
FreeBSD 11 semantics (lib/libc/gen/sigsetops.c): a 128-bit set of four
32-bit words, signal n at bit n - 1, signals 1 to 128 valid. sigfillset
sets every bit. An invalid signal returns -1 with errno EINVAL and leaves
the set untouched; sigismember returns 1 or 0. A null set throws.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ScreenTester5 (PPSA69420) stops at start-up on the unresolved sysctlbyname
import and also imports sysctl. hw.ncpu (CTL_HW.HW_NCPU) reports the same
processor count as sysconf(_SC_NPROCESSORS_ONLN). Other names and MIBs throw.
Three homebrew titles from boykopovar#1578 import it and stop at start-up without
it: ps5-opengl (PPSA99005), EVO-PLAYER-PS5 (PPSA99039) and
ProsperoRadio (PPSA99001).

It reports the port's resolution as the full and pane size and the
refresh rate ID as sceVideoOutGetOutputStatus does. The 48-byte layout
follows the declarations in ps5-opengl and EVO-PLAYER-PS5, which agree;
the screen size is 0 and the flags are 0. An invalid handle or a null
status throws, as in the other status getters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eNgs2 does

The PS4 firmware 11.00 libSceNgs2.sprx stores matrix levels clamped to
-4..4, keeps NaN, and rejects a level count of 0. Levels were stored
unclamped, so a level of 8 played twice as loud as on the console, and a
count of 0 cleared the matrix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hades (PPSA03355) stops at load on main with "undefined symbol:
9HzfhdtESio", the NID of pthread_getname_np, which it calls on
pthread_self() with a 32-byte buffer. Measured on a PS5 (firmware 13.60)
through dlopen of libkernel: the function takes (thread, buffer), copies
the name and its terminator, returns an empty name for an unnamed
thread, ESRCH for a null or joined thread and EFAULT for a null buffer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Returns the name of the directory under which a title's sandbox exposes
system files, which homebrew SDKs splice into paths such as
/<name>/common/cert/CA_LIST.cer and /<name>/common/lib/libc.sprx. The
name is fixed here; recorded in TechnicalDebt together with the fact
that nothing is mounted under it.

Registers guest_sandbox_random_word.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PICO-8 for PS5 (PPSA99808) stops at start-up on the unresolved import
a7ToDPsIQrc from libSceLibcInternal. The parser follows FreeBSD's inet_aton
(lib/libc/inet/inet_addr.c): one to four parts in decimal, octal or hex, the
last part filling the remaining bytes, input ending at a NUL or whitespace,
and a null address only validating.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Record the tessellation factor ring that sceAgcDriverSetTFRing receives and report it from sceAgcDriverGetTFRing, with the console's default of 0xFF00000000 and 128 KiB before a title sets one. VITA5 (PPSA39410) and Homebrew UI Lab (PPSA99050) stop on this import once sceVideoOutGetResolutionStatus resolves.
The hardware findings give 0xF_F000_0000, which is 0xFF0000000, not 0xFF00000000. Check the ring across threads, at ps5-opengl's size of 0x1b000 and with a canary after the outputs, and say in the debt entry that nothing is written to the ring.
sceSystemServiceLaunchWebBrowser fails with the kernel's EOPNOTSUPP: there is no web browser to open. RommPS PPSA76677 stops on main at this import and shows a QR code instead when the browser does not open.
…st any arguments

The param is void*, as RommPS declares it. The test also passes a null and an empty URI and a param that must stay untouched, and compares against 0x8002002D itself. The debt entries now say the error is returned for any arguments and that the param layout and the console's argument checks are unknown.
CachedPipeline keeps at most 256 pipelines. A level with a larger working
set thrashes the LRU: every miss creates a new VkPipeline (modules,
layout, render pass and a driver compile from the pipeline cache). The
bound is now 1024. A cached pipeline is a pure function of its key, so
the bound changes only memory and time.
…iew adjustment

A draw's guest buffer region inside a host import at an offset off
minStorageBufferOffsetAlignment was copied per build (a new buffer, an
input snapshot, a vkCmdCopyBuffer that ends the open render pass), and
DirectRegions refused such builds as templates.

Descriptor already binds any DWORD-aligned view from the aligned offset
below it and passes the remainder through the shader's memory offset
slot. A draw build that is not address-based and whose every guest buffer
element has such a slot (a push constant byte, or a shader data byte
inside its data buffer) now binds a region whose offset in its import is
DWORD aligned in place, like an aligned one: its read-only views are
snapshotted per draw by PrepareDrawBindings at address - adjustment, the
import serial keys the template, and Revalidate proves it per use.
Dispatches keep the copy (they read in-place inputs at execution, not at
record).

agc_driver_recorder_tests misalignedRegionTests: a misaligned read-only
draw region is read in place, the build is reusable, the push constant
carries the adjustment, and the per-draw snapshot starts at the aligned
offset below the view.
PS5 titles pass a 0x230-byte SceAvPlayerInitDataEx: auto start is the
byte after debug_level at 0x74, nine 0x30-byte thread parameter blocks
fill 0x78-0x228 and the output video frame buffer count is at 0x228.
sceAvPlayerInitEx read both fields at their PS4 offsets: auto start at
0xa8 is the video decoder thread priority and the buffer count at 0xa4
lies in the audio decoder block. PPSA25063 (Poppy Playtime: Chapter 4)
sets every thread priority to 700, so its intro video started on its
own and stopped at once; it writes auto start 0 and starts playback
itself.

With this_size 0x230 or more both fields are read at the PS5 offsets;
smaller structures are read as before. The layout follows KytyPS5,
prosper, SharpProspero and sharpemu and is recorded in TechnicalDebt.

guest_avplayer builds a 0x230-byte structure with auto start 0 and
nonzero thread parameters, then with auto start 1, and checks when
playback starts and how many video buffers are allocated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
llvmpipe builds the high dword of the shader clock with a left shift by
32 (emit_clock in Mesa's lp_bld_nir_soa.c), so it is always 0 and
s_memtime and s_memrealtime wrap every 2^32 ns. A pass of
agc_driver_shader_clock whose two reads straddle a wrap failed with
"memtime: lane N went back from 4294921902 to 3823"; dispatching right
before the boundary fails every lane of that pass.

On llvmpipe compare the low dwords modulo 2^32, which stays correct if
Mesa fixes the shift. Other devices keep the 64-bit comparison.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA
On a host without IPv6, socket(AF_INET6) fails with EAFNOSUPPORT, so
guest_sce_net aborted at its first IPv6 socket and never ran the TCP,
UDP, epoll, sendmsg/recvmsg, resolver and NetCtl checks after it.

When sceNetSocket reports guest EAFNOSUPPORT (47) for the IPv6 socket,
skip those socket checks, run the rest of the test, and exit with 77,
which ctest reports as skipped. Hosts with IPv6 run every check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA
libstdc++ already defines NOMINMAX on MinGW, so the unguarded define
warned on every Windows build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
libSceVdecsw is the software H.264 decoder interface PPSA05684 uses for
its movies. Decoder creation, compute queues, memory queries, decode
input and output, AVC picture info, reset and finalize are implemented
with the layouts of shadPS4's vdecsw.h, checked against the title's
movie decoder, and decode through libSceVideodec2. The H.264 stream the
Videodec2 test uses moves to a shared fixture, and a guest test decodes
it through Vdecsw.

Uncharted: Legacy of Thieves Collection (PPSA05684) plays its movies through libSceVdecsw and stopped at sceVdecswQueryComputeMemoryInfo.
…needs

The built Windows libraries import libgcc_s_seh-1.dll, libstdc++-6.dll and
libwinpthread-1.dll, and the entry stub loads them without searching PATH.
The release archives ship the DLLs in libs/, but the runtime layout only
told self-builders to copy the .prx files, which ends in error 126.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aths with spaces

MinGW windres passes -I paths to cc1 with backslash-escaped spaces and
parentheses, so compiling 3rdparty/freetype/src/base/ftver.rc fails when
the checkout or build path contains them. FreeType is linked as a static
library and the resource object is never pulled into an output, so it is
removed from the target's sources on Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
oneandonlydean and others added 27 commits October 10, 2026 10:45
…t-runtime

test(agc): bound the storage cache budget regression workload
…dit-exit-two

fix(tools): exit 2 when import_audit cannot read an input or write --json
…nmax-kernel-sync

build(windows): guard NOMINMAX in the kernel sync test
…-dlls

docs(usage): list the MinGW runtime DLLs a self-built libs directory needs
build: skip the FreeType version resource so Windows builds work in paths with spaces
…no-error-dialog

test(libs): report patched libraries that fail to load instead of a loader dialog
…-utf8

fix(tools): preserve UTF-8 in progress comparisons
…-ipv6

test(net): skip only the IPv6 socket checks on a host without IPv6
…tivirus

docs(build): note antivirus quarantine of relinker test executables on Windows
…c-timezone-bounds

fix(rtc): validate ticks before timezone conversion
…unread-close

test(net): reset the closed peer with unread data so the send fails at once
feat(libc): implement syslog and vsyslog
feat(libkernel): implement tcgetattr and tcsetattr
…-regions

perf(agc): bind misaligned draw buffer regions in place through the view adjustment
…sync

feat(libkernel): implement fdatasync
…tats-debt

docs: drop the stale unknown-signature entries for the memory pool stats
…s-event

feat(libSceVideoOut): register the VRR status event and accept the VRR status call
…thread-getname-np

feat(libkernel): implement pthread_getname_np
…eadlink

feat(libkernel_sys): implement readlink
…ox-random-word

feat(libkernel): implement sceKernelGetFsSandboxRandomWord
…tion-status

feat(videoout): implement sceVideoOutGetResolutionStatus
…rnel-system

feat(libkernel): implement system
read, write, _read and _write on a guest socket descriptor went to the
host, which has no such descriptor, so sceKernelRead and sceKernelWrite
threw. They now reach the socket as recv and send without flags, with
the socket layer's errno (EAGAIN 35, EPIPE 32, EBADF 9), a zero-length
read returns 0 as dofileread does, and the two sceKernel forms return
the kernel error for the same cases. The host's SIGPIPE on EPIPE, a
write above INT_MAX bytes, readv, writev and the positioned forms are
recorded in TechnicalDebt.

OpenSSL's QUIC notifier signals its socketpair with write and read.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@SleglJan
SleglJan force-pushed the feat/libkernel-socket-read-write branch from 62aa3af to 9d0b105 Compare October 9, 2026 21:59
@SleglJan
SleglJan merged commit a8ce7b7 into main Oct 10, 2026
7 checks passed
SleglJan pushed a commit that referenced this pull request Oct 16, 2026
…ll or crash

A GPU hang or a crash in a title leaves no record of what the queues
were last given: APS5_TRACE_GPU prints every submission (too much to
leave on), and the WAIT_REG_MEM timeout message shows only the packet
before the wait on the waiting queue.

Each queue now keeps a ring of its last 16 submissions
(APS5_PM4_RING=<n>, 0 to 4096, 0 disables): the TraceMs time in tenths
of a millisecond, serial, size, guest address and a copy of the first
512 dwords after indirect buffers are inlined, recorded under the
driver lock next to the serial, REWIND tails included. The ring of a
queue and the failure report registration are set up at the top of
Submit, before any output is reserved, so recording under the lock
does not allocate. Disabled, Submit only tests a static flag; enabled,
it costs one bounded copy per submission.

DumpSubmissionRings decodes the kept dwords with Pm4::PacketWords and
the new Pm4::KnownName, which returns the decoder's names as views of
static strings (empty for an unknown opcode, printed as UNKNOWN_0x%02x);
Pm4::Name now builds its std::string from it. Every line is formatted
with integer and string conversions only into a 512-byte stack buffer
and written with one WriteFile / write(2) on the stderr handle, so the
dump does not allocate from the C runtime heap, lines from other
threads do not interleave and the CRT stream lock is not taken (the
rule of boykopovar#3871). The header gives the number of queues and the ring
size, and each queue line the submissions recorded and kept.

libc gets a small registry of failure reports
(LibcRegisterFailureReport_nid_no_patch, Unregister, Run); Run runs
the registered reports the first time it is called and returns at once
after that. The Windows crash report runs it after the thread dump of
an unhandled exception, after SIGABRT (which also covers the
APS5_HANG_DUMP_SECS timed dump, since it aborts) and after an invalid
CRT parameter, so an abort after a reported exception does not print
the rings twice. The driver registers its dump at the first submission
and unregisters at static destruction; the dump there waits a bounded
time for each ring lock. What the crash-path dump cannot do (a ring
held by the crashed thread, rings corrupted by the crash, Linux) is
recorded in technical debt.

APS5_STALL_WATCHDOG_SECS=<n> (0 to 86400, 0 disables) makes a waiter
that has not progressed for n seconds dump the rings once per wait: a
WAIT_REG_MEM (progress as for APS5_GPU_WAIT_TIMEOUT_MS: another packet
runs or completes), a REWIND waiting for the title, and a compute
submission held behind queue 0. The last one prints under the driver
lock, so submissions and the other queues wait for it (USAGE.md says
so). Both variables are read at the top of Submit, so an invalid value
of either throws at the first submission and never later inside a GPU
worker. The order wait behind queue 0 releases its order hold on scope
exit, so a throwing dump cannot leave it counted.

No packet semantics change; packet sizes and names are those of the
existing decoder (PM4 type-3 header layout as in Mesa
src/amd/common/sid.h). The queue bound of Submit and of the rings is
one constant, SubmissionQueueEnd.

Tests: agc_driver_pm4_tests gets ten modes, each its own ctest:
agc_driver_pm4_ring (APS5_PM4_RING=4, six submissions on queue 0 and
two on 0x20: the dump holds #3..#6 and #1..#2 in order, nothing
older), agc_driver_pm4_ring_empty (one line), _ring_disabled (one
line), _ring_invalid and agc_driver_pm4_stall_watchdog_invalid
(APS5_STALL_WATCHDOG_SECS=1x: the submission throws and is not
executed), _ring_report (LibcRunFailureReports_nid_no_patch dumps the
rings), _ring_report_heap (Windows: with ucrtbase.dll's HeapAlloc and
HeapReAlloc imports counting calls from the reporting thread, the
failure report of a submission holding INDEX_BUFFER_SIZE names it and
allocates nothing, while Pm4::Name of the same header is seen
allocating), and with APS5_STALL_WATCHDOG_SECS=1 a WAIT_REG_MEM
(agc_driver_pm4_stall_watchdog), a REWIND (_stall_watchdog_rewind) and
a compute submission held behind a queue 0 rendering wait
(_stall_watchdog_order), each held for about 2.5 s, dump once and
complete when released. The PM4 catalog test checks KnownName against
every opcode and custom name. windows_backtrace_failure_report checks
that abort() runs the registered reports once and not an unregistered
one; windows_backtrace_failure_report_once that abort() does not run
them again after an earlier run. With the dump decoding through
Pm4::Name, agc_driver_pm4_ring_report_heap fails with "the failure
report allocated from the C runtime heap"; with Run resetting its
flag, windows_backtrace_failure_report_once prints the report twice.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.