Lane L3 needed to watch the trade-route and spy-program vectors across a whole game played forward, not one turn of one save. `modcount` and `tshn` arm once on purpose -- their targets are picked from one turn's state and re-picking them would move the measurement -- so this is a third mode rather than a change to either. `cont` puts all four debug slots on the two containers, `_Myfirst` as well as `_Mylast`. Both vectors are default-constructed with all three pointers zero, so the first element writes all three: watching only `_Mylast` cannot separate "allocated for the first time" from "appended to an existing buffer", and those are different events in the model this lane set out to falsify. It re-arms and re-logs on every End Turn, and the canary self-test's counter is therefore read as a delta -- on the arm-once modes the delta is the old value, so their log lines are byte-identical. `ReportContainer` is factored out of `ArmTshnSlots` so both modes emit the same container line. Lane W3's published count=0 is the control every later count is compared against, and a reformatted line would have made that comparison a judgement call. The defect: `Shim_Init` returned before `install_watchpoints` whenever the trace mode was `off`, so `hooks=off watch=on` printed `watch=on` in the banner and armed absolutely nothing -- a config that reports a confident zero, which is the failure method rule 1 exists to catch. The watchpoints are an independent instrument with their own arming detour and no trace records, and a long play session wants them without paying 30-45 s per End Turn for template hooks that measure nothing it is asking about. MinHook is now initialised and the module installed on the `hooks=off` path when `watch=on`. Configs: shim.cfg.l3cont / .l3control differ in exactly one key for rule 19; .l3probe is lane H's hp11 verbatim plus the three watch keys, so the entry counts stay comparable to lane H's empty-container baseline line for line. Gates: clean_room_check OK; host ctest 54/54; CT111 shim cross-build OK, exports 66 names identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
99 lines
6.2 KiB
Markdown
99 lines
6.2 KiB
Markdown
# W3 — a second watch mode, and what the two runs answered
|
|
|
|
Companion to `docs/W3-predictions.md` (written and committed before the build) and to
|
|
`sots-re/findings/subsystems/nvo-tshn-visible-owner.md` +
|
|
`sots-re/findings/control-flow/w3-containers-status-handlers.md` (the full reports).
|
|
|
|
Builds on lane W2's module without changing how it works.
|
|
|
|
## What was added
|
|
|
|
`watch.mode=modcount|tshn` in `src/shim/hooks/watchpoints.{h,cpp}`. **No second hook, no second
|
|
detour, no change to the VEH or to the arming mechanism** — only different arithmetic on the `S`
|
|
the existing `ApplyAllTurnCommands` detour already holds. `modcount` is W2's set and stays the
|
|
default so its run remains reproducible; `tshn` arms:
|
|
|
|
| slot | target |
|
|
|---|---|
|
|
| 0 | the `TShn` word of an `NVO` map record on a system with `AFlags == 0` |
|
|
| 1 | that map's `_Mysize` |
|
|
| 2 | trade-route vector `_Mylast` (`*(S+4+0x154) + 0x40`) |
|
|
| 3 | spy-program vector `_Mylast` (`*(S+4+0x158) + 0x14`) |
|
|
|
|
Two things about the `tshn` arming are worth keeping if the module is extended again:
|
|
|
|
- **The target is chosen by predicate at arm time, not hard-coded**, and all 28 systems are logged
|
|
with name, `AFlags` and both map sizes. A hard-coded pointer that lands on the wrong object
|
|
produces the same empty hit list as "nothing writes this" (method rule 1).
|
|
- **The `ServerSystem` base is probed, not assumed.** Two published accounts disagreed by 8 about
|
|
where the `NVO` map lives, because `ServerSystem::Write` runs on the IStreamable subobject at
|
|
`+0x8`. The code tries both candidate bases across every system and logs how many validated under
|
|
each (`+0x274`: 9 systems; `+0x26c`: 0). That turned a documentation dispute into a measurement.
|
|
|
|
Configs: `shim.cfg.w3tshn`, `shim.cfg.w3mod`, `shim.cfg.w3control`. `w3tshn` and `w3control` differ
|
|
in exactly one key (`watch=`), which is what makes the rule-19 control real.
|
|
|
|
## Rule 19: the control was taken again, and it passed again
|
|
|
|
One End Turn from `ref-turn2.sav` with the four `tshn` watchpoints armed reproduced the determinism
|
|
oracle byte for byte — `(Autosave EndTurn).sav` `bb4fd9ac…`, `(Autosave).sav` `978041ac…`, both
|
|
identical to the pre-run files. So the four *new* addresses are as neutral as W2's four were. That
|
|
mattered: W2 proved a data breakpoint is neutral for one address set, not for all of them, and this
|
|
lane's set includes a heap node that the game reallocates freely.
|
|
|
|
The second run (turn-1 workload, `watch.mode=modcount`) has **no oracle of its own** and says so.
|
|
|
|
## What the two runs answered
|
|
|
|
1. **`NVO.TShn`'s writer, trapped live**, with its whole call chain confirmed frame by frame from
|
|
the recorded `ebp` chain: `RefreshVisibleOwnerIfKnown 0x0075bd70` → `RecordVisibleOwner
|
|
0x0075bca0` → `SetVisibleOwner 0x0075b880` → `NVO::operator[] 0x0075a890` → the store
|
|
`0x0075b961`. The gate is `ServerSystem::IsKnownTo 0x00746390`, which is `IsVisibleTo` **or**
|
|
(sensor contact ∧ `CCC_AdvSens`) — not `AFlags` alone, which is why lane E3 could not fit it.
|
|
The resulting model predicts all 158 `NVO` records in the 11-save corpus with zero mismatches,
|
|
including the single frozen one.
|
|
2. **Four traps, not one, and the extra pair is a result**: the refresh runs twice per End Turn, in
|
|
driver phase 24 and again in combat-done phase 25. Both phase attributions confirmed for free.
|
|
3. **The trade and spy containers, read out of a live game for the first time.** Both managers
|
|
non-null, both vectors default-constructed with all three pointers zero, zero traps on either
|
|
`_Mylast` across the turn. The workload is *absent*, measured — which is the confirmation
|
|
instrument two earlier lanes needed and did not have.
|
|
4. **All ten command `ModCount` handlers are now named**, including two that only exist on a turn-1
|
|
workload (`OnCommand_SetResearchProject`, inlined at `0x0088fe0a`, and
|
|
`OnCommand_CreateDesign 0x00882910`).
|
|
|
|
## Reusing it
|
|
|
|
`watch.mode` is the extension point. To watch something else, add a mode and one arming function;
|
|
everything else — the canary, the VEH, the flusher, the hit format — is unchanged. Keep the canary
|
|
self-test and keep the `ref-turn2` oracle control: together they cost about two minutes and they
|
|
are what makes the numbers evidence rather than output.
|
|
|
|
## Mode `cont` (lane L3)
|
|
|
|
A third `watch.mode`, added when lane L3 needed to watch the trade-route and spy-program
|
|
containers across a whole game played forward rather than one turn of one save.
|
|
|
|
* All four debug slots go on the two containers: trade `_Myfirst` (`mgr+0x3c`), trade `_Mylast`
|
|
(`mgr+0x40`), spy `_Myfirst` (`mgr+0x10`), spy `_Mylast` (`mgr+0x14`). `_Myfirst` is armed as
|
|
well as `_Mylast` because these vectors are default-constructed with all three pointers zero:
|
|
the first element writes all three, and watching only `_Mylast` cannot tell "allocated for the
|
|
first time" from "appended to an existing buffer". Those are different events in the model the
|
|
lane was trying to falsify.
|
|
* It **re-arms and re-logs on every End Turn**. `modcount` and `tshn` arm once on purpose - their
|
|
targets are chosen from one turn's state and re-picking them would move the measurement. `cont`
|
|
has no such target selection, the two manager pointers are stable for the life of the server,
|
|
and the per-turn count line is the signal.
|
|
* The canary self-test therefore runs every turn too, and its counter is read as a **delta**. On
|
|
the arm-once modes the delta is the old value, so their log lines are unchanged.
|
|
|
|
`ReportContainer` is factored out of `ArmTshnSlots` so both modes emit a byte-identical container
|
|
line; lane W3's published counts are the control every later count is compared against.
|
|
|
|
**`hooks=off watch=on` used to arm nothing.** `Shim_Init` returned before `install_watchpoints`
|
|
whenever the trace mode was `off`, so that config printed `watch=on` in the banner and then
|
|
measured a confident zero - the exact failure mode method rule 1 exists to catch. The watchpoints
|
|
are an independent instrument (one arming detour of their own, no trace records), and a long play
|
|
session wants them without paying 30-45 s per End Turn for template hooks that measure nothing it
|
|
is asking about. `main.cpp` now initialises MinHook and installs the watchpoint module on the
|
|
`hooks=off` path when `watch=on`.
|