D326 - the guard carries its own setjmp and imports its signal primitives, so a native title is guarded

assumed - 2026-09-08

The fault guard (D325) worked on the package - it caught the futex crash and the suite ran on. On the native eboot it did not: the guard record read OBS|guard|off|no setjmp/longjmp export, so the one late uncaught crash - 166-agc/dcb-reset-queue (sceAgcDcbResetQueue, SIGBUS), in the last section of the run - took every record behind OBS|end with it. The eboot reached 1441 records and stopped one try short of finishing. This records why the guard could not arm there and what makes it arm now.

Why the guard could not arm on a native title

D325 resolved every guard primitive - sigsetjmp, siglongjmp, _sigaction, scePthreadSelf - by name through obs_module_symbol, i.e. the loader's sceKernelDlsym. That works on the package's loader. It does not on a native eboot, and the reason is specific: a native title's sceKernelDlsym resolves only the symbols the process already imports. The eboot's own output proves it - sceKernelDebugOutText and sceKernelWrite are in imports.c, the loader binds them, and 1441 records reach klog through them - while sceKernelDlsym returns 0x80020003 (ESRCH) for anything the eboot did not import (060-module/dlsym-resolves-known-symbol fails, and every OBS|module|...|0x0 shows the base never resolved). The guard primitives were dlsym-only, so on the eboot they resolved to nothing: the run's own bitmap said sa=0 spm=0 pex=0, and setjmp/longjmp (which live in libSceLibcInternal, not libkernel as D325 stated) were absent too.

So the guard needs its primitives the way every working call on the eboot has them: bound as imports, or carried in our own code - not fished out of a dlsym that cannot see them.

The fix, in two parts

setjmp/longjmp are our own. fault.c carries obs_local_setjmp/obs_local_longjmp, a freestanding x86-64 SysV save/restore (rbx, rbp, r12-r15, rsp, the return address - the whole of what a non-local jump needs) in a module-level __asm__ block. The guard no longer depends on libc exporting setjmp at all. Two properties matter beyond "it links": it is our own defined symbol, so the call is a link-time PC-relative branch and carries none of the GLOB_DAT/JUMP_SLOT split that leaves a native title's imports at the 0x2 sentinel (D323); and it does not save the signal mask, which is fine because the handler already unblocks the fault signal with sigprocmask before jumping, exactly as it did for the resolved plain-setjmp fallback. This is our own code implementing a published ABI, the same footing as any runtime.c helper (Principle 8), not a borrowed declaration.

The signal primitives are imports. _sigaction, _sigprocmask and scePthreadExit are declared in platform.h and listed in imports.c, so the native loader binds them like sceAgc* - they are real libkernel exports (data/hardware/libkernel-vaddrs.txt: _sigaction 0xd100, _sigprocmask 0xcf70; scePthreadExit in ps5-full.txt). obs_fault_init now prefers the bound import address (obs_fault_pick, callable-checked) and keeps dlsym and the POSIX spellings as fallbacks. The callable check is what keeps this safe where D323 warns: it rejects a weak-unbound 0 and the 0x2 unresolved sentinel and falls through to dlsym, and by the time obs_fault_init runs (the first thing in obs_run_all) the loader has bound the imports every check then calls, so the address is real.

The guard record now carries the resolution bitmap - sa=/spm=/pex= and which setjmp path won - so a run that is unguarded says which primitive was missing rather than only that it was.

What the hardware showed

OBS|guard|on|installed (local setjmp) sa=1 spm=1 pex=1; the crash that used to truncate the run became OBS|res|166-agc/dcb-reset-queue|crash|0xa|the call faulted (SIGBUS) and the run was recovered; OBS|tally|162|10|33|58|1 and OBS|end followed. 1805 records, the eboot leg completing the same way the package leg does. The freestanding longjmp-out-of-handler, which the package had never exercised (it resolved libc's sigsetjmp), works on real hardware.

Supersedes part of D325

D325's account of target resolution - "sigsetjmp, siglongjmp and _sigaction ... resolved through obs_module_open/obs_module_symbol from libkernel" - held only for the package loader. A native title's dlsym cannot see un-imported symbols, and setjmp/longjmp are not libkernel's. The guard's primitives are now imported or carried, not dlsym-resolved; dlsym remains only a fallback. The scope of D325 (faults not hangs, one pad per armed thread) is unchanged.

Also here: the sweep no longer hangs when a leg finishes fast

A guarded eboot now stops on OBS|end in under a minute instead of dying at the window. That hit a latent bug in scripts/sweep.sh: poll_and_stop killed the klog reader and the runner mid-stream, but they were ( cmd | tr ) subshells - kill -9 on the subshell orphaned the tool and the tr, and the orphaned tr held the tee pipe open so the sweep hung before writing the .obs.log, while an orphaned hw logs would keep klogsrv's single reader slot and starve the next leg. Each leg now runs the reader and runner as bare tool processes writing to files, so $! is the tool's own PID and kill -9 reaps it - no pipe on either, nothing to orphan.