diff --git a/findings/control-flow/tail-rng-ledger.md b/findings/control-flow/tail-rng-ledger.md index 65ee0ee..1d67655 100644 --- a/findings/control-flow/tail-rng-ledger.md +++ b/findings/control-flow/tail-rng-ledger.md @@ -33,36 +33,31 @@ Three sentences, then the evidence. ## 1. The instrument, and why it is not an RNG hook -Counting draws by hooking the primitives would have undercounted, and the static work this lane -commissioned says by how much. The image has **four** draw entry points, not three: +Counting draws by hooking the primitives would have undercounted, and the campaign now knows by exactly how +much. This lane's scan of the tempering immediates found **four** draw entry points where every prior lane's +primitive set had three, and twelve functions with inlined draws. Lane J then found fourteen inlined-draw +functions; lane I re-ran the scan at real instruction boundaries and audited it site by site, and its +numbers are the ones to use: **seven entry points** and **eleven game functions with inlined draws over 28 +sites**, with a brute byte scan finding zero orphans (`findings/control-flow/inlined-draws.md`). -| VA | primitive | words per call | -|---|---|---| -| 0x0047d830 | `Mars::RNG::NextFloat(&mt)` — `__thiscall`, no stack args, plain `ret` | **exactly 1** | -| 0x004271c0 | `Mars::RNG::NextInt(&mt, uint* pMax)` — `ret 4`, rejection loop, bound re-read each iteration | **1 or more** | -| 0x008e6dd0 | `Mars::RNG::Chance(RNG*, float p)` — `ret 4`; takes the **object** base and does the `add ecx,4` itself | **0 or 1** — see §6.1 | -| **0x004f7670** | **`Mars::RNG::NextUInt(RNG*)`** — plain `ret`, no stack args, 84 B. **In no previous lane's primitive set**, and called from 0x007b6700 inside `ProcessTurn`'s closure | 1 | +So the honest sequence is: three → four (this lane) → seven (lane I). The fourth, +`Mars::RNG::NextUInt` 0x004f7670, is the one this lane's static work surfaced; the fifth, sixth and seventh +— including `Mars_RNG_GaussianRange` 0x008e6e30, the only **unbounded** entry point at two words per +attempt — are lane I's. Exactly **one** of the eleven inlined-draw functions is reachable from +`StrategyServer::ProcessTurn` (0x007aa240, depth 4) and one more from the tail. -(Lane J's scan reaches 0x004f7670 from the other direction and calls it one of the fourteen *inlined-draw* -functions rather than a primitive. Both readings are of the same object and neither is wrong: it is a small -function whose draw is inlined, so it is a draw site that calls no primitive — which is exactly why a -call-graph sweep for `NextFloat`/`NextInt` cannot see it.) +**None of that changes a single number in this document**, and that is the point worth taking away. The +instrument was never built from the primitive set or from the call graph: it reads the generator's **state** +before and after a boundary and reports the difference. An inlined draw, a draw through an entry point +nobody had named, a draw through a vtable — all of them move `left`, and all of them are counted. That is why method rule 16 (*any RNG accounting built from the call graph alone is a +lower bound*) does not apply to it, and why three revisions of the primitive inventory landed underneath +this measurement without disturbing it. -plus **inlined draws**: this lane's scan for the MT tempering immediates found twelve such functions, two of -them reachable from the turn roots (0x007aa240 under `ProcessTurn`, 0x007a7f30 under the combat resolver). -Lane J's independent scan, landed while this run was in flight, found **fourteen** and turned it into method -rule 16 — *inlined draws are invisible to call-graph sweeps, so any RNG accounting built from the call graph -alone is a lower bound.* Take lane J's number over this one; the two scans agree on the shape and the point -is the same either way. A primitive-counting hook would have missed every one of them silently. - -**This instrument is not built from the call graph**, which is why rule 16 does not apply to it: it reads -the generator's state before and after a boundary and reports the difference. An inlined draw, a draw in a -function nobody has named, a draw through a vtable — all of them move `left`, and all of them are counted. - -So the instrument reads **state**, not calls. `Mars::RNG` is +The state it reads: `Mars::RNG` is `{void* vptr; uint32 mt[624]; uint32* next; int32 left}`, `sizeof 0x9cc` — `next` at +0x9c4 is a **pointer** -into the block and `left` at +0x9c8 is the counter, and the three inner primitives are entered with -`ECX = &mt[0] = RNG+4`, which is what lane T's "the generator is entered at `rng+4`" note was seeing. The +into the block and `left` at +0x9c8 is the counter, and the inner primitives are entered with +`ECX = &mt[0] = RNG+4` while the outer helpers take the object base and do the `add ecx,4` themselves — +which is what lane T's "the generator is entered at `rng+4`" note was seeing. The draw is `if (left == 0) Twist(); y = *next++; --left;` — a pre-check against **0**, never −1. The block transform is a pure function, so the blocks a generator visits form a forward-only chain.