obscene-tool
One binary, in tool/. It builds the module, reads it
back, and interprets the reports it produces.
cd tool && cargo build --release
./tool/target/release/obscene-tool --helpIt ships as a single binary with no runtime. Building it needs cargo.
Building a module
| Command | What it does |
|---|---|
mkmodule <file> |
Applies the header fixups the hardware loader requires |
mkmodule <file> --dynlib |
Also builds the vendor dynamic segment |
The linker produces an ordinary ELF that a loader rejects.
mkmodule sets EI_OSABI to FreeBSD and
e_type to the vendor dynamic-executable type. It is
idempotent, so it is safe in a build rule that runs
repeatedly, and it refuses a file it does not
understand rather than rewriting a field to make it look
right.
--dynlib rebuilds the string and symbol tables with
every import re-encoded to NID form, appends them as a
PT_SCE_DYNLIBDATA segment, and rewrites the dynamic table
with DT_SCE_* tags. Nothing in the file moves: the linker's
unused GNU_STACK header is repurposed and the dynamic table
is overwritten in place.
Reading a module
| Command | What it does |
|---|---|
imports <file> |
Every undefined dynamic symbol - what the loader must resolve |
inspect <file> |
Segments and the dynamic table, with each tag's role where known |
vaddrs <file> --source <label> |
Every exported symbol as name vaddr,
names resolved from the mined corpus |
imports exits non-zero on an empty
list. A module that imports nothing had its platform calls
resolved at link time or optimised away, and tests nothing.
inspect classifies each value of an unassigned vendor
tag by shape: offsets land inside the vendor segment, sizes do not,
entry sizes are exactly 0x18, and
DT_SCE_PLTREL holds 7. That is
evidence for an assignment, never the assignment
itself.
vaddrs reads a module's exports and prints one
name vaddr line each. A loader adds the vaddr to the
module's load base to reach the function, so a payload's
base + offset arithmetic can land on a reimplementation of
the named export. The vaddr is measured from the file;
the name is looked up in the mined corpus
(data/mined-names.txt) by the NID the export table stores.
An export whose NID the corpus does not know keeps its encoded NID
(hash#lib#mod), so no vaddr is dropped and nothing is
invented. --source labels the provenance in the file's
header. Orbistoun's libkernel-vaddrs.txt is generated by
it:
obscene-tool vaddrs libkernel_sys.sprx --source "12.40 libkernel_sys.sprx" \
> ../orbistoun/crates/orbistoun-firmware/data/libkernel-vaddrs.txt
SELFish's libkernel_vaddrs example reads the same table
without the corpus, printing NID and vaddr only, and confirms the vaddr
measurement independently of any name.
Symbol hashing
| Command | What it does |
|---|---|
nid <name> |
The NID and encoded symbol name for a function |
decode <encoded> |
The value behind an encoded NID |
selftest |
Checks the whole chain against a published pair |
$ obscene-tool nid sceKernelWrite
nid 0xe304b37bdd8184b2
encoded 4wSze92BhLI
symbol 4wSze92BhLI#A#A
Run selftest when resolution looks wrong. The suffix,
the byte order, the alphabet and the bit packing are each plausible when
wrong, and each produces eleven ordinary-looking characters that resolve
to nothing. One published pair constrains all four. decode
inverts the encoding, not the hash: a NID cannot be turned back into a
name.
Reading reports
| Command | What it does |
|---|---|
report [--seconds N] [--into F] [--name C] |
Capture obscene's records off the console system log into a file |
verify <file> |
Checks a report against the format contract |
diff <before> <after> |
What changed; exits 1 on regression |
pretty [file] |
Colour, grouped by section. Reads stdin if no file |
report is the source of a hardware report. A packaged
run's report file is sealed inside the title's sandbox, so the readable
channel is the system log. report listens to it, keeps only
the OBS| records, writes them to
reports/hardware/console-klog.txt, and prints the tally. It
is reached as ./bin/obscene report, and
./bin/obscene deploy calls it across a launch (D269). Its
output is what verify, diff and
pretty read.
verify answers whether the file is well-formed, not
whether the platform is good. An all-red report is well-formed, and on a
host build it is expected.
diff reports a regression when a check
got worse, not when it is failing. skip ranks below
fail, and a check that vanishes counts as a regression, so
deleting a check never reads as progress.
pretty colours only when writing to a terminal; piped
output is plain text.
Loader probes
| Command | What it does |
|---|---|
probe <tag> <out> [--value V] |
A minimal module carrying one candidate dynamic tag |
Run the result under a loader and read its output. A loader reports
the tags it does not recognise, so silence about the
candidate, with the controls still reported, means it knows that tag.
DT_SCE_FINGERPRINT is identified this way.
One tag per probe. A probe that stops early and a probe that finds nothing produce the same silence; the controls after the candidate tell them apart, and several candidates in one table defeat that.
--value chases a named failure rather than silence: if
the loader's message changes, that identifies the tag rather than merely
showing it is known.
Gates
Everything verify.sh runs. Each has a
--check form that fails rather than rewriting, so a
committed generated file cannot drift from its source unnoticed.
| Command | What it does |
|---|---|
guards --root . |
every check guards the platform symbols it calls (D058) |
caps --root . |
every prerequisite capability is granted before some check needs it |
rows --root . --report <file> |
the table parser sees every check a report says ran |
claims --root . |
prose anchored to a source token still describes what the source does (D170) |
counts --root . --check |
the numbers in the documentation equal the numbers in the code |
doccheck --root . |
nothing in prose names a file, rule, subcommand or decision that does not exist |
compat --check <name=report>… |
the per-loader results table matches the reports it was built from |
protocol --root . |
the captured exchanges match PROTOCOL.md |
protocol --root . --selftest |
a set of deliberate corruptions, every one of which must be rejected |
corpus --root . |
the mined corpus has been shown the current emulator checkouts |
decisions --root . |
every entry under docs/decisions/ has exactly one row
in the index, and every row a real file |
Generators
Each writes a committed artefact from a data file. The data file decides and the generator assembles, so a judgement is written once, in the data, and never retyped.
| Command | Writes | From |
|---|---|---|
surface |
include/obscene/surface.h |
data/surface.txt |
census corpus --platform |
include/obscene/corpus.h |
data/mined-names.txt |
census nids --platform |
include/obscene/nids.h |
data/unnamed-nids.txt |
font |
src/probe/font.c |
data/font.txt - glyph art, so a glyph is corrected in
place |
shaders |
include/obscene/gpu_shaders.gen.h |
src/probe/shaders/*.comp, needs
glslangValidator |
mine |
both corpus files | the emulator checkouts and the firmware trees |
shaders needs a shader compiler. gpusurface
cross-checks against LLVM's IntrinsicsAMDGPU.td and says so
in its output when it cannot.
Analysis
| Command | What it does |
|---|---|
gap --root . |
what the emulators implement that obSCEne does not reach |
gap --root . --current |
the same, counting only current-generation emulators |
unresolved <log>… |
a loader's unresolved-import log, translated to names |
consensus <name=report>… |
several implementations compared; only the disagreements |
consensus
A substitute oracle where no hardware is available.
obscene-tool consensus host=reports/host.txt shadps4=reports/full-sweep.txtIt prints only where implementations disagree.
- An OUTLIER is one implementation differing from a unanimous rest. It is the actionable shape: it supports "you are the only one that fails this" without anyone accepting this project's expectation.
- A SPLIT means the behaviour is unsettled between implementations. It is a question for hardware rather than a bug report.
- A skip is no opinion and is counted separately. An implementation that lacks the function says nothing about how it should behave.
- It names implementations and never counts them. These projects read each other's source, so agreement is not independent witness.
The host build is a real implementation of the POSIX and C library surface, so a disagreement against it is a disagreement with something known to work.
crack
Recovers names from NIDs by hashing candidates and matching.
obscene-tool crack --nids targets.txt --words candidates.txt --known data/nid-corpus.txt--nids accepts bare NIDs or the
<nid>#lib#mod form a symbol table carries, so a
module's imports can be fed in directly.
The header reports the suffix used, how many candidates were tried, and how many known pairs the candidate list reproduced. A list that cannot regenerate names already known is not evidence about names that are not. A match is proof; a miss says only that the candidate list did not contain the name.
decode is different: it reverses the encoding and leaves
the hash.
Scripts
Scripts do orchestration only: start a VM, launch a Windows emulator, wait, and collect what came back. Binary formats, checkers and generators are Rust (D026). The module builds inside WSL, because a Windows mount cannot carry the execute bit and a binary built into the tree refuses to run (D012). The emulators are Windows applications, launched from Git Bash.
Building (in WSL)
verify.sh |
everything that has to pass before a change is done |
build-all.sh |
every target, plus a re-derivation of the tag assignment from what it built |
emu-build.sh |
tool and module, for the change-rebuild-read-the-log loop |
sweep-build.sh |
the module, excluding every check in the sweep file |
lint.sh |
the tool's lints, in full |
format.sh |
clang-format, writing by redirect - -i renames, and a
mounted Windows share refuses that |
verify.sh and build-all.sh build without
exclusions, so sweep-build.sh is the last build before
anything runs the module.
Running (on the host)
run-emulator.sh |
one run: fetch the module from WSL, run it, extract the report |
sweep-emulators.sh |
every loader on the same module: wait for the report to end, screenshot the window, keep the log |
bulk-sweep.sh |
the blind prober across as many runs as it takes, resuming one past each call that did not return |
harvest-nids.sh |
grows the NID table from emulator logs; merges, never replaces |
An exclusion sweep reads the report for a try with no
matching res, excludes that check, and runs again. A
timeout doubles the budget and retries rather than excluding, because
one round cannot tell a hang from a check that was still going
(D144).
Inspecting
manifest.sh |
the symbol-to-library manifest, derived from the check tables |
imports.sh |
what a build imports, and which of those the manifest cannot place |
shapes.sh |
the module and payload shapes side by side |
nids.sh |
the NID for each name given; the answer to a loader reporting
resolved <nid> as Unknown |
census-summary.sh |
how much of each library's known surface a census run found |
Hardware round-trip
These cross the WSL/Windows split or talk to a console, so a portable
Makefile rule cannot carry them. Each is one bin/obscene
verb; the scripts behind them are implementation and are not run
directly (D269, scripts/README.md).
| verb | what it does |
|---|---|
./bin/obscene deploy |
build, install, launch, capture the report - the whole package round-trip |
./bin/obscene native --deploy |
build the current-generation native title directory and push it to a scan root |
./bin/obscene sweep |
the payload, package and native eboot legs in series, two report files per leg |
./bin/obscene restart-ui |
restart SceShellUI to recover a UI softlock without
rebooting |
./bin/obscene payload |
build the plain-ELF payload, run it via elfldr, capture the raw system log |
./bin/obscene report |
capture obscene's records off the console system log into a file
(obscene-tool report) |
./bin/obscene recover |
read-only: boot log, report file, and crash dumps the console kept |
./bin/obscene hwsweep |
iterate against hardware, excluding each call that does not return |
./bin/obscene minbuild |
the minimal diagnostic package |
./bin/obscene digcheck |
whether a built package still agrees with its own digests |
./bin/obscene prep |
send klogsrv/shsrv payloads to bring the readable services up |
Path conversion under Git Bash
Git Bash rewrites anything that looks like a Unix path before a
Windows program sees it, and wsl.exe is a Windows program.
A wsl.exe -d Ubuntu -- bash -lc '...' call carrying
/... arguments reaches the distribution with them rewritten
to C:/Program Files/Git/.... Every wsl.exe
call therefore goes through a one-line wrapper that sets
MSYS_NO_PATHCONV=1.