TOCTOU bugs in ActionHandler
The action-handler system in Penumbra works by running the checks of actions in parallel such that side effects of the execution of an action do not affect the checks of another action in the same transaction.
A more concise example of this would be the below call trace:
Action 1 -> check_stateless
Action 2 -> check_stateless
...
Action N -> check_stateless
Action 1 -> check_stateful
Action 2 -> check_stateful
...
Action N -> check_stateful
Action 1 -> execute
Action 2 -> execute
...
Action N -> execute
This causes issues where developers could expect these checks to act like the transaction handling in Cosmos, which is instead dispatched like this:
Action 1 -> check_stateless
Action 1 -> check_stateful
Action 1 -> execute
Action 2 -> check_stateless
Action 2 -> check_stateful
Action 2 -> execute
Action N -> check_stateless
Action N -> check_stateful
Action N -> execute
This mismatch between the intuitive mental model of sequential execution of each action's methods with the actual interleaved order leads to time-of-check time-of-use (TOCTOU) bugs, where action 1's execute
modifies state that was already checked by action 2's check_stateful
, and then action 2's execute
proceeds as if its check_stateful
's checks still held. Mitigating these bugs efficiently can often be done by ensuring that there is a corresponding check in Transaction::check_stateless
that ensures that multiple actions within the same transaction do not modify the same state.
As a concrete example, Spend::check_stateful
checks that the nullifier for the note is not present in the nullifier set, and Spend::execute
adds the nullifier to the set. This would be fine under a sequential order, but under the interleaved order, this would result in a note being able to be spent multiple times within one transaction — if not for Transaction::check_stateless
's no_duplicate_spends
check, which ensures that each nullifier occurs at most once within a transaction, which ensures that Spend::execute
(and SwapClaim::execute
) check and modify disjoint parts of the state.
We recommend that:
If it does not sacrifice too much performance (or possibly only under a
debug_assertions
-style feature flag),Transaction::execute
should rerun eachAction
'scheck_stateful
method immediately before the correspondingexecute
, eliminating this class of bugs.If the sequential execution of
check_stateful
is only done under a feature flag, differential fuzzing or property testing of transaction validation (with a mutator or strategy that provides valid proofs and signatures) with and without the flag may be able to detect instances of this bug class.Any implementation of
ActionHandler
with a nontrivalcheck_stateful
method should have a documentation comment onexecute
explaining why possible interleavings of otherAction
'sexecute
methods still meet the preconditions established as ifcheck_stateful
had been run sequentially, referencing the corresponding check inTransaction::check_stateless
if applicable.
Remediation
This issue has been acknowledged by Penumbra Labs, and fixes were implemented in the following commits:
In PR 3962↗, check_stateful
and execute
were renamed to check_historical
and check_and_execute
, with check_historical
's documentation explaining this risk.
Many former check_stateful
checks were moved to check_and_execute
pending profiling, and SAFETY:
comments describing invariants were added to the remaining check_historical
checks.