Repository: anza-xyz/agave
Pull request #5158: Fix - Invoke non program account owned by a builtin
Author: login: Lichtso
Created: 2025-03-05T19:31:10Z | Merged: 2025-03-10T12:25:08Z

#### Problem
Currently there is a slight bug once `remove_accounts_executable_flag_checks` is active:
One can invoke a built-in program by invoking any account owned by it instead. This leads to the built-in running as a different pubkey, thus all ownership checks fail and the built-in has no write access to anything.

This is benign as it only allows invoking built-in programs in a strange nonsensical way. But, it is still a stupid thing to support and would hinder future protocol changes such as [these in the account loader](https://github.com/anza-xyz/agave/blob/30c95ff6e82c4437946a8753c247144504dfbfdb/svm/src/account_loader.rs#L500-L515).

About rekeying: Well see [Discord discussion](https://discord.com/channels/428295358100013066/910937142182682656/1346924137418854432).

#### Summary of Changes
Blocks the execution of any non loader owned or non built-in account and adds `test_invoke_non_program_account_owned_by_a_builtin()` to demonstrate the change in behavior.

Feature Gate Issue: https://github.com/anza-xyz/feature-gate-tracker/issues/69


Diff:
diff --git a/program-runtime/src/invoke_context.rs b/program-runtime/src/invoke_context.rs
index 65c9cd1638827e..e2adf026fe4249 100644
--- a/program-runtime/src/invoke_context.rs
+++ b/program-runtime/src/invoke_context.rs
@@ -28,7 +28,9 @@ use {
         program::{BuiltinFunction, SBPFVersion},
         vm::{Config, ContextObject, EbpfVm},
     },
-    solana_sdk_ids::{bpf_loader_deprecated, native_loader, sysvar},
+    solana_sdk_ids::{
+        bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, loader_v4, native_loader, sysvar,
+    },
     solana_stable_layout::stable_instruction::StableInstruction,
     solana_timings::{ExecuteDetailsTimings, ExecuteTimings},
     solana_transaction_context::{
@@ -523,6 +525,19 @@ impl<'a> InvokeContext<'a> {
             let owner_id = borrowed_root_account.get_owner();
             if native_loader::check_id(owner_id) {
                 *borrowed_root_account.get_key()
+            } else if self
+                .get_feature_set()
+                .is_active(&remove_accounts_executable_flag_checks::id())
+            {
+                if bpf_loader_deprecated::check_id(owner_id)
+                    || bpf_loader::check_id(owner_id)
+                    || bpf_loader_upgradeable::check_id(owner_id)
+                    || loader_v4::check_id(owner_id)
+                {
+                    *owner_id
+                } else {
+                    return Err(InstructionError::UnsupportedProgramId);
+                }
             } else {
                 *owner_id
             }
diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs
index 1c88b586f70529..77184b6e7e0063 100644
--- a/runtime/src/bank/tests.rs
+++ b/runtime/src/bank/tests.rs
@@ -8392,6 +8392,46 @@ fn test_program_is_native_loader() {
     );
 }
 
+#[test]
+fn test_invoke_non_program_account_owned_by_a_builtin() {
+    let (genesis_config, mint_keypair) = create_genesis_config(10000000);
+    let mut bank = Bank::new_for_tests(&genesis_config);
+    bank.activate_feature(&feature_set::remove_accounts_executable_flag_checks::id());
+    let (bank, _bank_forks) = bank.wrap_with_bank_forks_for_tests();
+
+    let bogus_program = Pubkey::new_unique();
+    bank.transfer(
+        genesis_config.rent.minimum_balance(0),
+        &mint_keypair,
+        &bogus_program,
+    )
+    .unwrap();
+
+    let created_account_keypair = Keypair::new();
+    let mut ix = system_instruction::create_account(
+        &mint_keypair.pubkey(),
+        &created_account_keypair.pubkey(),
+        genesis_config.rent.minimum_balance(0),
+        0,
+        &system_program::id(),
+    );
+    // Calling an account owned by the system program, instead of calling the system program itself
+    ix.program_id = bogus_program;
+    let tx = Transaction::new_signed_with_payer(
+        &[ix],
+        Some(&mint_keypair.pubkey()),
+        &[&mint_keypair, &created_account_keypair],
+        bank.last_blockhash(),
+    );
+    assert_eq!(
+        bank.process_transaction(&tx),
+        Err(TransactionError::InstructionError(
+            0,
+            InstructionError::UnsupportedProgramId
+        ))
+    );
+}
+
 #[test]
 fn test_debug_bank() {
     let (genesis_config, _mint_keypair) = create_genesis_config(50000);
@@ -13556,7 +13596,7 @@ fn test_loader_v3_to_v4_migration() {
             finalized_migration_transaction.clone(),
             Err(TransactionError::InstructionError(
                 0,
-                InstructionError::InvalidInstructionData,
+                InstructionError::UnsupportedProgramId,
             )),
         ),
         (
@@ -13564,7 +13604,7 @@ fn test_loader_v3_to_v4_migration() {
             finalized_migration_transaction.clone(),
             Err(TransactionError::InstructionError(
                 0,
-                InstructionError::InvalidInstructionData,
+                InstructionError::UnsupportedProgramId,
             )),
         ),
         (


Reviews:
user: login: Lichtso
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-05T20:59:39Z
user: login: LucasSte
state: APPROVED
author_association: NONE
submitted_at: 2025-03-06T22:48:09Z
user: login: jstarry
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-10T14:37:47Z
user: login: Lichtso
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-10T14:39:49Z
user: login: jstarry
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-10T15:56:22Z

Review comments:
diff_hunk: @@ -13556,15 +13596,15 @@ fn test_loader_v3_to_v4_migration() {
             finalized_migration_transaction.clone(),
             Err(TransactionError::InstructionError(
                 0,
-                InstructionError::InvalidInstructionData,
+                InstructionError::UnsupportedProgramId,
path: runtime/src/bank/tests.rs
user: login: Lichtso
body: This is where I first noticed this issue.
created_at: 2025-03-05T20:59:39Z
author_association: NONE
diff_hunk: @@ -523,6 +525,19 @@ impl<'a> InvokeContext<'a> {
             let owner_id = borrowed_root_account.get_owner();
             if native_loader::check_id(owner_id) {
                 *borrowed_root_account.get_key()
+            } else if self
+                .get_feature_set()
+                .is_active(&remove_accounts_executable_flag_checks::id())
+            {
+                if bpf_loader_deprecated::check_id(owner_id)
+                    || bpf_loader::check_id(owner_id)
+                    || bpf_loader_upgradeable::check_id(owner_id)
+                    || loader_v4::check_id(owner_id)
path: program-runtime/src/invoke_context.rs
user: login: jstarry
body: Shouldn't we apply the same checks to account loading? We could get rid of all of the `validated_loaders` logic there.
created_at: 2025-03-10T14:37:47Z
author_association: NONE
diff_hunk: @@ -523,6 +525,19 @@ impl<'a> InvokeContext<'a> {
             let owner_id = borrowed_root_account.get_owner();
             if native_loader::check_id(owner_id) {
                 *borrowed_root_account.get_key()
+            } else if self
+                .get_feature_set()
+                .is_active(&remove_accounts_executable_flag_checks::id())
+            {
+                if bpf_loader_deprecated::check_id(owner_id)
+                    || bpf_loader::check_id(owner_id)
+                    || bpf_loader_upgradeable::check_id(owner_id)
+                    || loader_v4::check_id(owner_id)
path: program-runtime/src/invoke_context.rs
user: login: Lichtso
body: Yes, that is why I added you as reviewer ;)
created_at: 2025-03-10T14:39:49Z
author_association: NONE
diff_hunk: @@ -523,6 +525,19 @@ impl<'a> InvokeContext<'a> {
             let owner_id = borrowed_root_account.get_owner();
             if native_loader::check_id(owner_id) {
                 *borrowed_root_account.get_key()
+            } else if self
+                .get_feature_set()
+                .is_active(&remove_accounts_executable_flag_checks::id())
+            {
+                if bpf_loader_deprecated::check_id(owner_id)
+                    || bpf_loader::check_id(owner_id)
+                    || bpf_loader_upgradeable::check_id(owner_id)
+                    || loader_v4::check_id(owner_id)
path: program-runtime/src/invoke_context.rs
user: login: jstarry
body: Haha, great, follow up PR then? And these changes should be amended to the SIMD: https://github.com/solana-foundation/solana-improvement-documents/pull/162
created_at: 2025-03-10T15:56:22Z
author_association: NONE

PR comments:
user: login: mergify[bot]
created_at: 2025-03-10T12:25:42Z
body: Backports to the beta branch are to be avoided unless absolutely necessary for fixing bugs, security issues, and perf regressions. Changes intended for backport should be structured such that a minimum effective diff can be committed separately from any refactoring, plumbing, cleanup, etc that are not strictly necessary to achieve the goal. Any of the latter should go only into master and ride the normal stabilization schedule. Exceptions include CI/metrics changes, CLI improvements and documentation updates on a case by case basis.
author_association: NONE
user: login: jstarry
created_at: 2025-03-10T14:40:25Z
body: I don't think this should have been merged without first rekeying
author_association: NONE
user: login: Lichtso
created_at: 2025-03-10T14:42:36Z
body: Can't rekey in the same PR anymore because SDK was moved out. Have to do it in the sdk repo and then, once the backport lands we can bump the release there. See Discord discussion mentioned above.
author_association: NONE
user: login: jstarry
created_at: 2025-03-10T15:54:35Z
body: Actually yeah the rekey should land after the implementation, never mind.
author_association: NONE