Skip to main content

hypercall_journal/
frame.rs

1use crc::{Crc, CRC_32_ISO_HDLC};
2use std::io::{Read, Write};
3
4// SECURITY NOTE: CRC32 is for accidental corruption detection only, not tamper
5// resistance. An attacker with write access to the WAL file can trivially forge
6// valid CRC32 checksums. Rely on filesystem permissions (mode 0o600) to prevent
7// unauthorized writes. If stronger tamper detection is needed, replace with
8// HMAC-SHA256 keyed from a deployment secret.
9pub const WAL_CRC: Crc<u32> = Crc::<u32>::new(&CRC_32_ISO_HDLC);
10
11pub const WAL_FRAME_HEADER_LEN: u64 = 8;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct WalFrame {
15    pub payload: Vec<u8>,
16    pub record_start_offset: u64,
17    pub end_offset: u64,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum WalReadStatus {
22    Eof,
23    Torn,
24}
25
26pub fn write_frame(
27    writer: &mut impl Write,
28    payload: &[u8],
29    next_offset: &mut u64,
30    description: &str,
31) -> Result<u64, String> {
32    let payload_len =
33        u32::try_from(payload.len()).map_err(|_| format!("WAL {description} payload too large"))?;
34    let crc = WAL_CRC.checksum(payload);
35
36    writer
37        .write_all(&payload_len.to_le_bytes())
38        .map_err(|e| format!("failed to write WAL length prefix: {}", e))?;
39    writer
40        .write_all(&crc.to_le_bytes())
41        .map_err(|e| format!("failed to write WAL crc prefix: {}", e))?;
42    writer
43        .write_all(payload)
44        .map_err(|e| format!("failed to write WAL payload: {}", e))?;
45
46    *next_offset = next_offset
47        .checked_add(WAL_FRAME_HEADER_LEN + payload.len() as u64)
48        .ok_or_else(|| "WAL offset overflow".to_string())?;
49
50    Ok(*next_offset)
51}
52
53pub fn read_next_frame(
54    reader: &mut impl Read,
55    file_len: u64,
56    offset: &mut u64,
57) -> Result<Result<WalFrame, WalReadStatus>, String> {
58    let record_start_offset = *offset;
59    let mut len_buf = [0u8; 4];
60    match reader.read_exact(&mut len_buf) {
61        Ok(()) => {}
62        Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
63            return Ok(Err(WalReadStatus::Eof));
64        }
65        Err(e) => return Err(format!("failed reading WAL length prefix: {}", e)),
66    }
67
68    let payload_len = u32::from_le_bytes(len_buf) as usize;
69    if payload_len == 0 {
70        panic!(
71            "CRITICAL_FAILURE: WAL record length was zero at offset {}. Data corruption detected.",
72            offset
73        );
74    }
75    *offset += 4;
76
77    let remaining = file_len.saturating_sub(*offset);
78    if payload_len as u64 > remaining {
79        return Ok(Err(WalReadStatus::Torn));
80    }
81
82    let mut crc_buf = [0u8; 4];
83    match reader.read_exact(&mut crc_buf) {
84        Ok(()) => {}
85        Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
86            return Ok(Err(WalReadStatus::Torn));
87        }
88        Err(e) => return Err(format!("failed reading WAL crc prefix: {}", e)),
89    }
90    *offset += 4;
91
92    let expected_crc = u32::from_le_bytes(crc_buf);
93    let mut payload = vec![0u8; payload_len];
94    match reader.read_exact(&mut payload) {
95        Ok(()) => {}
96        Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
97            return Ok(Err(WalReadStatus::Torn));
98        }
99        Err(e) => return Err(format!("failed reading WAL payload: {}", e)),
100    }
101    *offset += payload_len as u64;
102
103    let actual_crc = WAL_CRC.checksum(&payload);
104    if actual_crc != expected_crc {
105        panic!(
106            "CRITICAL_FAILURE: WAL CRC mismatch at end_offset {}: expected {}, got {}. Data corruption detected.",
107            offset, expected_crc, actual_crc
108        );
109    }
110
111    Ok(Ok(WalFrame {
112        payload,
113        record_start_offset,
114        end_offset: *offset,
115    }))
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn frame_roundtrips_payload() {
124        let mut bytes = Vec::new();
125        let mut offset = 0;
126        write_frame(&mut bytes, b"payload", &mut offset, "test").unwrap();
127
128        let mut read_offset = 0;
129        let frame = read_next_frame(&mut bytes.as_slice(), bytes.len() as u64, &mut read_offset)
130            .unwrap()
131            .unwrap();
132
133        assert_eq!(frame.payload, b"payload");
134        assert_eq!(frame.record_start_offset, 0);
135        assert_eq!(frame.end_offset, offset);
136        assert_eq!(read_offset, offset);
137    }
138
139    #[test]
140    fn truncated_frame_reports_torn() {
141        let mut bytes = Vec::new();
142        let mut offset = 0;
143        write_frame(&mut bytes, b"payload", &mut offset, "test").unwrap();
144        bytes.truncate(bytes.len() - 1);
145
146        let mut read_offset = 0;
147        assert_eq!(
148            read_next_frame(&mut bytes.as_slice(), bytes.len() as u64, &mut read_offset).unwrap(),
149            Err(WalReadStatus::Torn)
150        );
151    }
152}