Producing something the hardware will load
For most workflows, the pipeline runs the complete assembly in a single invocation:
selfish --input <file> --target <orbis|neo|prospero|trinity> --format <elf|prx|eboot|title|pkg> --output <path>
--format elf: stamps the platform identity a loader checks before anything else.--format prx: the same stamping, for a shared library.--format eboot: the executable, stamped and wrapped in a signed-executable container.--format title: container plusparam.json,icon0.png, keystone, laid out as<output>/<TITLE_ID>/.--format pkg: that, plusparam.sfoand a PFS image, assembled into a.pkg.
--target carries the generation and is always required.
Nothing passes a generation number, and nothing picks one for you.
Underneath, between a compiler and the hardware there are four steps,
and then a fork. The first two are pipeline formats - elf
and prx stop after stamping, eboot after
wrapping - and the last two are commands of their own,
image and pack, so a package can be built a
layer at a time. That matters, because when a file is rejected the
useful question is which step produced the wrong bytes.
compiler output
| stamp the platform identity no linker sets --format elf, prx
v
module ------------------------------------> a payload, and it can stop here
| wrap the signed-executable container --format eboot
v
eboot.bin
| \
| image the filesystem \ --format title lay out a Prospero title
v the title mounts v
image file <TITLE_ID>/
| pack the package param.json + sce_sys/
v around it
package.pkg
The executable never changes across any of this. What changes is the wrapper, and each wrapper decides something different about where the code ends up running: a payload runs under a homebrew loader, a package installs through the previous generation's compatibility path, a title directory is a current-generation (Prospero) entry on the home screen.
Every transcript below is real output, from obSCEne's probe
executable (5.6 MB) and its libc module (166 KB), captured
on Windows - hence the separators in step 4b, where one temporary path
is also elided.
1. Stamp the identity
$ selfish --input payload.elf --target prospero --format elf --output module.elf
Prospero -> Elf (current generation)
elf 6 program header(s), entry 0x531270
stamp EI_ABIVERSION 0x0 -> 0x2
stamp e_type 0xfe00 -> 0xfe10
stamp p_flags (R+X -> X) 0x5 -> 0x1
wrote module.elf (5598248 bytes)
Writes exactly --output, and says which fields it
changed and what they were. No linker sets these, because no linker
knows about either console - so a freshly compiled ELF is not yet a
module, and nothing about it looks wrong until a loader refuses it.
A shared library is the same step with a different
e_type, and that is --format prx:
$ selfish --input libc.elf --target prospero --format prx --output libc.prx
Prospero -> Prx (current generation)
elf 5 program header(s), entry 0x0
stamp EI_ABIVERSION 0x0 -> 0x2
wrote libc.prx (166320 bytes)
One field this time. The input already carried e_type
0xfe18, the shared-library value, so only the ABI version
was left to write - stamping reports what it changed, not what it
checked.
A stamped file is already a payload. If a homebrew loader is going to map and run it, the remaining steps are not needed at all.
2. Wrap it in a container
$ selfish --input payload.elf --target orbis --format eboot --output eboot-gen4.bin
Orbis -> Eboot (previous generation)
stamp 1 field(s)
wrap 5469936 bytes from a 5598248 byte payload
wrote eboot-gen4.bin (5469936 bytes)
$ selfish --input payload.elf --target prospero --format eboot --sdk ps5-native --output eboot-gen5.bin
Prospero -> Eboot (current generation)
stamp 3 field(s)
tier App
sdk 0x08050001/0x02000009
wrap 5469936 bytes from a 5598248 byte payload
wrote eboot-gen5.bin (5469936 bytes)
--format eboot stamps on the way, so it takes the
compiler's output directly rather than a module stamped beforehand.
The generation changes the four bytes at the front:
$ head -c4 eboot-gen4.bin | od -An -tx1
4f 15 3d 1d
$ head -c4 eboot-gen5.bin | od -An -tx1
54 14 f5 ee
Do not read those as simply "old" and "new" - the
labelling is subtler than the flag makes it look, and the glossary has it: a
real current-generation app eboot carries the first, and a
title's bundled modules carry the second. What makes a title
native is param.json and native registration, not the
magic.
There is no default --target. Both
generations are accepted by current hardware, each proven on its own
delivery route: a package with the previous generation's magic
installs, mounts, loads and executes (worklog 040), and a native
title with the current magic installs, launches and ran 292 checks
to completion (obscene sweep 20260909-184538). Neither has been shown
accepted on the other's route, so a default would be right for one route
and wrong for the other.
The retired wrap verb did default, to
orbis, because that served the package path. Its
justification once included a population - "every container inside the
real packages sampled carries the previous generation's magic" - and
that was withdrawn: those containers were of homebrew
lineage, and every genuine container measured carries the
current magic, 23 of 23. (D097, D100)
--sdk takes an alias or a literal
version. ps5-native resolved to
0x08050001/0x02000009 above; 2.000.009 and
ps4-compat are equally valid. The dictionary is
data/sdk-versions.toml, outside the source, because a
version table is data rather than code, and it is validated against the
generation - a current-generation version on a previous-generation
container is refused rather than written. Getting this wrong is not
subtle: the loader refuses the process with an SDK-version error before
any of your code runs. --sdk-table reads a different
dictionary.
--privilege takes app,
sysmodule, system or
root, and defaults to app. Asking for
app produces the same bytes as not asking - checked with
cmp, not assumed:
$ selfish --input payload.elf --target prospero --format eboot --privilege root --sdk ps5-native --output eboot-root.bin
Prospero -> Eboot (current generation)
stamp 3 field(s)
tier Root
sdk 0x08050001/0x02000009
wrap 5469936 bytes from a 5598248 byte payload
wrote eboot-root.bin (5469936 bytes)
Both options apply to the formats that build a container -
eboot, title and pkg - and are
refused with elf and prx, which build none.
The refusal names the formats they do apply to, rather than accepting a
tier there is no container to put in.
The container declares itself fake in the field the format provides for exactly that, and its signature area is zero. No vendor signature is forged and none could be.
3. Build the filesystem image
app here holds the previous-generation
eboot.bin from step 2.
$ selfish image --root app --out title.pfs.img --content-id UP0000-OBSC00001_00-0000000000000000
sce_sys/keystone: generated from the passcode
title.pfs.img: 6553600 bytes
keyed to UP0000-OBSC00001_00-0000000000000000 - a package carrying this must declare the same id
The files become a plain filesystem, wrapped in a PFSC
container, carried as the single file of a signed and encrypted outer
filesystem.
The content id is not optional, and getting it wrong is the quiet failure. The image is encrypted under a key derived from the content id and the passcode, so an image built under one id cannot be opened by a package built under another - and nothing about the resulting file looks wrong until a console tries to mount it. That last line of output exists to make the mismatch hard to reach.
--passcode defaults to the fake one, which is what you
want unless you have a reason.
4. Pack it
$ selfish pack --image title.pfs.img --out title.pkg --content-id UP0000-OBSC00001_00-0000000000000000 --title-id OBSC00001 --title Demo
param.sfo: generated for OBSC00001 ("Demo", version 01.00)
icon0.png: none given, using selfish own - supply --entry 0x1200=FILE to replace
playgo-manifest.xml: generated the default manifest
title.pkg: 7077888 bytes, 14 entries, image at 0x80000
1 gap(s) left blank, because nothing established says what goes in them:
entry 0x80 at 0x60, 32 bytes - the header digest, filled by finalize_digests once the header exists
Nothing was supplied, and nothing was guessed. Every
entry is computed or generated: both digest tables, the block digests,
both licences, both key blobs, param.sfo, the playgo
manifest, the entry name table and the playgo chunk table.
The last two used to be demanded, and this section used to show
pack stopping without them. Both turned out to be facts the
builder was already holding: the name table is a function of which
entries are present, and the chunk table is fixed for a single-chunk
title apart from two sizes it can read out of the image. (D099)
What pack still does is list every region it left blank
rather than hiding it, and say what it knows about each.
--entry ID=FILE overrides any entry, which is for
rebuilding a package to match existing material - and for the one entry
that is genuinely yours, the icon:
selfish pack --image title.pfs.img --out title.pkg \
--content-id UP0000-OBSC00001_00-0000000000000000 \
--entry 0x1200=icon0.pngGiven an image whose inner filesystem is too small to mount,
pack warns before anything else. The threshold, the error
string and the workaround that did not work were all
established against hardware. (D071)
--dir runs the whole chain - step 3 and step 4 together
- which is the usual way in:
selfish pack --dir app --out title.pkg --content-id UP0000-OBSC00001_00-0000000000000000--image takes an image you already built, for when you
are iterating on the package around a filesystem that has not
changed.
A package's licence carries a signature under the published debug keyset, whose entire purpose is the fake licences a non-retail package holds. Signing with it asserts "this is a debug licence", which is true, and the licence says so in its own type field. The line is not "never compute a signature" - it is never claim to be the vendor.
A package installs as a previous-generation title, which is why homebrew shipped this way runs in the compatibility sandbox. That is what the install path is, not a limitation of this repository. obscene#D255 is the finding.
4b. Or lay out a title directory instead
The current generation's (Prospero) own format is not a package at
all, and this is the other half of the fork. It is a directory described
by param.json, registered by a call that needs kernel
privileges:
$ selfish --input payload.elf --target prospero --format title --title-id OBSC00001 --title Demo --output native
Prospero -> Title (current generation)
stamp 3 field(s)
wrap 5469936 bytes from a 5598248 byte payload
wrote <scratch>\root\eboot.bin (5469936 bytes)
1 file(s) copied from <scratch>\root
privilege: App
native\OBSC00001\sce_sys\param.json
native\OBSC00001\sce_sys\icon0.png (generated)
native\OBSC00001\sce_sys\keystone (generated)
native\OBSC00001\sce_sys\pfs-version.dat (generated)
native\OBSC00001\sce_sys\nptitle.dat (generated)
title native\OBSC00001
<scratch> is a temporary directory the eboot is
staged in and removed before the command returns.
The eboot is built from --input on the way - stamped,
wrapped, staged - and the title is laid out around it. Four of the five
metadata files are generated because they are derivable. Nothing is
installed: registration is
sceAppInstUtilAppInstallTitleDir("OBSC00001", "/user/app/", 0)
after copying the directory to /user/app/, and that call
needs kernel privileges, so it runs from a payload on the target rather
than from here.
What the title carries is set on the same
invocation. --icon FILE puts a 512x512 PNG on the
tile (flattened to no-alpha RGB; a non-512 icon is refused rather than
resized, and without one the default mark says selfish built this and
nobody supplied artwork); --content-id,
--title-version and --deeplink land in
param.json:
$ selfish --input payload.elf --target prospero --format title --title-id OBSC00001 --title Demo \
--content-id UP0000-OBSC00001_00-ABCDEF0123456789 --title-version 02.50 \
--deeplink http://127.0.0.1:8080/ --icon logo.png --output native
Prospero -> Title (current generation)
stamp 3 field(s)
wrote <scratch> (5470368 bytes)
1 file(s) copied from <scratch>
privilege: App
version: 02.50
contentId: UP0000-OBSC00001_00-ABCDEF0123456789
deeplinkUri: http://127.0.0.1:8080/
native\OBSC00001\sce_sys\param.json
native\OBSC00001\sce_sys\icon0.png (from logo.png)
...
--deeplink makes a launcher tile - an entry that opens a
URI instead of carrying its own executable, for when the code is already
running as a payload. These four are title metadata, so they apply to
title and pkg only; passing one to
elf, prx or eboot is refused
rather than ignored, naming the formats that take it.
--category defaults to system-app,
--privilege to app, and
--title-version to 01.00.
--content-id defaults to an id built from the title id,
announced in the output - for pkg it is not cosmetic,
because the filesystem image is keyed by it.
Choosing
--category and --privilege
Execution on PlayStation systems is governed by two orthogonal axes:
Privilege Tier (
paid/ Authority ID in the SELF header). The values are whatselfishwrites, read back withaudit; what each tier is granted on hardware is not measured here.app(0x3100000000000002, the format's default): Standard sandboxed execution.sysmodule(0x3100000000000002): currently identical toapp, byte for byte - nothing distinguishes the two when the container is built, so choosing it changes nothing.system(0x3800000000000001): Unsandboxed system service privileges.root(0x8000000000000001): Full kernel root authority (jailbreak syscalls, arbitrary/dev/*nodes).
--privilege systemalso sets the title's category to0x20000, a pairing carried over rather than measured.An earlier copy of this list gave
appas0x3800000000000000andsysmoduleas0x3800000000000001. Nothing writes either.Application Category (
applicationCategoryTypeinparam.json,CATEGORYinPARAM.SFO,--categoryon the pipeline): decides Direct Memory allocation, HDMI scanout ownership and process lifecycle.The values live in one place: the collection's four-axes table, and
selfish_title::categoryfor code. This table used to be repeated in five places and two of the copies had already drifted, which is the argument against a sixth.The one consequence worth stating here, because it is what people hit: a GUI or emulator that needs the screen must not be a system app. Under
system-appthe kernel grants zero direct memory andsceVideoOutOpenfails0x80290001, so the app starts and then cannot draw.big-appis the category with the display, and it brings its own requirements -/app0/sce_module/libc.prxpresent, andpltauth-patchactive under kstuff.
Prosperous can push the finished directory to a scan root, where an auto-mounter registers it without the privileged call at all.
When you do not know what an entry means
selfish derive package-a.pkg package-b.pkg package-c.pkgRe-derives what a package's entries mean from packages you supply. This is the tool for turning "there is an entry here nobody has named" into a row with a provenance, rather than a guess written into a table.
Checking what came out
Every format written here is also read here, which is what makes a
round trip a test. reading.md is that half:
container and title describe what you just
built, and audit checks a real file against the format
table and reports which rows it settles.