Skip to content

Conversation

@dbanks12
Copy link
Contributor

@dbanks12 dbanks12 commented Jan 9, 2026

Summary

Fixes ghost row injection vulnerabilities in three PIL traces where sub-selectors firing permutations were not constrained to be 0 when the main selector was inactive.

The vulnerability pattern: When sel=0 (gadget inactive), sub-selectors like is_write_memory_value could still be set to 1 by a malicious prover. This would fire permutations to memory, allowing arbitrary memory writes without going through legitimate execution paths.

The fix pattern: Add constraints of the form sub_selector * (1 - sel) = 0 which forces sub-selectors to 0 when sel=0.

Changes

PIL Fixes

File Constraint Added
emit_unencrypted_log.pil #[IS_WRITE_MEMORY_VALUE_REQUIRES_SEL]
get_contract_instance.pil #[IS_VALID_WRITES_IN_BOUNDS_REQUIRES_SEL]
to_radix_mem.pil #[SEL_SHOULD_WRITE_MEM_REQUIRES_SEL]

Test Updates

  • Updated ghost row injection tests to verify fixes block the attacks
  • Tests use EXPECT_THROW_WITH_MESSAGE to confirm constraints fail as expected
  • Removed debug output (cout statements)
  • Renamed tests from NegativeFullAttackWithAllTraces to NegativeGhostRowInjectionBlocked

Copy link
Contributor Author

dbanks12 commented Jan 9, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@dbanks12 dbanks12 force-pushed the db/ghost-row-injections branch 2 times, most recently from 62a7ade to 5632cdc Compare January 10, 2026 00:05
@dbanks12 dbanks12 marked this pull request as ready for review January 10, 2026 00:06
@dbanks12 dbanks12 requested review from sirasistant and removed request for IlyasRidhuan, Maddiaa0 and fcarreiro January 10, 2026 00:06
// if we are not ending, the next is_write_memory_value is equal to current is_write_memory_value or is_write_contract_address
// since both can't be on at the same time, we can use add.
NOT_END * (is_write_memory_value + is_write_contract_address - is_write_memory_value') = 0;
// Force is_write_memory_value to 0 when sel is 0 (prevents ghost row injection attacks)
Copy link
Contributor

Choose a reason for hiding this comment

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

We could instead do sel_should_read_memory = sel * is_write_memory_value * (1 - error_out_of_bounds);. However it might not be necessary, since it does control a permutation, but a read only one.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree it is not a problem because superfluous illegitimate memory reads do not harm.
However, it is cleaner and cost is very minimal.
I would also favor the proposal of @sirasistant to add the gate sel.

WRITES_OUT_OF_BOUNDS * is_valid_member_enum = 0;
// Force is_valid_member_enum to 0 when sel is 0 (prevents ghost row injection attacks)
#[IS_VALID_MEMBER_ENUM_REQUIRES_SEL]
is_valid_member_enum * (1 - sel) = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we instead do is_valid_writes_in_bounds*(1-sel) = 0? is upper in the chain of selectors, and we also disable is_valid_member_enum if is_valid_writes_in_bounds is zero

    #[IS_VALID_MEMBER_ENUM_ONLY_SET_BY_PRECOMPUTED_LOOKUP]
    WRITES_OUT_OF_BOUNDS * is_valid_member_enum = 0;

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it is a good idea @sirasistant. I would suggest then that @dbanks12 adds a comment that is_valid_member_enum == 0 outside of the active trace portion as a consequence of
#[IS_VALID_MEMBER_ENUM_ONLY_SET_BY_PRECOMPUTED_LOOKUP] and the new relation.

NOT_LAST * (sel_should_write_mem' - sel_should_write_mem) = 0;
// Force sel_should_write_mem to 0 when sel is 0 (prevents ghost row injection attacks)
#[SEL_SHOULD_WRITE_MEM_REQUIRES_SEL]
sel_should_write_mem * (1 - sel) = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

// if we are not ending, the next is_write_memory_value is equal to current is_write_memory_value or is_write_contract_address
// since both can't be on at the same time, we can use add.
NOT_END * (is_write_memory_value + is_write_contract_address - is_write_memory_value') = 0;
// Force is_write_memory_value to 0 when sel is 0 (prevents ghost row injection attacks)
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree it is not a problem because superfluous illegitimate memory reads do not harm.
However, it is cleaner and cost is very minimal.
I would also favor the proposal of @sirasistant to add the gate sel.

WRITES_OUT_OF_BOUNDS * is_valid_member_enum = 0;
// Force is_valid_member_enum to 0 when sel is 0 (prevents ghost row injection attacks)
#[IS_VALID_MEMBER_ENUM_REQUIRES_SEL]
is_valid_member_enum * (1 - sel) = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it is a good idea @sirasistant. I would suggest then that @dbanks12 adds a comment that is_valid_member_enum == 0 outside of the active trace portion as a consequence of
#[IS_VALID_MEMBER_ENUM_ONLY_SET_BY_PRECOMPUTED_LOOKUP] and the new relation.

// =====================================================================
// Ghost Row Injection Vulnerability Tests
// =====================================================================
// These tests verify that ghost rows (sel=0) cannot fire permutations.
Copy link
Contributor

Choose a reason for hiding this comment

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

If we keep this one, I would welcome a little comment that it is more of a "defensive/sanity" measure because this is a "memory read" and therefore there is no known exploitability.

@dbanks12 dbanks12 force-pushed the db/ghost-row-injections branch from 5632cdc to 1ae3f53 Compare January 12, 2026 15:47
@dbanks12 dbanks12 force-pushed the db/ghost-row-injections branch from 1ae3f53 to 1d5608f Compare January 12, 2026 16:10
Copy link
Contributor

@jeanmon jeanmon left a comment

Choose a reason for hiding this comment

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

Approved but please address my comment.

// since both can't be on at the same time, we can use add.
NOT_END * (is_write_memory_value + is_write_contract_address - is_write_memory_value') = 0;
// Force is_write_memory_value to 0 when sel is 0 (prevents ghost row injection attacks)
#[IS_WRITE_MEMORY_VALUE_REQUIRES_SEL]
Copy link
Contributor

Choose a reason for hiding this comment

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

But now @dbanks12 you can remove this one. Because, by definition, we have sel ==0 => sel_should_read_memory == 0.

@dbanks12 dbanks12 force-pushed the db/ghost-row-injections branch from 1d5608f to 5d70edb Compare January 12, 2026 16:53
@dbanks12 dbanks12 force-pushed the db/ghost-row-injections branch from 5d70edb to c5ce137 Compare January 12, 2026 17:28
@AztecBot
Copy link
Collaborator

Flakey Tests

🤖 says: This CI run detected 2 tests that failed, but were tolerated due to a .test_patterns.yml entry.

\033FLAKED\033 (8;;http://ci.aztec-labs.com/12aaab41e1425053�12aaab41e14250538;;�):  yarn-project/end-to-end/scripts/run_test.sh simple src/e2e_p2p/multiple_validators_sentinel.parallel.test.ts "collects attestations for validators in proposer node when block is not published" (106s) (code: 1) group:e2e-p2p-epoch-flakes (\033dbanks12\033: fix them)
\033FLAKED\033 (8;;http://ci.aztec-labs.com/43e8a8abfc6d8a58�43e8a8abfc6d8a588;;�):  yarn-project/end-to-end/scripts/run_test.sh simple src/e2e_p2p/broadcasted_invalid_block_proposal_slash.test.ts (196s) (code: 1) group:e2e-p2p-epoch-flakes (\033dbanks12\033: fix them)

@dbanks12 dbanks12 merged commit dd7eba0 into merge-train/avm Jan 12, 2026
9 checks passed
@dbanks12 dbanks12 deleted the db/ghost-row-injections branch January 12, 2026 17:59
@AztecBot AztecBot mentioned this pull request Jan 12, 2026
github-merge-queue bot pushed a commit that referenced this pull request Jan 12, 2026
BEGIN_COMMIT_OVERRIDE
feat(avm security): add static check for isolated/unused columns
(#19489)
feat(avm): use noop calldata hasher in fast sim (#19495)
chore(avm): rename indirect -> addressing mode (#19491)
chore(avm): small cursor optimizations
chore(avm):! rename indirect -> addressing mode (PIL) (#19493)
fix(avm): constraint when unwinding empty call stack (#19485)
feat(avm): Fuzz debug log and refactor env getter (#19494)
fix!: ecc add predicate completeness bug (#19471)
chore(avm): callstackmetadatacollector clarifications (#19490)
chore: sanity assert in execution for bytecode id (#19486)
fix!: sstore allowed injection of malicious write rows (#19470)
fix!: defensive ghost row constraints in bc_hashing pil (#19481)
fix(avm): fix execution::mov for mac? (#19507)
chore(avm)!: resolve execution TODOs (#19501)
fix!: multiple traces had ghost row injection vulnerabilities (#19480)
fix(avm): defensively copy MemoryValues (#19512)
feat: align TS and BB log levels (#19518)
END_COMMIT_OVERRIDE
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.

5 participants