Make fuzz targets deterministic
What changed, and why it matters
This commit changes how the Lightning Dev Kit code behaves when running under fuzz testing. Normally, the code uses the real wall-clock time and random hashing. Under fuzzing, it now uses fixed fallback values and deterministic hashing so that fuzz tests produce the same results every run. This is a testing-harness improvement, not a fix for an exploitable security bug in production. It does not change behavior for normal users.
No immediate action required. Reviewers may verify that the fuzzing cfg is not accidentally enabled in release builds and that fallback time values remain safe for fuzz coverage. Consider whether any newly introduced #[cfg(all(feature = "std", fuzzing))] branches need additional test coverage.
Security signals we found
Conditional compilation change (cfg gating) for time sources under fuzzing
Deterministic hashing automatically enabled under fuzzing cfg
Reuses existing no-std fallback paths for time values
No production code behavior change outside fuzzing builds
Evidence from the diff
The commit gates all SystemTime::now() and Instant::now() calls with #[cfg(all(feature = “std”, not(fuzzing)))], reusing the existing no-std fallback paths (highest_seen_timestamp, None, or constants) when the fuzzing cfg is active. It also makes hash tables use a deterministic SipHasher automatically under fuzzing, instead of only when LDK_TEST_DETERMINISTIC_HASHES is set. The changes are conditional on the fuzzing build configuration and do not alter production code paths.
Changed components
lightning/src/ln/channel.rslightning/src/ln/channelmanager.rslightning/src/ln/outbound_payment.rslightning/src/ln/peer_handler.rslightning/src/offers/flow.rslightning/src/onion_message/dns_resolution.rslightning/src/routing/gossip.rslightning/src/util/hash_tables.rsInspect captured patch +52 / −41
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 03f78dc..675c53a 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -16717,10 +16717,10 @@ impl<'a, 'b, 'c, ES: EntropySource, SP: SignerProvider>
}
fn duration_since_epoch() -> Option<Duration> {
- #[cfg(not(feature = "std"))]
+ #[cfg(any(not(feature = "std"), fuzzing))]
let now = None;
- #[cfg(feature = "std")]
+ #[cfg(all(feature = "std", not(fuzzing)))]
let now = Some(
std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index 660f61f..2e78270 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -8936,11 +8936,11 @@ impl<
let _ = self.handle_error(err, counterparty_node_id);
}
- #[cfg(feature = "std")]
+ #[cfg(all(feature = "std", not(fuzzing)))]
let duration_since_epoch = std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH");
- #[cfg(not(feature = "std"))]
+ #[cfg(any(not(feature = "std"), fuzzing))]
let duration_since_epoch = Duration::from_secs(
self.highest_seen_timestamp.load(Ordering::Acquire).saturating_sub(7200) as u64,
);
@@ -14129,7 +14129,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
let currency =
Network::from_chain_hash(self.chain_hash).map(Into::into).unwrap_or(Currency::Bitcoin);
- #[cfg(feature = "std")]
+ #[cfg(all(feature = "std", not(fuzzing)))]
let duration_since_epoch = {
use std::time::SystemTime;
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)
@@ -14139,7 +14139,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
// This may be up to 2 hours in the future because of bitcoin's block time rule or about
// 10-30 minutes in the past if a block hasn't been found recently. This should be fine as
// the default invoice expiration is 2 hours, though shorter expirations may be problematic.
- #[cfg(not(feature = "std"))]
+ #[cfg(any(not(feature = "std"), fuzzing))]
let duration_since_epoch =
Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64);
@@ -14996,9 +14996,9 @@ impl<
}
pub(super) fn duration_since_epoch(&self) -> Duration {
- #[cfg(not(feature = "std"))]
+ #[cfg(any(not(feature = "std"), fuzzing))]
let now = Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64);
- #[cfg(feature = "std")]
+ #[cfg(all(feature = "std", not(fuzzing)))]
let now = std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH");
diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs
index b08b0f5..9241e6c 100644
--- a/lightning/src/ln/outbound_payment.rs
+++ b/lightning/src/ln/outbound_payment.rs
@@ -446,14 +446,16 @@ impl Retry {
(Retry::Attempts(max_retry_count), PaymentAttempts { count, .. }) => {
max_retry_count > count
},
- #[cfg(feature = "std")]
+ #[cfg(all(feature = "std", not(fuzzing)))]
(Retry::Timeout(max_duration), PaymentAttempts { first_attempted_at, .. }) =>
*max_duration >= Instant::now().duration_since(*first_attempted_at),
+ #[cfg(all(feature = "std", fuzzing))]
+ (Retry::Timeout(_), _) => true,
}
}
}
-#[cfg(feature = "std")]
+#[cfg(all(feature = "std", not(fuzzing)))]
#[rustfmt::skip]
pub(super) fn has_expired(route_params: &RouteParameters) -> bool {
if let Some(expiry_time) = route_params.payment_params.expiry_time {
@@ -464,6 +466,11 @@ pub(super) fn has_expired(route_params: &RouteParameters) -> bool {
false
}
+#[cfg(all(feature = "std", fuzzing))]
+pub(super) fn has_expired(_route_params: &RouteParameters) -> bool {
+ false
+}
+
/// Storing minimal payment attempts information required for determining if a outbound payment can
/// be retried.
pub(crate) struct PaymentAttempts {
@@ -471,7 +478,7 @@ pub(crate) struct PaymentAttempts {
/// it means the result of the first attempt is not known yet.
pub(crate) count: u32,
/// This field is only used when retry is `Retry::Timeout` which is only build with feature std
- #[cfg(feature = "std")]
+ #[cfg(all(feature = "std", not(fuzzing)))]
first_attempted_at: Instant,
}
@@ -479,7 +486,7 @@ impl PaymentAttempts {
pub(crate) fn new() -> Self {
PaymentAttempts {
count: 0,
- #[cfg(feature = "std")]
+ #[cfg(all(feature = "std", not(fuzzing)))]
first_attempted_at: Instant::now(),
}
}
@@ -487,9 +494,9 @@ impl PaymentAttempts {
impl Display for PaymentAttempts {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
- #[cfg(not(feature = "std"))]
+ #[cfg(any(not(feature = "std"), fuzzing))]
return write!(f, "attempts: {}", self.count);
- #[cfg(feature = "std")]
+ #[cfg(all(feature = "std", not(fuzzing)))]
return write!(
f,
"attempts: {}, duration: {}s",
diff --git a/lightning/src/ln/peer_handler.rs b/lightning/src/ln/peer_handler.rs
index 759a1e7..69d0815 100644
--- a/lightning/src/ln/peer_handler.rs
+++ b/lightning/src/ln/peer_handler.rs
@@ -2327,7 +2327,7 @@ impl<
#[allow(unused_mut)]
let mut should_do_full_sync = true;
- #[cfg(feature = "std")]
+ #[cfg(all(feature = "std", not(fuzzing)))]
{
// Forward ad-hoc gossip if the timestamp range is less than six hours ago.
// Otherwise, do a full sync.
diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs
index 6e7293c..c1c3ce2 100644
--- a/lightning/src/offers/flow.rs
+++ b/lightning/src/offers/flow.rs
@@ -183,9 +183,9 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
}
fn duration_since_epoch(&self) -> Duration {
- #[cfg(not(feature = "std"))]
+ #[cfg(any(not(feature = "std"), fuzzing))]
let now = Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64);
- #[cfg(feature = "std")]
+ #[cfg(all(feature = "std", not(fuzzing)))]
let now = std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH");
@@ -942,7 +942,7 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
)
.map_err(|_| Bolt12SemanticError::MissingPaths)?;
- #[cfg(feature = "std")]
+ #[cfg(all(feature = "std", not(fuzzing)))]
let builder = refund.respond_using_derived_keys(
payment_paths,
payment_hash,
@@ -950,9 +950,9 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
entropy,
)?;
- #[cfg(not(feature = "std"))]
+ #[cfg(any(not(feature = "std"), fuzzing))]
let created_at = Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64);
- #[cfg(not(feature = "std"))]
+ #[cfg(any(not(feature = "std"), fuzzing))]
let builder = refund.respond_using_derived_keys_no_std(
payment_paths,
payment_hash,
@@ -1008,9 +1008,9 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
)
.map_err(|_| Bolt12SemanticError::MissingPaths)?;
- #[cfg(feature = "std")]
+ #[cfg(all(feature = "std", not(fuzzing)))]
let builder = invoice_request.respond_using_derived_keys(payment_paths, payment_hash);
- #[cfg(not(feature = "std"))]
+ #[cfg(any(not(feature = "std"), fuzzing))]
let builder = invoice_request.respond_using_derived_keys_no_std(
payment_paths,
payment_hash,
@@ -1067,9 +1067,9 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
)
.map_err(|_| Bolt12SemanticError::MissingPaths)?;
- #[cfg(feature = "std")]
+ #[cfg(all(feature = "std", not(fuzzing)))]
let builder = invoice_request.respond_with(payment_paths, payment_hash);
- #[cfg(not(feature = "std"))]
+ #[cfg(any(not(feature = "std"), fuzzing))]
let builder = invoice_request.respond_with_no_std(
payment_paths,
payment_hash,
diff --git a/lightning/src/onion_message/dns_resolution.rs b/lightning/src/onion_message/dns_resolution.rs
index e857a35..5f68fa7 100644
--- a/lightning/src/onion_message/dns_resolution.rs
+++ b/lightning/src/onion_message/dns_resolution.rs
@@ -501,7 +501,7 @@ impl OMNameResolver {
if let Ok(validated_rrs) = validated_rrs {
#[allow(unused_assignments, unused_mut)]
let mut time = self.latest_block_time.load(Ordering::Acquire) as u64;
- #[cfg(feature = "std")]
+ #[cfg(all(feature = "std", not(fuzzing)))]
{
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now().duration_since(UNIX_EPOCH);
@@ -512,7 +512,8 @@ impl OMNameResolver {
// (we assume no more than two hours, though the actual limits are rather
// complicated).
// Thus, we have to let the proof times be rather fuzzy.
- let max_time_offset = if cfg!(feature = "std") { 0 } else { 60 * 2 };
+ let max_time_offset =
+ if cfg!(all(feature = "std", not(fuzzing))) { 0 } else { 60 * 2 };
if validated_rrs.valid_from > time + max_time_offset {
return None;
}
diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs
index 3794c38..adeb67a 100644
--- a/lightning/src/routing/gossip.rs
+++ b/lightning/src/routing/gossip.rs
@@ -843,7 +843,7 @@ impl<G: Deref<Target = NetworkGraph<L>>, U: UtxoLookup, L: Logger> BaseMessageHa
let mut gossip_start_time = 0;
#[allow(unused)]
let should_sync = self.should_request_full_sync();
- #[cfg(feature = "std")]
+ #[cfg(all(feature = "std", not(fuzzing)))]
{
gossip_start_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
@@ -2195,7 +2195,7 @@ impl<L: Logger> NetworkGraph<L> {
#[allow(unused_mut, unused_assignments)]
let mut announcement_received_time = 0;
- #[cfg(feature = "std")]
+ #[cfg(all(feature = "std", not(fuzzing)))]
{
announcement_received_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
@@ -2235,11 +2235,11 @@ impl<L: Logger> NetworkGraph<L> {
///
/// The channel and any node for which this was their last channel are removed from the graph.
pub fn channel_failed_permanent(&self, short_channel_id: u64) {
- #[cfg(feature = "std")]
+ #[cfg(all(feature = "std", not(fuzzing)))]
let current_time_unix = Some(
SystemTime::now().duration_since(UNIX_EPOCH).expect("Time must be > 1970").as_secs(),
);
- #[cfg(not(feature = "std"))]
+ #[cfg(any(not(feature = "std"), fuzzing))]
let current_time_unix = None;
self.channel_failed_permanent_with_time(short_channel_id, current_time_unix)
@@ -2262,11 +2262,11 @@ impl<L: Logger> NetworkGraph<L> {
/// Marks a node in the graph as permanently failed, effectively removing it and its channels
/// from local storage.
pub fn node_failed_permanent(&self, node_id: &PublicKey) {
- #[cfg(feature = "std")]
+ #[cfg(all(feature = "std", not(fuzzing)))]
let current_time_unix = Some(
SystemTime::now().duration_since(UNIX_EPOCH).expect("Time must be > 1970").as_secs(),
);
- #[cfg(not(feature = "std"))]
+ #[cfg(any(not(feature = "std"), fuzzing))]
let current_time_unix = None;
let node_id = NodeId::from_pubkey(node_id);
@@ -2303,7 +2303,6 @@ impl<L: Logger> NetworkGraph<L> {
}
}
- #[cfg(feature = "std")]
/// Removes information about channels that we haven't heard any updates about in some time.
/// This can be used regularly to prune the network graph of channels that likely no longer
/// exist.
@@ -2320,6 +2319,7 @@ impl<L: Logger> NetworkGraph<L> {
///
/// This method is only available with the `std` feature. See
/// [`NetworkGraph::remove_stale_channels_and_tracking_with_time`] for non-`std` use.
+ #[cfg(all(feature = "std", not(fuzzing)))]
pub fn remove_stale_channels_and_tracking(&self) {
let time =
SystemTime::now().duration_since(UNIX_EPOCH).expect("Time must be > 1970").as_secs();
@@ -2403,10 +2403,10 @@ impl<L: Logger> NetworkGraph<L> {
if let Some(time) = time {
current_time_unix.saturating_sub(*time) < REMOVED_ENTRIES_TRACKING_AGE_LIMIT_SECS
} else {
- // NOTE: In the case of non-`std`, we won't have access to the current UNIX time at the time of removal,
- // so we'll just set the removal time here to the current UNIX time on the very next invocation
- // of this function.
- #[cfg(not(feature = "std"))]
+ // NOTE: In the case of non-`std` or fuzzing, we won't have access to the current UNIX
+ // time at the time of removal, so we'll just set the removal time here to the current
+ // UNIX time on the very next invocation of this function.
+ #[cfg(any(not(feature = "std"), fuzzing))]
{
let mut tracked_time = Some(current_time_unix);
core::mem::swap(time, &mut tracked_time);
@@ -2476,7 +2476,7 @@ impl<L: Logger> NetworkGraph<L> {
});
}
- #[cfg(all(feature = "std", not(test), not(feature = "_test_utils")))]
+ #[cfg(all(feature = "std", not(test), not(feature = "_test_utils"), not(fuzzing)))]
{
// Note that many tests rely on being able to set arbitrarily old timestamps, thus we
// disable this check during tests!
diff --git a/lightning/src/util/hash_tables.rs b/lightning/src/util/hash_tables.rs
index b655597..545b034 100644
--- a/lightning/src/util/hash_tables.rs
+++ b/lightning/src/util/hash_tables.rs
@@ -6,11 +6,11 @@
pub use hashbrown::hash_map;
mod hashbrown_tables {
- #[cfg(all(feature = "std", not(test)))]
+ #[cfg(all(feature = "std", not(test), not(fuzzing)))]
mod hasher {
pub use std::collections::hash_map::RandomState;
}
- #[cfg(all(feature = "std", test))]
+ #[cfg(all(feature = "std", any(test, fuzzing)))]
mod hasher {
#![allow(deprecated)] // hash::SipHasher was deprecated in favor of something only in std.
use core::hash::{BuildHasher, Hasher};
@@ -27,7 +27,10 @@ mod hashbrown_tables {
impl RandomState {
pub fn new() -> RandomState {
- if std::env::var("LDK_TEST_DETERMINISTIC_HASHES").map(|v| v == "1").unwrap_or(false)
+ if cfg!(fuzzing)
+ || std::env::var("LDK_TEST_DETERMINISTIC_HASHES")
+ .map(|v| v == "1")
+ .unwrap_or(false)
{
RandomState::Deterministic
} else {
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.