diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index d9e7144..36a33c5 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -119,6 +119,22 @@ unknown, what evidence exists, and what the first step would be. Move an item in earlier. The drain does operate on the container — what `sub_8226E458` does to it stays unread. 🟡 What starts the ~15 % of unreached code: still open, not this. +* ✅ **(2026-08-27) RESOLVED — TWO trigger appenders, and built-in 19's carries the + handler routine. [structures/isl-trigger-node](structures/isl-trigger-node.md).** + Last iteration's puzzle (`payload+28` is a 1–12 index, yet the drain spawns + `base + payload+28`) had a simple cause: **built-in 25 is not the only appender**. + Searching for functions writing a double at `+16` AND a word at `+28` of the same + register finds exactly two — `sub_8226A0D8` (built-in 25, slot 28) and + `sub_8226E160`, whose sole caller `sub_82269ED0` is **vtable slot 68 = + built-in 19**. Built-in 19 passes `local[4]`→+0, `local[12]`→+4, `local[24]`→+16 + (double), **`local[32]`→+28**. 🔑 **Those ARE code offsets: 79/79 land on the + instruction stream vs a 27.8 % control**, values 12 164–111 080, 73 distinct. So + **built-in 19 registers a trigger whose handler is a routine**, and the drain's + spawn is that handler. Observed: built-in 25 writes `+24 = 1`, built-in 19 writes + `+24 = 0` — kind-tag shaped, 🟡 but the drain was not shown branching on it. + 🟡 **Still does not explain the unreached code** — 0 of the 79 handler offsets are + unreached run-starts. + ## ✅✅ SOLVED — the mission freeze was a modal sign-in dialog (2026-08-26) `XamShowSigninUI` opens a modal dialog and `xeXamDispatchDialog` blocks the diff --git a/docs/re/structures/isl-trigger-node.md b/docs/re/structures/isl-trigger-node.md index 9bcd523..bda4ffb 100644 --- a/docs/re/structures/isl-trigger-node.md +++ b/docs/re/structures/isl-trigger-node.md @@ -85,13 +85,59 @@ argument. `sub_8226D740` does operate on the trigger container; what `sub_8226E458` does *to* it is still unread, so only the argument is corrected here, not the conclusion. +## ✅ RESOLVED: there are TWO appenders, and only one carries a routine + +The puzzle above — `payload+28` holding a 1–12 index while the drain hands it to +a spawner that wants a code offset — had a simple cause: **built-in 25 is not the +only thing that appends a trigger node.** + +Searching for functions that write a **double at +16** and a **word at +28** of +the same register finds exactly two: + +| builder | how | +|---|---| +| `sub_8226A0D8` — **built-in 25**, vtable slot 28 | fills the payload inline | +| `sub_8226E160` | a free-list recycling variant, called from `sub_82269ED0` | + +`sub_82269ED0` is **vtable slot 68 = built-in 19**, and it calls the builder as + +``` +8226A034 addi r3, r28, 272 ; the trigger container +8226A040 lwz r4, 4(r30) ; local[4] -> node+0 (the unit) +8226A03C lwz r5, 12(r30) ; local[12] -> node+4 +8226A038 lfd f1, 24(r30) ; local[24] -> node+16 (double) +8226A030 lwz r8, 32(r30) ; local[32] -> node+28 +``` + +### ✅ Built-in 19's `local[32]` IS a code offset + +| over all 79 call sites disc-wide | | +|---|---| +| land on the instruction stream | **79 / 79 = 100.0 %** | +| control, random 4-aligned offsets | 27.8 % | +| value range | 12 164 … 111 080, **73 distinct** | + +Large, distinct, and every one resolves — against a control that resolves barely +a quarter of the time. So **built-in 19 registers a trigger whose handler is a +routine**, and the drain's `base + payload+28` spawn is exactly that handler. + +Built-in 25's `payload+28` really is a small index; the two builders simply put +different things in the same slot. Observed alongside it: built-in 25 writes +**`+24 = 1`** and built-in 19 writes **`+24 = 0`**, which is the shape of a kind +tag — 🟡 but the drain was not shown branching on it, so that is an observation, +not a reading. + ## 🟡 Not settled * **What `local[12]` indexes.** Values 1–12, and `SYM1_SLOTS` lists slot 12 for built-in 25 (symbol table 1 — routes, messages, objectives), which is consistent with a small index but was not verified here. -* **What the drain actually spawns.** `[stack+112]` is filled from `payload+28`, - yet the spawner takes a code offset — so some step in that chain is not what I - read. Left open rather than guessed. +* ~~What the drain actually spawns.~~ ✅ Resolved above: built-in 19's nodes carry + a real routine offset at `+28`. +* **Whether `+24` selects between the two node kinds** — 1 from built-in 25, 0 + from built-in 19 — is unshown; the drain reads it into a stack slot that the + spawn path does not visibly use. +* **This still does not explain the unreached code.** All 79 of built-in 19's + handler offsets are already reachable — **0 are unreached run-starts**. * **`sub_8226E458`'s effect** on the container. * **What starts the ~15 % of unreached code** — still open, and this was not it.