From f2a71bc2995ba349f2d74b912a5853f575869fce Mon Sep 17 00:00:00 2001 From: meh Date: Wed, 5 Aug 2026 06:02:30 +0700 Subject: [PATCH] fix: contain persisted load panics --- codegen/src/generators/persist/table/impls.rs | 8 +-- .../src/generators/read_only/table/impls.rs | 8 +-- src/lib.rs | 2 +- src/persistence/error.rs | 63 +++++++++++++++++++ src/persistence/mod.rs | 2 +- 5 files changed, 73 insertions(+), 10 deletions(-) diff --git a/codegen/src/generators/persist/table/impls.rs b/codegen/src/generators/persist/table/impls.rs index 2dfa085..50658e7 100644 --- a/codegen/src/generators/persist/table/impls.rs +++ b/codegen/src/generators/persist/table/impls.rs @@ -245,10 +245,10 @@ impl PersistGenerator { if !std::path::Path::new(&table_path).exists() { return Self::new(engine).await; }; - let space = #space_ident::parse_file(&table_path) - .await - .map_err(|error| PersistenceLoadError::corrupt(&table_path, error))?; - let table = space.into_worktable(engine, &table_path).await?; + let table = load_persisted_state(&table_path, async { + let space = #space_ident::parse_file(&table_path).await?; + Ok::<_, eyre::Report>(space.into_worktable(engine, &table_path).await?) + }).await?; Ok(table) } } diff --git a/codegen/src/generators/read_only/table/impls.rs b/codegen/src/generators/read_only/table/impls.rs index 79bfdb4..2542cff 100644 --- a/codegen/src/generators/read_only/table/impls.rs +++ b/codegen/src/generators/read_only/table/impls.rs @@ -208,10 +208,10 @@ impl ReadOnlyGenerator { if !std::path::Path::new(&table_path).exists() { return Self::new(engine).await; }; - let space = #space_ident::parse_file(&table_path) - .await - .map_err(|error| PersistenceLoadError::corrupt(&table_path, error))?; - let table = space.into_worktable(&table_path)?; + let table = load_persisted_state(&table_path, async { + let space = #space_ident::parse_file(&table_path).await?; + Ok::<_, eyre::Report>(space.into_worktable(&table_path)?) + }).await?; Ok(table) } } diff --git a/src/lib.rs b/src/lib.rs index 5ba6ae0..21c25cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,7 +38,7 @@ pub mod prelude { IndexTableOfContents, InsertOperation, Operation, OperationId, PersistedWorkTable, PersistenceConfig, PersistenceEngine, PersistenceError, PersistenceLoadError, PersistenceResult, PersistenceState, PersistenceTask, ReadOnlyPersistenceEngine, SpaceArcticIndex, SpaceCongeeIndex, SpaceData, SpaceDataOps, - SpaceIndex, SpaceIndexOps, SpaceIndexUnsized, SpaceSecondaryIndexOps, UpdateOperation, + SpaceIndex, SpaceIndexOps, SpaceIndexUnsized, SpaceSecondaryIndexOps, UpdateOperation, load_persisted_state, map_index_pages_to_toc_and_general, map_unsized_index_pages_to_toc_and_general, reconstruct_multi_index_nodes, validate_events, }; diff --git a/src/persistence/error.rs b/src/persistence/error.rs index bbd98dd..ed077fc 100644 --- a/src/persistence/error.rs +++ b/src/persistence/error.rs @@ -1,8 +1,12 @@ use std::error::Error; use std::fmt::{Display, Formatter}; +use std::future::Future; +use std::panic::AssertUnwindSafe; use std::path::{Path, PathBuf}; use std::sync::Arc; +use futures::FutureExt; + /// A persisted table could not be loaded without risking invalid data. /// /// WorkTable persistence is best-effort rather than crash-atomic. Abrupt @@ -47,6 +51,34 @@ impl Display for PersistenceLoadError { impl Error for PersistenceLoadError {} +/// Contains dependency panics while decoding an existing persisted store. +/// +/// Some lower-level page decoders still panic when torn bytes violate their +/// internal invariants. A persisted table is not exposed until the entire +/// decode and validation pipeline succeeds, so the future and all partially +/// decoded state are discarded on unwind. `AssertUnwindSafe` is used only to +/// establish that containment boundary; no value from the failed future is +/// reused. +#[doc(hidden)] +pub async fn load_persisted_state(path: impl AsRef, future: F) -> Result +where + F: Future>, +{ + let path = path.as_ref(); + match AssertUnwindSafe(future).catch_unwind().await { + Ok(Ok(value)) => Ok(value), + Ok(Err(error)) => Err(PersistenceLoadError::corrupt(path, format!("{error:#}"))), + Err(payload) => { + let reason = payload + .downcast_ref::<&str>() + .copied() + .or_else(|| payload.downcast_ref::().map(String::as_str)) + .unwrap_or("persisted-state loader panicked"); + Err(PersistenceLoadError::corrupt(path, reason)) + } + } +} + /// Terminal and lifecycle errors reported by a persistence task. #[derive(Debug)] pub enum PersistenceError { @@ -80,3 +112,34 @@ pub enum PersistenceState { Failed(Arc), Closed, } + +#[cfg(test)] +mod tests { + use super::{PersistenceLoadError, load_persisted_state}; + + #[tokio::test] + async fn persisted_state_panics_are_typed_load_errors() { + let error = load_persisted_state("table/path", async { + panic!("dependency decoder rejected torn bytes"); + #[allow(unreachable_code)] + Ok::<(), eyre::Report>(()) + }) + .await + .unwrap_err(); + + assert_eq!(error.path(), std::path::Path::new("table/path")); + assert_eq!(error.reason(), "dependency decoder rejected torn bytes"); + } + + #[tokio::test] + async fn persisted_state_errors_are_typed_load_errors() { + let error: PersistenceLoadError = load_persisted_state("table/path", async { + Err::<(), _>(eyre::eyre!("invalid persisted page")) + }) + .await + .unwrap_err(); + + assert_eq!(error.path(), std::path::Path::new("table/path")); + assert_eq!(error.reason(), "invalid persisted page"); + } +} diff --git a/src/persistence/mod.rs b/src/persistence/mod.rs index 94917fe..df98e79 100644 --- a/src/persistence/mod.rs +++ b/src/persistence/mod.rs @@ -4,7 +4,7 @@ use crate::persistence::operation::BatchOperation; pub use engine::DiskConfig; pub use engine::DiskPersistenceEngine; -pub use error::{PersistenceError, PersistenceLoadError, PersistenceResult, PersistenceState}; +pub use error::{PersistenceError, PersistenceLoadError, PersistenceResult, PersistenceState, load_persisted_state}; pub use operation::{ AcknowledgeOperation, DeleteOperation, InsertOperation, Operation, OperationId, OperationType, UpdateOperation, validate_events,