Summary
In migration_status (lib.rs:98-106), child trie root values read from storage are copied directly into H::Out via child_root.as_mut()[..].copy_from_slice(&root[..]) with no length validation. If a child-storage-prefixed key stores a value whose length differs from H::Out (32 bytes for Blake2Hasher), the copy_from_slice call panics. Since this function is called from the StateMigration RPC handler call() which fetches state with TrieCacheContext::Untrusted, a malformed or adversarial trie can trigger a process-crashing panic instead of a graceful error response. The fix should validate root.len() == H::Out::len() before the copy and return an Err on mismatch.
Severity / Sensitivity
High severity, non-sensitive public issue. This is reported from a confirmed Runtime Whitebox Fuzzer finding against public Polkadot SDK code.
Source Evidence
Full Relevant PoC Test
Paste the snippet below into an in-crate unit test module for the affected package. It is self-contained apart from helpers and types already available in that crate's existing test context.
use super::*;
use sp_core::storage::{well_known_keys, StateVersion};
use sp_core::Blake2Hasher;
use sp_state_machine::new_in_mem;
fn assert_migration_status_panic_safe<H, B>(
backend: &B,
) -> std::result::Result<MigrationStatusResult, String>
where
H: Hasher,
H::Out: codec::Codec,
B: AsTrieBackend<H>,
{
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
migration_status::<H, B>(backend)
}));
match result {
Ok(inner) => inner,
Err(payload) => {
let msg = payload
.downcast_ref::<String>()
.map(|s| s.clone())
.or_else(|| payload.downcast_ref::<&'static str>().map(|s| s.to_string()))
.unwrap_or_else(|| "unknown panic".to_string());
panic!("migration_status panicked instead of returning Err: {}", msg);
},
}
}
fn assert_child_root_wrong_length_panic_safe(child_root_len: usize) {
let mut child_key = well_known_keys::DEFAULT_CHILD_STORAGE_KEY_PREFIX.to_vec();
child_key.extend_from_slice(b"bogus_child");
let backend = new_in_mem::<Blake2Hasher>().update(
vec![(None, vec![(child_key, Some(vec![0u8; child_root_len]))])],
StateVersion::V1,
);
let _ = assert_migration_status_panic_safe::<Blake2Hasher, _>(&backend);
}
// RWF-RISKS: risk_sij8a47m,risk_bu9ped8i
// RWF-INVARIANTS: inv_zdendbtr
#[test]
fn test_risk_sij8a47m_bu9ped8i_long_child_root_value_must_be_panic_safe() {
assert_child_root_wrong_length_panic_safe(64);
}
Reproduction Command
cargo test -p substrate-state-trie-migration-rpc test_risk_sij8a47m_bu9ped8i_long_child_root_value_must_be_panic_safe -- --nocapture
Observed Result
The PoC fails on the current implementation with:
migration_status panicked instead of returning Err: source slice length (64) does not match destination slice length (32)
Expected Behavior
The target should preserve the invariant described above instead of producing the confirmed failure. In particular, the affected operation should reject malformed or inconsistent input, preserve durable state invariants, or return an error rather than silently corrupting state or panicking.
Validation Rationale
The public source shows migration_status reading child root values from trie iteration and copying them into H::Out with copy_from_slice without validating the source length. The RPC handler fetches state with TrieCacheContext::Untrusted, so malformed state should become an error, not a process panic. The PoC builds an in-memory trie with a child-storage-prefixed key whose value is 64 bytes and asserts migration_status is panic-safe; it panics at the unchecked copy.
RWF Metadata
- Found by Runtime Whitebox Fuzzer
- Finding:
find_et38wze6w8
- Report:
freport_qnr2s96cm9
- Target:
substrate-state-trie-migration-rpc
- Run:
20260628-212807-a53980
- Severity:
high
- Category:
panic-on-adversarial-input
- Invariants:
inv_zdendbtr
- Risks:
risk_sij8a47m, risk_bu9ped8i
Summary
In
migration_status(lib.rs:98-106), child trie root values read from storage are copied directly intoH::Outviachild_root.as_mut()[..].copy_from_slice(&root[..])with no length validation. If a child-storage-prefixed key stores a value whose length differs fromH::Out(32 bytes for Blake2Hasher), thecopy_from_slicecall panics. Since this function is called from the StateMigration RPC handlercall()which fetches state withTrieCacheContext::Untrusted, a malformed or adversarial trie can trigger a process-crashing panic instead of a graceful error response. The fix should validateroot.len() == H::Out::len()before the copy and return anErron mismatch.Severity / Sensitivity
High severity, non-sensitive public issue. This is reported from a confirmed Runtime Whitebox Fuzzer finding against public Polkadot SDK code.
Source Evidence
substrate/utils/frame/rpc/state-trie-migration-rpc/src/lib.rs lines 98-103: Lines 98-103 show the child_roots loop wherechild_root.as_mut()[..].copy_from_slice(&root[..])is called without checking thatroot.len()equals the size ofH::Out. This panics on any length mismatch, as confirmed by the runtime panic at line 102.substrate/utils/frame/rpc/state-trie-migration-rpc/src/lib.rs lines 88-97: Lines 88-97 show that child root values come directly from trie iteration over potentially untrusted storage keys starting with DEFAULT_CHILD_STORAGE_KEY_PREFIX, with no validation that the value is a valid hash-length root before it reaches copy_from_slice.Full Relevant PoC Test
Paste the snippet below into an in-crate unit test module for the affected package. It is self-contained apart from helpers and types already available in that crate's existing test context.
Reproduction Command
cargo test -p substrate-state-trie-migration-rpc test_risk_sij8a47m_bu9ped8i_long_child_root_value_must_be_panic_safe -- --nocaptureObserved Result
Expected Behavior
The target should preserve the invariant described above instead of producing the confirmed failure. In particular, the affected operation should reject malformed or inconsistent input, preserve durable state invariants, or return an error rather than silently corrupting state or panicking.
Validation Rationale
The public source shows
migration_statusreading child root values from trie iteration and copying them intoH::Outwithcopy_from_slicewithout validating the source length. The RPC handler fetches state withTrieCacheContext::Untrusted, so malformed state should become an error, not a process panic. The PoC builds an in-memory trie with a child-storage-prefixed key whose value is 64 bytes and assertsmigration_statusis panic-safe; it panics at the unchecked copy.RWF Metadata
find_et38wze6w8freport_qnr2s96cm9substrate-state-trie-migration-rpc20260628-212807-a53980highpanic-on-adversarial-inputinv_zdendbtrrisk_sij8a47m,risk_bu9ped8i