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}