chunk: exhaustive OpCode operand tables close the verifier drift (#737) - #804
Merged
Merged
Conversation
op_name, op_verify_operands, and op_stack_effect each switched on uint8_t with a default: arm, so -Werror=switch (already in CFLAGS) could not enforce completeness — and three had already drifted: - op_verify_operands was missing OP_TRAJECTORY_SLOT while its NAME sibling was present. The untrusted-chunk verifier then walked that 3-byte instruction as 1 byte and marked its two operand bytes as valid instruction boundaries, so a crafted jump could land mid-instruction and still pass pass 2 — the #721 surface, reopened. - op_name printed 4 opcodes (REPORT_SLOT/REPORT_NAME/ OBSERVE_VALUE_SLOT/OBSERVE_VALUE_NAME) as "???". - the disassembler's separate op_has_u16 boolean had drifted on 8 opcodes and could not express the multi-operand superinstructions, desyncing on 15. All three now switch on OpCode with NO default arm — a new opcode is a build error at each site, using enforcement the Makefile already pays for. op_name's strings are derived from the enum spellings (#o + 3), so a name cannot drift from its opcode. The disassembler is driven off the verifier's operand table (op_has_u16 deleted) — one layout source, not two. Red-then-green: test_vm_run_bytecode.eigs asserts a jump into OP_TRAJECTORY_SLOT's operand is refused; on the drifted table (verified by reintroducing the default arm) that chunk ran to 7. Not done here (tracked in #737): collapsing the tables into one X-macro row, and the CLAUDE.md skill-path note (the three cited skills exist as hq roles, loaded via the Skill tool, not files under .claude/skills). Closes #737 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR hardens the bytecode verifier/disassembler metadata against opcode-table drift by making operand/name/stack-effect mappings compile-time enforced (exhaustive switch over OpCode with no default:), closing a verifier hole that could allow jumps into operand bytes.
Changes:
- Make
op_name,op_verify_operands, andop_stack_effectexhaustiveswitch ((OpCode)...)implementations so missing opcodes become-Werror=switchbuild failures. - Remove the disassembler’s separate operand-width logic and drive
chunk_disassemblestepping from the verifier’s operand-layout table (single source of truth). - Add a planted-fault regression test ensuring jumps into
OP_TRAJECTORY_SLOToperand bytes are rejected, and document the fix in the changelog.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| tests/test_vm_run_bytecode.eigs | Adds a regression test that rejects a jump landing inside OP_TRAJECTORY_SLOT’s operand bytes (the drift-enabled verifier hole). |
| src/compiler.c | Makes op_stack_effect an exhaustive OpCode switch (no default:) to prevent silent stack-depth drift. |
| src/chunk.c | Makes op_name and op_verify_operands exhaustive and drives the disassembler operand stepping from the verifier operand table. |
| CHANGELOG.md | Documents the drift-proofing change and the verifier-hole fix under Unreleased. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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.
Closes #737.
The fix: silent-wrong → build error
The three opcode operand-layout tables (
op_name,op_verify_operands,op_stack_effect) all switched onuint8_t opwith adefault:arm, so-Werror=switch— already in CFLAGS — structurally could not apply. Each is now an exhaustiveswitch ((OpCode)op)with nodefault:arm: a new opcode is a compile error at every site, using enforcement the Makefile already pays for. This is the same instinct as the#630operand-width lesson — make the drift impossible, not just fixed.The three drifts it closed
op_verify_operandswas missingOP_TRAJECTORY_SLOTwhile itsNAMEsibling was present, so the untrusted-chunk verifier walked that 3-byte instruction as 1 byte and marked its two operand bytes as valid instruction boundaries — a crafted jump could land mid-instruction and pass pass 2. This is the exact#721surface, reopened through the drifted table. Now verified.op_nameprinted 4 opcodes as"???"(REPORT_SLOT,REPORT_NAME,OBSERVE_VALUE_SLOT,OBSERVE_VALUE_NAME). The names are now derived from the enum spellings (#op + 3), so a name cannot drift from its opcode again.op_has_u16boolean had drifted on 8 single-operand opcodes and structurally couldn't express the multi-operand superinstructions.op_has_u16is deleted;chunk_disassemblenow steps off the verifier's operand table — one layout source, not two.Red-then-green
test_vm_run_bytecode.eigsgains a planted-fault gate for the verifier hole: a chunk whoseJUMPlands insideOP_TRAJECTORY_SLOT's operand is refused (returnsnull). I verified it bites by reintroducing thedefault:arm and removing theTRAJECTORY_SLOTcase — the chunk then passed verification and ran to7. (Note the exhaustive switch means the pre-fix state can only be reconstructed by also re-adding adefault:arm — removing just the case is now a build error, which is the point.) A positive control (jump one instruction past the operand → accepted, returns7) proves the fix doesn't blanket-reject trajectory-bearing chunks.Deliberately not in this PR (from the issue's "also noted")
eigenscript-extend-vm,eigenscript-jit,aot-differential). These are not missing — they exist as hq roles loaded via the Skill tool (I used two of them while writing this fix), just not as files under the repo's.claude/skills/. Accurate for the maintainer environment; a contributor-facing clarification is a docs question for a separate change._Static_assertcoverage. The#630operand-width break is already gated bytest_vm_run_bytecode.eigs(it pinsOP_LINE's 4-byte operand); broader static-assert hardening is its own hardening pass.Validation
-Werror=switch(the whole mechanism);make jit-smokegreen.detect_leaks=1suite 3368/3368, leak tally 0.got 7, expectednull) against a reintroduceddefault:arm.🤖 Generated with Claude Code