Files
RP412/tools/podium-repro/loop.ps1
T
CydandClaude Opus 5 b5b2efab28 The podium rig can run all night and count honestly
Repeated launches, escalation step 2. One mission per process is all the game
gives you - the teardown IS the exit - so more teardowns means running the
whole rig again, and loop.ps1 does that unattended.

The point of a loop is samples: one clean run of an intermittent fault says
almost nothing, twenty says something. The point of THIS loop is that a
launch which did not happen must never be counted as one of them. Three ways
it earns that, each of which it got wrong first and now gets right:

Wait-Quiet, not a sleep. Pods hold their console and game ports and the next
field cannot bind them; a three second gap had launch 2 come up on ports
launch 1 still owned, sit through the whole race timeout with nothing placed,
and report clean. It now waits for the processes to be gone AND the ports to
leave TIME_WAIT, and says so plainly if they never do.

Logs wiped before each launch, and a podium line only counted if the file was
written after the launch began. Without that the check read the PREVIOUS
run's log: the first shakedown reported two valid samples in one second, from
a feeder that had not run at all.

And the feeder invoked with an argument ARRAY, because `powershell -File`
cannot take a switch in the -Switch:$value form - it arrives as a string, the
callee refuses it, and the launch silently does not happen. That is what
produced the one-second samples.

It also refuses to start unless page heap is really on, reading IFEO's
GlobalFlag out of HKLM - which needs no elevation, unlike gflags itself, and
which is the bit that decides (gflags -hpa clears GlobalFlag and leaves
PageHeapFlags behind, so PageHeapFlags alone tells you nothing).

Shakedown: two launches, 4 pods, real races both times, scores 543..1253 so
they drove and collided, 4 of 4 on the podium each time, no fault. Launch 2 -
the one that used to stall - came up clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 10:24:45 -05:00

239 lines
9.3 KiB
PowerShell

# Repeated launches of the podium rig - escalation step 2.
#
# One mission per process is all the game gives you: the teardown IS the
# exit. So more teardowns means running the whole rig again, not raising
# -Races, and that is what this does.
#
# The point of the loop is samples. A single clean run of an intermittent
# fault says almost nothing; twenty says something. What it must never do is
# count a launch that never got to the podium as one of those samples - an
# earlier attempt did exactly that, allowing three seconds between launches
# so the next field came up on ports the last one had not released, sat
# through the race timeout with nothing placed, and recorded it as a clean
# result. Hence Wait-Quiet, and hence every iteration having to PROVE it
# reached the stand before it counts.
#
# powershell -NoProfile -ExecutionPolicy Bypass -File tools\podium-repro\loop.ps1
#
# Page heap must already be on (elevated, once, before the loop):
# & 'C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\gflags.exe' -i rpl4opt.exe +hpa
# ...loop.ps1 -Iterations 20 -PodCount 4
# & '...gflags.exe' -i rpl4opt.exe -hpa
param(
[int]$Iterations = 20,
[ValidateRange(2, 8)]
[int]$PodCount = 4,
[string]$Scratch = "$env:TEMP\rp412-podium-repro",
# cdbrun-gflags.txt: no PEB fiddling, so gflags is the only mechanism in
# play. cdbrun.txt's PEB trick is dead on Windows 26200 (see README).
[string]$CdbScript = 'cdbrun-gflags.txt',
[int]$RaceSeconds = 45,
# Run without the heap instrument on purpose. Says so in the tally.
[switch]$AllowNoPageHeap
)
$ErrorActionPreference = 'Stop'
$rig = Split-Path -Parent $MyInvocation.MyCommand.Path
$tally = Join-Path $Scratch 'loop-tally.log'
function Log([string]$m) {
$line = "{0} {1}" -f (Get-Date -Format 'HH:mm:ss'), $m
Write-Output $line
Add-Content -Path $tally -Value $line -ErrorAction SilentlyContinue
}
#-----------------------------------------------------------------
# Pre-flight: is the instrument actually on?
#
# gflags -hpa clears GlobalFlag but leaves PageHeapFlags behind, so
# PageHeapFlags alone means nothing - GlobalFlag is the one that decides.
# This reads HKLM, which does NOT need elevation, unlike gflags itself.
#-----------------------------------------------------------------
$ifeo = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\rpl4opt.exe'
$pageHeapOn = $false
if (Test-Path $ifeo) {
$gf = (Get-ItemProperty $ifeo -Name GlobalFlag -ErrorAction SilentlyContinue).GlobalFlag
if ($null -ne $gf) {
#
# gflags writes GlobalFlag as a STRING in hex ("0x02000000"), not a
# DWORD, so this has to parse rather than cast - and other tools write
# it as a plain decimal string, so handle both.
#
$text = ([string]$gf).Trim()
$gfv = 0
if ($text -match '^0[xX]([0-9a-fA-F]+)$') {
$gfv = [Convert]::ToUInt32($Matches[1], 16)
} elseif ($text -match '^\d+$') {
$gfv = [Convert]::ToUInt32($text, 10)
}
if ($gfv -band 0x02000000) { $pageHeapOn = $true } # FLG_HEAP_PAGE_ALLOCS
}
}
New-Item -ItemType Directory -Force $Scratch | Out-Null
Log "=== podium repro loop: $Iterations x $PodCount pods ==="
if (-not $pageHeapOn) {
Log 'PAGE HEAP IS OFF (IFEO GlobalFlag lacks FLG_HEAP_PAGE_ALLOCS)'
if (-not $AllowNoPageHeap) {
Log 'Refusing to run. From an ELEVATED shell:'
Log " & 'C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\gflags.exe' -i rpl4opt.exe +hpa"
Log ' ...then re-run this loop, and afterwards -hpa to put it back.'
Log 'Or pass -AllowNoPageHeap to gather uninstrumented samples on purpose.'
exit 1
}
Log 'CONTINUING UNINSTRUMENTED - these samples cannot catch an overrun or a'
Log 'double free, only a fault that happens to land on its own.'
} else {
Log 'page heap ON (IFEO GlobalFlag has FLG_HEAP_PAGE_ALLOCS)'
}
#-----------------------------------------------------------------
# A quiet machine before each launch.
#
# Not a sleep: leftover pods hold their console and game ports, and the
# next field cannot bind them. Wait for the processes to be gone AND the
# ports to leave TIME_WAIT, or say plainly that they did not.
#-----------------------------------------------------------------
$ports = @()
for ($i = 0; $i -lt $PodCount; $i++) { $ports += (1501 + 100 * $i); $ports += (1502 + 100 * $i) }
function Wait-Quiet([int]$TimeoutSeconds = 120) {
Get-Process rpl4opt, cdb -ErrorAction SilentlyContinue |
Stop-Process -Force -Confirm:$false -ErrorAction SilentlyContinue
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
while ((Get-Date) -lt $deadline) {
$live = @(Get-Process rpl4opt, cdb -ErrorAction SilentlyContinue).Count
$held = @(Get-NetTCPConnection -LocalPort $ports -ErrorAction SilentlyContinue).Count
if ($live -eq 0 -and $held -eq 0) { return $true }
Start-Sleep -Seconds 2
}
$live = @(Get-Process rpl4opt, cdb -ErrorAction SilentlyContinue).Count
$held = @(Get-NetTCPConnection -LocalPort $ports -ErrorAction SilentlyContinue).Count
Log " still busy after ${TimeoutSeconds}s: $live process(es), $held port(s)"
return $false
}
# A real fault, not the normal exit-at-teardown. cdb's `g` returns on exit
# too, so the break banner alone means nothing - ExceptionAddress is the tell.
function Find-Trap {
Get-ChildItem $Scratch -Filter 'pod?-cdb.log' -ErrorAction SilentlyContinue | ForEach-Object {
$e = Select-String -Path $_.FullName -Pattern 'ExceptionAddress' -ErrorAction SilentlyContinue
if ($e) { "$($_.Name): $($e[0].Line.Trim())" }
}
}
# Wipe every pod's log before a launch, so a podium line found afterwards
# can only have come from THIS launch. Without this the check reads the
# previous run's log and reports a launch that never happened as a clean
# sample - which it did, twice, before this existed.
function Clear-PodLogs {
for ($i = 0; $i -lt $PodCount; $i++) {
$log = Join-Path $Scratch (('pod' + [char](65 + $i)) + '\rpl4.log')
Remove-Item $log -Force -ErrorAction SilentlyContinue
}
}
# The launch only counts if every pod actually stood on the podium, in a log
# written after the launch began. Both halves matter.
function Count-Placed([datetime]$since) {
$n = 0
for ($i = 0; $i -lt $PodCount; $i++) {
$log = Join-Path $Scratch (('pod' + [char](65 + $i)) + '\rpl4.log')
if (Test-Path $log) {
$file = Get-Item $log
if ($file.LastWriteTime -ge $since -and
(Select-String -Path $log -Pattern 'WinnersCircle: \d+ placed' -ErrorAction SilentlyContinue)) {
$n++
}
}
}
return $n
}
$valid = 0; $invalid = 0; $trapped = $false
for ($iter = 1; $iter -le $Iterations; $iter++) {
Log "---------- launch $iter of $Iterations ----------"
if (-not (Wait-Quiet)) {
Log " launch ${iter}: SKIPPED - machine never went quiet"
$invalid++
continue
}
#
# To a file, not down the pipeline. Under $ErrorActionPreference='Stop'
# a native command's stderr comes back as ErrorRecords and the first
# warning the feeder prints would end the loop - which is how the first
# version of this died on launch 1. It also leaves a per-launch record.
#
Clear-PodLogs
$launchedAt = Get-Date
#
# An argument ARRAY, because `powershell -File` cannot take a switch in
# the -Switch:$value form - it arrives as a string and the callee refuses
# it, which silently turned two launches into no launch at all.
#
$feedArgs = @(
'-NoProfile', '-ExecutionPolicy', 'Bypass',
'-File', (Join-Path $rig 'feeder.ps1'),
'-PodCount', $PodCount, '-Races', 1,
'-RaceSeconds', $RaceSeconds, '-CdbScript', $CdbScript
)
if ($AllowNoPageHeap) { $feedArgs += '-AllowNoPageHeap' }
$runLog = Join-Path $Scratch ("loop-run-{0:d3}.log" -f $iter)
$previousEAP = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
& powershell @feedArgs *> $runLog
} finally {
$ErrorActionPreference = $previousEAP
}
Select-String -Path $runLog -Pattern 'FINAL SCORE|ABORTING|console connect FAILED' -ErrorAction SilentlyContinue |
ForEach-Object { Log " $($_.Line)" }
$trap = Find-Trap
if ($trap) {
Log '*** TRAPPED ***'
$trap | ForEach-Object { Log " $_" }
Log ' everything LEFT UP and dumps preserved - do not clean this one'
$trapped = $true
break
}
$placed = Count-Placed $launchedAt
if ($placed -eq $PodCount) {
$valid++
Log " launch ${iter}: VALID sample - $placed of $PodCount reached the podium, no fault"
} else {
$invalid++
Log " launch ${iter}: INVALID - only $placed of $PodCount reached the podium (proves nothing)"
}
# ~580 MB per pod per launch; a 20-launch loop would be 46 GB
Get-ChildItem $Scratch -Recurse -Filter 'podbreak*.dmp' -ErrorAction SilentlyContinue |
Remove-Item -Force -ErrorAction SilentlyContinue
Log " running tally: $valid valid, $invalid invalid"
}
Log '=== LOOP DONE ==='
if ($trapped) {
Log "TRAPPED after $valid valid sample(s). Read the pod?-cdb.log after"
Log '=== POD BREAK ===, and !heap -p -a on the plug address: the free stack'
Log 'that is NOT ~JointedMover is the first free, and that is the bug.'
} else {
Log "$valid valid sample(s), $invalid invalid, NO fault."
if (-not $pageHeapOn) {
Log 'UNINSTRUMENTED - not evidence of absence.'
} else {
Log 'Full page heap was on throughout, so a double free or an overrun in'
Log 'this path would have trapped. It did not happen in these samples.'
}
}
Log "tally: $tally"