52uint32_t
ibt_maxrecs(uint32_t blocksize, uint32_t header_size, uint32_t level) {
53 const uint32_t avail = blocksize - header_size;
60 uint32_t agno, uint32_t agbno) {
73 uint32_t expected_magic_v4,
74 uint32_t expected_magic_v5,
76 if (buf ==
nullptr)
return false;
81 if (out.
magic == expected_magic_v5) {
83 }
else if (out.
magic == expected_magic_v4) {
103 if (out.
level > 16)
return false;
114 out.
count = buf[0x06];
120 const uint32_t freec32 =
be32(buf + 0x04);
121 out.
freecount =
static_cast<uint8_t
>(std::min<uint32_t>(freec32, 64));
138 uint32_t expected_level,
141 const char* tree_label,
143 std::set<uint32_t>& visited,
144 size_t& nodes_visited,
145 std::vector<std::string>& logs,
147 bool& callback_stopped) {
148 if (callback_stopped)
return true;
151 std::ostringstream m;
152 m <<
"XFS " << tree_label <<
" (AG " << agno <<
"): invalid child pointer " << agbno;
153 logs.push_back(m.str());
158 error = std::string(
"XFS ") + tree_label
159 +
": traversal exceeded safety cap (possible loop)";
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());
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());
183 std::ostringstream m;
184 m <<
"XFS " << tree_label <<
" (AG " << agno
185 <<
"): bad magic at block " << agbno;
186 logs.push_back(m.str());
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());
199 const bool is_v5 = vol.
sb.
is_v5();
200 const bool is_v5_sparse =
203 if (hdr.
level == 0) {
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());
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 =
220 if (!on_record(rec, agno)) {
221 callback_stopped =
true;
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());
237 const uint32_t n = std::min<uint32_t>(hdr.
numrecs, maxrecs);
240 const uint32_t ptrs_offset = header_size + maxrecs *
IBT_KEY_SIZE;
242 for (uint32_t i = 0; i < n; ++i) {
243 const uint32_t ptr_off = ptrs_offset + i *
IBT_PTR_SIZE;
245 const uint32_t child =
be32(block.data() + ptr_off);
248 magic_v4, magic_v5, tree_label,
249 on_record, visited, nodes_visited,
250 logs, error, callback_stopped)) {
253 if (callback_stopped)
return true;
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";
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());
278 const uint32_t root = agctx.agi.root;
279 const uint32_t depth = agctx.agi.level;
282 std::ostringstream m;
283 m <<
"XFS IBT (AG " << agno <<
"): empty (root=" << root <<
")";
284 logs.push_back(m.str());
288 std::ostringstream m;
289 m <<
"XFS IBT (AG " << agno <<
"): implausible depth " << depth;
290 logs.push_back(m.str());
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);
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) {
312 logs.push_back(
"XFS IBT (AG " + std::to_string(agno) +
"): "
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";
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());
346 const uint32_t root = agctx.agi.free_root;
347 const uint32_t depth = agctx.agi.free_level;
350 std::ostringstream m;
351 m <<
"XFS FINOBT (AG " << agno <<
"): empty (root=" << root <<
")";
352 logs.push_back(m.str());
356 std::ostringstream m;
357 m <<
"XFS FINOBT (AG " << agno <<
"): implausible depth " << depth;
358 logs.push_back(m.str());
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);
374 std::vector<std::string>& logs) {
380 bool any_error =
false;
381 for (uint32_t agno = 0; agno < vol.
sb.
agcount && agno < vol.
ag.size(); ++agno) {
385 logs.push_back(
"XFS FINOBT (AG " + std::to_string(agno) +
"): " + err);
393 std::vector<AllocatedInode>& out) {
400 if (inopblock == 0 || inode_size == 0)
return;
402 const uint32_t chunk_count = std::min<uint32_t>(rec.
count, 64);
403 for (uint32_t i = 0; i < chunk_count; ++i) {
407 const uint32_t hole_bit = i / 4;
408 if (rec.
holemask & (1u << hole_bit))
continue;
411 if (rec.
freemask & (1ull << i))
continue;
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;
419 ai.
agbno = chunk_agbno;
422 ai.
ino = (
static_cast<uint64_t
>(agno) <<
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;
437 std::vector<AllocatedInode>& out) {
440 if (inopblock == 0 || inode_size == 0)
return;
442 const uint32_t chunk_count = std::min<uint32_t>(rec.
count, 64);
443 for (uint32_t i = 0; i < chunk_count; ++i) {
447 const uint32_t hole_bit = i / 4;
448 if (rec.
holemask & (1u << hole_bit))
continue;
451 if (!(rec.
freemask & (1ull << i)))
continue;
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;
459 ai.
agbno = chunk_agbno;
461 ai.
ino = (
static_cast<uint64_t
>(agno) <<
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;
475 uint32_t expected_magic_v4,
476 uint32_t expected_magic_v5,
478 if (buf ==
nullptr)
return false;
498 if (out.
level > 32)
return false;
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));
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);
578 uint64_t child_fsb, uint32_t expected_level,
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");
591 if (!visited.insert(child_fsb).second) {
592 logs.push_back(
"XFS BMBT: cycle at FSB " + std::to_string(child_fsb)
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));
609 logs.push_back(
"XFS BMBT: bad magic at FSB " + std::to_string(child_fsb));
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) +
")");
622 if (hdr.
level == 0) {
624 const uint32_t maxrecs_leaf =
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;
632 if (r.length_blocks == 0)
continue;
639 const uint32_t key_ptr_pair =
641 const uint32_t maxrecs_internal =
644 std::min<uint32_t>(hdr.
numrecs, maxrecs_internal);
645 const uint32_t ptrs_offset =
647 for (uint32_t i = 0; i < n; ++i) {
650 const uint64_t cf =
be64(blk.data() + poff);
652 out, logs, max_extents, visited,
665 const unsigned char* root_buf,
668 uint32_t max_extents,
669 std::vector<Extent>& out,
670 std::vector<std::string>& logs,
671 std::string& error) {
676 if (root_buf ==
nullptr || root_len < 4) {
677 error =
"BMBT: data fork too small for bmdr root";
680 const uint16_t level =
be16(root_buf + 0);
681 const uint16_t numrecs =
be16(root_buf + 2);
685 if (4u +
static_cast<uint32_t
>(numrecs) * 16u > root_len) {
686 error =
"BMBT: bmdr leaf records overflow data fork";
689 for (uint16_t i = 0; i < numrecs; ++i) {
691 decode_bmbt_record_local(root_buf + 4 + i * 16, r);
692 if (r.length_blocks == 0)
continue;
694 if (out.size() >= max_extents)
return true;
701 if (4u +
static_cast<uint32_t
>(numrecs) * 16u > root_len) {
702 error =
"BMBT: bmdr root records overflow data fork";
705 std::set<uint64_t> visited;
706 size_t nodes_visited = 0;
707 const uint32_t ptrs_off = 4u + numrecs * 8u;
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)) {
715 if (out.size() >= max_extents) {
716 logs.push_back(
"XFS BMBT: extent cap reached ("
717 + std::to_string(max_extents) +
")");
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)
constexpr uint32_t IBT_KEY_SIZE
constexpr size_t MAX_NODES_PER_AG
constexpr uint32_t IBT_REC_SIZE
constexpr uint32_t IBT_PTR_SIZE
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)
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
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)
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
uint64_t logical_off_blocks
uint32_t features_incompat
uint32_t features_ro_compat
std::vector< AGContext > ag
uint64_t logical_off_blocks