diff --git a/Cargo.lock b/Cargo.lock index ae9a71bc..16c43c08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -736,7 +736,7 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fastroll" -version = "0.1.34" +version = "0.1.35" dependencies = [ "clap", "fr-common", @@ -766,7 +766,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "fr-asn-types" -version = "0.1.34" +version = "0.1.35" dependencies = [ "bitvec", "fr-block", @@ -780,7 +780,7 @@ dependencies = [ [[package]] name = "fr-block" -version = "0.1.34" +version = "0.1.35" dependencies = [ "bitvec", "fr-codec", @@ -795,7 +795,7 @@ dependencies = [ [[package]] name = "fr-clock" -version = "0.1.34" +version = "0.1.35" dependencies = [ "fr-common", "time", @@ -803,7 +803,7 @@ dependencies = [ [[package]] name = "fr-codec" -version = "0.1.34" +version = "0.1.35" dependencies = [ "bitvec", "fr-codec-derive", @@ -813,7 +813,7 @@ dependencies = [ [[package]] name = "fr-codec-derive" -version = "0.1.34" +version = "0.1.35" dependencies = [ "quote", "syn", @@ -821,7 +821,7 @@ dependencies = [ [[package]] name = "fr-common" -version = "0.1.34" +version = "0.1.35" dependencies = [ "cfg-if", "fr-codec", @@ -838,14 +838,14 @@ dependencies = [ [[package]] name = "fr-config" -version = "0.1.34" +version = "0.1.35" dependencies = [ "fr-db", ] [[package]] name = "fr-crypto" -version = "0.1.34" +version = "0.1.35" dependencies = [ "ark-vrf", "base32", @@ -865,7 +865,7 @@ dependencies = [ [[package]] name = "fr-db" -version = "0.1.34" +version = "0.1.35" dependencies = [ "fr-codec", "fr-common", @@ -877,7 +877,7 @@ dependencies = [ [[package]] name = "fr-erasure-coding" -version = "0.1.34" +version = "0.1.35" dependencies = [ "fr-common", "rayon", @@ -888,7 +888,7 @@ dependencies = [ [[package]] name = "fr-extrinsics" -version = "0.1.34" +version = "0.1.35" dependencies = [ "fr-block", "fr-codec", @@ -904,7 +904,7 @@ dependencies = [ [[package]] name = "fr-fuzz" -version = "0.1.34" +version = "0.1.35" dependencies = [ "fr-block", "fr-codec", @@ -915,6 +915,7 @@ dependencies = [ "fr-state", "fr-storage", "fr-test-utils", + "futures", "tempfile", "thiserror 2.0.17", "tokio", @@ -923,7 +924,7 @@ dependencies = [ [[package]] name = "fr-integration" -version = "0.1.34" +version = "0.1.35" dependencies = [ "async-trait", "fr-asn-types", @@ -952,14 +953,14 @@ dependencies = [ [[package]] name = "fr-limited-vec" -version = "0.1.34" +version = "0.1.35" dependencies = [ "thiserror 2.0.17", ] [[package]] name = "fr-merkle" -version = "0.1.34" +version = "0.1.35" dependencies = [ "fr-codec", "fr-common", @@ -969,7 +970,7 @@ dependencies = [ [[package]] name = "fr-network" -version = "0.1.34" +version = "0.1.35" dependencies = [ "async-trait", "dashmap 6.1.0", @@ -988,7 +989,7 @@ dependencies = [ [[package]] name = "fr-node" -version = "0.1.34" +version = "0.1.35" dependencies = [ "clap", "fr-block", @@ -1015,7 +1016,7 @@ dependencies = [ [[package]] name = "fr-node-bench" -version = "0.1.34" +version = "0.1.35" dependencies = [ "criterion", "fr-clock", @@ -1027,7 +1028,7 @@ dependencies = [ [[package]] name = "fr-pvm-core" -version = "0.1.34" +version = "0.1.35" dependencies = [ "bitvec", "fr-codec", @@ -1039,7 +1040,7 @@ dependencies = [ [[package]] name = "fr-pvm-host" -version = "0.1.34" +version = "0.1.35" dependencies = [ "async-trait", "fr-codec", @@ -1056,7 +1057,7 @@ dependencies = [ [[package]] name = "fr-pvm-interface" -version = "0.1.34" +version = "0.1.35" dependencies = [ "fr-codec", "fr-common", @@ -1072,7 +1073,7 @@ dependencies = [ [[package]] name = "fr-pvm-invocation" -version = "0.1.34" +version = "0.1.35" dependencies = [ "fr-codec", "fr-common", @@ -1090,7 +1091,7 @@ dependencies = [ [[package]] name = "fr-pvm-types" -version = "0.1.34" +version = "0.1.35" dependencies = [ "fr-codec", "fr-common", @@ -1099,7 +1100,7 @@ dependencies = [ [[package]] name = "fr-state" -version = "0.1.34" +version = "0.1.35" dependencies = [ "async-trait", "bitvec", @@ -1126,7 +1127,7 @@ dependencies = [ [[package]] name = "fr-state-merkle-v2" -version = "0.1.34" +version = "0.1.35" dependencies = [ "bitvec", "fr-codec", @@ -1142,7 +1143,7 @@ dependencies = [ [[package]] name = "fr-storage" -version = "0.1.34" +version = "0.1.35" dependencies = [ "async-trait", "fr-block", @@ -1155,7 +1156,7 @@ dependencies = [ [[package]] name = "fr-test-utils" -version = "0.1.34" +version = "0.1.35" dependencies = [ "async-trait", "fr-asn-types", @@ -1181,7 +1182,7 @@ dependencies = [ [[package]] name = "fr-transition" -version = "0.1.34" +version = "0.1.35" dependencies = [ "fr-block", "fr-codec", diff --git a/Cargo.toml b/Cargo.toml index 14161127..752a0311 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ members = [ ] [workspace.package] -version = "0.1.34" +version = "0.1.35" edition = "2021" license = "Apache-2.0" authors = ["Junha Park <0xjunha@gmail.com>"] diff --git a/crypto/src/vrf/bandersnatch_vrf.rs b/crypto/src/vrf/bandersnatch_vrf.rs index 23ede06e..48042913 100644 --- a/crypto/src/vrf/bandersnatch_vrf.rs +++ b/crypto/src/vrf/bandersnatch_vrf.rs @@ -106,7 +106,7 @@ impl RingVrfVerifier { pub fn new(validator_set: &ValidatorKeySet) -> Result { let ring = validator_set_to_bandersnatch_ring(validator_set)?; Ok(Self { - core: RingVrfVerifierCore::new(ring), + core: RingVrfVerifierCore::new(ring)?, }) } diff --git a/crypto/src/vrf/vrf_core.rs b/crypto/src/vrf/vrf_core.rs index fc175c3d..dafda750 100644 --- a/crypto/src/vrf/vrf_core.rs +++ b/crypto/src/vrf/vrf_core.rs @@ -24,9 +24,9 @@ pub(crate) struct IetfVrfSignature { // Additional impl impl IetfVrfSignature { pub(crate) fn output_hash(&self) -> [u8; 32] { - self.output.hash()[..32] - .try_into() - .expect("Should not fail; 32-byte array") + let mut out = [0u8; 32]; + out.copy_from_slice(&self.output.hash()[..32]); + out } } @@ -40,22 +40,27 @@ pub(crate) struct RingVrfSignature { // Additional impl impl RingVrfSignature { pub(crate) fn output_hash(&self) -> [u8; 32] { - self.output.hash()[..32] - .try_into() - .expect("Should not fail; 32-byte array") + let mut out = [0u8; 32]; + out.copy_from_slice(&self.output.hash()[..32]); + out } } -fn ring_proof_params() -> &'static RingProofParams { +fn ring_proof_params() -> Result<&'static RingProofParams, CryptoError> { use std::sync::OnceLock; - static PARAMS: OnceLock = OnceLock::new(); - PARAMS.get_or_init(|| { + static PARAMS: OnceLock> = OnceLock::new(); + let params = PARAMS.get_or_init(|| { use bandersnatch::PcsParams; let buf = include_bytes!("../../data/zcash-srs-2-11-uncompressed.bin"); let pcs_params = PcsParams::deserialize_uncompressed_unchecked(&mut &buf[..]) - .expect("Failed to deserialize PCS params"); + .map_err(|e| format!("Failed to deserialize PCS params: {e}"))?; RingProofParams::from_pcs_params(RING_SIZE, pcs_params) - .expect("Failed to construct ring proof params from PCS params") + .map_err(|e| format!("Failed to construct ring proof params from PCS params: {e:?}")) + }); + + params.as_ref().map_err(|message| { + tracing::error!("{message}"); + CryptoError::RingContextResourceError }) } @@ -147,7 +152,7 @@ impl RingVrfProverCore { let pts: Vec<_> = self.ring.iter().map(|pk| pk.0).collect(); // Proof construction - let params = ring_proof_params(); + let params = ring_proof_params()?; let prover_key = params.prover_key(&pts); let prover = params.prover(prover_key, self.prover_idx); let proof = self.secret.prove(input, output, aux_data, &prover); @@ -193,9 +198,8 @@ impl IetfVrfVerifierCore { // `Y` hashed value; this is the actual value used as ticket-id/score // NOTE: as far as vrf_input_data is the same, this matches the one produced // using the ring-vrf (regardless of aux_data). - let vrf_output_hash: [u8; 32] = output.hash()[..32] - .try_into() - .expect("Should not fail; 32-byte array"); + let mut vrf_output_hash = [0u8; 32]; + vrf_output_hash.copy_from_slice(&output.hash()[..32]); tracing::trace!("vrf-output-hash: {}", hex::encode(vrf_output_hash)); Ok(vrf_output_hash) } @@ -212,11 +216,11 @@ pub(crate) struct RingVrfVerifierCore { } impl RingVrfVerifierCore { - pub(crate) fn new(ring: Vec) -> Self { + pub(crate) fn new(ring: Vec) -> Result { let pts: Vec<_> = ring.iter().map(|pk| pk.0).collect(); - let verifier_key = ring_proof_params().verifier_key(&pts); + let verifier_key = ring_proof_params()?.verifier_key(&pts); let commitment = verifier_key.commitment(); // The Ring Root - Self { ring, commitment } + Ok(Self { ring, commitment }) } #[instrument(level = "debug", skip_all, name = "compute_ring_root")] @@ -250,7 +254,7 @@ impl RingVrfVerifierCore { let input = vrf_input_point(vrf_input_data)?; let output = signature.output; // extracted from the signature - let params = ring_proof_params(); + let params = ring_proof_params()?; let verifier_key = params.verifier_key_from_commitment(self.commitment.clone()); let verifier = params.verifier(verifier_key); @@ -261,9 +265,8 @@ impl RingVrfVerifierCore { tracing::trace!("Ring signature verified"); // `Y` hashed value; the actual value used as ticket-id/score - let vrf_output_hash: [u8; 32] = output.hash()[..32] - .try_into() - .expect("Should not fail; 32-byte array"); + let mut vrf_output_hash = [0u8; 32]; + vrf_output_hash.copy_from_slice(&output.hash()[..32]); tracing::trace!("vrf-output-hash: {}", hex::encode(vrf_output_hash)); Ok(vrf_output_hash) } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 56df6b3a..c03f3840 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -16,6 +16,7 @@ fr-node = { workspace = true } fr-state = { workspace = true } fr-storage = { workspace = true } fr-test-utils = { workspace = true } +futures = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } diff --git a/fuzz/src/fuzz_target.rs b/fuzz/src/fuzz_target.rs index 55d620c9..3545d954 100644 --- a/fuzz/src/fuzz_target.rs +++ b/fuzz/src/fuzz_target.rs @@ -26,9 +26,12 @@ use fr_state::{ }, }; use fr_storage::node_storage::{NodeStorage, NodeStorageError}; +use futures::FutureExt; use std::{ + any::Any, collections::{BTreeSet, HashMap}, io::Error as IoError, + panic::AssertUnwindSafe, string::FromUtf8Error, sync::Arc, }; @@ -36,7 +39,7 @@ use tempfile::{tempdir, TempDir}; use thiserror::Error; use tokio::{ net::{UnixListener, UnixStream}, - time::error::Elapsed, + time::{error::Elapsed, sleep, Duration}, }; #[derive(Debug, Error)] @@ -69,6 +72,8 @@ pub enum FuzzTargetError { InvalidMessageKind(String), #[error("Invalid socket path: {0}")] InvalidSocketPath(String), + #[error("Fuzz protocol message exceeds maximum allowed size: observed={observed} max={max}")] + MessageTooLarge { observed: usize, max: usize }, } /// Collection of the state keys of the latest chain state. @@ -402,6 +407,16 @@ impl FuzzTargetRunner { Ok(()) } + fn panic_payload_message(payload: &(dyn Any + Send)) -> String { + if let Some(msg) = payload.downcast_ref::<&'static str>() { + (*msg).to_string() + } else if let Some(msg) = payload.downcast_ref::() { + msg.clone() + } else { + "non-string panic payload".to_string() + } + } + pub async fn run_as_fuzz_target(&mut self, socket_path: String) -> Result<(), FuzzTargetError> { // Validate socket path input validate_socket_path(&socket_path)?; @@ -415,20 +430,50 @@ impl FuzzTargetRunner { let mut is_first_session = true; // Continuously accept new connections after closing the previous session. - while let Ok((stream, _addr)) = listener.accept().await { + loop { + let (stream, _addr) = match listener.accept().await { + Ok(stream) => stream, + Err(e) => { + tracing::error!("Failed to accept fuzzer connection: {e}"); + sleep(Duration::from_millis(100)).await; + continue; + } + }; tracing::info!("Accepted a connection from the fuzzer"); if is_first_session { is_first_session = false; } else { // Reset storage & state for the new session - *self = FuzzTargetRunner::new(self.target_peer_info.clone())?; + if let Err(e) = self.reset_state_context() { + tracing::error!("Failed to reset fuzz target session state: {e}"); + continue; + } } - self.handle_fuzzer_session(stream).await?; - tracing::info!("Fuzzer session ended"); + // Session-level panic isolation: panics should only terminate the current session. + let session_result = AssertUnwindSafe(self.handle_fuzzer_session(stream)) + .catch_unwind() + .await; + match session_result { + // Successful session termination + Ok(Ok(())) => tracing::info!("Fuzzer session ended"), + // Runtime errors + Ok(Err(e)) => { + tracing::warn!("Fuzzer session terminated due to runtime error: {e}"); + } + // Runtime panics (unwinding) + Err(payload) => { + tracing::error!( + "Fuzzer session panicked; closing session and continuing: {}", + Self::panic_payload_message(&payload) + ); + if let Err(e) = self.reset_state_context() { + tracing::error!("Failed to recover fuzz target state after panic: {e}"); + } + } + } } - Ok(()) } async fn handle_fuzzer_session( @@ -457,11 +502,11 @@ impl FuzzTargetRunner { } } Err(e) => { - if Self::is_session_disconnect_error(&e) { + return if Self::is_session_disconnect_error(&e) { tracing::info!("Fuzzer session disconnected gracefully"); - return Ok(()); + Ok(()) } else { - return Err(e); + Err(e) } } } @@ -658,7 +703,7 @@ impl FuzzTargetRunner { // GetState must return the posterior state for the requested header hash. let state = self.build_state_for_hash(&requested_header_hash).await?; StreamUtils::send_message(stream, FuzzMessageKind::State(state)).await?; - tracing::info!("Session terminated by GetState request"); + tracing::info!("[SEND][GetState] Returned full state"); Ok(()) } e => Err(FuzzTargetError::InvalidMessageKind(format!("{e:?}"))), diff --git a/fuzz/src/types.rs b/fuzz/src/types.rs index 41df4ddc..0f6978ba 100644 --- a/fuzz/src/types.rs +++ b/fuzz/src/types.rs @@ -316,7 +316,11 @@ impl JamEncode for FuzzProtocolMessage { impl FuzzProtocolMessage { pub fn from_kind(kind: FuzzMessageKind) -> Result { let encoded = kind.encode()?; - let msg_length = encoded.len() as u32; + let msg_length = u32::try_from(encoded.len()).map_err(|_| { + JamCodecError::EncodingError( + "Fuzz protocol message length exceeds u32::MAX".to_string(), + ) + })?; Ok(Self { msg_length, kind }) } } diff --git a/transition/src/state/safrole.rs b/transition/src/state/safrole.rs index 91a6bd5c..ee6b08bf 100644 --- a/transition/src/state/safrole.rs +++ b/transition/src/state/safrole.rs @@ -171,7 +171,7 @@ async fn handle_ticket_accumulation( fn extract_epoch_marker_keys( validator_set: &ValidatorKeySet, -) -> FixedVec { +) -> Result, TransitionError> { let mut result = Vec::with_capacity(VALIDATOR_COUNT); for key in validator_set.iter() { result.push(EpochMarkerValidatorKey { @@ -180,7 +180,7 @@ fn extract_epoch_marker_keys( }); } - FixedVec::try_from(result).expect("size checked") + Ok(FixedVec::try_from(result)?) } #[instrument(level = "debug", skip_all, name = "header_markers")] @@ -199,7 +199,7 @@ pub async fn mark_safrole_header_markers( Some(EpochMarker { entropy: prior_entropy.current().clone(), tickets_entropy: prior_entropy.first_history().clone(), - validators: extract_epoch_marker_keys(&curr_pending_set), + validators: extract_epoch_marker_keys(&curr_pending_set)?, }) } else { None