Skip to content
API Documentation
BastionGuard™
xfs_btree.hpp
Go to the documentation of this file.
1// xfs_btree.hpp -- Generic B+tree walker for XFS (V4 + V5).
2//
3// XFS uses B+trees pervasively: free-space, inode allocation, extent
4// mapping (BMBT), reverse-mapping (V5), reference-count (V5). All share
5// the same node header layout and traversal pattern; only the record
6// format and key format differ per tree type.
7//
8// In session 2 we only USE this walker to enumerate the inode B+tree
9// (IBT). The walker is generic so the next sessions can plug in BMBT
10// for extent decoding without rewriting the traversal logic.
11//
12// NODE HEADER LAYOUTS
13// -------------------
14// V4 short-form header (used in per-AG B+trees: IBT, ABTB, ABTC):
15// offset 0x00: __be32 magic "IABT" (V4) or "IAB3" (V5)
16// offset 0x04: __be16 level 0 = leaf, >0 = internal
17// offset 0x06: __be16 numrecs number of records/keys in this block
18// offset 0x08: __be32 leftsib left sibling block (NULLAGBLOCK if none)
19// offset 0x0C: __be32 rightsib right sibling block
20// header length = 16 bytes
21//
22// V5 short-form header (per-AG B+trees, with CRC):
23// offset 0x00: __be32 magic "IAB3"
24// offset 0x04: __be16 level
25// offset 0x06: __be16 numrecs
26// offset 0x08: __be32 leftsib
27// offset 0x0C: __be32 rightsib
28// offset 0x10: __be64 blkno block number of THIS block
29// offset 0x18: __be64 lsn log sequence number
30// offset 0x20: uuid_t uuid (16 bytes)
31// offset 0x30: __be32 owner AG number
32// offset 0x34: __be32 crc CRC32
33// header length = 56 bytes
34//
35// We don't validate the V5 CRC -- we want to read damaged volumes too --
36// but we MUST account for the larger header when computing the offset of
37// the records/keys array.
38//
39// IBT RECORD (leaf level)
40// -----------------------
41// struct xfs_inobt_rec_t {
42// __be32 ir_startino; // starting AG-relative inode number
43// __be32 ir_freecount; // number of free inodes in this chunk (V4)
44// // OR ir_holemask (V5 SPARSE feature)
45// // We treat the field generically and pick
46// // the right meaning based on SB features.
47// __be64 ir_free; // bitmap: bit i set => inode (start+i) is FREE
48// };
49// Size: 16 bytes per record. A "chunk" is exactly 64 inodes.
50//
51// For V5 with SPARSE inodes (XFS_SB_FEAT_INCOMPAT_SPINODES), the layout
52// becomes:
53// __be32 ir_startino;
54// __be16 ir_holemask; // bit i set => sub-chunk i is a hole
55// __u8 ir_count; // total inodes in this chunk
56// __u8 ir_freecount; // free inodes in this chunk
57// __be64 ir_free;
58// Size still 16 bytes.
59//
60// IBT KEY (internal nodes)
61// ------------------------
62// struct xfs_inobt_key_t { __be32 ir_startino; };
63// Size: 4 bytes per key.
64//
65// IBT POINTER (internal nodes)
66// ----------------------------
67// __be32 ptr; // child block (AG-relative). Size: 4 bytes.
68//
69// In an internal block of `numrecs` entries, we have `numrecs` keys
70// followed by `maxrecs` pointers (XFS reserves space for max even if
71// not used). For traversal we only need numrecs keys + numrecs pointers.
72
73#pragma once
74
75#include "xfs_format.hpp"
76
77#include <cstdint>
78#include <functional>
79#include <string>
80#include <vector>
81
82namespace xfs {
83
84// "NULL" block sentinel for sibling/child pointers
85constexpr uint32_t NULLAGBLOCK = 0xFFFFFFFFu;
86constexpr uint64_t NULLFSBLOCK = 0xFFFFFFFFFFFFFFFFull;
87
88// Decoded short-form B+tree node header
90 uint32_t magic {0};
91 uint16_t level {0};
92 uint16_t numrecs {0};
93 uint32_t leftsib {NULLAGBLOCK};
95 // V5-only fields (zero on V4)
96 uint64_t blkno {0};
97 uint64_t lsn {0};
98 uint32_t owner {0};
99 uint32_t crc {0};
100 // Computed from the magic number / SB version
101 bool is_v5_header{false};
102};
103
104// Returns the byte length of the node header (16 for V4, 56 for V5).
105inline uint32_t btree_header_size(bool is_v5) {
106 return is_v5 ? 56u : 16u;
107}
108
109// Parse a short-form B+tree node header from a block buffer.
110// `buf` must point to at least `btree_header_size(is_v5)` bytes.
111// Returns false on invalid magic or implausible field values.
112//
113// `expected_magic_v4` and `expected_magic_v5` let the caller restrict
114// matches to a specific tree kind (IBT, ABTB, ABTC, BMBT, ...).
115bool parse_btree_header(const unsigned char* buf,
116 uint32_t expected_magic_v4,
117 uint32_t expected_magic_v5,
118 BtreeNodeHeader& out);
119
120// Decoded inode B+tree leaf record.
121struct InobtRec {
122 uint32_t startino {0}; // AG-relative starting inode number
123 uint16_t holemask {0}; // V5 sparse only; 0 on V4
124 uint8_t count {64};// total inodes in the chunk; defaults to 64
125 uint8_t freecount {0}; // free inodes in the chunk
126 uint64_t freemask {0}; // bit i set => inode (startino+i) is FREE
127 bool sparse {false}; // true when V5 SPARSE feature was used
128};
129
130// Parse one inode-B+tree leaf record (16 bytes).
131//
132// `is_v5_sparse` selects between classic V4 layout (ir_freecount as 32-bit
133// at offset 4) and V5 SPARSE layout (holemask + count + freecount packed
134// into bytes 4..7).
135//
136// The `freemask` semantics are unchanged across formats.
137void parse_inobt_record(const unsigned char* buf, bool is_v5_sparse,
138 InobtRec& out);
139
140// =============================================================================
141// Traversal API
142// =============================================================================
143//
144// We expose two callback flavours:
145//
146// * walk_inode_btree(reader, vol, agno, on_record, logs, error)
147// Calls `on_record(rec, agno)` for every IBT leaf record found
148// under AGI[agno].root, in B+tree order. Returns false on fatal
149// I/O / format error. Damage in one branch logs a warning but
150// does not abort the traversal of the other branches.
151//
152// * walk_all_inode_records(reader, vol, on_record, logs)
153// Convenience wrapper: walks the IBT of every AG and calls the
154// callback for every leaf record across the whole filesystem.
155//
156// The callback returns `false` to stop traversal early (e.g. on a
157// fatal user-level error); `true` to continue.
158
159} // namespace xfs
160
161class DiskReader; // forward (defined in disk_reader.hpp)
162
163namespace xfs {
164
166 std::function<bool(const InobtRec& rec, uint32_t agno)>;
167
168bool walk_inode_btree(DiskReader& reader,
169 const VolumeContext& vol,
170 uint32_t agno,
171 const InobtRecordCallback& on_record,
172 std::vector<std::string>& logs,
173 std::string& error);
174
176 const VolumeContext& vol,
177 const InobtRecordCallback& on_record,
178 std::vector<std::string>& logs);
179
180// =============================================================================
181// FINOBT (free-inode B+tree, V5 only)
182// =============================================================================
183//
184// Same record/key/pointer encoding as the IBT, but the magic numbers are
185// FIBT (V4 -- not used in practice since FINOBT is V5-only) and FIB3 (V5).
186// The root/depth come from agi_free_root / agi_free_level instead of
187// agi_root / agi_level.
188//
189// Walks one AG's free-inode B+tree and yields each leaf record. Records
190// expose `freemask`: bits set there mean the inode at (startino + bit) is
191// FREE -- i.e. either never used or recently deleted. By reading the
192// dinode at that disk offset we can attempt to recover deleted-file data
193// for cancellations that haven't yet been overwritten by a new alloc.
194//
195// IMPORTANT: this only works on V5 volumes with the FINOBT feature
196// (XFS_SB_FEAT_RO_COMPAT_FINOBT). On V4, agi.free_root/free_level are
197// zero and the walker will simply yield nothing.
198//
199// FINOBT magic numbers (XFS_FIBT_MAGIC_V4/V5) are already declared in
200// xfs_format.hpp.
201
202// Same callback type (records have identical layout).
204 const VolumeContext& vol,
205 uint32_t agno,
206 const InobtRecordCallback& on_record,
207 std::vector<std::string>& logs,
208 std::string& error);
209
211 const VolumeContext& vol,
212 const InobtRecordCallback& on_record,
213 std::vector<std::string>& logs);
214
215// =============================================================================
216// Inode iteration helpers
217// =============================================================================
218//
219// Once we have an InobtRec, we know the chunk of 64 (or fewer, with sparse)
220// inodes starting at `startino` and which of them are allocated. The
221// caller will typically want to:
222// 1. iterate bit positions 0..63 in the chunk
223// 2. skip bits where `holemask` says "hole" (V5 sparse)
224// 3. skip bits where `freemask` bit is set (free => not allocated)
225// 4. for each allocated bit, compute the global inode number and the
226// disk byte offset of the dinode
227//
228// The helper below does this packaging. It does NOT read the dinode
229// itself -- that's session 3.
230
232 uint64_t ino {0}; // global 64-bit inode number
233 uint32_t agno {0};
234 uint32_t agbno {0}; // AG-relative block where the chunk starts
235 uint32_t in_chunk_index {0}; // 0..63
236 uint64_t disk_offset {0}; // absolute byte offset to the dinode
237};
238
239// Expand a single InobtRec into a list of AllocatedInode entries.
240// Appends to `out`. Skips holes and free inodes.
241void expand_chunk(const InobtRec& rec, uint32_t agno,
242 const VolumeContext& vol,
243 std::vector<AllocatedInode>& out);
244
245// Mirror of expand_chunk(), but emits inodes whose freemask bit IS set
246// (= free / recently deleted). Used by FINOBT recovery: each entry
247// points to a dinode slot that may still hold recoverable content.
248void expand_chunk_free(const InobtRec& rec, uint32_t agno,
249 const VolumeContext& vol,
250 std::vector<AllocatedInode>& out);
251
252// =============================================================================
253// BMBT (block-map B+tree) -- LONG-FORM
254// =============================================================================
255//
256// Used by the data fork of an inode when di_format == 3 ("btree"). This
257// happens when the inline extent list overflows what fits inside the
258// fork buffer.
259//
260// Header layouts (DIFFERENT from the short-form per-AG B+trees!):
261//
262// V4 long-form header (24 bytes):
263// 0x00 __be32 magic ("BMAP" 0x424D4150)
264// 0x04 __be16 level
265// 0x06 __be16 numrecs
266// 0x08 __be64 leftsib FSB (NULLFSBLOCK if none)
267// 0x10 __be64 rightsib
268//
269// V5 long-form header (72 bytes):
270// 0x00 __be32 magic ("BMA3" 0x424D4133)
271// 0x04 __be16 level
272// 0x06 __be16 numrecs
273// 0x08 __be64 leftsib
274// 0x10 __be64 rightsib
275// 0x18 __be64 blkno
276// 0x20 __be64 lsn
277// 0x28 uuid_t uuid (16 bytes)
278// 0x38 __be64 owner (inode number)
279// 0x40 __be32 crc
280// 0x44 __be32 pad
281//
282// Internal-node layout (long-form):
283// [ header ][ keys[maxrecs] ][ ptrs[maxrecs] ]
284// key = __be64 startoff (8 bytes)
285// ptr = __be64 child FSB (8 bytes)
286//
287// Leaf records: 16-byte BMBT records with the same bit-packed encoding
288// as inline extents (see xfs_inode.cpp::decode_extent_record).
289//
290// THE ROOT:
291// When di_format=3, the data fork itself contains a TRUNCATED long-form
292// header (called the "root header" or "bmdr") followed by keys/pointers
293// to the first level of children. The "root" in the fork is NOT a full
294// 24/72-byte header -- it's a 4-byte mini-header (level + numrecs) plus
295// the keys/pointers list. From there we descend into real BMBT blocks
296// on disk that DO use the full 24/72-byte long-form header.
297//
298// bmdr (in inode fork):
299// 0x00 __be16 level
300// 0x02 __be16 numrecs
301// 0x04 keys[] (8 bytes each, __be64 startoff)
302// ... ptrs[] (8 bytes each, __be64 FSB)
303//
304// `walk_extent_btree_root` accepts the inode's data-fork buffer and
305// the volume context, decodes the bmdr root, then walks every child
306// down to the leaves yielding leaf extents into `out`. Per-branch
307// corruption is reported in `logs` but does not abort siblings
308// (BALANCED behaviour).
309
310constexpr uint32_t XFS_BMBT_MAGIC_V4 = 0x424D4150u; // "BMAP"
311constexpr uint32_t XFS_BMBT_MAGIC_V5 = 0x424D4133u; // "BMA3"
312
313// Long-form BMBT: 8-byte keys (startoff), 8-byte pointers (FSB), 16-byte recs
314constexpr uint32_t BMBT_KEY_SIZE_LONG = 8;
315constexpr uint32_t BMBT_PTR_SIZE_LONG = 8;
316constexpr uint32_t BMBT_REC_SIZE = 16;
317
318// Long-form B+tree node header (decoded form).
320 uint32_t magic {0};
321 uint16_t level {0};
322 uint16_t numrecs {0};
325 // V5 extras:
326 uint64_t blkno {0};
327 uint64_t lsn {0};
328 uint64_t owner {0};
329 uint32_t crc {0};
330 bool is_v5_header {false};
331};
332
333inline uint32_t btree_long_header_size(bool is_v5) {
334 return is_v5 ? 72u : 24u;
335}
336
337// Parse a long-form B+tree header. Returns false on bad magic.
338bool parse_btree_long_header(const unsigned char* buf,
339 uint32_t expected_magic_v4,
340 uint32_t expected_magic_v5,
341 BtreeLongHeader& out);
342
343// Forward-declared, defined in xfs_inode.hpp. We keep this header lean
344// by not pulling xfs_inode.hpp here.
345struct Extent;
346
347// Walk the BMBT whose ROOT lives in an inode's data fork (di_format=3).
348//
349// `root_buf` points to the start of the bmdr root (= the first byte
350// of the data fork in the dinode buffer).
351// `root_len` is the number of valid bytes available at `root_buf`,
352// i.e. the data-fork length.
353// `is_v5_volume` selects between V4 BMAP and V5 BMA3 magics on children.
354// `max_extents` is a safety cap (suggested: 1'000'000).
355//
356// Appends every leaf extent record found into `out`. Returns false ONLY
357// on fatal error (root header malformed, max_extents reached). Per-block
358// damage on children is logged and does not abort sibling branches.
360 const VolumeContext& vol,
361 const unsigned char* root_buf,
362 uint32_t root_len,
363 bool is_v5_volume,
364 uint32_t max_extents,
365 std::vector<Extent>& out,
366 std::vector<std::string>& logs,
367 std::string& error);
368
369} // namespace xfs
constexpr uint32_t NULLAGBLOCK
Definition xfs_btree.hpp:85
bool walk_inode_btree(DiskReader &reader, const VolumeContext &vol, uint32_t agno, const InobtRecordCallback &on_record, std::vector< std::string > &logs, std::string &error)
bool walk_all_free_inode_records(DiskReader &reader, const VolumeContext &vol, const InobtRecordCallback &on_record, std::vector< std::string > &logs)
bool parse_btree_long_header(const unsigned char *buf, uint32_t expected_magic_v4, uint32_t expected_magic_v5, BtreeLongHeader &out)
bool walk_extent_btree_root(DiskReader &reader, const VolumeContext &vol, const unsigned char *root_buf, uint32_t root_len, bool is_v5_volume, uint32_t max_extents, std::vector< Extent > &out, std::vector< std::string > &logs, std::string &error)
uint32_t btree_long_header_size(bool is_v5)
constexpr uint32_t XFS_BMBT_MAGIC_V5
constexpr uint32_t BMBT_PTR_SIZE_LONG
bool parse_btree_header(const unsigned char *buf, uint32_t expected_magic_v4, uint32_t expected_magic_v5, BtreeNodeHeader &out)
Definition xfs_btree.cpp:72
constexpr uint32_t XFS_BMBT_MAGIC_V4
void expand_chunk(const InobtRec &rec, uint32_t agno, const VolumeContext &vol, std::vector< AllocatedInode > &out)
uint32_t btree_header_size(bool is_v5)
void parse_inobt_record(const unsigned char *buf, bool is_v5_sparse, InobtRec &out)
constexpr uint32_t BMBT_KEY_SIZE_LONG
bool walk_all_inode_records(DiskReader &reader, const VolumeContext &vol, const InobtRecordCallback &on_record, std::vector< std::string > &logs)
bool walk_free_inode_btree(DiskReader &reader, const VolumeContext &vol, uint32_t agno, const InobtRecordCallback &on_record, std::vector< std::string > &logs, std::string &error)
constexpr uint32_t BMBT_REC_SIZE
std::function< bool(const InobtRec &rec, uint32_t agno)> InobtRecordCallback
void expand_chunk_free(const InobtRec &rec, uint32_t agno, const VolumeContext &vol, std::vector< AllocatedInode > &out)
constexpr uint64_t NULLFSBLOCK
Definition xfs_btree.hpp:86
uint8_t freecount
uint16_t holemask
uint32_t startino
uint64_t freemask