Skip to main content

vhdx/validation/
region.rs

1use super::{
2    Error, MIB, Result, SignaturePosition, SpecValidator, ValidationIssue, is_known_region_guid,
3};
4
5impl SpecValidator {
6    /// Validate the region tables.
7    ///
8    /// Checks:
9    /// - "regi" signature and CRC-32C for both region tables
10    /// - Entry alignment (1 MB)
11    /// - Entry overlap (no two regions' ranges overlap)
12    /// - Entry count <= 2047
13    /// - Required unknown region handling (strict mode)
14    ///
15    /// # Errors
16    ///
17    /// Returns an error when region table integrity checks fail.
18    ///
19    /// # Panics
20    ///
21    /// Panics on internal invariant violations where code unwraps region tables
22    /// after prior successful checks.
23    pub fn validate_region_table(&self) -> Result<Vec<ValidationIssue>> {
24        let mut issues = Vec::new();
25        let header = self.parse_header()?;
26
27        // Check both region tables.
28        let rt1 = header.region_table(1);
29        let rt2 = header.region_table(2);
30
31        match (&rt1, &rt2) {
32            (Err(e), _) | (_, Err(e)) => {
33                if let Error::InvalidSignature {
34                    position: SignaturePosition::RegionTable,
35                    ..
36                } = e
37                {
38                    Self::push_issue(
39                        &mut issues,
40                        ValidationIssue::new(
41                            "region_table",
42                            "REGION_SIGNATURE_INVALID",
43                            format!("region table signature error: {e}"),
44                            "MS-VHDX/2.2.3.1",
45                        ),
46                    );
47                    return Err(Error::InvalidRegionTable(format!("{e}")));
48                }
49                Self::push_issue(
50                    &mut issues,
51                    ValidationIssue::new(
52                        "region_table",
53                        "REGION_CHECKSUM_MISMATCH",
54                        format!("{e}"),
55                        "MS-VHDX/2.2.3.1",
56                    ),
57                );
58                return Err(Error::InvalidRegionTable(format!("{e}")));
59            }
60            _ => {}
61        }
62
63        let rt1 = rt1.unwrap();
64        let rt2 = rt2.unwrap();
65
66        // Entry count <= 2047 (already checked by Header::validate_region_table_at,
67        // but re-verify here for completeness)
68        for (idx, rt) in [rt1, rt2].iter().enumerate() {
69            let count = rt.header().entry_count();
70            if count > 2047 {
71                Self::push_issue(
72                    &mut issues,
73                    ValidationIssue::new(
74                        "region_table",
75                        "REGION_ENTRY_COUNT_EXCEEDS_MAXIMUM",
76                        format!("region table {idx} entry count {count} exceeds maximum of 2047"),
77                        "MS-VHDX/2.2.3.1",
78                    ),
79                );
80                return Err(Error::InvalidRegionTable(format!(
81                    "REGION_ENTRY_COUNT_EXCEEDS_MAXIMUM: region table {idx} entry count {count} exceeds maximum of 2047"
82                )));
83            }
84        }
85
86        // Check entries for alignment and overlap in the CURRENT region table.
87        let current_rt = match header.region_table(0) {
88            Ok(rt) => rt,
89            Err(e) => {
90                Self::push_issue(
91                    &mut issues,
92                    ValidationIssue::new(
93                        "region_table",
94                        "REGION_CHECKSUM_MISMATCH",
95                        format!("current region table: {e}"),
96                        "MS-VHDX/2.2.3.1",
97                    ),
98                );
99                return Err(Error::InvalidRegionTable(format!(
100                    "current region table: {e}"
101                )));
102            }
103        };
104
105        issues.extend(self.validate_region_entries(&current_rt)?);
106
107        Ok(issues)
108    }
109
110    /// Validate a region table's entries for alignment, overlap, and required-unknown.
111    fn validate_region_entries(
112        &self, rt: &crate::header::RegionTable<'_>,
113    ) -> Result<Vec<ValidationIssue>> {
114        let mut issues = Vec::new();
115        let entries: Vec<_> = rt.entries().collect();
116
117        for (i, entry) in entries.iter().enumerate() {
118            self.validate_region_entry(i, entry, &entries, &mut issues)?;
119        }
120
121        Ok(issues)
122    }
123
124    fn validate_region_entry(
125        &self, i: usize, entry: &crate::header::RegionTableEntry<'_>,
126        entries: &[crate::header::RegionTableEntry<'_>], issues: &mut Vec<ValidationIssue>,
127    ) -> Result<()> {
128        let file_offset = entry.file_offset();
129        let length = entry.length();
130
131        if !file_offset.is_multiple_of(u64::from(MIB)) {
132            Self::push_issue(
133                issues,
134                ValidationIssue::new(
135                    "region_table",
136                    "REGION_ENTRY_ALIGNMENT",
137                    format!("entry {i} file_offset {file_offset:#x} not 1MB-aligned"),
138                    "MS-VHDX/2.2.3.2",
139                ),
140            );
141            return Err(Error::InvalidRegionTable(format!(
142                "REGION_ENTRY_ALIGNMENT: entry {i} file_offset {file_offset:#x} not 1MB-aligned"
143            )));
144        }
145        if file_offset < u64::from(MIB) {
146            Self::push_issue(
147                issues,
148                ValidationIssue::new(
149                    "region_table",
150                    "REGION_ENTRY_OFFSET_MINIMUM",
151                    format!("entry {i} file_offset {file_offset} < 1MB minimum"),
152                    "MS-VHDX/2.2.3.2",
153                ),
154            );
155            return Err(Error::InvalidRegionTable(format!(
156                "REGION_ENTRY_OFFSET_MINIMUM: entry {i} file_offset {file_offset} < 1MB minimum"
157            )));
158        }
159        if u64::from(length) % u64::from(MIB) != 0 {
160            Self::push_issue(
161                issues,
162                ValidationIssue::new(
163                    "region_table",
164                    "REGION_ENTRY_ALIGNMENT",
165                    format!("entry {i} length {length} not 1MB-aligned"),
166                    "MS-VHDX/2.2.3.2",
167                ),
168            );
169            return Err(Error::InvalidRegionTable(format!(
170                "REGION_ENTRY_ALIGNMENT: entry {i} length {length} not 1MB-aligned"
171            )));
172        }
173
174        Self::validate_region_entry_overlap(i, file_offset, length, entries, issues)?;
175        self.validate_region_entry_guid(entry, issues)
176    }
177
178    fn validate_region_entry_overlap(
179        i: usize, file_offset: u64, length: u32, entries: &[crate::header::RegionTableEntry<'_>],
180        issues: &mut Vec<ValidationIssue>,
181    ) -> Result<()> {
182        let end = file_offset + u64::from(length);
183        for (j, prev) in entries[..i].iter().enumerate() {
184            let prev_end = prev.file_offset() + u64::from(prev.length());
185            if file_offset < prev_end && prev.file_offset() < end {
186                Self::push_issue(
187                    issues,
188                    ValidationIssue::new(
189                        "region_table",
190                        "REGION_ENTRY_OVERLAP",
191                        format!("entries {j} and {i} overlap"),
192                        "MS-VHDX/2.1",
193                    ),
194                );
195                return Err(Error::InvalidRegionTable(format!(
196                    "REGION_ENTRY_OVERLAP: entries {j} and {i} overlap"
197                )));
198            }
199        }
200        Ok(())
201    }
202
203    fn validate_region_entry_guid(
204        &self, entry: &crate::header::RegionTableEntry<'_>, issues: &mut Vec<ValidationIssue>,
205    ) -> Result<()> {
206        if is_known_region_guid(&entry.guid()) {
207            return Ok(());
208        }
209        if entry.required() {
210            Self::push_issue(
211                issues,
212                ValidationIssue::new(
213                    "region_table",
214                    "REGION_REQUIRED_UNKNOWN",
215                    format!("required unknown region GUID {}", entry.guid()),
216                    "RELAX",
217                ),
218            );
219            return Err(Error::RegionRequiredUnknown { guid: entry.guid() });
220        }
221        if self.strict {
222            Self::push_issue(
223                issues,
224                ValidationIssue::new(
225                    "region_table",
226                    "REGION_OPTIONAL_UNKNOWN",
227                    format!(
228                        "optional unknown region GUID {} in strict mode",
229                        entry.guid()
230                    ),
231                    "RELAX",
232                ),
233            );
234            return Err(Error::RegionOptionalUnknown { guid: entry.guid() });
235        }
236        Self::push_issue(
237            issues,
238            ValidationIssue::new(
239                "region_table",
240                "REGION_OPTIONAL_UNKNOWN",
241                format!(
242                    "optional unknown region GUID {} tolerated in non-strict mode",
243                    entry.guid()
244                ),
245                "RELAX",
246            ),
247        );
248        Ok(())
249    }
250}