Skip to content

Flag-transfer (pushfq/popfq) for the dispatch-region VM + recursed-layer resistance - #8

Merged
seifreed merged 5 commits into
mainfrom
dispatch-vm-flag-transfer
Aug 2, 2026
Merged

Flag-transfer (pushfq/popfq) for the dispatch-region VM + recursed-layer resistance#8
seifreed merged 5 commits into
mainfrom
dispatch-vm-flag-transfer

Conversation

@seifreed

@seifreed seifreed commented Aug 2, 2026

Copy link
Copy Markdown
Owner

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. Native pushfq/popfq bracketing a dispatch — previously unclassifiable, so the function was safely skipped — now lower to fsave/frestore items that save/restore the virtual RFLAGS through the vstack. Gated to the dispatch-region contract, so the straight-line contract is unchanged.

  • fsave/frestore items wired end to end (classify → op_key → item_size/encode → handler dispatch → handler asm).
  • Correctness fix: fsave reads the flags slot, so the dead-flag analysis counts it as a flags reader — otherwise an add/sub before a pushfq could be dead-eliminated and the save would capture a stale slot (discriminating unit test).
  • Load-bearing fixture dataset/elf_vm_interp_stack_x86_64 whose "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.

…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.
Copilot AI review requested due to automatic review settings August 2, 2026 20:29
@seifreed
seifreed merged commit 8d9494a into main Aug 2, 2026
2 of 14 checks passed
@seifreed
seifreed deleted the dispatch-vm-flag-transfer branch August 2, 2026 20:29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/frestore end-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 fsave as a flags reader to prevent incorrect elimination of flag-producing ops saved by a following pushfq.
  • 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`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants