From 26e678e57034230e16616ce22c0159f6c791aef6 Mon Sep 17 00:00:00 2001 From: arcattack Date: Mon, 27 Jul 2026 07:42:37 -0500 Subject: [PATCH] #67 part 2, ROOT-CAUSED AND FIXED: 1995 latent uninitialized fields, exposed by the port's allocator THE HUNT (the deterministic rig repro made it a two-hour arc): 1. cdb write-watch armed from the Torso ctor (bp plants `ba w4 this+0x21c` per torso -- ASLR-proof). The poison reproduced (atUpd=-1250 this run; -3750 and int-15-as-float before)... and the watch stayed SILENT. Nobody writes the garbage. The field is never INITIALIZED. 2. Confirmed in our ctor reconstruction: it inits every neighbour but skips targetTwist @0x218 and twistAtUpdate @0x21C (the function that zeroes them is a death-reset handler, not the ctor). 3. Confirmed in the BINARY: the real ctor @004b6b0c contains no store to either offset -- a genuine 1995 latent bug (uninitialized read on the copy path). 4. Why the pod never showed it: MemoryBlock arenas carve fresh OS-zeroed pages, one pool per type, low churn -- first allocations read as zero. The engine's own DEBUG_NEW_ON NaN-fill proves the developers knew the hazard class. Why WE show it: mechrecon.hpp's Memory::Allocate shim ("a plain heap allocation is behaviour-equivalent" -- false) recycles dirty heap. Replicant torsos spawned with garbage twist targets; the limit clamp turned any garbage into FULL TWIST -- rendered "twisted full right while not using TT" to every peer (playtest night 4, 3 reporters). THE FIX -- environmental, class-wide, byte-faithful to the binary's code: Memory::Allocate / AllocateArray / Alloc now ZERO-FILL, reproducing the pod's EFFECTIVE allocation semantics for every never-stored field in every reconstructed factory at once (Torso, Reservoir, all of them). No ctor gains stores the binary lacks. PROVEN on the rig: the replicant thor now spawns cur=0 target=0 atUpd=0 (was cur=-3.31613 = hard against the limit). Teardown clean. Ships with part 1 (the dead-reckoning clock fix) in the next zip. Remaining on #67 for the next games night: live confirmation that twist TRACKING looks right in play, and whether the fire-from-centre facet (very plausibly the same never-stored-field class, now zeroed) is cured with it. Tools kept: scratchpad/torso_watch.cdb + rig_watch.ps1 (the ctor-armed write-watch pattern -- reusable for any "who wrote this field" hunt). Co-Authored-By: Claude Opus 5 (1M context) --- game/reconstructed/mechrecon.hpp | 28 ++++++++++++++---- scratchpad/rig_watch.ps1 | 49 ++++++++++++++++++++++++++++++++ scratchpad/torso_watch.cdb | 8 ++++++ 3 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 scratchpad/rig_watch.ps1 create mode 100644 scratchpad/torso_watch.cdb diff --git a/game/reconstructed/mechrecon.hpp b/game/reconstructed/mechrecon.hpp index e0a9563..30d4c33 100644 --- a/game/reconstructed/mechrecon.hpp +++ b/game/reconstructed/mechrecon.hpp @@ -46,18 +46,34 @@ struct Recon //===========================================================================// // Memory shim -- the decomp's "Memory::Allocate / Memory::Free" map onto the -// engine's per-object MemoryBlock allocators; for the offline reconstruction a -// plain heap allocation is behaviour-equivalent. +// engine's per-object MemoryBlock allocators. +// +// ZERO-FILLED ON PURPOSE (#67 root cause, 2026-07-27). "A plain heap +// allocation is behaviour-equivalent" turned out FALSE: the 1995 binary +// leaves fields uninitialized that its ctors never store (proven: the Torso +// ctor @004b6b0c stores neither targetTwist @0x218 nor twistAtUpdate @0x21C), +// and on the POD that was latent -- MemoryBlock arenas carve fresh OS-zeroed +// pages, one pool per type, low churn, so first allocations read as zero +// (the engine's own DEBUG_NEW_ON NaN-fill shows the developers knew the +// hazard). Our ::operator new recycles dirty heap: replicant torsos spawned +// with garbage twist targets, clamped to the limit -- rendered "twisted full +// right" to everyone (playtest night 4, three reporters). Zero-filling +// reproduces the pod's EFFECTIVE allocation semantics for the whole class of +// never-stored fields at once; the ctors stay byte-faithful to the binary. //===========================================================================// namespace Memory { - inline void *Allocate(size_t bytes) { return ::operator new(bytes); } - inline void *AllocateArray(size_t bytes){ return ::operator new(bytes); } + inline void *Allocate(size_t bytes) + { void *p = ::operator new(bytes); ::memset(p, 0, bytes); return p; } + inline void *AllocateArray(size_t bytes) + { void *p = ::operator new(bytes); ::memset(p, 0, bytes); return p; } inline void Free(void *where) { ::operator delete(where); } } -inline void *Alloc(size_t bytes) { return ::operator new(bytes); } -inline void *AllocateArray(size_t bytes){ return ::operator new(bytes); } +inline void *Alloc(size_t bytes) + { void *p = ::operator new(bytes); ::memset(p, 0, bytes); return p; } +inline void *AllocateArray(size_t bytes) + { void *p = ::operator new(bytes); ::memset(p, 0, bytes); return p; } //===========================================================================// // Ref-count / name helpers (decomp artifacts). diff --git a/scratchpad/rig_watch.ps1 b/scratchpad/rig_watch.ps1 new file mode 100644 index 0000000..c38c64f --- /dev/null +++ b/scratchpad/rig_watch.ps1 @@ -0,0 +1,49 @@ +# Watch-rig: pod B runs UNDER cdb with the torso write-watch script. +$repo = 'C:\git\bt411' +$content = Join-Path $repo 'content' +$scratch = 'C:\Users\EPILECTRIK\AppData\Local\Temp\claude\C--git-bt411\89ab96e9-6cf0-4555-939d-05bf3708745a\scratchpad' +$exe = Join-Path $repo 'build\Release\btl4.exe' +$cdb = 'C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe' +if (-not (Test-Path $cdb)) { $cdb = 'C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe' } +$relaylog = Join-Path $scratch 'watch_relay.log' +$pidfile = Join-Path $scratch 'watch_pids.txt' + +if (@(Get-NetTCPConnection -State Listen -LocalPort 1520 -ErrorAction SilentlyContinue).Count -gt 0) { + "ABORT: port 1520 in use"; exit 1 +} +Remove-Item $relaylog, (Join-Path $scratch 'cdb_torso.log') -ErrorAction SilentlyContinue +$pids = @() + +$relay = Start-Process -FilePath 'python' ` + -ArgumentList '-u', (Join-Path $repo 'tools\btconsole.py'), '--relay', '1520', + (Join-Path $content '_TWIST.EGG'), '--bind', '127.0.0.1', '--manual-launch' ` + -WorkingDirectory $content -PassThru ` + -RedirectStandardOutput $relaylog -RedirectStandardError (Join-Path $scratch 'watch.err') +$pids += $relay.Id +Start-Sleep -Seconds 3 + +$env:BT_START_INSIDE = '1' +$env:BT_DEV_GAUGES = '1' +$env:BT_RELAY = '127.0.0.1:1520' +$env:BT_MATCHLOG = '0' +$env:BT_LOG_APPEND = '1' +$env:BT_TORSO_LOG = '1' + +# pod A: normal (the master we ignore) +$env:BT_LOG = 'w_a.log'; $env:BT_SELF = '127.0.0.1:1502' +$a = Start-Process -FilePath $exe -ArgumentList '-egg','_TWIST.EGG','-net','1502' ` + -WorkingDirectory $content -PassThru +$pids += $a.Id +Start-Sleep -Seconds 2 + +# pod B: UNDER CDB with the write-watch (its replicant torso is the victim) +$env:BT_LOG = 'w_b.log'; $env:BT_SELF = '127.0.0.1:1602' +$b = Start-Process -FilePath $cdb ` + -ArgumentList '-logo', (Join-Path $scratch 'cdb_torso.log'), + '-cf', (Join-Path $scratch 'torso_watch.cdb'), + '-o', $exe, '-egg', '_TWIST.EGG', '-net', '1602' ` + -WorkingDirectory $content -PassThru +$pids += $b.Id + +Set-Content -Path $pidfile -Value $pids +"relay=$($relay.Id) podA=$($a.Id) cdb=$($b.Id)" diff --git a/scratchpad/torso_watch.cdb b/scratchpad/torso_watch.cdb new file mode 100644 index 0000000..f2d01f5 --- /dev/null +++ b/scratchpad/torso_watch.cdb @@ -0,0 +1,8 @@ +* torso_watch.cdb -- arm a write-watch on twistAtUpdate (+0x21C) of every +* Torso the moment it is constructed; print a stack per write and continue. +* The poisoned field's writer will appear as the first non-Torso stack. +.symfix+ C:\git\bt411\build\Release +.sympath+ C:\git\bt411\build\Release +bu btl4!Torso::Torso ".echo ==TORSO-CTOR==; r @ecx; ba w4 @ecx+0x21c \"kb 10; gc\"; gc" +sxn av +g