@@ -18,9 +18,9 @@ struct AriCacheEntry {
1818 refresh_after : SystemTime ,
1919}
2020
21- static CACHE : std:: sync :: OnceLock <
22- std :: sync :: Mutex < std :: collections :: HashMap < AriCacheKey , AriCacheEntry > > ,
23- > = std:: sync:: OnceLock :: new ( ) ;
21+ type AriCache = std:: collections :: HashMap < AriCacheKey , AriCacheEntry > ;
22+
23+ static CACHE : std :: sync :: OnceLock < std :: sync :: Mutex < AriCache > > = std:: sync:: OnceLock :: new ( ) ;
2424
2525pub ( super ) async fn execute_due_queue (
2626 config : & Config ,
@@ -180,11 +180,7 @@ fn safe_ari_schedule(
180180}
181181
182182fn cached_decision ( key : & AriCacheKey , now : SystemTime , fallback : bool ) -> Option < bool > {
183- let cache = CACHE . get_or_init ( Default :: default) ;
184- let mut cache = cache. lock ( ) . unwrap_or_else ( |_| {
185- log:: error!( target: "fluxheim::security" , "ACME ARI cache lock poisoned" ) ;
186- std:: process:: abort ( ) ;
187- } ) ;
183+ let mut cache = lock_cache ( ) ;
188184 cache. retain ( |_, entry| entry. refresh_after > now) ;
189185 cache. get ( key) . map ( |entry| {
190186 entry
@@ -194,11 +190,8 @@ fn cached_decision(key: &AriCacheKey, now: SystemTime, fallback: bool) -> Option
194190}
195191
196192fn store_cache ( key : & AriCacheKey , scheduled : Option < u64 > , now : SystemTime , retry_after : Duration ) {
197- let cache = CACHE . get_or_init ( Default :: default) ;
198- let mut cache = cache. lock ( ) . unwrap_or_else ( |_| {
199- log:: error!( target: "fluxheim::security" , "ACME ARI cache lock poisoned" ) ;
200- std:: process:: abort ( ) ;
201- } ) ;
193+ let refresh_after = cache_refresh_after ( now, retry_after) ;
194+ let mut cache = lock_cache ( ) ;
202195 cache. retain ( |_, entry| entry. refresh_after > now) ;
203196 if cache. len ( ) >= MAX_CACHE_ENTRIES && !cache. contains_key ( key) {
204197 cache. clear ( ) ;
@@ -207,12 +200,33 @@ fn store_cache(key: &AriCacheKey, scheduled: Option<u64>, now: SystemTime, retry
207200 key. clone ( ) ,
208201 AriCacheEntry {
209202 scheduled_unix_secs : scheduled,
210- refresh_after : now
211- + retry_after. clamp ( Duration :: from_secs ( 60 ) , Duration :: from_secs ( 86_400 ) ) ,
203+ refresh_after,
212204 } ,
213205 ) ;
214206}
215207
208+ fn cache_refresh_after ( now : SystemTime , retry_after : Duration ) -> SystemTime {
209+ now. checked_add ( retry_after. clamp ( Duration :: from_secs ( 60 ) , Duration :: from_secs ( 86_400 ) ) )
210+ . unwrap_or ( now)
211+ }
212+
213+ fn lock_cache ( ) -> std:: sync:: MutexGuard < ' static , AriCache > {
214+ let cache = CACHE . get_or_init ( Default :: default) ;
215+ match cache. lock ( ) {
216+ Ok ( cache) => cache,
217+ Err ( poisoned) => {
218+ log:: error!(
219+ target: "fluxheim::security" ,
220+ "ACME ARI advisory cache lock poisoned; discarding cached decisions"
221+ ) ;
222+ let mut recovered = poisoned. into_inner ( ) ;
223+ recovered. clear ( ) ;
224+ cache. clear_poison ( ) ;
225+ recovered
226+ }
227+ }
228+ }
229+
216230fn unix_secs ( time : SystemTime ) -> u64 {
217231 time. duration_since ( UNIX_EPOCH )
218232 . map ( |duration| duration. as_secs ( ) )
@@ -283,6 +297,39 @@ mod tests {
283297 assert_eq ! ( cached_decision( & second, now, false ) , Some ( true ) ) ;
284298 }
285299
300+ #[ test]
301+ fn cache_refresh_deadline_overflow_expires_immediately ( ) {
302+ let near_limit = UNIX_EPOCH
303+ . checked_add ( Duration :: from_secs ( i64:: MAX as u64 ) )
304+ . unwrap ( ) ;
305+ assert_eq ! (
306+ cache_refresh_after( near_limit, Duration :: from_secs( 86_400 ) ) ,
307+ near_limit
308+ ) ;
309+ }
310+
311+ #[ test]
312+ fn poisoned_advisory_cache_is_cleared_and_recovers ( ) {
313+ let _test_lock = CACHE_TEST_LOCK . lock ( ) . unwrap ( ) ;
314+ let cache = CACHE . get_or_init ( Default :: default) ;
315+ let poisoned = std:: panic:: catch_unwind ( || {
316+ let _cache = cache. lock ( ) . unwrap ( ) ;
317+ panic ! ( "poison ARI cache for recovery test" ) ;
318+ } ) ;
319+ assert ! ( poisoned. is_err( ) ) ;
320+ assert ! ( cache. is_poisoned( ) ) ;
321+
322+ let now = UNIX_EPOCH + Duration :: from_secs ( 25_000 ) ;
323+ let key = AriCacheKey {
324+ issuer_directory : "https://issuer.example/directory" . to_owned ( ) ,
325+ certificate_identifier : "recovered-identifier" . to_owned ( ) ,
326+ } ;
327+ store_cache ( & key, Some ( 25_100 ) , now, Duration :: from_secs ( 300 ) ) ;
328+
329+ assert ! ( !cache. is_poisoned( ) ) ;
330+ assert_eq ! ( cached_decision( & key, now, true ) , Some ( false ) ) ;
331+ }
332+
286333 #[ test]
287334 fn ari_window_must_end_within_certificate_validity ( ) {
288335 let not_after = UNIX_EPOCH + Duration :: from_secs ( 30_000 ) ;
0 commit comments