Skip to content

Commit 4aa5760

Browse files
committed
fix(errors): replace generic HeaderUnverifiable with precise corruption variants
Every structural-decode failure across the btree, catalog, pager, recovery, segment, snapshot, and txn/rekey code paths previously collapsed into the same CorruptionDetail::HeaderUnverifiable, hiding which invariant actually failed. Split it into purpose-built variants (StructuralHeaderInvalid, FooterFramingInvalid, NodeBodyMalformed, NodeKindMismatch, OverflowBodyMalformed, JournalRecordMalformed, SnapshotArtifactInvalid) and route each call site through the matching constructor with a field name, so operators diagnosing corruption can tell what broke and where.
1 parent 4a6c853 commit 4aa5760

20 files changed

Lines changed: 239 additions & 188 deletions

File tree

src/btree/internal.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,7 @@ impl Internal {
2727
pub fn decode(body: &[u8]) -> Result<Self> {
2828
let h: NodeHeader = validate_node_body(body)?;
2929
if h.kind != NodeKind::Internal {
30-
return Err(PagedbError::corruption(
31-
crate::errors::CorruptionDetail::HeaderUnverifiable,
32-
));
30+
return Err(PagedbError::node_kind_mismatch(None, "internal", "leaf"));
3331
}
3432
let prefix_len = h.prefix_len as usize;
3533
let mut entries = Vec::with_capacity(h.slot_count as usize);
@@ -167,9 +165,7 @@ impl<'a> InternalAccessor<'a> {
167165
pub fn new(body: &'a [u8]) -> Result<Self> {
168166
let h = validate_node_body(body)?;
169167
if h.kind != NodeKind::Internal {
170-
return Err(PagedbError::corruption(
171-
crate::errors::CorruptionDetail::HeaderUnverifiable,
172-
));
168+
return Err(PagedbError::node_kind_mismatch(None, "internal", "leaf"));
173169
}
174170
// Internal nodes always encode with prefix_len = 0 today; if that ever
175171
// changes, this accessor needs the same prefix handling as LeafAccessor.

src/btree/leaf.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,7 @@ impl Leaf {
5151
pub fn decode(body: &[u8]) -> Result<Self> {
5252
let h: NodeHeader = validate_node_body(body)?;
5353
if h.kind != NodeKind::Leaf {
54-
return Err(PagedbError::corruption(
55-
crate::errors::CorruptionDetail::HeaderUnverifiable,
56-
));
54+
return Err(PagedbError::node_kind_mismatch(None, "leaf", "internal"));
5755
}
5856
let prefix_len = h.prefix_len as usize;
5957
let prefix_bytes = body[HEADER_LEN..HEADER_LEN + prefix_len].to_vec();
@@ -270,9 +268,7 @@ impl<'a> LeafAccessor<'a> {
270268
pub fn new(body: &'a [u8]) -> Result<Self> {
271269
let h = validate_node_body(body)?;
272270
if h.kind != NodeKind::Leaf {
273-
return Err(PagedbError::corruption(
274-
crate::errors::CorruptionDetail::HeaderUnverifiable,
275-
));
271+
return Err(PagedbError::node_kind_mismatch(None, "leaf", "internal"));
276272
}
277273
Ok(Self {
278274
body,

src/btree/node.rs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Node layout shared by leaf and internal pages.
22
33
use crate::Result;
4-
use crate::errors::{CorruptionDetail, PagedbError};
4+
use crate::errors::PagedbError;
55
use crate::pager::format::data_page::ENVELOPE_OVERHEAD;
66

77
/// `node_kind` byte value at offset 0 of the node body.
@@ -17,16 +17,24 @@ impl NodeKind {
1717
match b {
1818
0x00 => Ok(Self::Internal),
1919
0x01 => Ok(Self::Leaf),
20-
_ => Err(PagedbError::corruption(
21-
CorruptionDetail::HeaderUnverifiable,
22-
)),
20+
_ => Err(PagedbError::node_body_malformed("node_kind_byte")),
2321
}
2422
}
2523

2624
#[must_use]
2725
pub fn as_byte(self) -> u8 {
2826
self as u8
2927
}
28+
29+
/// Stable lowercase name, for naming both sides of a kind disagreement in
30+
/// [`CorruptionDetail::NodeKindMismatch`].
31+
#[must_use]
32+
pub const fn name(self) -> &'static str {
33+
match self {
34+
Self::Internal => "internal",
35+
Self::Leaf => "leaf",
36+
}
37+
}
3038
}
3139

3240
pub const HEADER_LEN: usize = 24;
@@ -91,9 +99,7 @@ pub struct NodeHeader {
9199

92100
pub fn read_header(body: &[u8]) -> Result<NodeHeader> {
93101
if body.len() < HEADER_LEN {
94-
return Err(PagedbError::corruption(
95-
CorruptionDetail::HeaderUnverifiable,
96-
));
102+
return Err(PagedbError::node_body_malformed("header_length"));
97103
}
98104
let kind = NodeKind::from_byte(body[OFF_NODE_KIND])?;
99105
let slot_count = read_u16_le(body, OFF_SLOT_COUNT);
@@ -117,7 +123,7 @@ pub fn read_header(body: &[u8]) -> Result<NodeHeader> {
117123
/// entry are attacker- or bug-controlled `u16`s that the decoders and the
118124
/// zero-copy accessors use directly as slice indices. Without this pass a
119125
/// malformed-but-authenticated page panics the library (an out-of-range slice
120-
/// index) instead of surfacing as [`CorruptionDetail::HeaderUnverifiable`],
126+
/// index) instead of surfacing as [`CorruptionDetail::NodeBodyMalformed`],
121127
/// which is a strictly worse failure than the one it would report.
122128
///
123129
/// Every constructor that turns raw bytes into a node runs this first, so the
@@ -186,7 +192,7 @@ fn field_end(body: &[u8], start: usize, len: usize) -> Result<usize> {
186192
}
187193

188194
fn malformed() -> PagedbError {
189-
PagedbError::corruption(CorruptionDetail::HeaderUnverifiable)
195+
PagedbError::node_body_malformed("slot_directory")
190196
}
191197

192198
pub fn write_header(
@@ -209,6 +215,7 @@ pub fn write_header(
209215
#[cfg(test)]
210216
mod tests {
211217
use super::*;
218+
use crate::errors::CorruptionDetail;
212219

213220
const CAP: usize = 4056;
214221

@@ -217,10 +224,10 @@ mod tests {
217224
matches!(
218225
validate_node_body(body),
219226
Err(PagedbError::Corruption(
220-
CorruptionDetail::HeaderUnverifiable
227+
CorruptionDetail::NodeBodyMalformed { .. }
221228
))
222229
),
223-
"{label}: expected HeaderUnverifiable"
230+
"{label}: expected NodeBodyMalformed"
224231
);
225232
}
226233

src/btree/overflow.rs

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ pub fn encode_overflow(body: &mut [u8], next: u64, data: &[u8]) -> Result<()> {
7171
/// Decode an overflow chain page body (non-root). Returns `(next, data_slice)`.
7272
pub fn decode_overflow(body: &[u8]) -> Result<(u64, &[u8])> {
7373
if body.len() < OVERFLOW_HEADER_LEN {
74-
return Err(PagedbError::corruption(
75-
crate::errors::CorruptionDetail::HeaderUnverifiable,
74+
return Err(PagedbError::overflow_body_malformed(
75+
"chain_page.header_length",
7676
));
7777
}
7878
let mut n = [0u8; 8];
@@ -82,8 +82,8 @@ pub fn decode_overflow(body: &[u8]) -> Result<(u64, &[u8])> {
8282
l.copy_from_slice(&body[8..12]);
8383
let data_len = u32::from_le_bytes(l) as usize;
8484
if 12 + data_len > body.len() {
85-
return Err(PagedbError::corruption(
86-
crate::errors::CorruptionDetail::HeaderUnverifiable,
85+
return Err(PagedbError::overflow_body_malformed(
86+
"chain_page.data_length",
8787
));
8888
}
8989
Ok((next, &body[12..12 + data_len]))
@@ -112,9 +112,7 @@ fn encode_overflow_root(body: &mut [u8], refcount: u32, next: u64, data: &[u8])
112112
/// Decode an overflow root page body. Returns `(refcount, next, data_slice)`.
113113
fn decode_overflow_root(body: &[u8]) -> Result<(u32, u64, &[u8])> {
114114
if body.len() < OVERFLOW_ROOT_HEADER_LEN {
115-
return Err(PagedbError::corruption(
116-
crate::errors::CorruptionDetail::HeaderUnverifiable,
117-
));
115+
return Err(PagedbError::overflow_body_malformed("root.header_length"));
118116
}
119117
let mut r = [0u8; 4];
120118
r.copy_from_slice(&body[0..4]);
@@ -126,9 +124,7 @@ fn decode_overflow_root(body: &[u8]) -> Result<(u32, u64, &[u8])> {
126124
l.copy_from_slice(&body[12..16]);
127125
let data_len = u32::from_le_bytes(l) as usize;
128126
if 16 + data_len > body.len() {
129-
return Err(PagedbError::corruption(
130-
crate::errors::CorruptionDetail::HeaderUnverifiable,
131-
));
127+
return Err(PagedbError::overflow_body_malformed("root.data_length"));
132128
}
133129
Ok((refcount, next, &body[16..16 + data_len]))
134130
}
@@ -323,8 +319,8 @@ pub async fn read_chain<V: Vfs>(
323319
next = n;
324320
}
325321
if out.len() as u64 != total_len {
326-
return Err(PagedbError::corruption(
327-
crate::errors::CorruptionDetail::HeaderUnverifiable,
322+
return Err(PagedbError::overflow_body_malformed(
323+
"chain.assembled_length",
328324
));
329325
}
330326
Ok(out)

src/btree/tree/core.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use std::collections::HashMap;
44
use std::sync::Arc;
55

6-
use crate::errors::{CorruptionDetail, PagedbError};
6+
use crate::errors::PagedbError;
77
use crate::pager::format::page_kind::PageKind;
88
use crate::pager::{PageGuard, Pager};
99
use crate::vfs::Vfs;
@@ -253,8 +253,13 @@ impl<V: Vfs> BTree<V> {
253253
_ => return Err(PagedbError::IllegalPageKind),
254254
};
255255
if decoded_kind != expected_kind {
256-
return Err(PagedbError::corruption(
257-
CorruptionDetail::HeaderUnverifiable,
256+
// The envelope is authenticated and the body is not, so name both
257+
// and the page: this is a mis-routed page, not damaged content, and
258+
// an operator chasing it needs to know which side to trust.
259+
return Err(PagedbError::node_kind_mismatch(
260+
Some(page_id),
261+
expected_kind.name(),
262+
decoded_kind.name(),
258263
));
259264
}
260265
Ok((guard, decoded_kind))

src/catalog/codec.rs

Lines changed: 31 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,7 @@ impl RekeyStage {
5757
3 => Ok(Self::HeaderTargetPublished),
5858
4 => Ok(Self::MainDone),
5959
5 => Ok(Self::SegmentsPending),
60-
_ => Err(PagedbError::corruption(
61-
crate::errors::CorruptionDetail::HeaderUnverifiable,
62-
)),
60+
_ => Err(PagedbError::catalog_row_invalid("rekey.stage")),
6361
}
6462
}
6563
}
@@ -111,8 +109,8 @@ impl RekeySegmentProgressState {
111109
fn from_byte(byte: u8) -> Result<Self> {
112110
match byte {
113111
1 => Ok(Self::Sealed),
114-
_ => Err(PagedbError::corruption(
115-
crate::errors::CorruptionDetail::HeaderUnverifiable,
112+
_ => Err(PagedbError::catalog_row_invalid(
113+
"rekey.segment_progress.state",
116114
)),
117115
}
118116
}
@@ -257,12 +255,13 @@ impl Catalog {
257255
/// positional progress is deliberately preserved only for diagnostics.
258256
pub fn decode_rekey_state(bytes: &[u8]) -> Result<RekeyStateRow> {
259257
if bytes.len() == LEGACY_REKEY_STATE_LEN {
260-
let target_mk_epoch = u64::from_le_bytes(bytes[0..8].try_into().map_err(|_| {
261-
PagedbError::corruption(crate::errors::CorruptionDetail::HeaderUnverifiable)
262-
})?);
258+
let target_mk_epoch =
259+
u64::from_le_bytes(bytes[0..8].try_into().map_err(|_| {
260+
PagedbError::catalog_row_invalid("rekey.legacy.target_mk_epoch")
261+
})?);
263262
if target_mk_epoch == 0 {
264-
return Err(PagedbError::corruption(
265-
crate::errors::CorruptionDetail::HeaderUnverifiable,
263+
return Err(PagedbError::catalog_row_invalid(
264+
"rekey.legacy.target_mk_epoch",
266265
));
267266
}
268267
return Ok(RekeyStateRow::Legacy(LegacyRekeyState {
@@ -271,15 +270,13 @@ impl Catalog {
271270
0 => false,
272271
1 => true,
273272
_ => {
274-
return Err(PagedbError::corruption(
275-
crate::errors::CorruptionDetail::HeaderUnverifiable,
273+
return Err(PagedbError::catalog_row_invalid(
274+
"rekey.legacy.main_db_done",
276275
));
277276
}
278277
},
279278
discarded_segments_index: u32::from_le_bytes(bytes[9..13].try_into().map_err(
280-
|_| {
281-
PagedbError::corruption(crate::errors::CorruptionDetail::HeaderUnverifiable)
282-
},
279+
|_| PagedbError::catalog_row_invalid("rekey.legacy.discarded_segments_index"),
283280
)?),
284281
}));
285282
}
@@ -289,28 +286,26 @@ impl Catalog {
289286
|| bytes[22..24].iter().any(|byte| *byte != 0)
290287
|| bytes[56..].iter().any(|byte| *byte != 0)
291288
{
292-
return Err(PagedbError::corruption(
293-
crate::errors::CorruptionDetail::HeaderUnverifiable,
294-
));
289+
return Err(PagedbError::catalog_row_invalid("rekey.framing"));
295290
}
296-
let source_mk_epoch = u64::from_le_bytes(bytes[4..12].try_into().map_err(|_| {
297-
PagedbError::corruption(crate::errors::CorruptionDetail::HeaderUnverifiable)
298-
})?);
299-
let target_mk_epoch = u64::from_le_bytes(bytes[12..20].try_into().map_err(|_| {
300-
PagedbError::corruption(crate::errors::CorruptionDetail::HeaderUnverifiable)
301-
})?);
291+
let source_mk_epoch = u64::from_le_bytes(
292+
bytes[4..12]
293+
.try_into()
294+
.map_err(|_| PagedbError::catalog_row_invalid("rekey.source_mk_epoch"))?,
295+
);
296+
let target_mk_epoch = u64::from_le_bytes(
297+
bytes[12..20]
298+
.try_into()
299+
.map_err(|_| PagedbError::catalog_row_invalid("rekey.target_mk_epoch"))?,
300+
);
302301
if target_mk_epoch == 0 || target_mk_epoch <= source_mk_epoch {
303-
return Err(PagedbError::corruption(
304-
crate::errors::CorruptionDetail::HeaderUnverifiable,
305-
));
302+
return Err(PagedbError::catalog_row_invalid("rekey.epoch_ordering"));
306303
}
307304
let same_kek = match bytes[2] {
308305
0 => false,
309306
1 => true,
310307
_ => {
311-
return Err(PagedbError::corruption(
312-
crate::errors::CorruptionDetail::HeaderUnverifiable,
313-
));
308+
return Err(PagedbError::catalog_row_invalid("rekey.same_kek"));
314309
}
315310
};
316311
crate::crypto::CipherId::from_byte(bytes[20])?;
@@ -352,8 +347,8 @@ impl Catalog {
352347
|| bytes[0] != 1
353348
|| bytes[2..4].iter().any(|byte| *byte != 0)
354349
{
355-
return Err(PagedbError::corruption(
356-
crate::errors::CorruptionDetail::HeaderUnverifiable,
350+
return Err(PagedbError::catalog_row_invalid(
351+
"rekey.segment_progress.framing",
357352
));
358353
}
359354
let mut replacement_segment_id = [0u8; 16];
@@ -385,9 +380,7 @@ impl Catalog {
385380
/// Decode a counter value from an 8-byte little-endian slice.
386381
pub fn decode_counter(bytes: &[u8]) -> Result<u64> {
387382
if bytes.len() != 8 {
388-
return Err(PagedbError::corruption(
389-
crate::errors::CorruptionDetail::HeaderUnverifiable,
390-
));
383+
return Err(PagedbError::catalog_row_invalid("counter.value"));
391384
}
392385
let mut b = [0u8; 8];
393386
b.copy_from_slice(bytes);
@@ -420,9 +413,7 @@ impl Catalog {
420413

421414
pub fn decode_realm_quotas(bytes: &[u8]) -> Result<RealmQuotas> {
422415
if bytes.len() != REALM_QUOTAS_LEN {
423-
return Err(PagedbError::corruption(
424-
crate::errors::CorruptionDetail::HeaderUnverifiable,
425-
));
416+
return Err(PagedbError::catalog_row_invalid("realm_quotas.length"));
426417
}
427418
let mask = bytes[0];
428419
let read = |off: usize| -> u64 {
@@ -490,9 +481,7 @@ impl Catalog {
490481

491482
pub fn decode_segment_meta(bytes: &[u8]) -> Result<SegmentMeta> {
492483
if bytes.len() != SEGMENT_META_LEN {
493-
return Err(PagedbError::corruption(
494-
crate::errors::CorruptionDetail::HeaderUnverifiable,
495-
));
484+
return Err(PagedbError::catalog_row_invalid("segment_meta.length"));
496485
}
497486
let segment_id = {
498487
let mut b = [0u8; 16];
@@ -547,9 +536,7 @@ impl Catalog {
547536
0 => Evictable::Authoritative,
548537
1 => Evictable::Replaceable,
549538
_ => {
550-
return Err(PagedbError::corruption(
551-
crate::errors::CorruptionDetail::HeaderUnverifiable,
552-
));
539+
return Err(PagedbError::catalog_row_invalid("segment_meta.evictable"));
553540
}
554541
};
555542
Ok(SegmentMeta {

0 commit comments

Comments
 (0)