Skip to main content

btrfs_core/
chunk.rs

1//! btrfs `sys_chunk_array` bootstrap chunk map (the P1 seed).
2//!
3//! Every btrfs tree except the superblock is addressed by **logical** address;
4//! nothing is readable until a logical→physical map exists. The bootstrap for
5//! that map lives inside the superblock: the `sys_chunk_array` region (offset
6//! [`SYS_CHUNK_ARRAY_OFFSET`] = 0x32b, `sys_chunk_array_size` bytes valid) holds
7//! a packed sequence of `[btrfs_disk_key][btrfs_chunk + btrfs_stripe…]` entries
8//! describing the SYSTEM block groups — enough to reach the chunk tree at the
9//! `chunk_root` logical address.
10//!
11//! P0 decodes these structs (key + chunk geometry + stripes) so the mapping is
12//! available; **full chunk-tree walking is P1**. Struct layouts were transcribed
13//! from the on-disk `btrfs_disk_key` / `btrfs_chunk` / `btrfs_stripe` and
14//! **verified byte-for-byte against `dump-super -f`** on the oracle image (see
15//! `tests/data/README.md`).
16
17use crate::bytes::{le_u16, le_u64, u8_at};
18
19/// Offset of the `sys_chunk_array` within the superblock block.
20pub const SYS_CHUNK_ARRAY_OFFSET: usize = 0x32b;
21
22/// `btrfs_disk_key` on-disk size: `objectid(u64) + type(u8) + offset(u64)`.
23pub const DISK_KEY_SIZE: usize = 17;
24
25/// The `btrfs_chunk` fixed header size, up to (but excluding) the variable
26/// stripe array: `length,owner,stripe_len,type` (4×u64) + `io_align,io_width,
27/// sector_size` (3×u32) + `num_stripes,sub_stripes` (2×u16) = 48 bytes.
28pub const CHUNK_HEADER_SIZE: usize = 48;
29
30/// `btrfs_stripe` on-disk size: `devid(u64) + offset(u64) + dev_uuid[16]`.
31pub const STRIPE_SIZE: usize = 32;
32
33/// `BTRFS_CHUNK_ITEM_KEY` — the disk-key type byte of a chunk item (228).
34pub const CHUNK_ITEM_KEY: u8 = 228;
35
36// btrfs_chunk `type` block-group flag bits.
37/// `BTRFS_BLOCK_GROUP_DATA` (1 << 0).
38pub const BLOCK_GROUP_DATA: u64 = 1 << 0;
39/// `BTRFS_BLOCK_GROUP_SYSTEM` (1 << 1).
40pub const BLOCK_GROUP_SYSTEM: u64 = 1 << 1;
41/// `BTRFS_BLOCK_GROUP_METADATA` (1 << 2).
42pub const BLOCK_GROUP_METADATA: u64 = 1 << 2;
43/// `BTRFS_BLOCK_GROUP_DUP` (1 << 5).
44pub const BLOCK_GROUP_DUP: u64 = 1 << 5;
45
46/// A `btrfs_disk_key` — the `(objectid, type, offset)` tuple every btrfs item is
47/// keyed by. Fields are LE on disk; compared as host integers on the tuple
48/// (that ordering is P2's concern; P0 just carries the decoded values).
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct DiskKey {
51    /// `objectid` — for a sys chunk item this is `FIRST_CHUNK_TREE_OBJECTID`
52    /// (256).
53    pub objectid: u64,
54    /// `type` — the item-type byte (228 = `CHUNK_ITEM` for a sys chunk entry).
55    pub key_type: u8,
56    /// `offset` — for a chunk item this is the chunk's logical start address.
57    pub offset: u64,
58}
59
60impl DiskKey {
61    /// Decode a `btrfs_disk_key` at `off` (bounds-checked; out-of-range fields
62    /// read as 0).
63    #[must_use]
64    pub fn parse(data: &[u8], off: usize) -> Self {
65        DiskKey {
66            objectid: le_u64(data, off),
67            key_type: u8_at(data, off + 8),
68            offset: le_u64(data, off + 9),
69        }
70    }
71}
72
73/// One stripe of a chunk: a physical placement on a device. For single-device
74/// single/DUP the physical byte address of a logical address `l` within the
75/// chunk is `stripe.offset + (l - chunk_key.offset)`.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct Stripe {
78    /// `devid` — the device this stripe lives on.
79    pub devid: u64,
80    /// `offset` — the physical byte offset on the device where the chunk's data
81    /// begins for this stripe.
82    pub offset: u64,
83    /// `dev_uuid` — the device UUID.
84    pub dev_uuid: [u8; 16],
85}
86
87/// A decoded chunk map entry from the `sys_chunk_array`: the chunk's logical
88/// key, its geometry, and its stripes. This is the bootstrap logical→physical
89/// record for a SYSTEM block group.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct SysChunk {
92    /// The chunk item's disk key; `key.offset` is the chunk's logical start.
93    pub key: DiskKey,
94    /// `length` — the chunk's logical byte span.
95    pub length: u64,
96    /// `owner` — the objectid that owns the chunk (2 = the extent tree).
97    pub owner: u64,
98    /// `stripe_len` — the RAID stripe unit (irrelevant for single/DUP mapping).
99    pub stripe_len: u64,
100    /// `type` — the block-group flags (`BLOCK_GROUP_SYSTEM | BLOCK_GROUP_DUP`,
101    /// etc.).
102    pub chunk_type: u64,
103    /// `num_stripes` — the number of physical stripes that follow.
104    pub num_stripes: u16,
105    /// `sub_stripes` — RAID10 sub-stripe count (1 for single/DUP).
106    pub sub_stripes: u16,
107    /// The decoded stripes (physical placements).
108    pub stripes: Vec<Stripe>,
109}
110
111impl SysChunk {
112    /// Translate a logical address `logical` to a physical byte offset on the
113    /// first stripe's device, for single-device single/DUP chunks.
114    ///
115    /// Returns `None` if `logical` lies outside this chunk's `[key.offset ..
116    /// key.offset + length)` span, or if the chunk has no stripes. For DUP there
117    /// are two stripes on the same device; the first mirror (`stripes[0]`) is
118    /// chosen (both hold identical data). Multi-device / striped RAID mapping is
119    /// deferred to a later phase.
120    #[must_use]
121    pub fn logical_to_physical(&self, logical: u64) -> Option<u64> {
122        let start = self.key.offset;
123        let end = start.checked_add(self.length)?;
124        if logical < start || logical >= end {
125            return None;
126        }
127        let stripe = self.stripes.first()?;
128        let delta = logical - start;
129        stripe.offset.checked_add(delta)
130    }
131
132    /// Parse the packed `sys_chunk_array` starting at `array_off` for
133    /// `array_size` valid bytes, yielding one [`SysChunk`] per
134    /// `[disk_key][chunk + stripes]` entry.
135    ///
136    /// Bounds-checked and panic-free: parsing stops at the first entry that does
137    /// not wholly fit within the declared `array_size` (or the block), and an
138    /// absurd `num_stripes` is capped by the remaining bytes — a malformed array
139    /// truncates rather than over-reads or panics.
140    #[must_use]
141    pub fn parse_array(block: &[u8], array_off: usize, array_size: u32) -> Vec<SysChunk> {
142        let mut out = Vec::new();
143        // The valid region is `[array_off .. array_off + array_size]`, clamped to
144        // the block we were handed.
145        let declared_end = array_off.saturating_add(array_size as usize);
146        let end = declared_end.min(block.len());
147
148        let mut p = array_off;
149        loop {
150            // A whole entry needs at least a disk key + a chunk header.
151            let key_end = p.saturating_add(DISK_KEY_SIZE);
152            let hdr_end = key_end.saturating_add(CHUNK_HEADER_SIZE);
153            if hdr_end > end {
154                break;
155            }
156
157            let key = DiskKey::parse(block, p);
158            let c = key_end; // chunk header starts right after the 17-byte key
159            let length = le_u64(block, c);
160            let owner = le_u64(block, c + 8);
161            let stripe_len = le_u64(block, c + 16);
162            let chunk_type = le_u64(block, c + 24);
163            // io_align (c+32), io_width (c+36), sector_size (c+40) are geometry
164            // hints not needed for single/DUP logical→physical mapping.
165            let num_stripes = le_u16(block, c + 44);
166            let sub_stripes = le_u16(block, c + 46);
167
168            let stripes_start = c.saturating_add(CHUNK_HEADER_SIZE);
169            let stripes_bytes = usize::from(num_stripes).saturating_mul(STRIPE_SIZE);
170            let stripes_end = stripes_start.saturating_add(stripes_bytes);
171            if stripes_end > end {
172                // A declared stripe count that overruns the valid region is a
173                // malformed/truncated array — stop rather than over-read.
174                break;
175            }
176
177            let mut stripes = Vec::with_capacity(usize::from(num_stripes));
178            let mut s = stripes_start;
179            for _ in 0..num_stripes {
180                let devid = le_u64(block, s);
181                let offset = le_u64(block, s + 8);
182                let mut dev_uuid = [0u8; 16];
183                for (i, b) in dev_uuid.iter_mut().enumerate() {
184                    *b = u8_at(block, s + 16 + i);
185                }
186                stripes.push(Stripe {
187                    devid,
188                    offset,
189                    dev_uuid,
190                });
191                s = s.saturating_add(STRIPE_SIZE);
192            }
193
194            out.push(SysChunk {
195                key,
196                length,
197                owner,
198                stripe_len,
199                chunk_type,
200                num_stripes,
201                sub_stripes,
202                stripes,
203            });
204
205            p = stripes_end;
206        }
207        out
208    }
209}
210
211#[cfg(test)]
212mod unit {
213    use super::{
214        DiskKey, Stripe, SysChunk, CHUNK_HEADER_SIZE, DISK_KEY_SIZE, STRIPE_SIZE,
215        SYS_CHUNK_ARRAY_OFFSET,
216    };
217
218    /// Build a minimal chunk entry: 17-byte key + 48-byte chunk header (with the
219    /// given `num_stripes` at header offset 44) + `present_stripes` stripes.
220    fn chunk_entry(key_offset: u64, num_stripes: u16, present_stripes: usize) -> Vec<u8> {
221        let mut e = vec![0u8; DISK_KEY_SIZE + CHUNK_HEADER_SIZE + present_stripes * STRIPE_SIZE];
222        // disk key: objectid=256, type=228, offset=key_offset
223        e[0..8].copy_from_slice(&256u64.to_le_bytes());
224        e[8] = 228;
225        e[9..17].copy_from_slice(&key_offset.to_le_bytes());
226        // chunk header: length at c+0, num_stripes at c+44
227        let c = DISK_KEY_SIZE;
228        e[c..c + 8].copy_from_slice(&8_388_608u64.to_le_bytes());
229        e[c + 44..c + 46].copy_from_slice(&num_stripes.to_le_bytes());
230        e
231    }
232
233    #[test]
234    fn parse_array_stops_when_stripes_overrun_the_declared_region() {
235        // A chunk header claims 2 stripes but the array only carries the header:
236        // `stripes_end > end` must break rather than over-read (chunk.rs:171).
237        let entry = chunk_entry(22_020_096, 2, 0);
238        let mut block = vec![0u8; SYS_CHUNK_ARRAY_OFFSET + entry.len()];
239        block[SYS_CHUNK_ARRAY_OFFSET..].copy_from_slice(&entry);
240        let size = entry.len() as u32; // declares only the header, not the 2 stripes
241        let chunks = SysChunk::parse_array(&block, SYS_CHUNK_ARRAY_OFFSET, size);
242        assert!(chunks.is_empty(), "overrunning stripe count truncates");
243    }
244
245    #[test]
246    fn logical_to_physical_guards_out_of_span_and_empty_stripes() {
247        let c = SysChunk {
248            key: DiskKey {
249                objectid: 256,
250                key_type: 228,
251                offset: 1000,
252            },
253            length: 100,
254            owner: 2,
255            stripe_len: 65536,
256            chunk_type: 0x22,
257            num_stripes: 1,
258            sub_stripes: 1,
259            stripes: vec![Stripe {
260                devid: 1,
261                offset: 5000,
262                dev_uuid: [0u8; 16],
263            }],
264        };
265        assert_eq!(c.logical_to_physical(1000), Some(5000), "start maps");
266        assert_eq!(c.logical_to_physical(1050), Some(5050), "mid maps");
267        assert_eq!(c.logical_to_physical(999), None, "below span");
268        assert_eq!(c.logical_to_physical(1100), None, "at/above end");
269
270        // No stripes → cannot map even an in-span address.
271        let mut no_stripes = c.clone();
272        no_stripes.stripes.clear();
273        assert_eq!(no_stripes.logical_to_physical(1000), None);
274
275        // A hostile chunk whose `key.offset + length` overflows u64: the span end
276        // is unrepresentable, so the mapping guards to `None` rather than
277        // wrapping (chunk.rs `checked_add` short-circuit).
278        let overflow = SysChunk {
279            key: DiskKey {
280                objectid: 256,
281                key_type: 228,
282                offset: u64::MAX - 10,
283            },
284            length: 100,
285            ..c
286        };
287        assert_eq!(overflow.logical_to_physical(u64::MAX - 5), None);
288    }
289}