Skip to content

fix(libkernel): poll pipe descriptors - #3644

Merged
oneandonlydean merged 2 commits into
boykopovar:mainfrom
DariusHQ:fix/libkernel-poll-pipes
Oct 13, 2026
Merged

oneandonlydean merged 2 commits into
boykopovar:mainfrom
DariusHQ:fix/libkernel-poll-pipes

Conversation

@DariusHQ

Copy link
Copy Markdown
Contributor

What

Fixes #3584. poll threw descriptor N is not a socket for any descriptor below the guest socket range. ProsperoTV (PPSA99003) polls the read end of a pipe next to its sockets and stopped right after its menu opened, on main a11c8f7f7, Windows, catalog build (eboot.bin and sce_module/libc.prx unwrapped as #2605 requires):

[debug:0] [TV] menu open session=1
terminate called after throwing an instance of 'std::runtime_error'
  what():  poll: descriptor 3 is not a socket

With this branch, run the same way, the menu finishes ([TV] menu ready display=3840x2160 ... in 1259 ms), its catalog request goes out, and it stops later at the AGC stop that #3581 tracks for five other titles:

  what():  AGC graphics: last provoking vertex with explicit per-vertex flat interpolation is unsupported

Pipe readiness follows FreeBSD 11's pipe_poll:

  • the read end reports POLLIN/POLLRDNORM when data is buffered;
  • once the other end is closed, the read end reports POLLIN/POLLRDNORM and POLLHUP, and the write end reports POLLOUT and POLLHUP;
  • the write end reports POLLOUT when at least PIPE_BUF (512) bytes are free;
  • each end reports only what its access mode allows, so the write end is never readable and the read end never writable.

On Linux the pipe goes to the host poll with the sockets. Linux reports POLLERR for a write end whose reader is gone; that becomes POLLOUT | POLLHUP, as on FreeBSD. On Windows a CRT pipe can't be waited on with WSAPoll, so while a pipe is in the set, poll reads its buffered and free bytes and the pipe state with NtQueryInformationFile (FilePipeLocalInformation, and FileAccessInformation for the end's access) and waits on the sockets in 1 ms slices until something is ready or the timeout passes. Sets without a pipe wait exactly as before.

A closed descriptor below the socket range now reports POLLNVAL, as FreeBSD does and as closed sockets already did. Other descriptors there (regular files, terminals) still throw. TechnicalDebt's poll entry is updated.

select on non-socket descriptors is a separate path (Socket.cpp, #3462) and is unchanged.

Tested

Windows 11 (26200), MinGW-w64 GCC 15.2.0 (WinLibs r7), Release, full build with BUILD_TESTING=ON on a11c8f7f7: ctest 832/836 passed. The four failures fail the same way on main here:

guest_poll now also checks, on a pipe:

  • an empty pipe (the write end writable, the read end not readable), and a 50 ms timeout that waits;
  • the read end readable after a write, with POLLIN | POLLRDNORM asked;
  • the write end never readable, and the read end never writable;
  • a pipe and a socket in one set, both with data arriving from another thread during a 2 s timeout and during an infinite wait;
  • filling the pipe until it stops being writable, then writable again once a block is read;
  • the write end closed: POLLIN | POLLRDNORM | POLLHUP with data left and once drained;
  • the read end closed: POLLOUT | POLLHUP on the write end;
  • a closed pipe descriptor: POLLNVAL.

It passed 20/20 runs. Mutation check, caught:

  • buffered data ignored;
  • the write end reported readable, or the read end writable;
  • writability without a free-space check;
  • no POLLHUP;
  • a closed descriptor throwing instead of POLLNVAL;
  • pipe readiness checked only once;
  • the timeout not waited for;
  • events reported that were not asked for.

One survived: writable at any free byte instead of 512. The test fills the pipe 256 bytes at a time, so it can't tell the two apart.

Not run on Linux (no Linux host here). The Linux path is the existing host poll with pipes added to the set, plus the POLLERR mapping.

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 (fix(libkernel): map host socket errors to guest errno #3592 changes the next line of the same TechnicalDebt paragraph; whichever lands second needs a one-line rebase)
  • AI-assisted: yes

@blawesom

Copy link
Copy Markdown
Contributor

Independent pre-review on Linux (full CI runs only happen on manual trigger, so recording one): CachyOS, kernel 7.2.9, GCC 16.2.1, Ninja, Release, BUILD_TESTING=ON.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 11, 2026
ProsperoTV (PPSA99003) stops after its menu opens because poll threw for
the read end of a pipe it polls next to its sockets. poll now reports pipe
readiness as FreeBSD's pipe_poll does: readable when data is buffered,
POLLHUP once the other end is closed, writable when PIPE_BUF bytes are
free. On Windows the pipe is checked with NtQueryInformationFile while the
sockets are waited on in 1 ms slices; on Linux the host poll waits on both.
A closed descriptor below the socket range reports POLLNVAL.
@DariusHQ
DariusHQ force-pushed the fix/libkernel-poll-pipes branch from 8cc3402 to 7250306 Compare October 12, 2026 21:02
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 12, 2026
@github-actions

github-actions Bot commented Oct 12, 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
#4525 core/libs/tests/GuestPoll.cpp L0-1, L5, L21, L28-29
docs/dev/debt/libkernel.md L143

@coltken95

Copy link
Copy Markdown

Tested on Linux (CachyOS, RTX 2070, NVIDIA 615.71.09) with Nuvio Max'd (PPSA99288), built from main at e6ecece with this PR merged on top.

Without this PR the title aborts about 14 s after start with poll: descriptor 64 is not a socket (from poll_nid_postfix, called on a worker thread). With it, the title runs and renders its UI at 59.94 FPS; the next stop is a network failure unrelated to poll.

AI-assisted: yes

@Amoenus

Amoenus commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Independent Windows validation of this implementation, cleanly carried onto pinned main e6ecece94e5b8f241dc86c14d326eed2347044fb with original authorship preserved: tested fork head45e3f7b56, derived from upstream head7250306b7. This is an adapted current-main validation, not a retest of the older base.

  • Full Release build, patched system libraries, conventions, registration and diff checks pass.
  • Six related groups ×20 pass (120 executions, no skips): guest_poll, guest_udp, guest_tcp, guest_socket_pair, guest_socket_read_write, guest_dup.
  • Keeping the extended fixture and rebuilding main's Udp.cpp reproduces poll: descriptor ... is not a socket; restoring the implementation passes. Source is restored/clean.
  • Full NVIDIA: 1011 passed / 0 failed / 8 skipped of1019, maximum observed test 22.172s, with two CTest workers and a30s bound. Skips are stencil-reference export and the seven listed video-output cases.

Windows11 Enterprise26200, WinLibs GCC15.2.0r7 /Ninja Release/testing ON, bundled dependencies; RTX5080 driver610.62, AnyPS5 shader cache disabled. Driver-cache coldness and Linux execution are unverified in this campaign.

Additional title needing the pipe path: Sonic Unleashed Recompiled, PPSA99902 /01.003.000. Our conditional #4822 libc-only replay stops on the original non-socket guard. A scalar observer confirms pipe read ends requesting POLLIN with timeout=-1 and count1; #3521's existing comment has the exact pair/event records. Kernel-only conditional title comparison completed: on the fixed-libc #4822 profile, the original libkernel aborts on non-socket poll after9.981s. Replacing only libkernel.prx with this implementation removes that observed abort. A captured frame was inspected and shows the SEGA startup logo; the process continues until 789.240s, then exits0xC0000409 without timeout at the existing private integration guard:

AGC graphics: guest writes to a resident multisample target or its metadata are not implemented

The other123 library hashes, including the fixed libc, and executable hash are unchanged. Native selected groups ×5 and conventions/registration also pass on the experimental adaptation. Original executable/bundled libc, baseline runtime, converted guest and user data are restored/hash-verified; tracked source is clean. The four scheduled input pulses and two additional3000ms pulses are recorded separately; consumption and reliable menu navigation remain unverified. Early frame captures establish startup rendering, not gameplay or unique-frame FPS. The MSAA guard is local to the preserved experimental stack, already investigated in #2132/#419; this replay supplies no new actual guest-write/coherence verdict and does not justify a new main-defect issue or relaxing the guard. Validated experimental fork headfac106b2b. Bring-up: #4443.

This validation does not select between the overlapping #3644/#3649 submissions or validate their combination with the low-socket-descriptor change #4391. AI-assisted: Codex; author responsible.

@coltken95

Copy link
Copy Markdown

guest_poll aborts on Linux (Ubuntu 24.04 container, main at e6ecece with only this PR applied), at the check after draining 1 KiB from the filled pipe:

Require(read_nid_postfix(full[0], block, sizeof(block)) == sizeof(block));
Require(poll_nid_postfix(&fill, 1, 0) == 1 && fill.revents == Out);

Linux merges the 256-byte writes into 4 KiB pipe pages and reports POLLOUT only once a whole page slot is free, so reading 1 KiB leaves the pipe full and the host poll reports no POLLOUT, as the debt entry allows ("writable … when the host kernel says so on Linux"). Reading the whole pipe back before that check, or at least 4096 bytes, should hold on both hosts. The pipe read-end path itself works with Nuvio Max'd as reported above.

AI-assisted: yes

FreeBSD's pollscan clears POLLOUT whenever POLLHUP is set, so a write end
whose read end is closed reports POLLHUP alone, with POLLIN and POLLRDNORM
when they were asked for; both host paths reported POLLOUT | POLLHUP.

POLLOUT on a read end now throws. FreeBSD opens both ends FREAD | FWRITE
and pipe_poll reports the read end writable, which a one-way host pipe
cannot back.

guest_poll drained 1 KiB from the full pipe before expecting POLLOUT, but
Linux reports it only once a whole 4 KiB page is free, so the test aborted
there. It now drains the pipe, which is writable on every host.
@oneandonlydean

Copy link
Copy Markdown
Collaborator

Pushed 64ca6e3 on top of your commit, so this can be built:

  • guest_poll aborted on Linux at GuestPoll.cpp:91: Linux gives POLLOUT back only when a whole 4 KiB page is free, and the test read 1 KiB. It now drains the pipe before that check, which holds on any host (the Windows pipe is 4096 bytes, so a fixed 4 KiB read would not).
  • A write end whose reader is gone reports POLLHUP alone, on both hosts. FreeBSD's pollscan clears POLLOUT when POLLHUP is set (sys_generic.c:1597).
  • POLLOUT on a read end throws. FreeBSD opens both ends FREAD | FWRITE (sys_pipe.c:422) and reports it writable, which a one-way host pipe can't back, and no title asks for it.

The debt entry is updated to match. On Linux, merged with main: full build, ctest 995/995 (7 skipped), guest_poll 10/10, conventions check passes. The Windows path is not run here; the full build covers it.

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.

libkernel: poll throws 'descriptor 3 is not a socket' for a pipe descriptor (ProsperoTV)

5 participants