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::{anyhow, ensure, Error};
6use crc::Hasher32 as _;
7use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
8
9const MAX_PARTITION_ENTRIES: u32 = 128;
10
11pub const GPT_SIGNATURE: [u8; 8] = [0x45, 0x46, 0x49, 0x20, 0x50, 0x41, 0x52, 0x54];
12pub const GPT_REVISION: u32 = 0x10000;
13pub const GPT_HEADER_SIZE: usize = 92;
14
15/// GPT disk header.
16#[derive(Clone, Debug, Eq, PartialEq, Immutable, IntoBytes, KnownLayout, FromBytes)]
17#[repr(C)]
18pub struct Header {
19    /// Must be GPT_SIGNATURE
20    pub signature: [u8; 8],
21    /// Must be GPT_REVISION
22    pub revision: u32,
23    /// Must be GPT_HEADER_SIZE
24    pub header_size: u32,
25    /// CRC32 of the header with crc32 section zeroed
26    pub crc32: u32,
27    /// reserved; must be 0
28    pub reserved: u32,
29    /// Must be 1
30    pub current_lba: u64,
31    /// LBA of backup header
32    pub backup_lba: u64,
33    /// First usable LBA for partitions (primary table last LBA + 1)
34    pub first_usable: u64,
35    /// Last usable LBA (secondary partition table first LBA - 1)
36    pub last_usable: u64,
37    /// UUID of the disk
38    pub disk_guid: [u8; 16],
39    /// Starting LBA of partition entries
40    pub part_start: u64,
41    /// Number of partition entries
42    pub num_parts: u32,
43    /// Size of a partition entry, usually 128
44    pub part_size: u32,
45    /// CRC32 of the partition table
46    pub crc32_parts: u32,
47    /// Padding to satisfy zerocopy's alignment requirements.
48    /// Not actually part of the header, which should be GPT_HEADER_SIZE bytes.
49    zerocopy_padding: u32,
50}
51
52impl Header {
53    pub fn new(block_count: u64, block_size: u32, num_parts: u32) -> Result<Self, Error> {
54        ensure!(block_size > 0 && block_size.is_power_of_two(), "Invalid block size");
55        let bs = block_size as u64;
56
57        let part_size = std::mem::size_of::<PartitionTableEntry>();
58        let partition_table_len = num_parts as u64 * part_size as u64;
59        let partition_table_blocks = partition_table_len.checked_next_multiple_of(bs).unwrap() / bs;
60
61        // Ensure there are enough blocks for both copies of the metadata, plus the protective MBR
62        // block.
63        ensure!(block_count > 1 + 2 * (1 + partition_table_blocks), "Too few blocks");
64
65        let mut this = Self {
66            signature: GPT_SIGNATURE,
67            revision: GPT_REVISION,
68            header_size: GPT_HEADER_SIZE as u32,
69            crc32: 0,
70            reserved: 0,
71            current_lba: 1,
72            backup_lba: block_count - 1,
73            first_usable: 2 + partition_table_blocks,
74            last_usable: block_count - (2 + partition_table_blocks),
75            disk_guid: uuid::Uuid::new_v4().into_bytes(),
76            part_start: 2,
77            num_parts,
78            part_size: part_size as u32,
79            crc32_parts: 0,
80            zerocopy_padding: 0,
81        };
82        this.update_checksum();
83        Ok(this)
84    }
85
86    // NB: This is expensive as it deeply copies the header.
87    pub fn compute_checksum(&self) -> u32 {
88        let mut header_copy = self.clone();
89        header_copy.crc32 = 0;
90        crc::crc32::checksum_ieee(&header_copy.as_bytes()[..GPT_HEADER_SIZE])
91    }
92
93    fn update_checksum(&mut self) {
94        self.crc32 = 0;
95        let crc = crc::crc32::checksum_ieee(&self.as_bytes()[..GPT_HEADER_SIZE]);
96        self.crc32 = crc;
97    }
98
99    // NB: This does *not* validate the partition table checksum.
100    pub fn ensure_integrity(&self, block_count: u64, block_size: u64) -> Result<(), Error> {
101        ensure!(self.signature == GPT_SIGNATURE, "Bad signature {:x?}", self.signature);
102        ensure!(self.revision == GPT_REVISION, "Bad revision {:x}", self.revision);
103        ensure!(
104            self.header_size as usize == GPT_HEADER_SIZE,
105            "Bad header size {}",
106            self.header_size
107        );
108
109        // Now that we've checked the basic fields, check the CRC.  All other checks should be below
110        // this.
111        ensure!(self.crc32 == self.compute_checksum(), "Invalid header checksum");
112
113        ensure!(self.num_parts <= MAX_PARTITION_ENTRIES, "Invalid num_parts {}", self.num_parts);
114        ensure!(
115            self.part_size as usize == std::mem::size_of::<PartitionTableEntry>(),
116            "Invalid part_size {}",
117            self.part_size
118        );
119        let partition_table_blocks = (self
120            .num_parts
121            .checked_mul(self.part_size)
122            .and_then(|v| v.checked_next_multiple_of(block_size as u32))
123            .ok_or_else(|| anyhow!("Partition table size overflow"))?
124            as u64)
125            / block_size;
126        ensure!(partition_table_blocks < block_count, "Invalid partition table size");
127
128        // NB: The current LBA points to *this* header, so it's either at the start or the end.
129        // The last LBA points to the *other* header.  Since we want to check the absolute offsets,
130        // figure out which is which.
131        ensure!(
132            self.current_lba == 1 || self.current_lba == block_count - 1,
133            "Invalid current_lba {}",
134            self.current_lba
135        );
136        let (first_lba, second_lba) = if self.current_lba == 1 {
137            (self.current_lba, self.backup_lba)
138        } else {
139            (self.backup_lba, self.current_lba)
140        };
141
142        ensure!(
143            self.first_usable >= first_lba + partition_table_blocks,
144            "Invalid first_usable {}",
145            self.first_usable
146        );
147        ensure!(
148            self.first_usable <= self.last_usable
149                && self.last_usable + partition_table_blocks <= second_lba,
150            "Invalid last_usable {}",
151            self.last_usable
152        );
153
154        if first_lba == self.current_lba {
155            ensure!(self.part_start == first_lba + 1, "Invalid part_start {}", self.part_start);
156        } else {
157            ensure!(
158                self.part_start == self.last_usable + 1,
159                "Invalid part_start {}",
160                self.part_start
161            );
162        }
163
164        Ok(())
165    }
166}
167
168#[derive(Clone, Debug, Eq, PartialEq, Immutable, IntoBytes, KnownLayout, FromBytes)]
169#[repr(C)]
170pub struct PartitionTableEntry {
171    pub type_guid: [u8; 16],
172    pub instance_guid: [u8; 16],
173    pub first_lba: u64,
174    pub last_lba: u64,
175    pub flags: u64,
176    pub name: [u16; 36],
177}
178
179impl PartitionTableEntry {
180    pub fn is_empty(&self) -> bool {
181        self.as_bytes().iter().all(|b| *b == 0)
182    }
183
184    pub fn empty() -> Self {
185        Self {
186            type_guid: [0u8; 16],
187            instance_guid: [0u8; 16],
188            first_lba: 0,
189            last_lba: 0,
190            flags: 0,
191            name: [0u16; 36],
192        }
193    }
194
195    pub fn ensure_integrity(&self) -> Result<(), Error> {
196        ensure!(self.type_guid != [0u8; 16], "Empty type GUID");
197        ensure!(self.instance_guid != [0u8; 16], "Empty instance GUID");
198        ensure!(self.first_lba != 0, "Invalid first LBA");
199        ensure!(self.last_lba != 0 && self.last_lba >= self.first_lba, "Invalid last LBA");
200        Ok(())
201    }
202}
203
204#[derive(Eq, thiserror::Error, Clone, Debug, PartialEq)]
205pub enum FormatError {
206    #[error("Invalid arguments")]
207    InvalidArguments,
208    #[error("No space")]
209    NoSpace,
210}
211
212/// Serializes the partition table, and updates `header` to reflect the changes (including computing
213/// the CRC).  Returns the raw bytes of the partition table.
214/// Fails if any of the entries in `entries` are invalid.
215pub fn serialize_partition_table(
216    header: &mut Header,
217    block_size: usize,
218    num_blocks: u64,
219    entries: &[PartitionTableEntry],
220) -> Result<Vec<u8>, FormatError> {
221    let mut digest = crc::crc32::Digest::new(crc::crc32::IEEE);
222    let partition_table_len = header.part_size as usize * entries.len();
223    let partition_table_len = partition_table_len
224        .checked_next_multiple_of(block_size)
225        .ok_or(FormatError::InvalidArguments)?;
226    let partition_table_blocks = (partition_table_len / block_size) as u64;
227    let mut partition_table = vec![0u8; partition_table_len];
228    let mut partition_table_view = &mut partition_table[..];
229    // The first two blocks are resered for the PMBR and the primary GPT header.
230    let first_usable = partition_table_blocks + 2;
231    // The last block is reserved for the backup GPT header.  We subtract one more to get to the
232    // offset of the last usable block.
233    let last_usable = num_blocks.saturating_sub(partition_table_blocks + 2);
234    if first_usable > last_usable {
235        return Err(FormatError::NoSpace);
236    }
237    let mut used_ranges = vec![0..first_usable, last_usable + 1..num_blocks];
238    let part_size = header.part_size as usize;
239    for entry in entries {
240        let part_raw = entry.as_bytes();
241        assert!(part_raw.len() == part_size);
242        if !entry.is_empty() {
243            entry.ensure_integrity().map_err(|_| FormatError::InvalidArguments)?;
244            used_ranges.push(entry.first_lba..entry.last_lba + 1);
245            partition_table_view[..part_raw.len()].copy_from_slice(part_raw);
246        }
247        digest.write(part_raw);
248        partition_table_view = &mut partition_table_view[part_size..];
249    }
250    used_ranges.sort_by_key(|range| range.start);
251    for ranges in used_ranges.windows(2) {
252        if ranges[0].end > ranges[1].start {
253            return Err(FormatError::InvalidArguments);
254        }
255    }
256    header.first_usable = first_usable;
257    header.last_usable = last_usable;
258    header.num_parts = entries.len() as u32;
259    header.crc32_parts = digest.sum32();
260    header.crc32 = header.compute_checksum();
261    Ok(partition_table)
262}
263
264#[cfg(test)]
265mod tests {
266    use super::{Header, GPT_HEADER_SIZE};
267
268    #[fuchsia::test]
269    fn header_crc() {
270        let nblocks = 8;
271        let partition_table_nblocks = 1;
272        let mut header = Header {
273            signature: [0x45, 0x46, 0x49, 0x20, 0x50, 0x41, 0x52, 0x54],
274            revision: 0x10000,
275            header_size: GPT_HEADER_SIZE as u32,
276            crc32: 0,
277            reserved: 0,
278            current_lba: 1,
279            backup_lba: nblocks - 1,
280            first_usable: 2 + partition_table_nblocks,
281            last_usable: nblocks - (2 + partition_table_nblocks),
282            disk_guid: [0u8; 16],
283            part_start: 2,
284            num_parts: 1,
285            part_size: 128,
286            crc32_parts: 0,
287            zerocopy_padding: 0,
288        };
289        header.crc32 = header.compute_checksum();
290
291        header.ensure_integrity(nblocks, 512).expect("Header should be valid");
292
293        // Flip one bit, leaving an otherwise valid header.
294        header.num_parts = 2;
295
296        header.ensure_integrity(nblocks, 512).expect_err("Header should be invalid");
297    }
298}