Skip to main content

gpt/
format.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use anyhow::{Error, anyhow, ensure};
6use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
7
8const MAX_PARTITION_ENTRIES: u32 = 128;
9
10pub const GPT_SIGNATURE: [u8; 8] = [0x45, 0x46, 0x49, 0x20, 0x50, 0x41, 0x52, 0x54];
11pub const GPT_REVISION: u32 = 0x10000;
12pub const GPT_HEADER_SIZE: usize = 92;
13
14/// GPT disk header.
15#[derive(Clone, Debug, Eq, PartialEq, Immutable, IntoBytes, KnownLayout, FromBytes)]
16#[repr(C)]
17pub struct Header {
18    /// Must be GPT_SIGNATURE
19    pub signature: [u8; 8],
20    /// Must be GPT_REVISION
21    pub revision: u32,
22    /// Must be GPT_HEADER_SIZE
23    pub header_size: u32,
24    /// CRC32 of the header with crc32 section zeroed
25    pub crc32: u32,
26    /// reserved; must be 0
27    pub reserved: u32,
28    /// Must be 1
29    pub current_lba: u64,
30    /// LBA of backup header
31    pub backup_lba: u64,
32    /// First usable LBA for partitions (primary table last LBA + 1)
33    pub first_usable: u64,
34    /// Last usable LBA (secondary partition table first LBA - 1)
35    pub last_usable: u64,
36    /// UUID of the disk
37    pub disk_guid: [u8; 16],
38    /// Starting LBA of partition entries
39    pub part_start: u64,
40    /// Number of partition entries
41    pub num_parts: u32,
42    /// Size of a partition entry, usually 128
43    pub part_size: u32,
44    /// CRC32 of the partition table
45    pub crc32_parts: u32,
46    /// Padding to satisfy zerocopy's alignment requirements.
47    /// Not actually part of the header, which should be GPT_HEADER_SIZE bytes.
48    zerocopy_padding: u32,
49}
50
51impl Header {
52    pub fn new(block_count: u64, block_size: u32, num_parts: u32) -> Result<Self, Error> {
53        ensure!(block_size > 0 && block_size.is_power_of_two(), "Invalid block size");
54        let bs = block_size as u64;
55
56        let part_size = std::mem::size_of::<PartitionTableEntry>();
57        let partition_table_len = num_parts as u64 * part_size as u64;
58        let partition_table_blocks = partition_table_len.checked_next_multiple_of(bs).unwrap() / bs;
59
60        // Ensure there are enough blocks for both copies of the metadata, plus the protective MBR
61        // block.
62        ensure!(block_count > 1 + 2 * (1 + partition_table_blocks), "Too few blocks");
63
64        let mut this = Self {
65            signature: GPT_SIGNATURE,
66            revision: GPT_REVISION,
67            header_size: GPT_HEADER_SIZE as u32,
68            crc32: 0,
69            reserved: 0,
70            current_lba: 1,
71            backup_lba: block_count - 1,
72            first_usable: 2 + partition_table_blocks,
73            last_usable: block_count - (2 + partition_table_blocks),
74            disk_guid: uuid::Uuid::new_v4().into_bytes(),
75            part_start: 2,
76            num_parts,
77            part_size: part_size as u32,
78            crc32_parts: 0,
79            zerocopy_padding: 0,
80        };
81        this.update_checksum();
82        Ok(this)
83    }
84
85    // NB: This is expensive as it deeply copies the header.
86    pub fn compute_checksum(&self) -> u32 {
87        let mut header_copy = self.clone();
88        header_copy.crc32 = 0;
89        crc::Crc::<u32>::new(&crc::CRC_32_ISO_HDLC)
90            .checksum(&header_copy.as_bytes()[..GPT_HEADER_SIZE])
91    }
92
93    fn update_checksum(&mut self) {
94        self.crc32 = 0;
95        let crc = crc::Crc::<u32>::new(&crc::CRC_32_ISO_HDLC)
96            .checksum(&self.as_bytes()[..GPT_HEADER_SIZE]);
97        self.crc32 = crc;
98    }
99
100    // NB: This does *not* validate the partition table checksum.
101    pub fn ensure_integrity(&self, block_count: u64, block_size: u64) -> Result<(), Error> {
102        ensure!(self.signature == GPT_SIGNATURE, "Bad signature {:x?}", self.signature);
103        ensure!(self.revision == GPT_REVISION, "Bad revision {:x}", self.revision);
104        ensure!(
105            self.header_size as usize == GPT_HEADER_SIZE,
106            "Bad header size {}",
107            self.header_size
108        );
109
110        // Now that we've checked the basic fields, check the CRC.  All other checks should be below
111        // this.
112        ensure!(self.crc32 == self.compute_checksum(), "Invalid header checksum");
113
114        ensure!(self.num_parts <= MAX_PARTITION_ENTRIES, "Invalid num_parts {}", self.num_parts);
115        ensure!(
116            self.part_size as usize == std::mem::size_of::<PartitionTableEntry>(),
117            "Invalid part_size {}",
118            self.part_size
119        );
120        let partition_table_blocks = (self
121            .num_parts
122            .checked_mul(self.part_size)
123            .and_then(|v| v.checked_next_multiple_of(block_size as u32))
124            .ok_or_else(|| {
125                anyhow!(
126                    "Partition table size overflow \
127                     (num_parts: {}, part_size: {}, block_size: {block_size})",
128                    self.num_parts,
129                    self.part_size
130                )
131            })? as u64)
132            / block_size;
133        ensure!(
134            partition_table_blocks < block_count,
135            "Invalid partition table size: \
136             {partition_table_blocks} blocks >= {block_count} block_count"
137        );
138
139        // NB: The current LBA points to *this* header, so it's either at the start or the end.
140        // The last LBA points to the *other* header.  Since we want to check the absolute offsets,
141        // figure out which is which.
142        ensure!(
143            self.current_lba == 1 || self.current_lba == block_count - 1,
144            "Invalid current_lba {}",
145            self.current_lba
146        );
147        if self.current_lba == 1 {
148            // We tolerate a backup_lba which is lesser than block_count, since a resizable disk
149            // might have grown past the backup header.  This is arguably an invalid GPT (the backup
150            // header can no longer be found for recovery), but it happens in practice in some
151            // cases, such as an emulator which grows its disk.
152            ensure!(
153                self.backup_lba < block_count,
154                "backup_lba out of bounds {} (block_count {block_count})",
155                self.backup_lba,
156            );
157        } else {
158            ensure!(self.backup_lba == 1, "Invalid backup_lba {}", self.backup_lba);
159        }
160        let (first_lba, second_lba) = if self.current_lba == 1 {
161            (self.current_lba, self.backup_lba)
162        } else {
163            (self.backup_lba, self.current_lba)
164        };
165
166        let min_first_usable = first_lba
167            .checked_add(1)
168            .and_then(|v| v.checked_add(partition_table_blocks))
169            .ok_or_else(|| {
170                anyhow!(
171                    "Overflow calculating min_first_usable \
172                     (first_lba: {first_lba}, partition_table_blocks: {partition_table_blocks})"
173                )
174            })?;
175        ensure!(
176            self.first_usable >= min_first_usable,
177            "Invalid first_usable {} (minimum: {})",
178            self.first_usable,
179            min_first_usable
180        );
181        let last_usable_end =
182            self.last_usable.checked_add(partition_table_blocks).ok_or_else(|| {
183                anyhow!(
184                    "Overflow calculating last_usable_end \
185                     (last_usable: {}, partition_table_blocks: {partition_table_blocks})",
186                    self.last_usable
187                )
188            })?;
189        ensure!(
190            self.first_usable <= self.last_usable && last_usable_end < second_lba,
191            "Invalid last_usable {} (first_usable: {}, last_usable_end: {}, second_lba: {})",
192            self.last_usable,
193            self.first_usable,
194            last_usable_end,
195            second_lba
196        );
197
198        if first_lba == self.current_lba {
199            ensure!(self.part_start == first_lba + 1, "Invalid part_start {}", self.part_start);
200        } else {
201            let expected_part_start = self.last_usable.checked_add(1).ok_or_else(|| {
202                anyhow!(
203                    "Overflow calculating expected part_start (last_usable: {})",
204                    self.last_usable
205                )
206            })?;
207            ensure!(
208                self.part_start == expected_part_start,
209                "Invalid part_start {} (expected: {})",
210                self.part_start,
211                expected_part_start
212            );
213        }
214
215        Ok(())
216    }
217}
218
219#[derive(Clone, Debug, Eq, PartialEq, Immutable, IntoBytes, KnownLayout, FromBytes)]
220#[repr(C)]
221pub struct PartitionTableEntry {
222    pub type_guid: [u8; 16],
223    pub instance_guid: [u8; 16],
224    pub first_lba: u64,
225    pub last_lba: u64,
226    pub flags: u64,
227    pub name: [u16; 36],
228}
229
230impl PartitionTableEntry {
231    pub fn is_empty(&self) -> bool {
232        self.as_bytes().iter().all(|b| *b == 0)
233    }
234
235    pub fn empty() -> Self {
236        Self {
237            type_guid: [0u8; 16],
238            instance_guid: [0u8; 16],
239            first_lba: 0,
240            last_lba: 0,
241            flags: 0,
242            name: [0u16; 36],
243        }
244    }
245
246    pub fn ensure_integrity(&self, first_usable: u64, last_usable: u64) -> Result<(), Error> {
247        ensure!(self.type_guid != [0u8; 16], "Empty type GUID");
248        ensure!(self.instance_guid != [0u8; 16], "Empty instance GUID");
249        ensure!(
250            self.first_lba >= first_usable,
251            "Invalid first LBA {} (first_usable: {})",
252            self.first_lba,
253            first_usable
254        );
255        ensure!(
256            self.last_lba <= last_usable && self.last_lba >= self.first_lba,
257            "Invalid last LBA {} (first_usable: {}, last_usable: {}, first_lba: {})",
258            self.last_lba,
259            first_usable,
260            last_usable,
261            self.first_lba
262        );
263        Ok(())
264    }
265}
266
267#[derive(Eq, thiserror::Error, Clone, Debug, PartialEq)]
268pub enum FormatError {
269    #[error("Invalid arguments")]
270    InvalidArguments,
271    #[error("No space")]
272    NoSpace,
273}
274
275/// Serializes the partition table, and updates `header` to reflect the changes (including computing
276/// the CRC).  Returns the raw bytes of the partition table.
277/// Fails if any of the entries in `entries` are invalid.
278pub fn serialize_partition_table(
279    header: &mut Header,
280    block_size: usize,
281    num_blocks: u64,
282    entries: &[PartitionTableEntry],
283) -> Result<Vec<u8>, FormatError> {
284    let crc_algo = crc::Crc::<u32>::new(&crc::CRC_32_ISO_HDLC);
285    let mut digest = crc_algo.digest();
286    let partition_table_len = header.part_size as usize * entries.len();
287    let partition_table_len = partition_table_len
288        .checked_next_multiple_of(block_size)
289        .ok_or(FormatError::InvalidArguments)?;
290    let partition_table_blocks = (partition_table_len / block_size) as u64;
291    let mut partition_table = vec![0u8; partition_table_len];
292    let mut partition_table_view = &mut partition_table[..];
293    // The first two blocks are resered for the PMBR and the primary GPT header.
294    let first_usable = partition_table_blocks + 2;
295    // The last block is reserved for the backup GPT header.  We subtract one more to get to the
296    // offset of the last usable block.
297    let last_usable = num_blocks.saturating_sub(partition_table_blocks + 2);
298    if first_usable > last_usable {
299        return Err(FormatError::NoSpace);
300    }
301    let last_usable_end = last_usable.checked_add(1).ok_or(FormatError::InvalidArguments)?;
302    let mut used_ranges = vec![0..first_usable, last_usable_end..num_blocks];
303    let part_size = header.part_size as usize;
304    for entry in entries {
305        let part_raw = entry.as_bytes();
306        assert!(part_raw.len() == part_size);
307        if !entry.is_empty() {
308            entry
309                .ensure_integrity(first_usable, last_usable)
310                .map_err(|_| FormatError::InvalidArguments)?;
311            let end = entry.last_lba.checked_add(1).ok_or(FormatError::InvalidArguments)?;
312            used_ranges.push(entry.first_lba..end);
313            partition_table_view[..part_raw.len()].copy_from_slice(part_raw);
314        }
315        digest.update(part_raw);
316        partition_table_view = &mut partition_table_view[part_size..];
317    }
318    used_ranges.sort_by_key(|range| range.start);
319    for [a, b] in used_ranges.array_windows() {
320        if a.end > b.start {
321            return Err(FormatError::InvalidArguments);
322        }
323    }
324    header.first_usable = first_usable;
325    header.last_usable = last_usable;
326    header.num_parts = entries.len() as u32;
327    header.crc32_parts = digest.finalize();
328    header.crc32 = header.compute_checksum();
329    Ok(partition_table)
330}
331
332#[cfg(test)]
333mod tests {
334    use super::{
335        FormatError, GPT_HEADER_SIZE, Header, PartitionTableEntry, serialize_partition_table,
336    };
337
338    #[fuchsia::test]
339    fn header_crc() {
340        let nblocks = 8;
341        let partition_table_nblocks = 1;
342        let mut header = Header {
343            signature: [0x45, 0x46, 0x49, 0x20, 0x50, 0x41, 0x52, 0x54],
344            revision: 0x10000,
345            header_size: GPT_HEADER_SIZE as u32,
346            crc32: 0,
347            reserved: 0,
348            current_lba: 1,
349            backup_lba: nblocks - 1,
350            first_usable: 2 + partition_table_nblocks,
351            last_usable: nblocks - (2 + partition_table_nblocks),
352            disk_guid: [0u8; 16],
353            part_start: 2,
354            num_parts: 1,
355            part_size: 128,
356            crc32_parts: 0,
357            zerocopy_padding: 0,
358        };
359        header.crc32 = header.compute_checksum();
360
361        header.ensure_integrity(nblocks, 512).expect("Header should be valid");
362
363        // Flip one bit, leaving an otherwise valid header.
364        header.num_parts = 2;
365
366        header.ensure_integrity(nblocks, 512).expect_err("Header should be invalid");
367    }
368
369    #[fuchsia::test]
370    fn test_backup_lba_validation() {
371        let nblocks = 10;
372        let partition_table_nblocks = 1;
373        let mut header = Header {
374            signature: [0x45, 0x46, 0x49, 0x20, 0x50, 0x41, 0x52, 0x54],
375            revision: 0x10000,
376            header_size: GPT_HEADER_SIZE as u32,
377            crc32: 0,
378            reserved: 0,
379            current_lba: 1,
380            backup_lba: nblocks - 1,
381            first_usable: 2 + partition_table_nblocks,
382            last_usable: nblocks - (2 + partition_table_nblocks),
383            disk_guid: [0u8; 16],
384            part_start: 2,
385            num_parts: 1,
386            part_size: 128,
387            crc32_parts: 0,
388            zerocopy_padding: 0,
389        };
390
391        // 1. Primary header (current_lba == 1):
392        // backup_lba == nblocks - 1 should be valid.
393        header.crc32 = header.compute_checksum();
394        header.ensure_integrity(nblocks, 512).expect("Header should be valid");
395
396        // backup_lba < nblocks - 1 should be valid.
397        header.backup_lba = nblocks - 2;
398        header.last_usable = 6;
399        header.crc32 = header.compute_checksum();
400        header
401            .ensure_integrity(nblocks, 512)
402            .expect("Header should be valid with relaxed backup_lba");
403
404        // backup_lba >= nblocks should be invalid.
405        header.backup_lba = nblocks;
406        header.last_usable = nblocks - (2 + partition_table_nblocks);
407        header.crc32 = header.compute_checksum();
408        header.ensure_integrity(nblocks, 512).expect_err("backup_lba >= nblocks should be invalid");
409
410        // 2. Backup header (current_lba == nblocks - 1):
411        header.current_lba = nblocks - 1;
412        header.backup_lba = 1;
413        header.part_start = header.last_usable + 1;
414        header.crc32 = header.compute_checksum();
415        header
416            .ensure_integrity(nblocks, 512)
417            .expect("Backup header should be valid with backup_lba == 1");
418
419        header.backup_lba = 2;
420        header.crc32 = header.compute_checksum();
421        header
422            .ensure_integrity(nblocks, 512)
423            .expect_err("Backup header should be invalid with backup_lba != 1");
424    }
425
426    #[fuchsia::test]
427    fn test_header_ensure_integrity_last_usable_overflow() {
428        let nblocks = 10;
429        let partition_table_nblocks = 1;
430        let mut header = Header {
431            signature: [0x45, 0x46, 0x49, 0x20, 0x50, 0x41, 0x52, 0x54],
432            revision: 0x10000,
433            header_size: GPT_HEADER_SIZE as u32,
434            crc32: 0,
435            reserved: 0,
436            current_lba: 1,
437            backup_lba: nblocks - 1,
438            first_usable: 2 + partition_table_nblocks,
439            last_usable: u64::MAX,
440            disk_guid: [0u8; 16],
441            part_start: 2,
442            num_parts: 1,
443            part_size: 128,
444            crc32_parts: 0,
445            zerocopy_padding: 0,
446        };
447        header.crc32 = header.compute_checksum();
448        header
449            .ensure_integrity(nblocks, 512)
450            .expect_err("last_usable = u64::MAX should fail ensure_integrity");
451    }
452
453    #[fuchsia::test]
454    fn test_serialize_partition_table_overflow_entry() {
455        let block_count = 1024;
456        let block_size = 512;
457        let mut header = Header::new(block_count, block_size, 128).unwrap();
458        let mut entries = vec![PartitionTableEntry::empty(); 128];
459        entries[0] = PartitionTableEntry {
460            type_guid: [1; 16],
461            instance_guid: [1; 16],
462            first_lba: u64::MAX,
463            last_lba: u64::MAX,
464            flags: 0,
465            name: [0; 36],
466        };
467        let result =
468            serialize_partition_table(&mut header, block_size as usize, block_count, &entries[..]);
469        assert_eq!(result, Err(FormatError::InvalidArguments));
470    }
471}