Skip to content

Cyberpunk 2077 (PPSA04029) on Linux: sceKernelOpen("") opens a directory and the read throws EISDIR on main 961b25fe #3308

Closed

Description

@snoozedDev

Title: Cyberpunk 2077 (PPSA04029 v02.120.000, EU disc, owned). Converted from the dump's eboot.bin with relinker --registry built from the same main, no --to-intel. Requested in #2010 ("If Cyberpunk 2077 ... stops on main, please open an issue with the title ID and the log line").
Host: Linux (Omarchy, kernel 7.2.5), GCC 16.2.1, RTX 3090, NVIDIA 610.57.04 (subgroup 32), selected with ANYPS5_GPU=3090. DualSense connected, no input given. vm.max_map_count is 1048576 on this host.
Build: main 961b25f, Release, -DBUILD_TESTING=ON -DANYPS5_ENABLE_SPIRV_TOOLS=ON (what build.yml's linux job uses). Every stop below is CPU-side, so no build without validation and no RADV run.
Launch: APS5_TRACE_DISPATCH_IO=1 ANYPS5_GPU=3090 timeout 300 ./app.elf inside gamescope --backend headless -W 2304 -H 1296 -r 60, one launch at a time, cold shader cache then warm.
sce_module/: libc.prx, libSceFace.prx, libSceFaceTracker.prx, libSceJobManager.prx, libSceNpCppWebApi.prx, libScePfs.prx. The dump also ships modules/libREDGalaxy64.prx (the title's GOG Galaxy wrapper, host replacement dropped in #676); the relinker only scans sce_module/, sce_modules/ and prx/, so it was copied into sce_module/ for conversion. The converted module was also made reachable as app0/modules/libREDGalaxy64.prx.guest.prx, because the title calls sceKernelLoadStartModule("/app0/modules/libREDGalaxy64.prx") at runtime (call site at address 0x1d762d2 in the converted app.elf, PLT slot of NID wzvqT4UqKX8) and, when that fails, asserts s_galaxyModuleHandle > 0 with Failed to load Galaxy module %hs: 0x%08X (strings in the executable; the run ends in the title's int 0x41 trap).

On Linux every libc import of the executable (3180 references, declared from libc.prx) binds to the converted title libc.prx (#guest), whose exports are real code: eLdDw6l0-bU (snprintf) is a 219-byte function there, Ye20uNnlglA (abs) 8 bytes, H+8UBOwfScI (__powidf2) 92 bytes. The host libc placeholders libcCyberUnknown02/08 (#3161, #1980, #2158) are not reached by this title on Linux (and the empty string in stop 1 is not the #2652 case: this snprintf is real code). tools/import_audit.py: 980 unique imports, 669 implemented, 39 stubs (Agc workload/occlusion/capture, Coredump, ContentExport, Http, AudioOut2 mastering, Json RTTI, select), 0 absent, 272 resolved by the title's own modules.

Stop 1 — main 961b25f, about 2 s after start, in every run (no shader is registered before it, so the cache cannot matter): sceKernelOpen("") succeeds on the guest root directory and sceKernelRead throws

[src/Open.cpp:123 sceKernelOpen] path=/app0/engine/config/platform/ps5/cmdline.txt flags=0x0 nativeFlags=0x0 mode=00
[src/Open.cpp:123 sceKernelOpen] path= flags=0x0 nativeFlags=0x0 mode=0777
[libc] unhandled exception 'St13runtime_error' thrown from 0x729eb668c223
terminate called after throwing an instance of 'std::runtime_error'
  what():  sceKernelRead: read failed, fd=6, errno=21

The title's launcher stage (the function at address 0x1db65b0 in the converted app.elf, reached from cpLauncher startup) builds an empty string on its stack and hands it to its file-open routine, which calls sceKernelOpen(path, 0, 0777) and then sceKernelRead on the result (return address chain checked with a temporary print in sceKernelOpen and a frame-pointer walk). ResolvePath("") resolves to the guest root /, the host directory the title was started in; open(2) on a directory with O_RDONLY succeeds on Linux (strace: openat(AT_FDCWD, "<start dir>/", O_RDONLY) = 6), and the following read gets EISDIR, which sceKernelRead turns into a throw instead of an error code. FreeBSD fails open("") with ENOENT, and libc's chdir("") in General.cpp already returns errno 2 for the empty path. The title expects the open to fail: with a local probe that returns SceErrorFromErrno(ENOENT) from sceKernelOpen for an empty path (not proposed as a fix) it continues normally. I found no issue or PR for this (searched errno=21, EISDIR, "empty path").

Stop 2 — main + the empty-path probe (which shifts Open.cpp line numbers by 6), about 10 s: select on the /dev/urandom descriptor from libREDGalaxy64

[src/Open.cpp:129 sceKernelOpen] path=/dev/urandom flags=0x8004 nativeFlags=0x0 mode=045056
[libc] unhandled exception 'St13runtime_error' thrown from 0x7414c1d77ac0
terminate called after throwing an instance of 'std::runtime_error'
  what():  select on descriptors that are not libSceNet sockets not implemented

A print in select_nid_postfix shows nfds=7, readfds={6}, timeout 10 ms, called from libREDGalaxy64.prx.guest.prx; descriptor 6 is the /dev/null placeholder File::OpenRandomDevice returns for the title's /dev/urandom open, so the libSceNet poller reports it unknown (Socket.cpp:88). This is the text of #2811 (Dungeons, where the descriptor was stdin), and the first commit on the branch posted there, Silverash-i8/AnyPS5-fork:fix/select-random-device (45c387fd, "report /dev/urandom descriptors ready in select"), is exactly this case. With that branch applied (three commits, git cherry-pick, clean) the title continues.

Stop 3 — main + probe + that branch, about 70 s cold and warm: the title's own watchdog trap, behind a quadratic WatchMapping

!!! EngineWatchdog timeout!

followed by the title's int 0x41 trap (SIGSEGV, rax=0xdeadbeefcafebabe). The watchdog is a title thread (its function at address 0x377a640 in the converted app.elf) that sleeps in sceKernelUsleep slices and traps when its heartbeat flag has not been set for TimeoutSeconds; the main thread was spinning in the title's allocator lock behind a thread inside sceKernelBatchMap2. The title maps its direct memory 64 KiB at a time: 42,412 sceKernelBatchMap2 entries of OpMapDirect with length=0x10000 in the 75 s before the trap (2.6 GiB, 35,719 live entries in g_directMappings at the end). Split timers in DoMapDirect (temporary, removed): 24.3 s inside DoMapDirect over those calls, of which 23.2 s in WatchMapping (DirectMemory.cpp:398, whose inner loop walks all of g_directMappings for every mapping added, so each call costs more as the count grows: about 0.1 ms per call at 10,000 to 14,000 entries, 6 to 7 ms at 35,700), 0.75 s in MapAligned, the rest (ValidatePhysicalRange, mutation.Add, EraseReservations, RecordProtection) under 0.1 s together. Same in the warm run (shader cache does not matter here; 613 cold and 870 warm shaders were registered before the trap, 0 dispatches, no flip).

With the alias search in WatchMapping skipped behind a local switch (experiment, not a fix: it drops the write-watch unregistration for aliased physical ranges), 55,000 maps (the full startup sequence, against 42,412 before the trap) take 1.3 s in total and the watchdog does not fire.

Past stop 3 (experiment build), what was reached

Three runs of 210 s, 286 s and 210 s, each ended by me (no stop of its own): swapchain 1381x777 created, DualSense opened, 1414 shaders from the disk cache, 1,677 / 10,594 / 4,873 [dispatch-io] compute dispatches, the title reads r6/cache/tweakdb_ep1.bin, probes /temp0/CrashInfo.json, reads its input configuration (r6/config/inputUserMappings.xml and siblings), lists /data/temp/screenshots/, and keeps dispatching at a low rate with no further host stop. No frame was seen (headless, no capture kept).

In four other runs of the same build my timeout wrapper forwarded a SIGTERM to the title at 40 to 60 s (my side, not the title's); on that shutdown path the title drew with PA_CL_VS_OUT_CNTL = 0x00400001 (CLIP_DIST_ENA_0 | VS_OUT_CCDIST0_VEC_ENA) and main refused it:

[videoout] presentation failed: clip distances, layer, viewport or auxiliary vertex exports is unsupported (register 0x207 = 0x00400001)

That is #423 (draft); the draw did not come within the 210 to 286 s of the three undisturbed runs.

Seen, not stops

  • could not be prepared at registration (its s_swappc_b64 at program counter 0x… calls a computed target): 79 per run (fix(agc): defer shaders that cannot be prepared at registration to their first use #1836's deferral).
  • Shader 0x… has unsupported registered shader type 8; it is not prepared at registration: 4 per run.
  • Three background preparation failures: Shader 0x1aa4d3a100 could not be prepared after registration: ShaderRecompiler::Recompile: GetImageResource dword 0 is not a valid runtime value; chain: ReadConstBuffer@pc=0x00000474(GetBufferResource(ReadConst(GetSrtResource, … and the same text at pc=0x000004d0 (0x1aa4d18100) and pc=0x00000410 (0x1aa4f05d00); the shader addresses vary between runs, the pc values do not. None was dispatched before the runs ended; the RecompileRequest blobs are in the logs.
  • sceSysmoduleLoadModule unknown id: 267 and 271, once each.
  • [gpu] storage image 0x…: guest memory changed while GPU results were pending; keeping the CPU's blocks and … destroyed with GPU results pending, a handful per run past stop 3.
  • The title's direct-memory mapping style means about 45,000 live mmap ranges by 70 s; a host with the default vm.max_map_count of 65530 would hit that limit later.

Logs for every run and the temporary probes are kept locally; say which one you want. AI-assisted: yes (Claude Code, Claude Fable 5.1), operating under my supervision; I am accountable for this report and will respond to review.

Activity

  1. fdfawef324 commented on Oct 11, 2026

    @fdfawef324
    Contributor

    Stop 1: a fix branch is ready, fdfawef324:fix/libkernel-empty-path, one commit on main 1690db9. Intake blocks me from opening a PR; anyone eligible is welcome to carry it.

    Change. sceKernelOpen("") now returns 0x80020002 (ENOENT), so the title's open fails as it expects. This follows FreeBSD's namei, which refuses every empty pathname with ENOENT (sys/kern/vfs_lookup.c L331-L335, "Don't allow empty pathnames"). Here the POSIX forms and sceKernelMkdir already did this.

    The other path calls had the same hole, so the same rule now applies to them. Each still runs its argument checks first, as FreeBSD does (a negative length or bad times is still EINVAL):

    • sceKernelStat("") reported the guest root.
    • sceKernelRmdir("") would remove the root if it were empty.
    • sceKernelRename("", x) would rename the root.
    • sceKernelUnlink, sceKernelTruncate and sceKernelUtimes returned EPERM, EIO and success respectively.

    sceKernelCheckReachability is left as it is, since it has no FreeBSD counterpart to follow.

    Platforms. This stop happens on Windows too: "" resolves to the root there as well, and sceKernelOpen hands back a directory descriptor for a read-only open. guest_filesystem gains the empty-path cases for all seven calls; on main it fails at the first of them.

    Tested. Windows Server 2025, WinLibs GCC 15.2.0 r7, Release, full ctest (BDA tests excluded, see #3291). I don't have the title, so I haven't run it with this change.

    On stop 3: WatchMapping is compiled only on Linux (#if defined(__linux__) in DirectMemory.cpp), so the quadratic alias search does not run on Windows. I can't build or test Linux here, so I haven't touched it.

    AI-assisted: yes.

  2. snoozedDev commented on Oct 11, 2026

    @snoozedDev
    ContributorAuthor

    Retested on main a11c8f7, which contains #3428 and #3465 (empty paths) and #3455 and #3476 (select on non-socket descriptors): stops 1 and 2 no longer reproduce; stop 3 is now the first stop on bare main.

    Setup. main a11c8f7, Release, -DBUILD_TESTING=ON -DANYPS5_ENABLE_SPIRV_TOOLS=ON, Linux, GCC 16.2.1, RTX 3090, NVIDIA 610.57.04. eboot.bin reconverted with that tree's relinker, no probes or cherry-picks, same launch as in the report, cold shader cache then warm. tools/import_audit.py on the new conversion: 980 unique imports, 672 implemented, 36 stubs, 0 absent, 272 resolved by the title's own modules.

    Stop 1 and stop 2, cold and warm: no empty-path open (path= ) is logged any more; the title goes on to open engine/config/base/*.ini, r6/config/user.ini, r6/config/settings/.../options.json and its archive/ps5/ files (94 sceKernelOpen calls per run). /dev/urandom is opened (flags=0x8004) and the run passes the point where stop 2 threw; no not implemented line in either run. (Before #3428 merged, fdfawef324:fix/libkernel-empty-path 92cb301 on main 2a4e03c also got the title past stop 1, cold and warm, and then stopped at stop 2 as before.)

    Stop 3, cold and warm: after the eighth 10-second shader-cache tick since the cache opened (the report's runs had seven, given there as about 70 s; the log has no timestamps), the report's watchdog line followed by the timeout wrapper's exit line:

    !!! EngineWatchdog timeout!
    timeout: the monitored command dumped core
    

    The report traced this to the title's watchdog trap (int 0x41, SIGSEGV, main thread behind a thread in sceKernelBatchMap2); these runs show only the core dump; the trap was not re-instrumented. WatchMapping (DirectMemory.cpp:396 on a11c8f7) is unchanged since 961b25f, so the report's measurement stands. Reached before it, cold / warm: swapchain 1381x777 created, DualSense opened, 771 / 1169 shader-cache lookups (warm: 804 hits, 365 misses), 0 dispatches, no flip.

    Seen, not stops, same as the report: 79 s_swappc_b64 deferrals, 4 unsupported registered shader type 8, the background preparation failures GetImageResource dword 0 is not a valid runtime value at pc=0x00000474 and pc=0x000004d0 (cold) plus pc=0x00000410 (warm), sceSysmoduleLoadModule unknown id: 271 and 267.

    AI-assisted: yes (Claude Code, Claude Fable 5.1), operating under my supervision; I am accountable for this comment and will respond to review.

  3. BharatYadav007 commented on Oct 12, 2026

    @BharatYadav007
    Contributor

    The remaining WatchMapping bottleneck described in stop 3 overlaps with open PR #4216. That PR adds a physical-offset index for direct mappings and uses it in WatchMapping, replacing the scan over all mappings with a search for overlapping physical ranges while retaining alias write-watch handling.

    Stops 1 and 2 are already reported resolved on main in the retest above. #4216 is still unmerged, and its published title validation is for Minecraft and Zombie Cure Lab, not Cyberpunk 2077. A Cyberpunk retest with #4216 would establish whether it also removes this watchdog stop; this issue should remain open until that is verified.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions