Skip to content

fix: only allow select instructions in privileged mode#19

Closed
bmuddha wants to merge 1 commit into
mainfrom
bmuddha/fix/tighten-privileged-access
Closed

fix: only allow select instructions in privileged mode#19
bmuddha wants to merge 1 commit into
mainfrom
bmuddha/fix/tighten-privileged-access

Conversation

@bmuddha
Copy link
Copy Markdown
Collaborator

@bmuddha bmuddha commented May 19, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Refined access validation for privileged fee payers to only bypass writable-account restrictions when executing allowlisted Magic program control instructions.
  • Tests

    • Enhanced test coverage for Magic program instruction validation and account access control enforcement.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

Walkthrough

This PR implements a conditional access-validation bypass for privileged fee payers when transactions contain allowlisted Magic program instructions. The feature adds Magic program ID and discriminant constants, implements security logic to scan instructions and validate they match Magic and an allowlist, expands writable-account permissions to include confined accounts, and validates the changes through unit and integration tests.

Changes

Privileged Magic Instruction Allowlisting

Layer / File(s) Summary
Magic program constants and documentation
src/access_permissions.rs
Defines MAGIC_PROGRAM_ID and PRIVILEGED_MAGIC_DISCRIMINANTS constants; updates validate_accounts_access documentation to describe the conditional privilege bypass and expanded writable-account categories (now including confined).
Core validation logic with privilege bypass
src/access_permissions.rs
Implements conditional security check: privileged fee payers bypass non-delegated writable-account validation only when all instructions target Magic program and their decoded discriminants are in the allowlist. Expands allowed writable-account set to include confined alongside delegated, undelegating, and ephemeral for non-privileged cases.
Unit tests for Magic instruction validation
src/access_permissions.rs
Introduces test module with helpers for constructing privileged payer accounts and synthetic loaded transactions; covers three test cases validating acceptance of allowlisted Magic instructions, rejection of non-Magic programs, and rejection of unlisted Magic discriminants.
Integration tests for privileged fee payer scenarios
tests/integration_test.rs
Clarifies account-state assertion format; updates transfer transaction expectations in svm_inspect_account to explicitly expect ExecutedFailed status; renames privileged fee payer test case and validates that non-Magic instructions are rejected even for privileged payers, with expected lamport deductions.

🎯 3 (Moderate) | ⏱️ ~25 minutes


Possibly Related PRs

  • magicblock-labs/magicblock-svm#6: Both PRs modify validate_accounts_access logic—this PR adds privileged Magic-instruction-based bypass, while the referenced PR adjusts validation to return richer diagnostics.
  • magicblock-labs/magicblock-svm#12: Both PRs update allowed writable-account states in validate_accounts_access; this PR adds confined via Magic-based privileged bypass, while the referenced PR adds support for ephemeral.
  • magicblock-labs/magicblock-svm#5: Both PRs adjust fee-payer access eligibility; this PR conditionally bypasses checks for privileged payers with allowlisted Magic instructions, while the referenced PR strengthens validation that writable fee payers must be delegated or privileged.

Suggested Reviewers

  • GabrielePicco
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: restricting privileged access bypass to only allowlisted Magic program instructions, which is the primary focus of the modifications.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bmuddha/fix/tighten-privileged-access

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/access_permissions.rs`:
- Around line 128-145: The test coverage for the scan-all bypass is incomplete:
the helper message() (returning SanitizedMessage::Legacy via LegacyMessage::new)
and the existing tests only exercise single-instruction messages, so the bypass
could incorrectly use any()-style logic; add new regression tests that construct
multi-instruction messages using the message(...) helper (or a similar helper)
with one allowlisted Magic instruction and one instruction that is either a
non-Magic program or a Magic program not in the allowlist, then assert the
scan-all rule does NOT bypass (i.e., it must require full scanning). Update both
places referenced (the existing single-instruction tests around the message(...)
helper and the other block noted at lines ~148-185) to include this
mixed-instruction case so the bypass only applies when every instruction is
allowlisted Magic.
- Around line 38-62: The current logic lets a privileged payer with zero
instructions bypass because `privileged` remains true; change the loop in the
block that iterates `message.instructions_iter()` to require at least one
allowlisted Magic instruction before returning early. Add a boolean (e.g.,
`found_magic`) initialized false, set it true only when you encounter an
instruction whose program equals `MAGIC_PROGRAM_ID` and whose discriminant is in
`PRIVILEGED_MAGIC_DISCRIMINANTS`; keep using the existing checks inside the
loop, but at the end only treat the transaction as privileged (and reach the
existing `return Ok(())`) if both the original `privileged` condition and
`found_magic` are true. Update references to `privileged`/early-break behavior
accordingly so an empty instruction list no longer bypasses.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: da597afb-343e-431f-ac28-571e4c29f9f9

📥 Commits

Reviewing files that changed from the base of the PR and between 5ea30a7 and 05bf475.

📒 Files selected for processing (2)
  • src/access_permissions.rs
  • tests/integration_test.rs

Comment thread src/access_permissions.rs
Comment on lines +38 to 62
let mut privileged = payer.is_some_and(AccountSharedData::privileged);
if privileged {
for i in message.instructions_iter() {
let Some(program) = message.account_keys().get(i.program_id_index as usize) else {
privileged = false;
break;
};
if *program != MAGIC_PROGRAM_ID {
privileged = false;
break;
}
let discriminant = i
.data
.get(0..4)
.and_then(|b| <[u8; 4]>::try_from(b).ok())
.map(u32::from_le_bytes)
.unwrap_or(u32::MAX);
if !PRIVILEGED_MAGIC_DISCRIMINANTS.contains(&discriminant) {
privileged = false;
break;
}
}
}
if privileged {
return Ok(());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Require at least one allowlisted Magic instruction before bypassing.

As written, a privileged payer with an empty instruction list keeps privileged == true and returns Ok(()), even though the transaction does not contain any allowlisted Magic instruction. That broadens the bypass beyond the documented contract.

Suggested fix
         let payer = self.accounts.first().map(|(_, acc)| acc);
         let mut privileged = payer.is_some_and(AccountSharedData::privileged);
+        let mut saw_instruction = false;
         if privileged {
             for i in message.instructions_iter() {
+                saw_instruction = true;
                 let Some(program) = message.account_keys().get(i.program_id_index as usize) else {
                     privileged = false;
                     break;
                 };
                 if *program != MAGIC_PROGRAM_ID {
@@
                 if !PRIVILEGED_MAGIC_DISCRIMINANTS.contains(&discriminant) {
                     privileged = false;
                     break;
                 }
             }
         }
-        if privileged {
+        if privileged && saw_instruction {
             return Ok(());
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let mut privileged = payer.is_some_and(AccountSharedData::privileged);
if privileged {
for i in message.instructions_iter() {
let Some(program) = message.account_keys().get(i.program_id_index as usize) else {
privileged = false;
break;
};
if *program != MAGIC_PROGRAM_ID {
privileged = false;
break;
}
let discriminant = i
.data
.get(0..4)
.and_then(|b| <[u8; 4]>::try_from(b).ok())
.map(u32::from_le_bytes)
.unwrap_or(u32::MAX);
if !PRIVILEGED_MAGIC_DISCRIMINANTS.contains(&discriminant) {
privileged = false;
break;
}
}
}
if privileged {
return Ok(());
let mut privileged = payer.is_some_and(AccountSharedData::privileged);
let mut saw_instruction = false;
if privileged {
for i in message.instructions_iter() {
saw_instruction = true;
let Some(program) = message.account_keys().get(i.program_id_index as usize) else {
privileged = false;
break;
};
if *program != MAGIC_PROGRAM_ID {
privileged = false;
break;
}
let discriminant = i
.data
.get(0..4)
.and_then(|b| <[u8; 4]>::try_from(b).ok())
.map(u32::from_le_bytes)
.unwrap_or(u32::MAX);
if !PRIVILEGED_MAGIC_DISCRIMINANTS.contains(&discriminant) {
privileged = false;
break;
}
}
}
if privileged && saw_instruction {
return Ok(());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/access_permissions.rs` around lines 38 - 62, The current logic lets a
privileged payer with zero instructions bypass because `privileged` remains
true; change the loop in the block that iterates `message.instructions_iter()`
to require at least one allowlisted Magic instruction before returning early.
Add a boolean (e.g., `found_magic`) initialized false, set it true only when you
encounter an instruction whose program equals `MAGIC_PROGRAM_ID` and whose
discriminant is in `PRIVILEGED_MAGIC_DISCRIMINANTS`; keep using the existing
checks inside the loop, but at the end only treat the transaction as privileged
(and reach the existing `return Ok(())`) if both the original `privileged`
condition and `found_magic` are true. Update references to
`privileged`/early-break behavior accordingly so an empty instruction list no
longer bypasses.

Comment thread src/access_permissions.rs
Comment on lines +128 to +145
fn message(program: Pubkey, data: Vec<u8>) -> SanitizedMessage {
SanitizedMessage::Legacy(LegacyMessage::new(
Message {
account_keys: vec![Pubkey::new_unique(), Pubkey::new_unique(), program],
header: MessageHeader {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 1,
},
instructions: vec![CompiledInstruction {
program_id_index: 2,
accounts: vec![1],
data,
}],
recent_blockhash: Hash::default(),
},
&ReservedAccountKeys::empty_key_set(),
))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add a mixed-instruction regression test for the scan-all rule.

The bypass logic is only safe if every instruction is Magic + allowlisted, but the new tests only cover single-instruction messages. Please add a case with one allowlisted Magic instruction plus one non-Magic or unlisted Magic instruction so an accidental any()-style regression gets caught.

Also applies to: 148-185

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/access_permissions.rs` around lines 128 - 145, The test coverage for the
scan-all bypass is incomplete: the helper message() (returning
SanitizedMessage::Legacy via LegacyMessage::new) and the existing tests only
exercise single-instruction messages, so the bypass could incorrectly use
any()-style logic; add new regression tests that construct multi-instruction
messages using the message(...) helper (or a similar helper) with one
allowlisted Magic instruction and one instruction that is either a non-Magic
program or a Magic program not in the allowlist, then assert the scan-all rule
does NOT bypass (i.e., it must require full scanning). Update both places
referenced (the existing single-instruction tests around the message(...) helper
and the other block noted at lines ~148-185) to include this mixed-instruction
case so the bypass only applies when every instruction is allowlisted Magic.

@bmuddha bmuddha closed this May 19, 2026
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.

1 participant