Flag-transfer (pushfq/popfq) for the dispatch-region VM + recursed-layer resistance - #8
Merged
Merged
Conversation
…tems A stack-based interpreter brackets its computed jump with pushfq/popfq to preserve the CPU flags across dispatch. The region VM synthesizes an operation's readable flags into its own flags slot rather than native RFLAGS, so a native flag save/restore now lowers to fsave/frestore items that copy that slot to and from the virtual operand stack. Both the flags slot and the vstack persist across the vm_dispatch re-entry the ijmp performs, so flag state crosses the dispatch inside the VM's own frame. Classification is gated to the dispatch-region contract (the only shape where flag state crosses the jump), so the straight-line contract keeps leaving pushfq/popfq native. Covered by tests/unit/test_code_virtualization_fsave_classify.py (classification, handler key, item size) and tests/unit/test_code_virtualization_fsave_handler.py (the handlers read/write the flags slot, advance one opcode byte, and a region carrying the items assembles to real machine code).
fsave (the virtualized pushfq) copies the flags slot onto the vstack, so it reads the flags. The dead-flag analysis only counted jcc and exit as readers, so an add/sub whose flags are saved by a following pushfq - with no jcc after it - was wrongly marked flag-dead (lowered to an MBA handler that never writes the flags slot), and the save would capture a stale slot. Count fsave among the flag readers so such an op stays flag-live and writes the slot before it is saved. Covered by tests/unit/test_code_virtualization_fsave_flag_liveness.py: an add followed by fsave is not dead-eliminated (fails without this change), while an add overwritten with no reader in between still is.
Add a hand-built bytecode interpreter whose "check" opcode preserves a condition flag across a flag-live scratch computation by bracketing it with pushfq/popfq - the flag-preservation idiom, the shape where flag state crosses the computed jump. Before flag-transfer support the pushfq made the function unclassifiable and the opt-in dispatch path left it native; now it extracts into an ijmp region carrying fsave/frestore items and the mutated binary still emulates to 45. The fixture is load-bearing: the accumulator only stays 45 if the preserved condition survives the bracket, so a broken save/restore changes the exit code. Covered by tests/integration/test_vm_interpreter_flagcross_real.py: with virtualize_dispatch the function virtualizes and keeps exit 45; with the flag off it stays native. Runs the real pass on the real binary through the Unicorn exit-code harness - no mocks.
Literal recursion wraps the interpreter in the region VM, whose dispatch is anti-devirtualization by design: threaded, no central hub, register-indexed with XOR-encrypted offsets - not the absolute memory-indirect table the recovery oracle reconstructs. Recursion is proven behaviorally by the exit-code round-trip; this pins the resistance that shape buys, turning the honest finding into an enforced property. Virtualizing the interpreter raises its structural resistance sharply (more instructions, more indirect dispatch, higher score than the original) and the oracle cannot reconstruct the region VM's four real handlers. Covered by tests/integration/test_vm_interpreter_recursion_resistance_real.py, driving the real structural probe and the real VMHandlerAnalyzer on the real recursively-virtualized binary - no mocks.
…ursion note The design note's progress log still listed pushfq/popfq flag-transfer as deferred and the recursed layer's resistance as unpinned. Both shipped: flag-transfer is now a built-and-green item (fsave/frestore through the vstack, gated to the dispatch-region contract, with fsave counted as a flags reader), and the recursed layer's devirtualization resistance is pinned by a test. Update section 5 so the log tracks reality; the frozen pre-implementation analysis (sections 1-4) is left as the record it is.
There was a problem hiding this comment.
Pull request overview
Adds opt-in support for virtualizing dispatch-shaped (computed-jump) interpreter loops in the region VM when they use native flag save/restore (pushfq/popfq), by lowering them into VM-level fsave/frestore items that preserve the region’s virtual RFLAGS slot across vm_dispatch. Also pins “recursed-layer” devirtualization resistance as an enforced, tested property.
Changes:
- Implement
fsave/frestoreend-to-end (classification, op-keying, encoding sizing, handler emission, and handler micro-op asm) for the dispatch-region contract. - Update dead-flag analysis to treat
fsaveas a flags reader to prevent incorrect elimination of flag-producing ops saved by a followingpushfq. - Add unit + integration tests covering classification, handler asm wiring, flag-liveness correctness, real-binary flag-crossing behavior, and recursion resistance.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/test_code_virtualization_fsave_handler.py | Unit tests asserting handler asm reads/writes the flags slot and returns to vm_dispatch; plus region assembly wiring smoke test. |
| tests/unit/test_code_virtualization_fsave_flag_liveness.py | Unit tests pinning that fsave counts as a flag reader in dead-flag analysis. |
| tests/unit/test_code_virtualization_fsave_classify.py | Unit tests for pushfq/popfq lowering under opt-in plus encoding/op-key expectations. |
| tests/integration/test_vm_interpreter_recursion_resistance_real.py | Integration test pinning increased structural resistance and oracle non-reconstruction on a recursively virtualized interpreter. |
| tests/integration/test_vm_interpreter_flagcross_real.py | Integration test virtualizing a real interpreter fixture whose flags cross dispatch, asserting preserved exit code with opt-in and unchanged behavior without it. |
| r2morph/mutations/code_virtualization_region.py | Classifier support for pushf*/popf* under allow_computed_jump; dead-flag analysis treats fsave as a reader. |
| r2morph/mutations/code_virtualization_region_models.py | Adds distinct op-key families for fsave/frestore. |
| r2morph/mutations/code_virtualization_region_microops.py | Implements _fsave_handler_asm / _frestore_handler_asm micro-op handlers. |
| r2morph/mutations/code_virtualization_region_codegen.py | Wires handler emission for fsave/frestore. |
| r2morph/mutations/code_virtualization_region_codegen_encode.py | Defines opcode-only item sizing and emits fsave/frestore opcodes. |
| docs/vm-literal-recursion.md | Updates literal-recursion documentation to include flag-transfer support and the new pinned resistance property. |
| dataset/elf_vm_interp_stack_x86_64.S | Adds the load-bearing stack-interpreter fixture source that uses pushfq/popfq around a flag-live scratch op. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+240
to
+242
| `tests/unit/test_code_virtualization_fsave_classify.py`, | ||
| `..._fsave_handler.py`, `..._fsave_flag_liveness.py`, and the load-bearing round trip | ||
| `tests/integration/test_vm_interpreter_flagcross_real.py` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lifts the two deferred boundaries of the opt-in dispatch-region VM (docs/vm-literal-recursion.md).
Flag-transfer (pushfq/popfq)
The region VM keeps a persistent virtual RFLAGS slot and vstack that both survive
vm_dispatch. Nativepushfq/popfqbracketing a dispatch — previously unclassifiable, so the function was safely skipped — now lower tofsave/frestoreitems that save/restore the virtual RFLAGS through the vstack. Gated to the dispatch-region contract, so the straight-line contract is unchanged.fsave/frestoreitems wired end to end (classify → op_key → item_size/encode → handler dispatch → handler asm).fsavereads the flags slot, so the dead-flag analysis counts it as a flags reader — otherwise an add/sub before apushfqcould be dead-eliminated and the save would capture a stale slot (discriminating unit test).dataset/elf_vm_interp_stack_x86_64whose "check" opcode preserves a condition across a flag-live scratch op; a broken save/restore changes the exit code (verified: 88 vs 45).Recursed-layer resistance
Recovering the recursed layer symbolically would defeat the region VM's own anti-devirtualization design, so it is kept by design and instead pinned as an enforced property: structural devirtualization resistance rises sharply after recursive virtualization and the recovery oracle cannot reconstruct the region VM's handler set.
Tests
New unit tests (classify, handler, flag-liveness) and integration tests (flag-crossing round trip, recursion resistance). Full VM/code_virtualization suite: 673 passed. black/ruff clean; no new mypy/bandit findings.