OpenZeppelin / OpenZeppelin/stellar-contracts

🐞 [Bug]: Role enumeration counter-derived storage keys cause footprint invalidation under concurrency

Open
#752 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
95
Forks
68
Avg merge
4d 42m
Merged PRs (30d)
20

Description

What happened?

add_to_role_enumeration in packages/access/src/access_control/storage.rs derives write keys from a shared mutable counter:

let count = e.storage().persistent().get(&count_key).unwrap_or(0);
let new_key = AccessControlStorageKey::RoleAccounts(RoleAccountKey { role, index: count });
e.storage().persistent().set(&new_key, account);

On Soroban, transaction footprints must declare exact storage keys at simulation time. If RoleAccountsCount advances between simulation and execution (another grant_role lands first), the contract writes to a key not in the footprint and the transaction traps:

"trying to access contract data key outside of the footprint"
["RoleAccounts", {"index": 7044, "role": "WLISTED"}]

Any two concurrent grant_role calls on the same role will race on the counter. Observed on Mainnet with a role containing ~7000 members.

Reproduction: two concurrent grant_role(env, caller, user_a, role) and grant_role(env, caller, user_b, role) (sent with channel accounts to have concurrency) — both simulate with count = N and both declare RoleAccounts({role, N}) in their write footprint. First lands and increments the counter to N+1. Second executes, reads the now-updated count = N+1, and tries to write RoleAccounts({role, N+1}) which is not in its declared footprint.

Expected behavior

grant_role should be safe to call concurrently on the same role. Storage keys in the write path should be deterministic from the function inputs.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in packages/access/src/access_control/storage.rs at add_to_role_enumeration, then trace its callers from grant_role. Reproduce the race with two concurrent grant_role calls using channel accounts and inspect the simulation and execution footprints. Done means concurrent grants on the same role no longer trap because the write keys are deterministic from the function inputs.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
blockchain
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.