#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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a3329a64da
commit
26e678e570
@@ -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).
|
||||
|
||||
@@ -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)"
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user