Skip to content

fix(realloc): only top up rent when the target holds less than the new minimum - #8

Open
SwineCoder101 wants to merge 2 commits into
mainfrom
fix/realloc-lamport-underflow
Open

fix(realloc): only top up rent when the target holds less than the new minimum#8
SwineCoder101 wants to merge 2 commits into
mainfrom
fix/realloc-lamport-underflow

Conversation

@SwineCoder101

Copy link
Copy Markdown
Owner

Bug: ReallocateWithoutZeroInit panics when the target account already holds more than the new rent-exempt minimum

The native and pinocchio realloc programs compute the rent top-up as lamports_required - target_account.lamports() with a plain unchecked subtraction. Whenever the target account already holds more lamports than the enlarged rent-exempt minimum, the subtraction overflows and the instruction aborts, so the account can never be reallocated. Anyone can put any target account into that state with a single system transfer (the surplus needed here is about 84k lamports, well under a cent), making this a cheap, permanent denial of service against the instruction. Severity: medium (no funds at risk, but the instruction is bricked for that account).

Affected

  • native: basics/realloc/native/program/src/instructions/reallocate.rs (reallocate_without_zero_init)
  • pinocchio: basics/realloc/pinocchio/program/src/instructions/reallocate.rs (reallocate_without_zero_init)
  • anchor: not affected. basics/realloc/anchor uses Anchor's realloc constraint, which only transfers when the new minimum exceeds the current balance.

Functionality

ReallocateWithoutZeroInit grows an existing AddressInfo account into an EnhancedAddressInfo account: it computes the rent-exempt minimum for the larger size, has the payer top the account up so it stays rent exempt, resizes the account and writes the extended data.

The bug

  • native reallocate.rs:24: let diff = lamports_required - target_account.lamports();
  • pinocchio reallocate.rs:19: let diff = lamports_required - target_account.lamports();

Both assume the target holds at most the new minimum. The programs are built with overflow-checks = true (root Cargo.toml), so when target_account.lamports() > lamports_required the subtraction panics with attempt to subtract with overflow and the transaction fails with ProgramFailedToComplete. Without overflow checks it would wrap to roughly u64::MAX and the system transfer would fail with insufficient funds; either way the instruction cannot succeed.

Concrete scenario: an AddressInfo account is created (25 bytes, rent exempt). Anyone sends it enough lamports to exceed the rent-exempt minimum for 37 bytes (any amount above ~83,520 lamports at default rent). Every subsequent ReallocateWithoutZeroInit for that account now fails, regardless of who signs as payer.

Reproduce

cd basics/realloc/native && pnpm install --frozen-lockfile && pnpm build-and-test
cd basics/realloc/pinocchio && pnpm install --frozen-lockfile && pnpm build-and-test

Test (both variants, tests/realloc.test.ts): Reallocate WITHOUT zero init when the account already holds more than the new rent-exempt minimum. It creates a fresh account, sends it the enlarged rent-exempt minimum with a plain system transfer, then calls ReallocateWithoutZeroInit, and asserts the enlarged data, that the target balance is unchanged and that the payer was only debited the transaction fee.

Before the fix:

native:
  1) Realloc!
       Reallocate WITHOUT zero init when the account already holds more than the new rent-exempt minimum:
     AssertionError: transaction failed: FailedTransactionMetadata(FailedTransactionMetadata { err: InstructionError(0, ProgramFailedToComplete), meta: TransactionMetadata { ... logs: [..., "Program log: attempt to subtract with overflow", ..., "Program A45iVziebTT8Lr3C4AEmZ3JhyR6vqkd2S4ssADsRqt1M failed: SBF program Panicked in basics/realloc/native/program/src/instructions/reallocate.rs at 24:16"], ... } })

pinocchio:
  1) Realloc!
       Reallocate WITHOUT zero init when the account already holds more than the new rent-exempt minimum:
     AssertionError: transaction failed: FailedTransactionMetadata(FailedTransactionMetadata { err: InstructionError(0, ProgramFailedToComplete), meta: TransactionMetadata { ... logs: [..., "Program 5oNmTpn96MCHqUEcR2NUZ9jH2VqWvXhsMVbG8VmkgW4P failed: SBF program Panicked in basics/realloc/pinocchio/program/src/instructions/reallocate.rs at 19:16"], ... } })

Fix

Both variants now compute lamports_required.saturating_sub(target_account.lamports()) and only invoke the system transfer when the result is non-zero. An account that already covers the new minimum is resized without touching the payer; an under-funded one is topped up exactly as before. This mirrors what Anchor's realloc constraint does.

Verification

cd basics/realloc/native && pnpm build-and-test
cd basics/realloc/pinocchio && pnpm build-and-test
cargo test --manifest-path=basics/realloc/native/program/Cargo.toml
cargo test --manifest-path=basics/realloc/pinocchio/program/Cargo.toml
cargo clippy -p realloc-program -p realloc-pinocchio-program -- -D warnings
  Realloc!
    ✔ Create the account with data
    ✔ Reallocate WITHOUT zero init
    ✔ Reallocate WITH zero init
    ✔ Reallocate WITHOUT zero init when the account already holds more than the new rent-exempt minimum

  4 passing

(identical output for native and pinocchio; the Rust test_realloc integration test in each program/tests/test.rs also passes.)

@SwineCoder101
SwineCoder101 force-pushed the fix/realloc-lamport-underflow branch from 279b7b4 to 608f775 Compare August 27, 2026 12:23
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