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

14 KiB

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, but does not initialize the temporary EvTurn at +4 ([EBP-0x20]). It then calls outer vector<TurnEvents>::push_back at 0x00884cb0; append copies that incoming stack word as its scalar EvTurn before deep-copying the nested vector. Get/create destroys the temporary's nested vector through 0x00629580, only then writes the requested turn to the stored element at _Mylast[-1]+4, and returns the new element. The transient word is not measured randomness or necessarily nonzero; do not infer a zero default. This correction and its fresh four-window reproduction are in run-16f8e9b6376b278c4870be09/ under decision d-b51f3f76803e852ed250846a.
  • 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.
  5. The corrected TurnEvents miss path has a fresh analyst reproduction, not independent reproduction. Full/spare outer capacity, nonempty nested copy, transient observation and failure/unwind remain unexecuted; final requested-turn publication does not certify those paths.

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.