Skip to content

Commit d8c8afe

Browse files
committed
Merge #1731: fix(core): Fix checkpoint Drop stack overflow
2df5861 test(core): test `Drop` implementation of `CPInner` (valued mammal) 67e1dc3 fix(core): Fix checkpoint Drop stack overflow (LLFourn) Pull request description: Fixes #1634 This needs a test that demonstrates the issue is fixed. I was hoping @ValuedMammal could do that for me. ACKs for top commit: ValuedMammal: self-ACK 2df5861 notmandatory: ACK 2df5861 Tree-SHA512: a431cb93505cbde2a9287de09d5faac003b8dfa01342cd22c6ca197d70a73948f94f55dfa365cc06b5be36f78458ed34d4ef5fa8c9e5e2989a21c7ce5b55d9ca
2 parents d949fe4 + 2df5861 commit d8c8afe

File tree

2 files changed

+49
-2
lines changed

2 files changed

+49
-2
lines changed

crates/core/src/checkpoint.rs

+28
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,34 @@ struct CPInner {
2121
prev: Option<Arc<CPInner>>,
2222
}
2323

24+
/// When a `CPInner` is dropped we need to go back down the chain and manually remove any
25+
/// no-longer referenced checkpoints. Letting the default rust dropping mechanism handle this
26+
/// leads to recursive logic and stack overflows
27+
///
28+
/// https://github.com/bitcoindevkit/bdk/issues/1634
29+
impl Drop for CPInner {
30+
fn drop(&mut self) {
31+
// Take out `prev` so its `drop` won't be called when this drop is finished
32+
let mut current = self.prev.take();
33+
while let Some(arc_node) = current {
34+
// Get rid of the Arc around `prev` if we're the only one holding a ref
35+
// So the `drop` on it won't be called when the `Arc` is dropped.
36+
//
37+
// FIXME: When MSRV > 1.70.0 this should use Arc::into_inner which actually guarantees
38+
// that no recursive drop calls can happen even with multiple threads.
39+
match Arc::try_unwrap(arc_node).ok() {
40+
Some(mut node) => {
41+
// Keep going backwards
42+
current = node.prev.take();
43+
// Don't call `drop` on `CPInner` since that risks it becoming recursive.
44+
core::mem::forget(node);
45+
}
46+
None => break,
47+
}
48+
}
49+
}
50+
}
51+
2452
impl PartialEq for CheckPoint {
2553
fn eq(&self, other: &Self) -> bool {
2654
let self_cps = self.iter().map(|cp| cp.block_id());

crates/core/tests/test_checkpoint.rs

+21-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use bdk_core::CheckPoint;
2-
use bdk_testenv::block_id;
1+
use bdk_core::{BlockId, CheckPoint};
2+
use bdk_testenv::{block_id, hash};
33

44
/// Inserting a block that already exists in the checkpoint chain must always succeed.
55
#[test]
@@ -32,3 +32,22 @@ fn checkpoint_insert_existing() {
3232
}
3333
}
3434
}
35+
36+
#[test]
37+
fn checkpoint_destruction_is_sound() {
38+
// Prior to the fix in https://github.com/bitcoindevkit/bdk/pull/1731
39+
// this could have caused a stack overflow due to drop recursion in Arc.
40+
// We test that a long linked list can clean itself up without blowing
41+
// out the stack.
42+
let mut cp = CheckPoint::new(BlockId {
43+
height: 0,
44+
hash: hash!("g"),
45+
});
46+
let end = 10_000;
47+
for height in 1u32..end {
48+
let hash = bitcoin::hashes::Hash::hash(height.to_be_bytes().as_slice());
49+
let block = BlockId { height, hash };
50+
cp = cp.push(block).unwrap();
51+
}
52+
assert_eq!(cp.iter().count() as u32, end);
53+
}

0 commit comments

Comments
 (0)