sots-re/ghidra/addresses.d/bu.json
alex ca6782f4ff lane BU: the fleet-visit-order prediction, committed before the run
Stage 2 of the chain lane BR unblocked. H2 (an RNG draw) is dead by
measurement, so the visit order over the ship groups is either a function of
the walked elements' addresses (H1) or of nothing in particular (H1b).

The committed prediction is P3: the visit order is ascending element address,
and across two processes the element order differs exactly where the visit
order differs.

Also the bounded static read this lane took to place the hook: the walk is a
plain index walk of a std::vector<StarFleet*> whose _Mylast is refetched every
iteration, bracketed by two loops over the same vector that call one function
each -- and those two functions have exactly ONE caller each in the whole
image. So the hook is two ordinary function entries plus a bracket, not a
mid-function patch of a loop body whose first instruction is a branch target.
Rule 17 discharged: the body is 0x006c16c0-0x006c247a ret, with a four-entry
jump table living past the ret at 0x006c247c.

ghidra/addresses.d/bu.json carries the two new names and the agent offset; it
deliberately does not re-declare AssignFleetsAndIssueOrders, which lane AI3
owns.
2026-09-09 03:15:19 -04:00

29 lines
4.6 KiB
JSON

{
"_note": "Lane BU 2026-09-09. Stage 2 of the fleet-visit-order chain: the two per-element callees of the walk inside StrategyAIAgent::AssignFleetsAndIssueOrders 0x006c16c0, which are this lane's hook targets. Both were reached from the disassembly of 0x006c16c0 read to the NEXT function start (rule 17): the real body is 0x006c16c0-0x006c247a `ret`, followed by a FOUR-ENTRY JUMP TABLE AT 0x006c247c (target of `jmp [eax*4+0x6c247c]` at 0x006c22fb) and int3 padding to the next function start 0x006c2490. Ghidra's reported size (3504 B, end 0x006c247a) is right about the ret and stops one instruction before the jump table, so anything that sweeps `fva + sizeInBytes` loses the table. AssignFleetsAndIssueOrders itself is already carried by lane AI3 and is deliberately NOT re-declared here (the generator rejects a same-address-different-name collision). The walk this lane hooks: `param_4` is a std::vector<StarFleet*>; the cursor lives at [ebp-0x38], the element loop body starts at 0x006c17e0 (`mov ebx,[ecx]` at 0x006c17e6 loads the element), the advance is `add eax,4` at 0x006c21b5 and the back-edge `jne 0x6c17e0` at 0x006c21be, with `_Mylast` REFETCHED from [param_4+4] every iteration. So the visit order is the vector's index order, and it is bracketed by two loops over the same vector that call one function each -- which is why those two functions, and not a mid-function patch, are the instrument.",
"entries": [
{
"name": "StrategyAIAgent_ClaimShipsOfFleet",
"addr": "0x006a4290",
"convention": "thiscall",
"prototype": "void __thiscall Game::StrategyAIAgent::ClaimShipsOfFleet(StrategyAIAgent* agent /*ecx*/, StarFleet* fleet /*stack*/) -- EXACTLY ONE CALLER in the whole image, 0x006c1735, the FIRST loop of AssignFleetsAndIssueOrders (0x006c1730-0x006c1740), which walks `param_4` once at function entry before any pass gate. Body: n = ([fleet+0xa8] - [fleet+0xa4]) (the ships vector, StarFleet_off_Ships), and for each StarShip* s in it, push_back(s ? [s+4] : 0) onto the agent's int vector at agent+0x2d8 -- a linear scan of that vector first (0x006a42e0) so an id already present is not added twice, then the MSVC push_back grow path (capacity check against 0x3ffffffe, 0x00483410 to reallocate). `[s+4]` is StarShip's id, the same word StarFleet_off_Id names on a fleet. BECAUSE IT HAS ONE CALLER AND IS CALLED ONCE PER ELEMENT AT THE HEAD OF THE WALK, a detour on it records the acquired-fleet vector in visit order with no return-address filter and no mid-function patch: it is the whole instrument of lane BU's stage-2 probe",
"status": "verified",
"source": "findings/control-flow/fleet-visit-order-mechanism.md (lane BU 2026-09-09); disassembled from dumps/sots.exe to the next function start; single caller confirmed by Ghidra cross-references"
},
{
"name": "StrategyAIAgent_ReleaseShipsOfFleet",
"addr": "0x0069da10",
"convention": "thiscall",
"prototype": "void __thiscall Game::StrategyAIAgent::ReleaseShipsOfFleet(StrategyAIAgent* agent /*ecx*/, StarFleet* fleet /*stack*/) -- RET 4. EXACTLY ONE CALLER, 0x006c1765, the second loop of AssignFleetsAndIssueOrders (0x006c1760-0x006c1770), which is entered at 0x006c1753 AFTER the element loop has walked the vector to exhaustion (`jmp 0x6c1753` at 0x006c21c4). The exact inverse of ClaimShipsOfFleet: for each StarShip* s in the fleet's ships vector it finds [s+4] in the agent's int vector at agent+0x2d8, memmoves the tail down (import 0x009dd30c) and does `[agent+0x2dc] -= 4`. Same one-call-per-element shape over the SAME vector in the SAME order, so it is a free second witness of the visit order taken after the loop rather than before it",
"status": "verified",
"source": "findings/control-flow/fleet-visit-order-mechanism.md (lane BU 2026-09-09); disassembled from dumps/sots.exe to the next function start; single caller confirmed by Ghidra cross-references"
},
{
"name": "StrategyAIAgent_off_ClaimedShipIds",
"offset": "0x000002d8",
"convention": "offset",
"prototype": "std::vector<int> claimed ship ids (_Myfirst @+0x2d8, _Mylast @+0x2dc, _Myend @+0x2e0). Read and written ONLY by the claim/release pair above, both of which are exclusive to AssignFleetsAndIssueOrders: the fleets acquired for a task have their ships' ids parked here for the duration of the assignment walk and removed when it is done. Enumerated from the push_back grow path at 0x006a4338-0x006a4356 (which reads +0x2e0 as _Myend via `mov ecx,[esi+8]` with esi = agent+0x2d8) and the erase path at 0x0069da63-0x0069da81",
"status": "verified",
"source": "findings/control-flow/fleet-visit-order-mechanism.md (lane BU 2026-09-09)"
}
]
}