Summary
In migration_status (lib.rs:98-106), values stored under child-storage-prefixed keys are treated as child trie root hashes and copied verbatim into an H::Out buffer using copy_from_slice. This call performs no length validation and panics when the stored value is not exactly the hash output length (e.g., 32 bytes for Blake2b256). Since the function is called through the StateMigrationApiServer::call RPC handler which fetches state via TrieCacheContext::Untrusted, processing adversarial or corrupted chain state that contains a wrong-length value under a child-storage-prefixed key will crash the node process rather than returning a graceful RPC error.
Severity / Sensitivity
Medium 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_264qyrx2
// RWF-INVARIANTS: inv_zdendbtr
#[test]
fn test_risk_264qyrx2_short_child_root_value_must_be_panic_safe() {
assert_child_root_wrong_length_panic_safe(1);
}
Reproduction Command
cargo test -p substrate-state-trie-migration-rpc test_risk_264qyrx2_short_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 (1) 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 1 byte and asserts migration_status is panic-safe; it panics at the unchecked copy.
RWF Metadata
- Found by Runtime Whitebox Fuzzer
- Finding:
find_6mg4iwewgs
- Report:
freport_nf7ib8bpng
- Target:
substrate-state-trie-migration-rpc
- Run:
20260628-212807-a53980
- Severity:
medium
- Category:
Denial of Service
- Invariants:
inv_zdendbtr
- Risks:
risk_264qyrx2
Summary
In
migration_status(lib.rs:98-106), values stored under child-storage-prefixed keys are treated as child trie root hashes and copied verbatim into anH::Outbuffer usingcopy_from_slice. This call performs no length validation and panics when the stored value is not exactly the hash output length (e.g., 32 bytes for Blake2b256). Since the function is called through theStateMigrationApiServer::callRPC handler which fetches state viaTrieCacheContext::Untrusted, processing adversarial or corrupted chain state that contains a wrong-length value under a child-storage-prefixed key will crash the node process rather than returning a graceful RPC error.Severity / Sensitivity
Medium 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-106: The loop over child_roots doeschild_root.as_mut()[..].copy_from_slice(&root[..])at line 102 without checking thatroot.len()equalsH::Outbyte length, causing a panic on any length mismatch.substrate/utils/frame/rpc/state-trie-migration-rpc/src/lib.rs lines 163-176: The RPC handler callsstate_at(hash, TrieCacheContext::Untrusted)and thenmigration_status, confirming the panic-safety expectation for untrusted state data.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_264qyrx2_short_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 1 byte and assertsmigration_statusis panic-safe; it panics at the unchecked copy.RWF Metadata
find_6mg4iwewgsfreport_nf7ib8bpngsubstrate-state-trie-migration-rpc20260628-212807-a53980mediumDenial of Serviceinv_zdendbtrrisk_264qyrx2