Skip to content

Commit b966ea3

Browse files
committed
Safe verified↔chain cascade: deferred destruction prevents UAF
Python p2pool pattern (node.py:355-398): shares must be removed from verified BEFORE chain frees share data, because verified borrows pointers from chain. ShareChain::trim() now accepts deferred_destroy vector — share variants are moved out instead of freed, allowing cascade to verified.remove() before destruction. ShareChain::remove() gets owns_data parameter. Sequence: trim chain (defer destroy) → cascade to verified → destroy.
1 parent 7159bc3 commit b966ea3

2 files changed

Lines changed: 73 additions & 16 deletions

File tree

src/impl/ltc/node.cpp

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -729,26 +729,72 @@ void NodeImpl::run_think()
729729
[this, result = std::move(result)]()
730730
{
731731
// Trim stale shares (on ioc thread — safe with phase2)
732+
// Python p2pool pattern (node.py:355-398):
733+
// 1. Collect hashes to evict from chain (dry run)
734+
// 2. Remove from verified FIRST (borrows share data from chain)
735+
// 3. Remove from chain (frees share data)
736+
// This prevents use-after-free: verified must release its
737+
// borrowed references before chain destroys the share data.
732738
const size_t keep_per_head = PoolConfig::chain_length() * 2 + 10;
733739
std::vector<uint256> evicted_from_chain;
734-
auto trim_chain = [&](auto& sc, const char* label, bool owns_data = true,
735-
std::vector<uint256>* evicted = nullptr) {
736-
if (sc.size() <= keep_per_head)
737-
return;
738-
auto heads_copy = sc.get_heads();
740+
std::vector<ltc::ShareType> deferred_shares;
741+
742+
// Step 1: Trim chain, deferring share destruction
743+
// Share data is moved to deferred_shares instead of being freed,
744+
// because verified may still hold borrowed references to it.
745+
if (m_tracker.chain.size() > keep_per_head)
746+
{
747+
auto heads_copy = m_tracker.chain.get_heads();
739748
size_t total_removed = 0;
740749
for (auto& [head_hash, tail_hash] : heads_copy) {
741-
auto removed = sc.trim(head_hash, keep_per_head, owns_data, evicted);
750+
auto removed = m_tracker.chain.trim(head_hash, keep_per_head,
751+
/*owns_data=*/true, &evicted_from_chain, &deferred_shares);
742752
total_removed += removed;
743753
}
744754
if (total_removed > 0)
745-
LOG_INFO << "[Pool] Trimmed " << total_removed << " old shares from " << label
746-
<< " (now " << sc.size() << ")";
747-
};
748-
trim_chain(m_tracker.verified, "verified", /*owns_data=*/false);
749-
trim_chain(m_tracker.chain, "chain", /*owns_data=*/true, &evicted_from_chain);
755+
LOG_INFO << "[Pool] Trimmed " << total_removed
756+
<< " old shares from chain (now " << m_tracker.chain.size() << ")";
757+
}
758+
759+
// Step 2: Cascade — remove evicted shares from verified BEFORE
760+
// destroying share data (Python p2pool pattern: node.py:396-398)
761+
if (!evicted_from_chain.empty())
762+
{
763+
size_t cascade_removed = 0;
764+
for (const auto& h : evicted_from_chain)
765+
{
766+
if (m_tracker.verified.contains(h))
767+
{
768+
m_tracker.verified.remove(h, /*owns_data=*/false);
769+
++cascade_removed;
770+
}
771+
}
772+
if (cascade_removed > 0)
773+
LOG_INFO << "[Pool] Cascaded " << cascade_removed
774+
<< " evictions to verified (now " << m_tracker.verified.size() << ")";
775+
}
776+
777+
// Step 3: Now safe to destroy deferred share data
778+
for (auto& share : deferred_shares)
779+
share.destroy();
780+
deferred_shares.clear();
781+
782+
// Step 4: Also trim verified independently (may have excess)
783+
if (m_tracker.verified.size() > keep_per_head)
784+
{
785+
auto heads_copy = m_tracker.verified.get_heads();
786+
size_t total_removed = 0;
787+
for (auto& [head_hash, tail_hash] : heads_copy) {
788+
auto removed = m_tracker.verified.trim(head_hash, keep_per_head,
789+
/*owns_data=*/false);
790+
total_removed += removed;
791+
}
792+
if (total_removed > 0)
793+
LOG_INFO << "[Pool] Trimmed " << total_removed
794+
<< " old shares from verified (now " << m_tracker.verified.size() << ")";
795+
}
750796

751-
// Prune evicted shares from LevelDB
797+
// Step 5: Prune evicted shares from LevelDB
752798
if (!evicted_from_chain.empty() && m_storage && m_storage->is_available())
753799
{
754800
size_t pruned = 0;

src/sharechain/sharechain.hpp

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -427,8 +427,11 @@ class ShareChain
427427
/// destroyed. Set to false for chains that borrow share
428428
/// pointers from another chain (e.g. verified borrows from chain).
429429
/// @param evicted_hashes If non-null, hashes of removed shares are appended here.
430+
/// @param deferred_destroy If non-null, share variants are moved here instead of
431+
/// destroyed. Caller must call destroy() on each after cascade cleanup.
430432
size_t trim(const hash_t& head, size_t max_size, bool owns_data = true,
431-
std::vector<hash_t>* evicted_hashes = nullptr)
433+
std::vector<hash_t>* evicted_hashes = nullptr,
434+
std::vector<share_t>* deferred_destroy = nullptr)
432435
{
433436
if (!m_shares.contains(head) || max_size == 0)
434437
return 0;
@@ -489,8 +492,15 @@ class ShareChain
489492
auto it = m_shares.find(h);
490493
if (it != m_shares.end())
491494
{
492-
if (owns_data)
495+
if (deferred_destroy && owns_data)
496+
{
497+
// Move share variant out for deferred destruction
498+
deferred_destroy->push_back(std::move(it->second.share));
499+
}
500+
else if (owns_data)
501+
{
493502
it->second.share.destroy();
503+
}
494504
delete it->second.index;
495505
m_shares.erase(it);
496506
}
@@ -509,7 +519,7 @@ class ShareChain
509519
/// If the share is mid-chain, the chain is split: children above it
510520
/// become a separate fork whose tail now points past the removed share.
511521
/// Returns true if the share was found and removed.
512-
bool remove(const hash_t& hash)
522+
bool remove(const hash_t& hash, bool owns_data = true)
513523
{
514524
auto it = m_shares.find(hash);
515525
if (it == m_shares.end())
@@ -625,7 +635,8 @@ class ShareChain
625635
}
626636

627637
// Free the share object and index, then remove from m_shares
628-
it->second.share.destroy();
638+
if (owns_data)
639+
it->second.share.destroy();
629640
delete idx;
630641
m_shares.erase(it);
631642
return true;

0 commit comments

Comments
 (0)