Skip to main content

vmdk/
header.rs

1//! Sparse extent header (Virtual Disk Format 1.1, §4.1).
2
3use crate::error::{Result, VmdkError};
4
5pub const MAGIC: u32 = 0x564D_444B;
6pub const VERSION: u32 = 1;
7/// Version 2 enables the zeroed-grain feature (GTE == 1 → explicit zero grain).
8pub const VERSION_ZEROED_GRAIN: u32 = 2;
9pub const VERSION_STREAM_OPT: u32 = 3;
10pub const SECTOR_SIZE: u64 = 512;
11
12/// Maximum grain-table entries per grain table (VDF 1.1 §4.1: `numGTEsPerGT` = 512).
13///
14/// QEMU's `vmdk_open_vmdk4` rejects any larger value. The read path allocates
15/// `num_gtes_per_gt * 4` bytes per grain table, so this bound caps that allocation
16/// at 2 KiB and prevents a crafted header from forcing a huge allocation.
17pub const MAX_NUM_GTES_PER_GT: u32 = 512;
18
19/// Sentinel `gdOffset` in the *primary* header of a `streamOptimized` extent.
20///
21/// When `gdOffset == GD_AT_END` the real GD location is in the *footer* header
22/// appended to the end of the file: `SparseExtentHeader` at `file_end − 1024`,
23/// followed by an EOS marker at `file_end − 512` (VDF 1.1 §4.6).
24pub const GD_AT_END: u64 = 0xffff_ffff_ffff_ffff;
25
26/// Parsed fields from the 512-byte `SparseExtentHeader`.
27pub struct SparseExtentHeader {
28    pub version: u32,           // 1 = monolithicSparse, 3 = streamOptimized
29    pub capacity: u64,          // virtual disk size in sectors
30    pub grain_size: u64,        // grain size in sectors
31    pub descriptor_offset: u64, // in sectors
32    pub descriptor_size: u64,   // in sectors
33    pub num_gtes_per_gt: u32,
34    pub rgd_offset: u64, // redundant grain directory offset in sectors (0 if absent)
35    pub gd_offset: u64,  // grain directory offset in sectors
36    /// `true` when `compress_algorithm == 1` (stream-optimised / DEFLATE).
37    pub compressed: bool,
38}
39
40impl SparseExtentHeader {
41    pub fn parse(data: &[u8]) -> Result<Self> {
42        if data.len() < 512 {
43            return Err(VmdkError::FileTooSmall);
44        }
45
46        let magic = u32::from_le_bytes(data[0..4].try_into().expect("4 bytes"));
47        if magic != MAGIC {
48            return Err(VmdkError::BadMagic);
49        }
50
51        let version = u32::from_le_bytes(data[4..8].try_into().expect("4 bytes"));
52        // Accept v1 (base), v2 (zeroed-grain feature) and v3 (streamOptimized).
53        // QEMU accepts any VMDK4-magic version; we cap at the three defined values.
54        if version != VERSION && version != VERSION_ZEROED_GRAIN && version != VERSION_STREAM_OPT {
55            return Err(VmdkError::UnsupportedVersion(version));
56        }
57
58        let capacity = u64::from_le_bytes(data[12..20].try_into().expect("8 bytes"));
59        let grain_size = u64::from_le_bytes(data[20..28].try_into().expect("8 bytes"));
60        let descriptor_offset = u64::from_le_bytes(data[28..36].try_into().expect("8 bytes"));
61        let descriptor_size = u64::from_le_bytes(data[36..44].try_into().expect("8 bytes"));
62        let num_gtes_per_gt = u32::from_le_bytes(data[44..48].try_into().expect("4 bytes"));
63        let rgd_offset = u64::from_le_bytes(data[48..56].try_into().expect("8 bytes"));
64        let gd_offset = u64::from_le_bytes(data[56..64].try_into().expect("8 bytes"));
65        let compress_algorithm = u16::from_le_bytes(data[77..79].try_into().expect("2 bytes"));
66
67        // v1: compression must be absent; v3 (streamOptimized): deflate (1) is expected.
68        // Spec note (VDF 1.1 §4.4): COMPRESSION_DEFLATE is described as RFC 1951 (raw
69        // DEFLATE), but both VMware tooling and QEMU actually produce RFC 1950 payloads
70        // (2-byte zlib header + DEFLATE stream + Adler-32 trailer).  Use ZlibDecoder,
71        // not DeflateDecoder — the spec has a documentation error.
72        match (version, compress_algorithm) {
73            (VERSION | VERSION_ZEROED_GRAIN, 0) | (VERSION_STREAM_OPT, 1) => {}
74            _ => return Err(VmdkError::CompressedNotSupported),
75        }
76
77        // Validate geometry before these values feed division arithmetic in the reader.
78        // VDF 1.1 §4.1: minimum grain size is 8 sectors (4 KiB).
79        if grain_size < 8 {
80            return Err(VmdkError::FieldOutOfRange {
81                field: "grain_size",
82                value: grain_size,
83                reason: "must be >= 8 sectors (VDF 1.1 §4.1)",
84            });
85        }
86        if num_gtes_per_gt == 0 {
87            return Err(VmdkError::FieldOutOfRange {
88                field: "num_gtes_per_gt",
89                value: u64::from(num_gtes_per_gt),
90                reason: "must be > 0",
91            });
92        }
93        // VDF 1.1 defines numGTEsPerGT as 512; QEMU's vmdk_open_vmdk4 rejects any
94        // larger value. Enforcing it here bounds the read path's grain-table
95        // allocation (`vec![0u8; num_gtes_per_gt * 4]`) at parse time, so no caller
96        // can be driven into a multi-gigabyte allocation by a crafted header.
97        if num_gtes_per_gt > MAX_NUM_GTES_PER_GT {
98            return Err(VmdkError::FieldOutOfRange {
99                field: "num_gtes_per_gt",
100                value: u64::from(num_gtes_per_gt),
101                reason: "exceeds the spec maximum of 512",
102            });
103        }
104
105        Ok(SparseExtentHeader {
106            version,
107            capacity,
108            grain_size,
109            descriptor_offset,
110            descriptor_size,
111            num_gtes_per_gt,
112            rgd_offset,
113            gd_offset,
114            compressed: compress_algorithm != 0,
115        })
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    fn valid_header() -> Vec<u8> {
124        let mut h = vec![0u8; 512];
125        h[0..4].copy_from_slice(&MAGIC.to_le_bytes());
126        h[4..8].copy_from_slice(&VERSION.to_le_bytes());
127        h[12..20].copy_from_slice(&8u64.to_le_bytes()); // capacity
128        h[20..28].copy_from_slice(&8u64.to_le_bytes()); // grain_size
129        h[44..48].copy_from_slice(&512u32.to_le_bytes()); // num_gtes_per_gt
130        h
131    }
132
133    #[test]
134    fn parse_rejects_short_buffer() {
135        assert!(matches!(
136            SparseExtentHeader::parse(&[0u8; 100]),
137            Err(VmdkError::FileTooSmall)
138        ));
139    }
140
141    #[test]
142    fn parse_rejects_bad_magic() {
143        let h = vec![0u8; 512];
144        assert!(matches!(
145            SparseExtentHeader::parse(&h),
146            Err(VmdkError::BadMagic)
147        ));
148    }
149
150    #[test]
151    fn parse_rejects_unsupported_version() {
152        let mut h = valid_header();
153        h[4..8].copy_from_slice(&4u32.to_le_bytes()); // version 4 is undefined
154        assert!(matches!(
155            SparseExtentHeader::parse(&h),
156            Err(VmdkError::UnsupportedVersion(4))
157        ));
158    }
159
160    #[test]
161    fn parse_accepts_version_2() {
162        let mut h = valid_header();
163        h[4..8].copy_from_slice(&VERSION_ZEROED_GRAIN.to_le_bytes());
164        let hdr = SparseExtentHeader::parse(&h).expect("v2 parses");
165        assert_eq!(hdr.version, 2);
166    }
167
168    #[test]
169    fn parse_rejects_grain_size_below_minimum() {
170        let mut h = valid_header();
171        h[20..28].copy_from_slice(&4u64.to_le_bytes()); // < 8
172        assert!(matches!(
173            SparseExtentHeader::parse(&h),
174            Err(VmdkError::FieldOutOfRange {
175                field: "grain_size",
176                value: 4,
177                ..
178            })
179        ));
180    }
181
182    #[test]
183    fn parse_rejects_zero_num_gtes() {
184        let mut h = valid_header();
185        h[44..48].copy_from_slice(&0u32.to_le_bytes());
186        assert!(matches!(
187            SparseExtentHeader::parse(&h),
188            Err(VmdkError::FieldOutOfRange {
189                field: "num_gtes_per_gt",
190                value: 0,
191                ..
192            })
193        ));
194    }
195
196    #[test]
197    fn parse_rejects_num_gtes_above_spec_max() {
198        // VDF 1.1 defines numGTEsPerGT as 512; QEMU rejects anything larger.
199        // Without this bound a crafted header drives an unguarded
200        // `vec![0u8; num_gtes_per_gt * 4]` in the read path — e.g. 0xFFFFFFFF
201        // yields a ~17 GiB allocation (allocation-amplification DoS).
202        let mut h = valid_header();
203        h[44..48].copy_from_slice(&513u32.to_le_bytes());
204        assert!(matches!(
205            SparseExtentHeader::parse(&h),
206            Err(VmdkError::FieldOutOfRange {
207                field: "num_gtes_per_gt",
208                value: 513,
209                ..
210            })
211        ));
212
213        // The extreme crafted value must also be rejected, not allocated.
214        let mut h = valid_header();
215        h[44..48].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
216        assert!(matches!(
217            SparseExtentHeader::parse(&h),
218            Err(VmdkError::FieldOutOfRange {
219                field: "num_gtes_per_gt",
220                value: 0xFFFF_FFFF,
221                ..
222            })
223        ));
224    }
225
226    #[test]
227    fn parse_accepts_num_gtes_at_spec_max() {
228        // Exactly 512 is the canonical value and must remain valid.
229        let mut h = valid_header();
230        h[44..48].copy_from_slice(&512u32.to_le_bytes());
231        assert!(SparseExtentHeader::parse(&h).is_ok());
232    }
233
234    #[test]
235    fn parse_rejects_compressed_flag_on_v1() {
236        let mut h = valid_header();
237        h[77..79].copy_from_slice(&1u16.to_le_bytes()); // compress on v1 is invalid
238        assert!(matches!(
239            SparseExtentHeader::parse(&h),
240            Err(VmdkError::CompressedNotSupported)
241        ));
242    }
243}