sots-re/verify/results/research-completion-abi/recovered-static.md

167 lines
13 KiB
Markdown

# Fresh static observations
Static evidence only. It does not establish live allocator safety, a compatible replacement
allocator, or replacement acceptance. Raw local evidence and exact regeneration commands are in
`objdump-2026-09-09-ownership.txt`; the owner-supplied input is `dumps/sots.exe`, SHA-256
`970b7de729956a53094c7eb98aba4270aee98e2fed5daf0d39e290013c90c841` / MD5
`9969481c39f4b33a8a21c48b62abee4c`.
The original dedup archive's first-window provenance was overturned by independent review and Astra
decision `d-d2a9b8be6399a6abaa0e05a5`. Fresh narrow/wide raw streams and complete identities are in
`objdump-2026-09-10-boundary-repair.md`. The widened stream supplies the complete terminal bytes;
semantic promotion still requires a new independent reproduction and challenge.
The ownership archive's selected terminal rows have the same superseded provenance limitation.
Fresh full streams, narrow/wide repairs, section-byte dumps and an audit of all ten declared windows
are in `run-79357a65226f61d6a86c042d/`. Six historical stops truncate `ret imm16` after `c2`;
the widened/static raw bytes establish five `c2 04 00` encodings and one `c2 08 00` encoding.
This repairs terminal-byte provenance under decision `d-d4c494ba02ada278030ef473`; it does not
convert the static interpretations below into live allocator-safety or accepted ABI claims.
## Recorded instruction facts
* `ObservedTech::ObservedTech` at `0x008562a0` is an ECX receiver, returns that receiver in EAX,
and uses plain `ret`. It installs vtable `0x00a2439c`; initializes the string rooted at `+0x0c`
to `_Myres=15`, `_Mysize=0`, empty first byte; zeros `+4` (therefore both 16-bit turns), `+8`, and
`+0x28`; and calls `0x00425550` for the empty-string setup.
* `vector<ObservedTech>::push_back` at `0x007b7320` is ECX receiver plus one stack word (`ret 4`).
It grows only when `_Mylast == _Myend`, through `0x007b5820(this, 1)`, then invokes the
`0x0079a150` element-copy helper and advances `_Mylast` by exactly `0x2c`. The source-inside-vector
and source-outside-vector branches both lead to this copy helper; the former recomputes the source
from its pre-growth index. Thus append copies the temporary rather than adopting its string header.
* The `0x0079a150` helper constructs a destination `ObservedTech`: vptr, words `+4/+6`, byte `+8`,
string copy via `0x00425430`, and word `+0x28`. It is a copy construction operation, not a raw
44-byte memcpy. Independent Astra cross-check pins a **cdecl-style three-stack-argument** ABI:
unused allocator argument, destination at `[ebp+0xc]`, source at `[ebp+0x10]`; plain `ret` and
caller cleanup of 12 bytes. Incoming ECX is not a receiver (its initial push only allocates a
local slot that is overwritten). Exact C++ template name is unnecessary for this machine boundary.
* `0x007b5820` computes required capacity as old size plus its one stack-word count and uses the
1.5x growth rule when sufficient. It calls `0x007b34e0`; that reallocator calls `0x0057e590` with
new element count. `0x0057e590` multiplies by `0x2c` and calls `0x00924fb6` (scalar `operator new`
import thunk). Reallocation copy-constructs old elements through `0x0085e650`, calls each old
element's virtual destructor slot 0 with pushed zero, then frees the old array through
`0x00924faa` (scalar `operator delete` import thunk), and updates all three vector pointers.
* `PlayerEvent` vector append at `0x0086c580` is ECX receiver plus one stack word (`ret 4`), grows
through `0x00869500` if full, copy-constructs the element through `0x007693f0`, then advances
`_Mylast` by `0x74`. The copy helper copies scalar fields and independently assigns all three
strings at `+8`, `+0x24`, `+0x50` through `0x00425430`; it is not a 116-byte header copy.
* `PlayerEvent` destructor body at `0x0061ae90` tests each string capacity (`+0x1c`, `+0x38`,
`+0x64`) against `0x10`; for long strings it frees the buffer at `+8`, `+0x24`, `+0x50` through
`0x00924faa`, then restores empty/SSO values. This establishes three independent owned-string
cleanup paths in a copied event.
* `0x004249a0`, called from the string assignment `0x00425430`, allocates new character storage via
`0x00924fb6` and frees an existing long destination buffer through `0x00924faa` before installing
the replacement pointer/size/capacity. The branch condition for long ownership is capacity
`>= 0x10`; short strings stay inline. Per the independently captured PE imports, these thunks map
to MSVCR100 scalar `operator delete` and scalar `operator new` respectively.
### Nested TurnEvents machine boundary
* `0x00885380` is an ECX-receiver operation over the outer vector at receiver `+4`, takes one
stack `int turn`, returns a `TurnEvents*` in EAX, and uses `ret 4`. It divides the outer byte span
by `0x18`, scans every element, and overwrites its candidate on every `EvTurn` match. Therefore a
hit returns the **last** matching bucket and performs no construction, allocation, ID update, or
RNG draw.
* On a miss it initializes a stack `TurnEvents` with vptr `0x00a0f07c` and zero nested-vector
pointers, then calls outer `vector<TurnEvents>::push_back` at `0x00884cb0`. Only after append does
it write the requested turn to the stored element at `_Mylast[-1]+4`. It destroys the temporary's
nested vector through `0x00629580` and returns the new element. The temporary itself starts with
turn zero; append deep-copies that zero before the stored turn is patched.
* `0x00884cb0` is ECX receiver plus one source pointer and `ret 4`; stride is `0x18`. It handles a
source pointer inside its own vector separately so growth cannot invalidate the source. Both
branches install the TurnEvents vptr, copy `EvTurn`, and copy-construct the nested PlayerEvent
vector through `0x00779850`; this is not a 24-byte header copy. It advances outer `_Mylast` only
after the nested copy call returns.
* Outer full-capacity growth is `0x008841a0` -> `0x00883a60`. Capacity selection is old capacity
plus half where sufficient, otherwise required size. `0x006e8f50` allocates `count * 0x18` through
`0x00924fb6`. `0x0077fed0` copy-constructs every old TurnEvents, including an independent nested
vector via `0x00779850`; then `0x00883a60` invokes each old TurnEvents virtual destructor with
deleting flag zero, frees the old outer allocation through `0x00924faa`, and writes all three
outer vector pointers.
* The vtable bytes at `0x00a0f07c` identify slot zero as `0x0062e120`. That scalar-deleting
destructor calls `0x00629580` on the nested vector at `+8`; with flag bit zero it does not free the
inline TurnEvents object. `0x00629580` invokes every nested PlayerEvent virtual destructor in
`0x74` steps, frees the nested allocation through `0x00924faa`, and zeros all three nested vector
pointers.
* `0x00779850` is ECX destination nested vector plus one source-vector pointer and `ret 4`. An empty
source leaves three zero pointers. A nonempty source allocates `count * 0x74` through
`0x0078af40` -> `0x00924fb6`, then `0x007725a0` copy-constructs each PlayerEvent through
`0x007693f0`. Its unwind destroys already completed PlayerEvents; `0x00779850` then calls
`0x00629580`, and outer range-copy unwind at `0x0077fed0` destroys already completed TurnEvents.
Outer reallocation's landing path frees the newly allocated outer block before continuing the
exception through `0x00924fbc`. These are observed cleanup edges, not a claim that allocation
failure has been executed live.
### Duplicate and prune branches
* `0x00825d40` receives bucket and candidate pointers as two stack words, ignores incoming ECX,
and uses `ret 8`. A null bucket or empty nested vector returns zero. It scans in `0x74` steps and
checks, in order: `EvAct`, `EvLoc`, all three `EvPos` floats, `EvMsg`, `EvImg`, then calls
`0x0046f8c0` with the strings rooted at `EvDsc +8`. Complete fresh capture establishes that
`0x0046f8c0` is a two-stack-argument, caller-cleaned string-inequality operation: first argument is
the stored string, second is the candidate string, EAX/AL is one iff the strings differ, and it
uses plain `ret`. `0x00825e21` therefore reaches the matched-element return only when AL is zero,
i.e. when descriptions are equal. A description-only difference continues the scan. This
contradicts the inherited claim that `EvDsc` was excluded; decision
`d-2ff30c9f5355116bea822924` required this evidence repair after resolving surprise
`s-8996365dab2cd6dc0e17bb9f`. The first element equal in every listed field returns its pointer;
exhaustion returns zero. The wrapper performs no writes, allocation, destruction, ID change,
event append, or RNG draw.
* `0x0046f8c0` selects the candidate's inline bytes when capacity is `<0x10`, otherwise its heap
pointer, and passes candidate length/data plus stored length and offset zero to `0x004236a0`.
`0x004236a0` independently selects the stored string's inline/heap bytes, compares the minimum
byte count through `0x00422720`, then orders unequal lengths. Equal bytes and equal lengths return
zero; any byte or length difference returns nonzero, which `0x0046f8c0` normalizes to one. Thus
empty/short/long combinations are covered statically without allocation or copying. The
imported failure helper at IAT slot `0x009dd154` is called only when offset exceeds stored length;
it is unreachable for this offset-zero call, and no failure path was executed live. Its exact
imported symbol remains unresolved in this package.
* `0x00879eb0` is ECX receiver plus one stack `turn` and `ret 4`. It computes cutoff `turn-50` and
inspects only the leading run with `EvTurn < cutoff`. Its selected pointer is the **last** stale
member. No stale member, exactly one leading stale member, or an initially empty vector returns
without writes. For two or more leading stale buckets, it copies from the last stale bucket
through the old end into the old beginning: each destination gets source `EvTurn`, then its nested
vector is deep-assigned by `0x0077a6b0`. It destroys the trailing shifted-from TurnEvents through
virtual slot zero and sets outer `_Mylast` to the end of the retained prefix. Thus it removes
`stale_count-1`, deliberately retaining one stale bucket; a stale bucket after the first fresh
bucket is never inspected. No outer allocation is visible in this wrapper, but nested assignment
may allocate/free/copy/destroy PlayerEvents according to destination capacity.
## Ordering and boundary
`RecordObservedTech` first tests for an existing matching name; name absence, not vector fullness,
reaches the constructor/append sequence. The wrapper later destroys its temporary long string, so
the stored element must already own an independent string allocation where applicable. Existing
records retain first turn/name and update last turn/with mask; this wrapper itself does not expose a
live allocation failure outcome. `EventStorage::PostEvent` constructs a temporary PlayerEvent,
prunes/selects/deduplicates, and only its no-duplicate branch appends; IDs and event vector pointers
remain part of the caller-level behavior documented in `findings/subsystems/events.md`.
## Remaining blockers / minimum probes
1. `0x0084ee30` remains covered only by the initial ReVa capture in this package; its full local
instruction window should still be added by the independent verifier when checking default-field
values. The nested construction/growth, duplicate, and prune helpers now have local raw captures.
2. Exact C++ template declarations and the runtime outcome of allocation failure remain unresolved.
Static unwind edges are captured, but no live throw was induced and no allocator-safety claim is
made.
3. A read-only synthetic long-string/full-capacity fixture (or a safe existing archived trace with
pre/post memory) is required to falsify the static ownership interpretation. It cannot prove
allocator compatibility by coherent parsing alone.
4. `EvDsc` equality participation is now statically pinned, but has not been exercised in a live
same-bucket description-only fixture. Independent review must reproduce the complete helper and
branch windows before this becomes accepted ABI evidence.
Independent cross-check captures: `verify/results/research-callback/observed-copy-helper-objdump.txt`
and `event-copy-helper-objdump.txt`. The PlayerEvent copy operation is different: ECX is the
destination, one stack argument is source, EAX returns destination and `ret 4` cleans the argument.
Do not reuse the ObservedTech copy-helper calling convention for PlayerEvent.
Nested raw capture: `verify/results/research-completion-abi/objdump-2026-09-09-turnevents.txt`.
Complete dedup/helper/callee capture:
`verify/results/research-completion-abi/objdump-2026-09-09-dedup-helper.txt`.
Verifier falsifiers should include: otherwise-identical records with equal versus description-only
different `EvDsc`, separately using empty/short and long descriptions (static prediction: only equal
descriptions deduplicate); one versus two
leading stale buckets; a stale bucket after a fresh one; existing duplicate turn buckets (last-match
selection); and outer spare/full capacity with nested empty/nonempty PlayerEvent vectors.