From b5b2efab28f399a957a03b3839f41df96cfa6fcc Mon Sep 17 00:00:00 2001 From: Cyd Date: Fri, 14 Aug 2026 10:24:45 -0500 Subject: [PATCH] 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) --- tools/podium-repro/loop.ps1 | 238 ++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 tools/podium-repro/loop.ps1 diff --git a/tools/podium-repro/loop.ps1 b/tools/podium-repro/loop.ps1 new file mode 100644 index 0000000..bf2ff87 --- /dev/null +++ b/tools/podium-repro/loop.ps1 @@ -0,0 +1,238 @@ +# 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"