What a real PS5 answered

Findings from running obSCEne on real hardware, each traceable to a record in data/hardware/. Recorded 2026-08-30, on the first runs this project has ever completed on a console.

Every claim here cites a line. A consumer - orbistoun above all - has to be able to say how something was determined and have somebody else reach the same answer. Where a finding rests on inference rather than a record, it says so.

Not to be confused with HARDWARE-PROBE.md, which is the orbistoun side's scoping of what it would want asked and was written before any of this ran. That file is a request; this one is the answer, and the two are worth keeping apart because a wish and a measurement read identically once they are a fortnight old.

The shape of the run

The complete suite, census included, from one binary:

OBS|meta|1|30|528          30 sections, 528 checks
OBS|tally|232|75|192|29    pass / partial / fail / skip
obscene: suite complete

499 of 528 checks produced a result. The 192 failures are almost all one thing - a library the census could not load - and that is the finding, not a fault.

The first complete run, before the import defect below was found, was OBS|tally|218|74|189|40 over 521 checks. Both files are kept: data/hardware/ps5-full.txt is that run, data/hardware/ps5-imports.txt is the one that carries import records. Each names the build that produced it.

The loader maps the libraries, and for a long time did not bind their symbols

This section previously said the loader mapped five of the twelve libraries the eboot declares. That was inferred from which symbols resolved, and it was wrong. The system log names each library it maps, with an address range and a fingerprint:

# /<sandbox>/common/lib/libScePad.sprx
#  xotext: 0000000800968000:0000000800980000 nsegs: 4
#  fingerprint: c68f5edf5ba4c424ce9468cbc64b016900000000

165 libraries are mapped into the process, and ten of our twelve are among them - only libScePosix and libSceVideoRecording are not. libScePad is mapped, at a known address, and every one of its imports is null.

So mapping and binding are separate, and this project was reading the absence of the second as the absence of the first.

Which is our defect and which is the platform's

061-imports puts the two axes side by side - did the import bind, and does a run-time lookup find the same name in the same library:

OBS|import|libScePad|scePadOpen|unlinked|resolvable

unlinked with resolvable is the repairable case: the platform has it, and this module failed to ask for it correctly. There were fourteen. There are now none.

Every import was weak, and the loader took that at its word

This program declares its platform functions weak so an absent one is null rather than a link error. That binding travelled into the module, and all 203 imports were STB_WEAK, undefined - which to a loader means "if resolving this costs anything, do not bother."

ours   WEAK   FUNC   x203
real   GLOBAL FUNC   x126   GLOBAL OBJECT x13      (a title that launches: no weak imports)

libkernel and libSceLibcInternal bound because they are already resident and resolving against them is free. Every other library was mapped - address range and fingerprint in the log

Re-binding imports as STB_GLOBAL when the module is built fixed it. Same console, one flag:

before after
imports bound 127 141
the platform has it and we did not bind it 14 0
the platform does not offer it 6 6
checks skipped for an unresolved symbol 24 10
tally 220/75/192/41 232/75/192/29

100-input, 090-audio and 165-gnm pass outright. 080-video, 070-user and 130-layout run and report for the first time - and found three things no run had ever reached:

070-user/initialise            0x80960003  the user service refused to initialise
080-video/open                 0x80290009  the main video output would not open
130-layout/query-size-ladder               every size accepted, so the size is not validated

Six imports remain unbound, and none of them is repairable here.

sceKernelIsCex is absent from a library that binds 84 other symbols, so it is simply not offered. The other five are libScePosix names - posix_getpagesize, posix_sigemptyset, posix_usleep, posix_pthread_rwlock_init, posix_pthread_rwlock_tryrdlock - and the reason is one level up from the names:

OBS|res|900-surface/posix|fail||this library could not be loaded

libScePosix does not load at all. It is one of the two declared libraries the loader never mapped, and the census cannot reach it either. The shared posix_ prefix on all five is therefore not evidence about the spelling: nothing in that library resolves under any name, so the names are untested rather than wrong.

Three candidates were eliminated on the way, each against the oracle or the console - the import-library attribute (0x1 where a real title writes 0x9; genuinely wrong, now fixed, changed nothing), sdk_version (no effect on binding, and it stops sceKernelDlsym answering entirely - D244), and .prx versus .sprx filenames (a real title writes .prx too).

All three were fields somebody had chosen. The answer was in one nobody chose. (D248)

A run-time load does not repair it

OBS|res|060-module/runtime-load-binds-imports|fail||loading the library at run time does not
bind an import the loader left unresolved

libScePad loaded, sceKernelDlsym returned scePadOpen, &scePadOpen stayed null in the same process. So "load everything at startup" is not the repair, and a check that needs an unbound symbol has to reach it through a resolved pointer. (D239)

What the census measured

symbols confirmed present 10,243
symbols confirmed absent 3,188
libraries fully present 105
libraries partly present 70
libraries a title cannot load 180

About a third of the 35,518-symbol corpus gets a verdict. The remainder sits inside libraries a title cannot reach at all, which bounds what an emulator needs to implement for a title of this shape.

Loading a media codec library kills the process

Isolated one at a time by the iterative sweep (./bin/obscene hwsweep), each identified by a try record with no matching res - which is what announce-before-attempting is for:

libSceM4aacEnc   libSceOpusCeltDec  libSceOpusCeltEnc  libSceOpusDec  libSceOpusSilkEnc
libSceSrcUtl     libSceUlt          libSceVideoCoreServerInterface
libSceVideoOutSecondary             libSceVideoRecording

Audio and video codecs and media-server interfaces, without exception across the whole corpus. sceKernelLoadStartModule on any of them ends the process rather than returning an error. Ten is the complete list: with these excluded the suite runs to the end, so nothing else in the 370-library corpus does it.

Behavioural failures

018-relational/handle-fits-its-out-parameter  the call wrote past the end of the int it was given
035-libc/wide-strings                          wcslen counted the wrong number of wide characters
010-kernel/is-stack                            a stack address and a static one were reported alike
110-modules/names            0x80020016        the platform would not describe any module

Three more became visible only once imports bound (D248). No run had ever reached them, because the symbol they need was null and the check skipped:

070-user/initialise          0x80960003        the user service refused to initialise
080-video/open               0x80290009        the main video output would not open
130-layout/query-size-ladder                   every size accepted, so the size is not validated

080-video/open is the one to start from for anything wanting a picture on screen: the output does not open, and the code is the platform's own rather than a guess.

is-stack fails identically on PS5PCEM, so it is a genuine console behaviour rather than an emulator gap - worth stating because orbistoun fixed its own sceKernelIsStack on the strength of the emulator result alone.

A ps4_game title cannot load current-generation libraries

### ERROR: ABIVERSION mismatch. /<sandbox>/common/lib/libSceAgcDriver.sprx
[rtld] ERROR self_load_shared_object:2826: B: res 0 (libSceAgcDriver.sprx)  val 2

val 2 is the library's EI_ABIVERSION; the eboot's is 0. This is not a defect in the build: a real launching homebrew eboot is also EI_ABIVERSION 0, measured directly from its container. A title of this category genuinely cannot be given the current generation's graphics libraries.

The path forward is native process injection. Running ./bin/obscene inject deploys obscene-injector.elf to hijack a running native PS5 retail title, running obSCEne inside payload/ps5-native mode where libSceAgc and native Prospero APIs execute unrestricted. See docs/INJECTOR.md for the complete runbook.

The screen works, and the answer was an alignment

obSCEne running on a retail PS5, drawing its own report

Photographed through a KVM rather than captured on the console, because nothing in this program can screenshot itself: the framebuffer is write-only from where the drawing happens, and a capture path would be a second thing that could be wrong. What is on the glass is the artefact.

obSCEne draws its report on the console's own display:

OBS|display|ready|1920x1080 framebuffer|0x0
OBS|display|presenting|a submitted frame reached the display|0x0

presenting is measured rather than assumed - the frame counter moved, which is a stronger statement than the flip having been accepted.

The defect was the framebuffer's alignment, three steps earlier than anyone was looking:

onion,0x4000,1     0x80290015
garlic,0x4000,1    0x80290015
garlic,0x10000,1   0x0          accepted

0x4000 is not coarse enough for a scanout buffer; 0x10000 is. Memory type does not matter - onion and garlic both refuse at 0x4000.

Why it hid for so long

0x80290015 comes back identically whatever else is right or wrong. Every attempt that varied a display argument got the same number and read it as "still wrong somewhere":

baseline        0x80290015
tiling=0        0x80290015
format=0        0x80290003     <- the argument is read, and the baseline value is fine
aspect=1        0x80290008     <- so is this one
720p            0x80290015
pitch=width*4   0x80290015

Two codes moving is what proves the attribute is parsed and that the baseline passes. That is what made it possible to stop varying the attribute and go and vary the allocation instead - which is where the fault was, in an argument to sceKernelAllocateDirectMemory rather than to anything in libSceVideoOut. (D253)

Both tables came out of one run each, from 085-videobuf, which varies one argument per call inside the probe. Doing it by rebuilding costs about five minutes and a healthy console per guess. (D251)

Codes established for libSceVideoOut

Each of these was produced by a call that differs from a working one in a single known way, so each is measured rather than looked up:

code produced by
0x8029000b any call with an invalid handle - two negative checks pass on it
0x80290015 registering a buffer aligned to 0x4000
0x80290003 registering with the pixel format cleared
0x80290008 registering with a different aspect ratio
0x80290004 the current generation's sceVideoOutRegisterBuffers2 with our arguments
0x80290009 opening an output this process already holds

The last one is here because this project reported it as "the main video output would not open" - a hardware finding, with provenance, produced entirely by its own leaked handle (D250). The output opens fine and always did.

The report exists only while the title does, and only the title could read it

Two separate things, both of which look like a broken retrieval path.

The mount is torn down when the title exits. /download0 appears outside the sandbox at /mnt/sandbox/download/<TITLEID>; listing it during a run shows obscene-boot.txt and obscene-report.txt, and a minute after the run the whole path is No such file or directory. Retrieval happens during a run or through a channel that leaves the sandbox as it goes - the system log (D233), which is the second reason that is written unconditionally rather than chosen between. (D238) In practice you never chase the file: ./bin/obscene report reads the records off that log, and ./bin/obscene deploy captures them across the launch (D269).

The file was created 0600. A title writes it as whatever user the platform runs titles as; the shell server and the file-transfer server are somebody else, and both got Permission denied on a file they could see in a listing. The mode was chosen by reasoning about the writer, and the reader is never the writer. It is 0666 now. (D237)

/download0 is writable from inside a title

OBS|sink|/download0/obscene-report.txt

/data is not - it is reachable by an elfldr payload and not by a sandboxed title. The report sink tries candidates in order and names the one that worked, so this is measured rather than assumed.

The console state a run depends on

Not a finding about the platform, but it invalidates every run if it is wrong, and it cost several hours before it was understood.

pldmgr reads autoload.txt and loads the payload chain. With pldmgr absent, four of six payloads never load - including kstuff-lite, which patches NPDRM for fake licences. The symptom is that every fake-licensed package refuses to launch, and so do retail dumps:

[SceLncService] PrepareProcessLaunchDirCheck()
scePs4AppCategoryGetForTitleId return 3
CheckPrepareProcessLaunchPkgApp() ret = 80a40086
preLaunchCheck: LNC_ISOK::0x80a40086          → CE-105773-3 on screen

Nothing about the package is implicated. Check ps for kstuff.elf before concluding anything about a launch failure.

Bundled modules are looked up by name

/app0/sce_module must contain modules the system is looking for. A well-formed module under a name a title never bundles produced the same complaint as an empty directory:

# === Lack of a .prx file in /app0/sce_module is detected!!! ===

with no rtld line naming it, because nothing looked at it. The names that work are the ones a real package uses - libc.prx, libSceFios2.prx. A real title also imports from libc, so a stub under that name is only safe while nothing in the build imports from it; sce-module-guard enforces that against the eboot's own manifest.