D559 - The Agc argument classes, and how to get past the arity gate without guessing
Status: measured Date: 2026-09-04
The situation
libSceAgc became reachable on hardware. The question stopped being can we measure Agc and became what exactly should a probe call, and which calls are safe.
Two things stood in the way, and neither was the hardware.
The gate
obSCEne's D008 forbids calling a function whose arity is uncertain - a wrong one corrupts the stack and surfaces far from the call. Its standing test for satisfying that, D107, is two independent open reimplementations agreeing, which is how the Gnm command builders were cleared.
That standard cannot be met for Agc. shadPS4 and GPCS4 are PS4/Gnm projects; there is no mature open Agc reimplementation for a second source to agree with. Read literally, the gate blocks the entire ask, permanently, for the one library that matters most.
The way past it, which is not a weakening
In System V AMD64 the first six integer arguments are passed in registers, and a callee ignores register arguments it does not take. A call that sets all six to individually safe values is therefore safe for any arity up to six, whichever it turns out to be - the stack is never involved, and the stack is what D008 exists to protect.
So the risk was never really arity. It is value: a pointer parameter handed a non-pointer, or a size parameter handed a huge number. That is a different problem, and unlike arity it can be bounded by measurement rather than by citation.
What was measured
A run with a 4KB region planted at *arg0 of
sceAgcCreateShader takes PPSA02664 past the wall -
197 imports to 215, one shader created to 33 - and into
the command-buffer layer. Reading the arg0 the guest passes
to each of the twenty Agc functions it then calls sorts them into four
classes, and the three distinct pointer values are the finding:
functions sharing a value are taking the same object.
| Class | arg0 |
Functions | Probeable standalone? |
|---|---|---|---|
| B writer struct on the caller's stack | 0x6000007fbe38 |
5 | Yes - construct it |
| A a Dcb object in the guest's own heap | 0x740002447868 |
9 | Yes - corrected below |
| C handle from an earlier call | 0x7fff0001 / 0x140081c3bf21 |
7 | No |
| D static descriptor in the guest's image | 0x4000019c9620 |
1 | Yes, zeroed block |
Class B is the entry point. The guest's stack at
that address holds
{begin = 0x6000007fbe70, end = 0x6000007fc270, begin, end}
- a 0x400-byte command buffer with a begin/end pair 0x38 in front of it.
A probe can build that from nothing, which makes five functions -
sceAgcCbNop, sceAgcCbReleaseMem,
sceAgcDcbDmaData, sceAgcDcbWaitRegMem and one
unnamed - safe to call today, under exactly the gnm.c
pattern.
Class C is the one worth naming loudly. Four of
those seven receive 0x7fff0001 in arg0 -
orbistoun's own placeholder. The guest is feeding them
the return value of a call nothing implements. A probe that fabricated a
handle for them would be manufacturing precisely the value risk the
safety argument above does not cover, so they are excluded
until their producer is identified. They come free once A and B are
answered.
The decision
Ask for Class B first, sceAgcCbNop as the control, under
the six-safe-registers rule; ask for the Class A constructor to be
identified rather than for Class A to be called; refuse Class C
for now and say why. Written up in docs/HANDOVER-OBSCENE.md
with the counts, the pointer values and what each unblocks.
Correction, same day: Class A was never library-owned
0x7400_0000_0000 is orbistoun's own fixed-base
heap (docs/ADDRESS_MAP.md), so
0x740002447868 is memory the guest
allocated, not something libSceAgc handed back. This entry
called it "a library-owned Dcb handle" and asked hardware to find the
constructor that issues it. There is probably no such constructor.
What there is instead is an initialiser taking a
caller-allocated block, and one candidate stands out:
sceAgcDcbResetQueue is called on that exact
pointer, twice, before every other use of it. "Reset queue" on
a fresh block is what an initialiser looks like.
That moves all nine functions from Class A to Class
B - a caller-owned buffer is the shape obSCEne already probes
successfully four times over (D565). The ask stops being "find a
constructor" and becomes "call sceAgcDcbResetQueue on a
zeroed block and dump what it writes", which needs no new technique.
The mistake was reading an address without checking the
map. 0x740002447868 looked like a handle because
it was large and opaque; one line of ADDRESS_MAP.md says it
is the heap. The map is gated against the source precisely so it cannot
go stale (D513), and it was right there.
What this does not establish
That any of these arities is known - the argument is that it does not need to be, which is a different claim and a weaker one. A function taking more than six integer arguments, or a variadic one, is still outside the rule; nothing here shows that none of these is either, only that the common case is covered.
Nor that the class boundaries are real rather than incidental. Two functions sharing a pointer value in one run of one title is strong evidence they take the same object and is not proof. A second title calling the same functions with a different sharing pattern would refute it, and no second title in the corpus reaches this code.
Nor that Class B's struct is
{begin, end}. That is the obvious reading of two
pointers 0x400 apart with the buffer immediately behind them, and the
repeat suggests a cursor that has not moved yet - but nothing observed
writes through it, because nothing here implements these functions.