D193 - obSCEne writes a dynamic table no retail current-generation module uses, and six real dumps say so

Building prosper produced self_dump, a module reader that runs standalone. Pointed at obSCEne it parses the file correctly - raw-ELF fallback engaged, type=0xfe10 ET_SCE_DYNEXEC (main exe) named independently, all six program headers, DYNAMIC mapped, 1,428 tags read, 35,519 symbols found. Then:

[NEEDED MODULES] (0)
[IMPORT LIBRARIES] (0)
[SYMBOLS] total=35519  imports=0  exports=0

It finds the symbols and none of the imports. Pointed at the six retail titles in the sibling project's library, the same tool finds 27 to 62 import libraries each.

Two conventions, and this module is on the wrong one

module DT_STRTAB standard DT_SCE_STRTAB vendor NEEDED_MODULE 0x45 import libs found
six retail titles 1 0 26-59 27-62
obSCEne GEN=5 0 1 0 0
obSCEne GEN=4 0 1 0 0

A retail current-generation module uses the standard ELF tags for the standard tables - DT_STRTAB (0x05), DT_SYMTAB (0x06), DT_SYMENT (0x0b) - and spends vendor tags only on vendor concepts, in a high range: DT_SCE_SYMTABSZ 0x3f, DT_SCE_ORIG_FILENAME 0x41, DT_SCE_NEEDED_MODULE 0x45, DT_SCE_IMPORT_LIB 0x49.

obSCEne spends vendor tags on everything - DT_SCE_STRTAB 0x35, DT_SCE_SYMTAB 0x39 - and names its needed modules with 0x0f and its import libraries with 0x15. Of nineteen vendor tags it writes, retail modules use five.

Why five loaders never noticed

They all accept it. shadPS4, fpPS4 and Kyty are previous-generation emulators where this is the convention; PS5PCEM and orbistoun accept it too. So every existing measurement stands - and none of them is evidence about the console, because not one of them is the console.

This is the failure this project is built to avoid, found one layer below where it was looking: a thing that works everywhere it has been tried, and may not work on the only platform that matters. obscene-tool derive re-derives the tag assignment from a finished module and passes, which proves the module is self-consistent - it says nothing about whether the convention is right.

What it does not establish

That the previous-generation convention is invalid on hardware. A console loader may accept both; the previous generation's software has to keep working somehow. What is established is narrower and still serious: no retail current-generation module looks like ours, so we are outside the set the platform is known to load, and we would find out on hardware.

Fixed, in two halves, and the second half was the one that mattered

The numbering was the easy half. Tags::of(table) holds both assignments and derive checks either. Done first, and on its own it made things worse: prosper began reporting 352 import libraries where it had reported none, which looked like the fix landing and was not. The counts come from tag records prosper could now see; the strings they pointed at still resolved against the wrong segment. A plausible number from a wrong address - the same shape as every constant D008 forbids inventing.

The layout was the real half. The two go together: a vendor tag holds an offset because there is a PT_SCE_DYNLIBDATA to be an offset into, and a standard tag holds a virtual address because the table is in the mapped image. So build() now computes a table_base - 0 under legacy, the end of the last PT_LOAD rounded to the 0x4000 page under current - and emits the header as a real PT_LOAD rather than a vendor segment. derive finds the segment through the string table's address instead of by segment type.

GEN=4 → legacy GEN=5 → current
prosper's verdict not a PS5 module PS5 layout
prosper's imports 0 35,518

Neither convention is the better one, so TABLE follows GEN

The sweep splits on it. current breaks shadPS4 and fpPS4 outright - unsupported dynamic tag 0x02, 0x03, 0x07, 0x61000043, 0x61000047 - where under legacy shadPS4 runs 36,575 records. legacy is invisible to prosper.

So this is not a quality dial and --table is not a flag to pick by preference. The Makefile derives it: TABLE ?= $(if $(filter 5,$(GEN)),current,legacy). Asking for a generation now gets you that generation's module shape, which is what GEN always claimed to mean.

Status: assumed - six retail dumps agree and a fourth reader now accepts our module; no console has been asked. Still ranked first on docs/HARDWARE-PROBE.md, because agreeing with an open-source reader is not the same as loading on hardware.

Writers are told the convention; readers detect it

The first version of this shipped the switch to mkmodule and stopped, and verify.sh failed on two gates that had nothing to do with writing:

=== derive (module)
not a vendor-format module: no PT_SCE_DYNLIBDATA segment ... nothing here to derive from.
--- target object ---
imports      0   no imports: this module asks the platform for nothing

Both about the module the same run had just built and checked nine relations against. derive and imports are handed a file, not a build flag, so a default was the wrong shape for them entirely - and the default silently made every current-generation module unreadable to this project's own tools. imports 0 is the same false zero as the sweep's Kyty row: it reads as "this module asks for nothing", which is a damning finding, rather than "this reader cannot see the table".

So dynlib::detect reads the convention off the module and both callers route through it. mkmodule still takes --table, because a writer genuinely does know.

It detects on the vendor tags, not on DT_STRTAB, and that is load-bearing: an ordinary ELF payload has a DT_STRTAB too, so keying on it would have made derive accept a payload and quietly broken the gate that exists to stop exactly that. The high-range vendor tags are the part a module can only have on purpose.