Skip to content
API Documentation
BastionGuard™
xfs_btree.cpp
Go to the documentation of this file.
1// xfs_btree.cpp -- Generic B+tree traversal for XFS, plus IBT specialisation.
2//
3// In session 2 we only walk the inode allocation B+tree (IBT). The walker
4// is structured so the caller plugs in:
5// - the expected magic numbers (V4 + V5) for the tree kind
6// - the per-AG root block (taken from AGI for IBT)
7// - the per-AG depth (taken from AGI.level)
8// - a leaf-record callback
9//
10// IMPORTANT layout details (see xfs_btree.hpp banner for the full picture):
11//
12// * V5 nodes have a 56-byte header (vs 16 on V4). All record/key/ptr
13// offsets must be measured relative to the right header size.
14//
15// * In an INTERNAL node, the record area for IBT is laid out as
16// keys[maxrecs] followed by ptrs[maxrecs]
17// where `maxrecs` is the max that fits in a block of size `blocksize`,
18// NOT the actual `numrecs` of the node. This matters: pointers begin
19// at offset `header + maxrecs*key_size`, regardless of how many keys
20// are actually used.
21//
22// * IBT records are 16 bytes each. IBT keys are 4 bytes (just startino).
23// IBT pointers are 4 bytes (AG-relative block number).
24//
25// * Leaf records are stored consecutively starting right after the header.
26//
27// We don't validate V5 CRCs: the goal is to read damaged volumes too.
28// We DO validate magic, level, numrecs, and pointer ranges before
29// recursing into a child block.
30
31#include "xfs_btree.hpp"
32#include "disk_reader.hpp"
33
34#include <algorithm>
35#include <array>
36#include <set>
37#include <sstream>
38#include <vector>
39
40namespace xfs {
41
42namespace {
43
44// IBT field sizes (constant across V4/V5)
45constexpr uint32_t IBT_REC_SIZE = 16; // ir_startino(4) + ir_*(4) + ir_free(8)
46constexpr uint32_t IBT_KEY_SIZE = 4; // ir_startino
47constexpr uint32_t IBT_PTR_SIZE = 4; // AG-relative block number
48
49// Compute maxrecs for an IBT node at `level`.
50// level 0 (leaf): records of IBT_REC_SIZE bytes
51// level > 0 (intern): pairs of (key + ptr) = IBT_KEY_SIZE + IBT_PTR_SIZE
52uint32_t ibt_maxrecs(uint32_t blocksize, uint32_t header_size, uint32_t level) {
53 const uint32_t avail = blocksize - header_size;
54 return (level == 0) ? (avail / IBT_REC_SIZE)
55 : (avail / (IBT_KEY_SIZE + IBT_PTR_SIZE));
56}
57
58// Convert AG#+AG-relative block to absolute disk byte offset.
60 uint32_t agno, uint32_t agbno) {
61 return vol.volume_offset
62 + static_cast<uint64_t>(agno) * vol.sb.agblocks * vol.sb.blocksize
63 + static_cast<uint64_t>(agbno) * vol.sb.blocksize;
64}
65
66// Defensive: max nodes we'll ever traverse in a single AG. A pathological
67// SB or a corruption loop should not walk forever.
68constexpr size_t MAX_NODES_PER_AG = 1'000'000;
69
70} // anonymous namespace
71
72bool parse_btree_header(const unsigned char* buf,
73 uint32_t expected_magic_v4,
74 uint32_t expected_magic_v5,
75 BtreeNodeHeader& out) {
76 if (buf == nullptr) return false;
77 out = {};
78
79 out.magic = be32(buf + 0x00);
80
81 if (out.magic == expected_magic_v5) {
82 out.is_v5_header = true;
83 } else if (out.magic == expected_magic_v4) {
84 out.is_v5_header = false;
85 } else {
86 return false;
87 }
88
89 out.level = be16(buf + 0x04);
90 out.numrecs = be16(buf + 0x06);
91 out.leftsib = be32(buf + 0x08);
92 out.rightsib = be32(buf + 0x0C);
93
94 if (out.is_v5_header) {
95 out.blkno = be64(buf + 0x10);
96 out.lsn = be64(buf + 0x18);
97 // uuid at +0x20..+0x30 ignored
98 out.owner = be32(buf + 0x30);
99 out.crc = be32(buf + 0x34);
100 }
101
102 // Sanity: level must be plausible (XFS B+trees never go beyond ~16)
103 if (out.level > 16) return false;
104 return true;
105}
106
107void parse_inobt_record(const unsigned char* buf, bool is_v5_sparse,
108 InobtRec& out) {
109 out = {};
110 out.startino = be32(buf + 0x00);
111 if (is_v5_sparse) {
112 // V5 sparse layout: 16-bit holemask, 8-bit count, 8-bit freecount
113 out.holemask = be16(buf + 0x04);
114 out.count = buf[0x06];
115 out.freecount = buf[0x07];
116 if (out.count == 0) out.count = 64; // some early kernels left 0; default
117 out.sparse = true;
118 } else {
119 // Classic layout: 32-bit freecount at offset 4
120 const uint32_t freec32 = be32(buf + 0x04);
121 out.freecount = static_cast<uint8_t>(std::min<uint32_t>(freec32, 64));
122 out.holemask = 0;
123 out.count = 64;
124 out.sparse = false;
125 }
126 out.freemask = be64(buf + 0x08);
127}
128
129// Recursive traversal of one IBT subtree rooted at `block`.
130// Returns false on FATAL error (read/format) that should abort the whole
131// walk for this AG; per-branch warnings are appended to `logs` instead.
132namespace {
133
135 const VolumeContext& vol,
136 uint32_t agno,
137 uint32_t agbno,
138 uint32_t expected_level,
139 uint32_t magic_v4,
140 uint32_t magic_v5,
141 const char* tree_label,
142 const InobtRecordCallback& on_record,
143 std::set<uint32_t>& visited,
144 size_t& nodes_visited,
145 std::vector<std::string>& logs,
146 std::string& error,
147 bool& callback_stopped) {
148 if (callback_stopped) return true;
149
150 if (agbno == 0 || agbno == NULLAGBLOCK || agbno >= vol.sb.agblocks) {
151 std::ostringstream m;
152 m << "XFS " << tree_label << " (AG " << agno << "): invalid child pointer " << agbno;
153 logs.push_back(m.str());
154 return true; // not fatal: skip this branch
155 }
156
157 if (++nodes_visited > MAX_NODES_PER_AG) {
158 error = std::string("XFS ") + tree_label
159 + ": traversal exceeded safety cap (possible loop)";
160 return false;
161 }
162
163 if (!visited.insert(agbno).second) {
164 std::ostringstream m;
165 m << "XFS " << tree_label << " (AG " << agno
166 << "): cycle detected at block " << agbno << ", skipping";
167 logs.push_back(m.str());
168 return true;
169 }
170
171 const uint64_t blk_off = ag_block_to_disk_offset(vol, agno, agbno);
172 std::vector<unsigned char> block(vol.sb.blocksize, 0);
173 if (!reader.read_at(blk_off, block.data(), block.size())) {
174 std::ostringstream m;
175 m << "XFS " << tree_label << " (AG " << agno
176 << "): read failed for block " << agbno;
177 logs.push_back(m.str());
178 return true; // not fatal -- just lose this branch
179 }
180
181 BtreeNodeHeader hdr;
182 if (!parse_btree_header(block.data(), magic_v4, magic_v5, hdr)) {
183 std::ostringstream m;
184 m << "XFS " << tree_label << " (AG " << agno
185 << "): bad magic at block " << agbno;
186 logs.push_back(m.str());
187 return true;
188 }
189 if (hdr.level != expected_level) {
190 std::ostringstream m;
191 m << "XFS " << tree_label << " (AG " << agno
192 << "): block " << agbno << " has level " << hdr.level
193 << ", expected " << expected_level;
194 logs.push_back(m.str());
195 // Don't abort -- some damaged trees can still yield records
196 }
197
198 const uint32_t header_size = btree_header_size(hdr.is_v5_header);
199 const bool is_v5 = vol.sb.is_v5();
200 const bool is_v5_sparse =
202
203 if (hdr.level == 0) {
204 // ----------- LEAF -----------
205 const uint32_t maxrecs = ibt_maxrecs(vol.sb.blocksize, header_size, 0);
206 if (hdr.numrecs > maxrecs) {
207 std::ostringstream m;
208 m << "XFS " << tree_label << " (AG " << agno
209 << "): leaf at block " << agbno
210 << " reports numrecs=" << hdr.numrecs
211 << " > maxrecs=" << maxrecs << ", clamping";
212 logs.push_back(m.str());
213 }
214 const uint32_t n = std::min<uint32_t>(hdr.numrecs, maxrecs);
215 for (uint32_t i = 0; i < n; ++i) {
216 const unsigned char* recp =
217 block.data() + header_size + i * IBT_REC_SIZE;
218 InobtRec rec;
219 parse_inobt_record(recp, is_v5_sparse, rec);
220 if (!on_record(rec, agno)) {
221 callback_stopped = true;
222 return true;
223 }
224 }
225 } else {
226 // ----------- INTERNAL -----------
227 const uint32_t maxrecs = ibt_maxrecs(vol.sb.blocksize, header_size,
228 hdr.level);
229 if (hdr.numrecs > maxrecs) {
230 std::ostringstream m;
231 m << "XFS " << tree_label << " (AG " << agno
232 << "): internal at block " << agbno
233 << " reports numrecs=" << hdr.numrecs
234 << " > maxrecs=" << maxrecs << ", clamping";
235 logs.push_back(m.str());
236 }
237 const uint32_t n = std::min<uint32_t>(hdr.numrecs, maxrecs);
238
239 // Pointers start AFTER the maxrecs key array (not numrecs!)
240 const uint32_t ptrs_offset = header_size + maxrecs * IBT_KEY_SIZE;
241
242 for (uint32_t i = 0; i < n; ++i) {
243 const uint32_t ptr_off = ptrs_offset + i * IBT_PTR_SIZE;
244 if (ptr_off + IBT_PTR_SIZE > vol.sb.blocksize) break;
245 const uint32_t child = be32(block.data() + ptr_off);
246 if (!walk_ibt_block(reader, vol, agno, child,
247 hdr.level - 1,
248 magic_v4, magic_v5, tree_label,
249 on_record, visited, nodes_visited,
250 logs, error, callback_stopped)) {
251 return false;
252 }
253 if (callback_stopped) return true;
254 }
255 }
256 return true;
257}
258
259} // anonymous namespace
260
262 const VolumeContext& vol,
263 uint32_t agno,
264 const InobtRecordCallback& on_record,
265 std::vector<std::string>& logs,
266 std::string& error) {
267 if (agno >= vol.ag.size()) {
268 error = "walk_inode_btree: agno out of range";
269 return false;
270 }
271 const auto& agctx = vol.ag[agno];
272 if (!agctx.agi_loaded) {
273 std::ostringstream m;
274 m << "XFS IBT (AG " << agno << "): AGI not loaded, skipping";
275 logs.push_back(m.str());
276 return true;
277 }
278 const uint32_t root = agctx.agi.root;
279 const uint32_t depth = agctx.agi.level;
280
281 if (root == 0 || root == NULLAGBLOCK) {
282 std::ostringstream m;
283 m << "XFS IBT (AG " << agno << "): empty (root=" << root << ")";
284 logs.push_back(m.str());
285 return true;
286 }
287 if (depth > 16) {
288 std::ostringstream m;
289 m << "XFS IBT (AG " << agno << "): implausible depth " << depth;
290 logs.push_back(m.str());
291 return true;
292 }
293
294 std::set<uint32_t> visited;
295 size_t nodes_visited = 0;
296 bool callback_stopped = false;
297 return walk_ibt_block(reader, vol, agno, root, depth,
299 on_record, visited, nodes_visited,
300 logs, error, callback_stopped);
301}
302
304 const VolumeContext& vol,
305 const InobtRecordCallback& on_record,
306 std::vector<std::string>& logs) {
307 bool any_error = false;
308 for (uint32_t agno = 0; agno < vol.sb.agcount && agno < vol.ag.size(); ++agno) {
309 std::string err;
310 if (!walk_inode_btree(reader, vol, agno, on_record, logs, err)) {
311 any_error = true;
312 logs.push_back("XFS IBT (AG " + std::to_string(agno) + "): "
313 + err);
314 }
315 }
316 return !any_error;
317}
318
319// =============================================================================
320// FINOBT walker (V5 only)
321// =============================================================================
322
324 const VolumeContext& vol,
325 uint32_t agno,
326 const InobtRecordCallback& on_record,
327 std::vector<std::string>& logs,
328 std::string& error) {
329 if (agno >= vol.ag.size()) {
330 error = "walk_free_inode_btree: agno out of range";
331 return false;
332 }
333 const auto& agctx = vol.ag[agno];
334 if (!agctx.agi_loaded) {
335 std::ostringstream m;
336 m << "XFS FINOBT (AG " << agno << "): AGI not loaded, skipping";
337 logs.push_back(m.str());
338 return true;
339 }
340 if (!vol.sb.is_v5() ||
342 // No FINOBT on this volume.
343 return true;
344 }
345
346 const uint32_t root = agctx.agi.free_root;
347 const uint32_t depth = agctx.agi.free_level;
348
349 if (root == 0 || root == NULLAGBLOCK) {
350 std::ostringstream m;
351 m << "XFS FINOBT (AG " << agno << "): empty (root=" << root << ")";
352 logs.push_back(m.str());
353 return true;
354 }
355 if (depth > 16) {
356 std::ostringstream m;
357 m << "XFS FINOBT (AG " << agno << "): implausible depth " << depth;
358 logs.push_back(m.str());
359 return true;
360 }
361
362 std::set<uint32_t> visited;
363 size_t nodes_visited = 0;
364 bool callback_stopped = false;
365 return walk_ibt_block(reader, vol, agno, root, depth,
367 on_record, visited, nodes_visited,
368 logs, error, callback_stopped);
369}
370
372 const VolumeContext& vol,
373 const InobtRecordCallback& on_record,
374 std::vector<std::string>& logs) {
375 if (!vol.sb.is_v5() ||
377 // V4 or feature missing -- nothing to do.
378 return true;
379 }
380 bool any_error = false;
381 for (uint32_t agno = 0; agno < vol.sb.agcount && agno < vol.ag.size(); ++agno) {
382 std::string err;
383 if (!walk_free_inode_btree(reader, vol, agno, on_record, logs, err)) {
384 any_error = true;
385 logs.push_back("XFS FINOBT (AG " + std::to_string(agno) + "): " + err);
386 }
387 }
388 return !any_error;
389}
390
391void expand_chunk(const InobtRec& rec, uint32_t agno,
392 const VolumeContext& vol,
393 std::vector<AllocatedInode>& out) {
394 // The chunk starts at AG-relative inode number `rec.startino`.
395 // Convert that to an AG-relative block number for the chunk start:
396 // chunk_agbno = rec.startino / inopblock
397 // in-block-offset of inode i = (rec.startino + i) % inopblock
398 const uint32_t inopblock = vol.sb.inopblock;
399 const uint32_t inode_size = vol.sb.inodesize;
400 if (inopblock == 0 || inode_size == 0) return;
401
402 const uint32_t chunk_count = std::min<uint32_t>(rec.count, 64);
403 for (uint32_t i = 0; i < chunk_count; ++i) {
404 // Sparse: skip holes (16-bit holemask in 4-inode granularity:
405 // each bit covers 4 consecutive inode positions)
406 if (rec.sparse) {
407 const uint32_t hole_bit = i / 4;
408 if (rec.holemask & (1u << hole_bit)) continue;
409 }
410 // Skip inodes marked free
411 if (rec.freemask & (1ull << i)) continue;
412
413 const uint32_t agino = rec.startino + i;
414 const uint32_t chunk_agbno = agino / inopblock;
415 const uint32_t in_block_off = agino % inopblock;
416
418 ai.agno = agno;
419 ai.agbno = chunk_agbno;
420 ai.in_chunk_index = i;
421 // Reconstruct global 64-bit inode number
422 ai.ino = (static_cast<uint64_t>(agno) <<
423 (vol.sb.agblklog + vol.sb.inopblog))
424 | static_cast<uint64_t>(agino);
425 ai.disk_offset = ag_block_to_disk_offset(vol, agno, chunk_agbno)
426 + static_cast<uint64_t>(in_block_off) * inode_size;
427 out.push_back(ai);
428 }
429}
430
431// Mirror of expand_chunk(), but emits inodes whose freemask bit IS set
432// (= free / recently deleted). Skips holes (sparse). Used by FINOBT
433// recovery -- each entry points to a dinode slot that may still hold
434// recoverable content from a deleted file, until XFS reuses the slot.
435void expand_chunk_free(const InobtRec& rec, uint32_t agno,
436 const VolumeContext& vol,
437 std::vector<AllocatedInode>& out) {
438 const uint32_t inopblock = vol.sb.inopblock;
439 const uint32_t inode_size = vol.sb.inodesize;
440 if (inopblock == 0 || inode_size == 0) return;
441
442 const uint32_t chunk_count = std::min<uint32_t>(rec.count, 64);
443 for (uint32_t i = 0; i < chunk_count; ++i) {
444 // Sparse: skip holes -- a hole means there's no real inode slot
445 // at this position, so there's nothing to recover.
446 if (rec.sparse) {
447 const uint32_t hole_bit = i / 4;
448 if (rec.holemask & (1u << hole_bit)) continue;
449 }
450 // Emit ONLY free slots (the opposite of expand_chunk).
451 if (!(rec.freemask & (1ull << i))) continue;
452
453 const uint32_t agino = rec.startino + i;
454 const uint32_t chunk_agbno = agino / inopblock;
455 const uint32_t in_block_off = agino % inopblock;
456
457 AllocatedInode ai; // re-using the type, but it's a "free" slot here
458 ai.agno = agno;
459 ai.agbno = chunk_agbno;
460 ai.in_chunk_index = i;
461 ai.ino = (static_cast<uint64_t>(agno) <<
462 (vol.sb.agblklog + vol.sb.inopblog))
463 | static_cast<uint64_t>(agino);
464 ai.disk_offset = ag_block_to_disk_offset(vol, agno, chunk_agbno)
465 + static_cast<uint64_t>(in_block_off) * inode_size;
466 out.push_back(ai);
467 }
468}
469
470// =============================================================================
471// BMBT (long-form) walker -- session 4
472// =============================================================================
473
474bool parse_btree_long_header(const unsigned char* buf,
475 uint32_t expected_magic_v4,
476 uint32_t expected_magic_v5,
477 BtreeLongHeader& out) {
478 if (buf == nullptr) return false;
479 out = {};
480 out.magic = be32(buf + 0x00);
481 if (out.magic == expected_magic_v5) out.is_v5_header = true;
482 else if (out.magic == expected_magic_v4) out.is_v5_header = false;
483 else return false;
484
485 out.level = be16(buf + 0x04);
486 out.numrecs = be16(buf + 0x06);
487 out.leftsib = be64(buf + 0x08);
488 out.rightsib = be64(buf + 0x10);
489
490 if (out.is_v5_header) {
491 out.blkno = be64(buf + 0x18);
492 out.lsn = be64(buf + 0x20);
493 // uuid at +0x28..+0x38 ignored
494 out.owner = be64(buf + 0x38);
495 out.crc = be32(buf + 0x40);
496 // pad at +0x44 ignored
497 }
498 if (out.level > 32) return false; // nonsense depth
499 return true;
500}
501
502namespace {
503
504// We need to forward-declare Extent's existence; the actual decoder
505// lives in xfs_inode.cpp. To keep xfs_btree.cpp self-contained for
506// header parsing, we implement a local 16-byte BMBT record decoder.
507//
508// This MUST stay byte-identical to xfs::decode_extent_record in
509// xfs_inode.cpp -- both decode the same on-disk format.
511 uint64_t logical_off_blocks {0};
512 uint64_t physical_block {0};
513 uint32_t length_blocks {0};
514 bool unwritten {false};
515};
516
517void decode_bmbt_record_local(const unsigned char* buf16, LocalBmbtRec& r) {
518 const uint64_t w0 = be64(buf16 + 0);
519 const uint64_t w1 = be64(buf16 + 8);
520 r.unwritten = ((w0 >> 63) & 1ull) != 0;
521 r.logical_off_blocks = (w0 >> 9) & ((1ull << 54) - 1ull);
522 const uint64_t hi9 = w0 & 0x1FFull;
523 const uint64_t lo43 = (w1 >> 21) & ((1ull << 43) - 1ull);
524 r.physical_block = (hi9 << 43) | lo43;
525 r.length_blocks = static_cast<uint32_t>(w1 & ((1ull << 21) - 1ull));
526}
527
528uint64_t fsb_to_byte_offset_local(uint64_t fsb, const VolumeContext& vol) {
529 if (vol.sb.agblklog == 0) return vol.volume_offset + fsb * vol.sb.blocksize;
530 const uint64_t mask = (1ull << vol.sb.agblklog) - 1ull;
531 const uint32_t agno = static_cast<uint32_t>(fsb >> vol.sb.agblklog);
532 const uint32_t agbno = static_cast<uint32_t>(fsb & mask);
533 return vol.volume_offset
534 + static_cast<uint64_t>(agno) * vol.sb.agblocks * vol.sb.blocksize
535 + static_cast<uint64_t>(agbno) * vol.sb.blocksize;
536}
537
538// Append a LocalBmbtRec to the user's `out` vector via the public
539// xfs::Extent type. We do this by writing the four fields directly --
540// xfs::Extent has the same logical/physical/length/unwritten fields,
541// but the type itself is forward-declared in this TU. To avoid
542// pulling xfs_inode.hpp in, we let xfs_recovery.cpp / callers convert
543// from LocalBmbtRec -- but in practice the pattern is so simple that
544// we just memcpy via a struct-shaped buffer.
545//
546// To keep things truly clean: this function is implemented in the .cpp
547// where xfs::Extent is FULLY known, by including xfs_inode.hpp once.
548//
549// (We do that via a separate trampoline included from the integration
550// site below.)
551
552} // anonymous namespace
553
554} // namespace xfs
555
556// We need xfs::Extent fully visible for the next function. Including
557// the inode header here (only in the .cpp) is the cleanest way without
558// creating a circular header chain.
559#include "xfs_inode.hpp"
560
561namespace xfs {
562
563namespace {
564
565void push_extent(std::vector<Extent>& out, const LocalBmbtRec& r) {
566 Extent e;
567 e.logical_off_blocks = r.logical_off_blocks;
568 e.physical_block = r.physical_block;
569 e.length_blocks = r.length_blocks;
570 e.unwritten = r.unwritten;
571 out.push_back(e);
572}
573
574// Recursively walk a BMBT subtree rooted at FSB `child_fsb` with
575// expected level `expected_level`. Per-branch errors get logged but
576// don't abort sibling branches (balanced policy).
577bool walk_bmbt_block(DiskReader& reader, const VolumeContext& vol,
578 uint64_t child_fsb, uint32_t expected_level,
579 bool is_v5,
580 std::vector<Extent>& out,
581 std::vector<std::string>& logs,
582 uint32_t max_extents,
583 std::set<uint64_t>& visited,
584 size_t& nodes_visited) {
585 if (out.size() >= max_extents) return true;
586 if (child_fsb == NULLFSBLOCK || child_fsb == 0) return true;
587 if (++nodes_visited > 1'000'000) {
588 logs.push_back("XFS BMBT: traversal cap reached (1M nodes), stopping");
589 return false;
590 }
591 if (!visited.insert(child_fsb).second) {
592 logs.push_back("XFS BMBT: cycle at FSB " + std::to_string(child_fsb)
593 + " (skipped)");
594 return true;
595 }
596
597 const uint64_t off = fsb_to_byte_offset_local(child_fsb, vol);
598 std::vector<unsigned char> blk(vol.sb.blocksize, 0);
599 if (!reader.read_at(off, blk.data(), blk.size())) {
600 logs.push_back("XFS BMBT: read failed at FSB " + std::to_string(child_fsb));
601 return true;
602 }
603
604 BtreeLongHeader hdr;
605 if (!parse_btree_long_header(blk.data(),
608 hdr)) {
609 logs.push_back("XFS BMBT: bad magic at FSB " + std::to_string(child_fsb));
610 return true;
611 }
612 if (hdr.level != expected_level) {
613 logs.push_back("XFS BMBT: level mismatch at FSB "
614 + std::to_string(child_fsb)
615 + " (got " + std::to_string(hdr.level)
616 + ", expected " + std::to_string(expected_level) + ")");
617 // Continue anyway -- damaged trees can still yield records
618 }
619
620 const uint32_t hsize = btree_long_header_size(hdr.is_v5_header);
621
622 if (hdr.level == 0) {
623 // Leaf: numrecs records of 16 bytes each starting at hsize
624 const uint32_t maxrecs_leaf =
625 (vol.sb.blocksize - hsize) / BMBT_REC_SIZE;
626 const uint32_t n =
627 std::min<uint32_t>(hdr.numrecs, maxrecs_leaf);
628 for (uint32_t i = 0; i < n; ++i) {
629 if (out.size() >= max_extents) return true;
630 LocalBmbtRec r;
631 decode_bmbt_record_local(blk.data() + hsize + i * BMBT_REC_SIZE, r);
632 if (r.length_blocks == 0) continue; // tolerate
633 push_extent(out, r);
634 }
635 } else {
636 // Internal: numrecs keys followed by maxrecs_internal keys (XFS
637 // reserves space for max). Pointers start at hsize + maxrecs *
638 // KEY_SIZE.
639 const uint32_t key_ptr_pair =
641 const uint32_t maxrecs_internal =
642 (vol.sb.blocksize - hsize) / key_ptr_pair;
643 const uint32_t n =
644 std::min<uint32_t>(hdr.numrecs, maxrecs_internal);
645 const uint32_t ptrs_offset =
646 hsize + maxrecs_internal * BMBT_KEY_SIZE_LONG;
647 for (uint32_t i = 0; i < n; ++i) {
648 const uint32_t poff = ptrs_offset + i * BMBT_PTR_SIZE_LONG;
649 if (poff + BMBT_PTR_SIZE_LONG > vol.sb.blocksize) break;
650 const uint64_t cf = be64(blk.data() + poff);
651 if (!walk_bmbt_block(reader, vol, cf, hdr.level - 1, is_v5,
652 out, logs, max_extents, visited,
653 nodes_visited)) {
654 return false;
655 }
656 }
657 }
658 return true;
659}
660
661} // anonymous namespace
662
664 const VolumeContext& vol,
665 const unsigned char* root_buf,
666 uint32_t root_len,
667 bool is_v5_volume,
668 uint32_t max_extents,
669 std::vector<Extent>& out,
670 std::vector<std::string>& logs,
671 std::string& error) {
672 // bmdr "root" header in the data fork: 2 bytes level + 2 bytes numrecs
673 // followed by numrecs keys (8 each) and numrecs pointers (8 each).
674 // No magic, no CRC. The compact root is what fits inside the dinode
675 // and points to the first level of REAL on-disk BMBT blocks.
676 if (root_buf == nullptr || root_len < 4) {
677 error = "BMBT: data fork too small for bmdr root";
678 return false;
679 }
680 const uint16_t level = be16(root_buf + 0);
681 const uint16_t numrecs = be16(root_buf + 2);
682 if (level == 0) {
683 // No internal levels -- the "root" is itself a leaf. In bmdr, leaf
684 // records are 16-byte BMBT records.
685 if (4u + static_cast<uint32_t>(numrecs) * 16u > root_len) {
686 error = "BMBT: bmdr leaf records overflow data fork";
687 return false;
688 }
689 for (uint16_t i = 0; i < numrecs; ++i) {
690 LocalBmbtRec r;
691 decode_bmbt_record_local(root_buf + 4 + i * 16, r);
692 if (r.length_blocks == 0) continue;
693 push_extent(out, r);
694 if (out.size() >= max_extents) return true;
695 }
696 return true;
697 }
698
699 // Internal root: layout is keys[numrecs] then ptrs[numrecs] (in
700 // bmdr, no maxrecs reservation -- it's compact).
701 if (4u + static_cast<uint32_t>(numrecs) * 16u > root_len) {
702 error = "BMBT: bmdr root records overflow data fork";
703 return false;
704 }
705 std::set<uint64_t> visited;
706 size_t nodes_visited = 0;
707 const uint32_t ptrs_off = 4u + numrecs * 8u; // keys are 8 each
708 for (uint16_t i = 0; i < numrecs; ++i) {
709 const uint64_t child_fsb = be64(root_buf + ptrs_off + i * 8u);
710 if (!walk_bmbt_block(reader, vol, child_fsb, level - 1,
711 is_v5_volume, out, logs, max_extents,
712 visited, nodes_visited)) {
713 return false;
714 }
715 if (out.size() >= max_extents) {
716 logs.push_back("XFS BMBT: extent cap reached ("
717 + std::to_string(max_extents) + ")");
718 break;
719 }
720 }
721 return true;
722}
723
724} // namespace xfs
bool read_at(uint64_t offset, void *buffer, size_t len)
void push_extent(std::vector< Extent > &out, const LocalBmbtRec &r)
uint64_t fsb_to_byte_offset_local(uint64_t fsb, const VolumeContext &vol)
uint64_t ag_block_to_disk_offset(const VolumeContext &vol, uint32_t agno, uint32_t agbno)
Definition xfs_btree.cpp:59
void decode_bmbt_record_local(const unsigned char *buf16, LocalBmbtRec &r)
bool walk_ibt_block(DiskReader &reader, const VolumeContext &vol, uint32_t agno, uint32_t agbno, uint32_t expected_level, uint32_t magic_v4, uint32_t magic_v5, const char *tree_label, const InobtRecordCallback &on_record, std::set< uint32_t > &visited, size_t &nodes_visited, std::vector< std::string > &logs, std::string &error, bool &callback_stopped)
uint32_t ibt_maxrecs(uint32_t blocksize, uint32_t header_size, uint32_t level)
Definition xfs_btree.cpp:52
bool walk_bmbt_block(DiskReader &reader, const VolumeContext &vol, uint64_t child_fsb, uint32_t expected_level, bool is_v5, std::vector< Extent > &out, std::vector< std::string > &logs, uint32_t max_extents, std::set< uint64_t > &visited, size_t &nodes_visited)
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
uint16_t be16(const unsigned char *p)
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 XFS_FIBT_MAGIC_V5
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)
uint64_t be64(const unsigned char *p)
constexpr uint32_t BMBT_REC_SIZE
uint32_t be32(const unsigned char *p)
constexpr uint32_t XFS_FIBT_MAGIC_V4
constexpr uint32_t XFS_SB_FEAT_INCOMPAT_SPINODES
constexpr uint32_t XFS_SB_FEAT_RO_COMPAT_FINOBT
std::function< bool(const InobtRec &rec, uint32_t agno)> InobtRecordCallback
constexpr uint32_t XFS_IBT_MAGIC_V5
constexpr uint32_t XFS_IBT_MAGIC_V4
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
uint64_t physical_block
uint64_t logical_off_blocks
uint32_t length_blocks
uint8_t freecount
uint16_t holemask
uint32_t startino
uint64_t freemask
uint32_t features_incompat
uint32_t features_ro_compat
bool is_v5() const
std::vector< AGContext > ag