diff --git a/website/public/blog/reverse-engineering-models/index.html b/website/public/blog/reverse-engineering-models/index.html index 2b3c4c1ce..9a94ee811 100644 --- a/website/public/blog/reverse-engineering-models/index.html +++ b/website/public/blog/reverse-engineering-models/index.html @@ -167,7 +167,7 @@ >Model capability changes the hypotheses an agent can find. - Later sessions have opened functions that earlier work left + Later sessions have solved problems that earlier work left unresolved.
Much of that work aims at exact reconstruction: we compile the maintained source under a pinned historical toolchain and check - whether it reproduces the target function. That gives us something - concrete to examine when a later model closes a function that - earlier work left unresolved. The source changes show what made - the breakthrough possible. + whether it reproduces the target function. When a later model + succeeds where earlier work stalled, the source changes show what + made that progress possible.
- In one TH09 attack function, much of the logic was already in - place, but the compiled candidate remained a byte short. Although - its stack frame had the right size, three temporary vectors - occupied different slots from the original. Matching the overall - size of the workspace had concealed a difference inside it. + Much of a TH09 attack function's logic had already been + reconstructed, but the compiler still stored temporary results + differently from the original. The remaining problem was how long + those results needed to stay alive and when their storage could be + reused.
A @@ -257,22 +256,19 @@ values needed to stay alive.
- Each vector was returned from a calculation and copied into an - animation's position. Because C++ keeps a returned temporary alive - until the end of the expression using it, grouping the real - operations into one expression kept the three values alive - together. The compiler could no longer reuse the same storage for - all of them. + The function produced temporary results and copied them into + animation positions. In C++, a temporary result stays alive until + the expression using it finishes. Grouping the real operations + into one expression kept those results alive together, so the + compiler could no longer reuse the same storage for all of them.
- That recovered the distinct temporary slots, but one still shared - storage with a real collision workspace used later. Keeping that - workspace at function scope separated it from the returned values. - With both changes in place, the seventh targeted candidate in the - closing session matched. A cold build confirmed + One result still shared storage with a value used later for + collision checks. Giving that value its own lifetime resolved the + remaining overlap. After several targeted experiments, the complete 1,436-byte function and all 44 encoded - fieldsa clean rebuild confirmed that the full function + matched.
@@ -290,22 +286,19 @@ href="https://github.com/N0zoM1z0/th09/commit/f8d12d72aaf02212ee1b585a8f3f86d7ed000269" >GPT-6.1-Sol's earlier work - reproduced two diagnostic regions, yet the complete candidate - remained 1,510 bytes against a 1,491-byte target. Those matching - regions did not resolve the way the branches joined up. + got parts of the function to match, but the branches still joined + up differently from the original.
GPT-6-Astra changed the surrounding control-flow graph, making completion and frame processing complementary branches - followed by one common frame increment and return. Once the - compiler saw the branches in that context, it produced the - required block sharing. The entire 1,491-byte function matched, - and a + >. The main paths now led to a shared final update and return. In + that source structure, the compiler combined the branches in the + same way as the original. The complete function matched, and a recompile replay was acceptedseparate rebuild check confirmed it.
@@ -351,21 +344,20 @@
In TH095, for example,
even the identifier backing a real local variable could change
- where VC7.1 put it on the stackeven the name used for a local variable could change where
+ VC7.1 stored it. Moving the declaration alone did not solve the layout, but an
- earlier background function had exposed a useful identifier
- bucket. That gave us a more promising hypothesis for
+ earlier background function had revealed a naming pattern that
+ affected the compiler's choice. That gave us a more promising
+ hypothesis for
the reconstruction of Supervisor::LoadDat.
- Reusing the known backing identifier for the real file-size - variable put it and the character buffer in the required homes, - without changing the buffer's genuine 128-byte size. With that - layout, all 151 bytes of the function and its 15 relocations + Reusing that pattern for the file-size variable produced the + required layout without adding storage. The full function then matched.
@@ -384,16 +376,17 @@
The useful patterns would have to retain their context, though. - TH08 uses VC7, while TH095 and TH09 use VC7.1. TH04's segmented - DOS environment uses Turbo C++, and its + TH08 uses an earlier Visual C++ version than TH095 and TH09. TH04 + uses a different DOS-era compiler, and its player reconstruction - needed a different set of lifetime and linker-order hypotheses. - Shared authorship gives us a prior, but each target tells us - whether it applies. Distinguishing those contexts is part of what - a specialist would need to learn. + needed different hypotheses about source patterns and how the + compiled code was combined into an executable. Shared authorship + gives us a prior, but each target tells us whether it applies. + Distinguishing those contexts is part of what a specialist would + need to learn.
- For the 373-byte TH09 enemy-spawn function, a + When reconstructing TH09's enemy-spawn function, a GPT-6-Luna-max session drew a hypothesis from TH08's corresponding source family. That - earlier source suggested an ordinary loop with branch-local - failure handling. TH09's own evidence supplied the bounds and - calls, and two cold builds confirmed the complete match. + earlier source suggested how to structure the loop and handle + failures within it. TH09's own evidence supplied the game-specific + details, and clean rebuilds confirmed the complete match.
- Consider the temporary-vector example again. Its final source
+ Consider the temporary-results example again. Its final source
shows the arrangement that worked, but the route to that source
contains more information. Each of the following experiments
helped identify what still needed to change.
@@ -469,33 +462,35 @@
- Selected steps from the recorded Type18/24 search. The closing
- session tested seven candidates; this table groups the lessons.
+ Selected experiments from the attack-function reconstruction
+ above, grouped by what they taught us.
A training record that keeps this sequence can teach more than the
@@ -554,11 +549,10 @@
measurement. In TH08, for example, a
later collision divergence led back to a one-ULP numerical
- difference
- and the old x87 behavior. Before the agent could test a useful
- correction, it had to work out what to measure.
+ >collision difference led back to how the original program
+ handled floating-point calculations. Before the agent could test a useful correction, it had to work
+ out what to measure.
This suggests another role for a broadly capable model: helping
@@ -590,10 +584,9 @@
href="https://github.com/N0zoM1z0/th08/commit/a393f400786e4cc6cbb1000af0dad3276da12c78"
>wider audit
- examined all 1,548 floating-literal references. It corrected
- twelve stale rows across five accepted functions, including a
- bullet-spread increment of
replay-save candidate
- already had the target's 1,825-byte length, yet 467 byte positions
- differed. Even a byte-difference count can change abruptly when a
- branch encoding changes. Such diagnostics can guide the next
- experiment, but I would validate any reward for "getting closer"
- against the full acceptance check.
+ looked promising because the compiled function had the expected
+ size, but a full comparison still found substantial differences.
+ Changes in how the compiler arranges instructions can also make a
+ candidate look closer without resolving the underlying problem.
+ Such diagnostics can guide the next experiment, but I would
+ validate any reward for "getting closer" against the full
+ acceptance check.
I would be interested in the pattern across these results. If a
@@ -741,10 +736,10 @@
- The examples describe historical reconstruction checks recorded
- in public commits. Their byte counts apply to the stated
- functions, not whole-game runtime correctness. Source and
- matching records were reviewed on 13 October 2026.
+ The examples describe historical function-level comparisons
+ recorded in the linked commits. Whole-game runtime checks are
+ separate milestones. Source and matching records were reviewed
+ on 13 October 2026.
The TH09 label
-
Use a typed position assignment
+ Try a different way to copy the results
- The copy behavior improves, but the compiled source still
- differs.
+ The copy code changes, but the function still differs.
-
Consume the returned vectors directly
+ Use the returned values directly
- The function reaches the target size. Temporary storage
- remains wrong.
+ Copying follows the original pattern, but temporary
+ storage still differs.
-
Group the real operations into one full expression
- The returned values remain alive together.
+ Keep the temporary results alive together
+
+ They get separate storage, but one still overlaps a value
+ used later for collision checks.
+
-
Keep the real collision workspace separate
- The complete function matches in a cold build.
+ Keep the collision-check value separate
+ A clean rebuild confirms the complete match.
1.0f where the target
- required 32.0f.
+ found incorrect constants in functions that had already been
+ accepted. In one calculation, the reconstructed source added
+ 1.0f where the original added 32.0f.
Evidence and model attribution
gpt-dots identifies GPT-6-Astra.