java-topology/defects/ceph/patch/ceph-0002-bluestore-unshared-blobs-hashset.md
russell@unturf.com 25c2bafdee undf: assign 694-720; stamp patches; ruby-0003/elixir-0002/r-source-0002/victoria-metrics-0002
New UNDF assignments (693→720):
  elixir-0002 → UNDF-2026-000000698 (typespec used_type_pairs O(T²))
  r-source-0002 → UNDF-2026-000000711 (.walkClassGraph match dedup O(S²))
  ruby-0003 → UNDF-2026-000000712 (RubyGems dependent_gems O(N²×D))
  victoria-metrics-0002 → UNDF-2026-000000717 (MetricName tag-filter O(T×I))

Total: 720 UNDF assigned
2026-03-29 22:28:31 -04:00

4.4 KiB
Raw Blame History

UNDF: UNDF-2026-000000694

UNDF: (pending)

ceph-0002: BlueStore::_do_remove — O(E×U) std::find over unshared_blobs vector

CWE-407 — Algorithmic Complexity

Field Value
ID ceph-0002
Severity MEDIUM
Ecosystem Ceph
File src/os/bluestore/BlueStore.cc
Lines 1809918115 (function BlueStore::_do_remove)
Complexity O(E×U) where E = extents per object, U = unshared blobs
Hot path Object deletion — every rados rm, RGW object delete, CephFS unlink

Defect

In BlueStore::_do_remove(), after computing which shared blobs can be "unshared" (because the object being deleted was the last reference), the code iterates the object's full extent map and for each extent calls std::find over the unshared_blobs vector to check membership:

vector<SharedBlob*> unshared_blobs;
unshared_blobs.reserve(maybe_unshared_blobs.size());
for (auto& p : expect) {
    if (p.first->persistent->ref_map == p.second) {
        SharedBlob *sb = p.first;
        unshared_blobs.push_back(sb);           // U entries
        ...
    }
}

if (unshared_blobs.empty()) {
    return 0;
}

// Scan every extent in the object to clear just-unshared blobs
for (auto& e : h->extent_map.extent_map) {          // O(E) loop
    const bluestore_blob_t& b = e.blob->get_blob();
    SharedBlob *sb = e.blob->get_shared_blob().get();
    if (b.is_shared() &&
        std::find(unshared_blobs.begin(), unshared_blobs.end(),  // O(U) scan
                  sb) != unshared_blobs.end()) {
        bluestore_blob_t& blob = e.blob->dirty_blob();
        blob.clear_flag(bluestore_blob_t::FLAG_SHARED);
        ...
    }
}

unshared_blobs is a vector<SharedBlob*> and membership is tested with std::find — O(U) per extent. Total cost: O(E × U).

Typical sizes:

  • A 1 GiB RGW object with 4 KiB blocks: E ≈ 262,144 extents
  • A highly-shared object with many clones: U can reach tens of thousands
  • Product: billions of pointer comparisons per delete

Even for moderate objects (E=10,000, U=100): 1,000,000 comparisons that could be 100 with a hash set.

Fix

Replace vector<SharedBlob*> unshared_blobs with unordered_set<SharedBlob*> unshared_set for O(1) lookup:

// CWE-407 fix: use an unordered_set for O(1) membership test
std::unordered_set<SharedBlob*> unshared_set;
unshared_set.reserve(maybe_unshared_blobs.size());

for (auto& p : expect) {
    dout(20) << " ? " << *p.first << " vs " << p.second << dendl;
    if (p.first->persistent->ref_map == p.second) {
        SharedBlob *sb = p.first;
        dout(20) << __func__ << "  unsharing " << *sb << dendl;
        unshared_set.insert(sb);
        txc->unshare_blob(sb);
        uint64_t sbid = c->make_blob_unshared(sb);
        string key;
        get_shared_blob_key(sbid, &key);
        txc->t->rmkey(PREFIX_SHARED_BLOB, key);
    }
}

if (unshared_set.empty()) {
    return 0;
}

// And now a run through .head extents to clear up freshly unshared blobs.
for (auto& e : h->extent_map.extent_map) {
    const bluestore_blob_t& b = e.blob->get_blob();
    SharedBlob *sb = e.blob->get_shared_blob().get();
    if (b.is_shared() &&
        unshared_set.count(sb)) {                            // O(1) lookup
        dout(20) << __func__ << "  unsharing " << e << dendl;
        bluestore_blob_t& blob = e.blob->dirty_blob();
        blob.clear_flag(bluestore_blob_t::FLAG_SHARED);
        e.blob->get_dirty_shared_blob() = nullptr;
        h->extent_map.dirty_range(e.logical_offset, e.length);
    }
}
txc->write_onode(h);

return 0;

Required include: #include <unordered_set> (already available via #include "include/ceph_hash.h" or add directly).

Speedup

Extents (E) Unshared (U) Before After Ratio
1,000 10 10,000 1,000 10×
10,000 100 1,000,000 10,000 100×
262,144 1,000 262,144,000 262,144 1,000×

At E=262,144 (1 GiB object, 4 KiB blocks) and U=1,000 shared blobs the speedup is ~1,000× — the delete operation goes from O(billions of pointer comparisons) to O(262,144 hash lookups).

Notes

The maybe_unshared_blobs source set at line 18010 is already a set<SharedBlob*> (ordered), so it inherently avoids duplicates. The subsequent unshared_blobs vector was needlessly downgraded to linear-scan membership. Converting to unordered_set restores O(1) lookup without any correctness risk — SharedBlob* pointer equality is the intended comparison.