From 3b31620358166460b83774e80c4efccb62599fde Mon Sep 17 00:00:00 2001 From: Wilco van Beijnum Date: Thu, 17 Sep 2026 14:36:02 +0200 Subject: [PATCH] scan/resolve: honor partition boundaries when a finding overruns them A finding's size comes from its own header, so a stale superblock left in unpartitioned space can claim an extent covering partitions written long after it. The scanner skipped that whole extent, so the superblocks of the partitions inside it were never validated: they went missing entirely rather than being demoted to also_matched. Clamp the skip-ahead at the next partition start, and stop resolve() from demoting a finding that begins exactly on a partition boundary. --- src/resolve.cpp | 12 +++++++++++- src/scan.cpp | 20 ++++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/resolve.cpp b/src/resolve.cpp index 2cf11c1..4643fba 100644 --- a/src/resolve.cpp +++ b/src/resolve.cpp @@ -27,6 +27,16 @@ std::vector resolve(std::vector candidates, size_t file_size, const std::set& coalesce_types) { std::sort(candidates.begin(), candidates.end(), less); + // A partition table is authoritative about the medium's layout, so a finding + // starting exactly on a partition boundary is a real region, not interior + // noise — even when an earlier finding's self-declared size overruns it + // (see scan.cpp). Otherwise a stale superblock recording a pre-repartition + // size demotes every partition it spans to a footnote on itself. + std::set part_starts; + for (const auto& f : candidates) + for (const auto& m : f.members) + if (m.offset != SIZE_MAX) part_starts.insert(m.offset); + std::vector kept; size_t owner_end = 0; // end of the span owned by the current owner int owner = -1; // index in `kept` of the owning region, or -1 @@ -38,7 +48,7 @@ std::vector resolve(std::vector candidates, size_t file_size, continue; } // Starts inside a region already owned by a confident, sized finding. - if (f.offset < owner_end) { + if (f.offset < owner_end && !part_starts.count(f.offset)) { if (owner >= 0) kept[owner].also_matched.push_back(demote(f)); continue; } diff --git a/src/scan.cpp b/src/scan.cpp index a4b98c2..803aae9 100644 --- a/src/scan.cpp +++ b/src/scan.cpp @@ -125,6 +125,13 @@ std::vector scan(const Reader& r, const std::vector& sigs) { // Scan with skip-ahead: once a confident, sized finding claims a region, // restart the automaton past it so we neither validate nor traverse its // interior (the compressed blocks inside a squashfs, etc.). + // That size is self-declared, though, and can overrun a region the partition + // table already accounts for (a stale superblock in unpartitioned space + // still recording its pre-repartition size). Skipping the whole extent would + // step over those partitions' superblocks without ever validating them, so + // they would be missed entirely rather than demoted. Clamp the jump at the + // next partition start. + std::set part_starts; // absolute partition offsets seen so far std::vector candidates; size_t next_scan = 0; while (next_scan < n) { @@ -138,9 +145,18 @@ std::vector scan(const Reader& r, const std::vector& sigs) { if (!f) return true; const bool owns_region = f->confidence >= static_cast(Confidence::Structural) && f->size > 0; - const size_t end = f->offset + f->size; + const size_t foff = f->offset; + const size_t end = foff + f->size; + for (const auto& m : f->members) + if (m.offset != SIZE_MAX) part_starts.insert(m.offset); candidates.push_back(std::move(*f)); - if (owns_region) { skip_to = end; return false; } + if (owns_region) { + size_t lim = end; + auto it = part_starts.upper_bound(foff); // first boundary after it + if (it != part_starts.end() && *it < lim) lim = *it; + skip_to = lim; // > foff >= next_scan, so the loop still advances + return false; + } return true; }); if (skip_to > next_scan)