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 --help

It 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.txt

It prints only where implementations disagree.

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.