From 70aec622442419df3e6322be1a7cfcbf08734409 Mon Sep 17 00:00:00 2001 From: "Nuem.dev" <84929587+Manuel1234477@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:55:20 +0000 Subject: [PATCH] Handle clock stepping back in mailbox prune next_prune() called created.elapsed().expect(...), which panics when a mailbox timestamp is later than the current time. That happens if the system clock is stepped back after a mailbox was written, e.g. NTP correcting the time on a host without an RTC. Timestamps loaded from file metadata at startup can be affected the same way. prune() runs on every post and read through has_capacity(), so while the clock is behind, every mailbox request panics. The background prune task panics too and is never restarted. Treat a future timestamp as zero elapsed time instead. The entry still expires once the clock passes created + ttl. --- payjoin-mailroom/CHANGELOG.md | 2 ++ payjoin-mailroom/src/db/files.rs | 40 +++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/payjoin-mailroom/CHANGELOG.md b/payjoin-mailroom/CHANGELOG.md index be386fee6..92cfcaedd 100644 --- a/payjoin-mailroom/CHANGELOG.md +++ b/payjoin-mailroom/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Stop mailbox pruning from panicking when the system clock moves backwards + ## 0.1.3 - Enforce `blocked_regions` and `blocked_ips` on the ACME and manual TLS serve paths (#1941) diff --git a/payjoin-mailroom/src/db/files.rs b/payjoin-mailroom/src/db/files.rs index 1f6a77ece..b2e734953 100644 --- a/payjoin-mailroom/src/db/files.rs +++ b/payjoin-mailroom/src/db/files.rs @@ -485,8 +485,10 @@ impl Mailboxes { self.insert_order .front() .map(|(created, _id)| { + // A timestamp in the future means the clock was stepped back + // since the mailbox was written. Treat it as just created. self.ttl - .checked_sub(created.elapsed().expect("system clock moved back")) + .checked_sub(created.elapsed().unwrap_or(Duration::ZERO)) .unwrap_or(self.ttl) }) .unwrap_or(self.ttl) @@ -971,6 +973,42 @@ mod tests { Ok(()) } + #[tokio::test] + async fn test_prune_with_future_timestamp() -> std::io::Result<()> { + let dir = tempfile::tempdir()?; + let ttl = Duration::from_secs(600); + + let db = FilesDb::init(Duration::from_millis(2), dir.path().to_owned(), ttl) + .await + .expect("initializing mailbox database should succeed"); + + let id = ShortId([0u8; 8]); + db.post_v2_payload(&id, b"fooo".to_vec()) + .await + .expect("posting payload should succeed") + .expect("contents should be accepted"); + + // Simulate the system clock being stepped back after the write + { + let mut guard = db.mailboxes.lock().await; + for (ts, _) in guard.insert_order.iter_mut() { + *ts += Duration::from_secs(60); + } + } + + assert_eq!(db.prune().await.expect("pruning should not fail"), ttl); + assert_eq!(db.mailboxes.lock().await.len(), 1); + + // Other requests must keep working while the clock catches up + db.post_v2_payload(&ShortId([1u8; 8]), b"barr".to_vec()) + .await + .expect("posting payload should succeed") + .expect("contents should be accepted"); + assert_eq!(db.mailboxes.lock().await.len(), 2); + + Ok(()) + } + async fn assert_all_paths_over_capacity(db: &FilesDb) { let id = ShortId([0; 8]);