167 lines
13 KiB
Markdown
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.
|