Summary
Opening a chainstate manager with SetWipeDbs(true, true) against a previously-populated data directory leaves chainman.m_best_header null, so btck_chainstate_manager_get_best_entry returns a null btck_BlockTreeEntry* (assumed never null but not explicitly documented that way). Every entry accessor then reinterpret-casts the null and SEGVs.
Reproduction
Regression test added at repro-kernel-best-entry-null-after-wipe:
./build/bin/test_kernel --run_test=btck_get_best_entry_after_full_wipe_regression crashes with SIGSEGV. Fresh data dir with the same flags works and the bug is specifically a wipe over a previously-populated dir.
Stack trace
(lldb) breakpoint set --name __cxa_throw
Breakpoint 1: where = libc++abi.dylib`__cxa_throw, address = 0x00000001845fa040
(lldb) run --run_test=btck_get_best_entry_after_full_wipe_regression
Process 85763 launched: '/Users/csjones/Developer/bitcoin/build/bin/test_kernel' (arm64)
Running 1 test case...
Kernel warning was unset.
Process 85763 stopped
* thread [#1](/bitcoin-bitcoin/1/), queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame [#0](/bitcoin-bitcoin/0/): 0x00000001845fa040 libc++abi.dylib`__cxa_throw
libc++abi.dylib`__cxa_throw:
-> 0x1845fa040 <+0>: pacibsp
0x1845fa044 <+4>: stp x22, x21, [sp, #-0x30]!
0x1845fa048 <+8>: stp x20, x19, [sp, [#0](/bitcoin-bitcoin/0/)x10]
0x1845fa04c <+12>: stp x29, x30, [sp, [#0](/bitcoin-bitcoin/0/)x20]
Target 0: (test_kernel) stopped.
(lldb) bt
* thread [#1](/bitcoin-bitcoin/1/), queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame [#0](/bitcoin-bitcoin/0/): 0x00000001845fa040 libc++abi.dylib`__cxa_throw
frame [#1](/bitcoin-bitcoin/1/): 0x0000000100051e5c test_kernel`btck_BlockTreeEntry const* btck::check<btck_BlockTreeEntry const*>(ptr=0x0000000000000000) at bitcoinkernel_wrapper.h:187:9 [opt] [inlined]
frame [#2](/bitcoin-bitcoin/2/): 0x0000000100051e2c test_kernel`btck::View<btck_BlockTreeEntry>::View(this=<unavailable>, ptr=0x0000000000000000) at bitcoinkernel_wrapper.h:330:45 [opt] [inlined]
frame [#3](/bitcoin-bitcoin/3/): 0x0000000100051e2c test_kernel`btck::BlockTreeEntry::BlockTreeEntry(this=<unavailable>, entry=0x0000000000000000) at bitcoinkernel_wrapper.h:915:11 [opt] [inlined]
frame [#4](/bitcoin-bitcoin/4/): 0x0000000100051e2c test_kernel`btck::BlockTreeEntry::BlockTreeEntry(this=<unavailable>, entry=0x0000000000000000) at bitcoinkernel_wrapper.h:916:5 [opt] [inlined]
frame [#5](/bitcoin-bitcoin/5/): 0x0000000100051e2c test_kernel`btck::ChainMan::GetBestEntry(this=<unavailable>) const at bitcoinkernel_wrapper.h:1339:16 [opt] [inlined]
frame [#6](/bitcoin-bitcoin/6/): 0x0000000100051e2c test_kernel`btck_get_best_entry_after_full_wipe_regression::test_method(this=<unavailable>) at test_kernel.cpp:964:35 [opt]
frame [#7](/bitcoin-bitcoin/7/): 0x000000010005145c test_kernel`btck_get_best_entry_after_full_wipe_regression_invoker() at test_kernel.cpp:938:1 [opt]
frame [#8](/bitcoin-bitcoin/8/): 0x0000000100090bcc test_kernel`boost::function_n<void>::operator()(this=<unavailable>) const at function_template.hpp:789:14 [opt] [inlined]
frame [#9](/bitcoin-bitcoin/9/): 0x0000000100090bb4 test_kernel`boost::detail::forward::operator()(this=<unavailable>) at execution_monitor.ipp:1416:32 [opt] [inlined]
frame [#10](/bitcoin-bitcoin/10/): 0x0000000100090bb0 test_kernel`boost::detail::function::function_obj_invoker<boost::detail::forward, int>::invoke(function_obj_ptr=<unavailable>) at function_template.hpp:79:18 [opt]
frame [#11](/bitcoin-bitcoin/11/): 0x00000001000141e4 test_kernel`boost::function_n<int>::operator()(this=0x000000016fdfe4b8) const at function_template.hpp:789:14 [opt] [inlined]
frame [#12](/bitcoin-bitcoin/12/): 0x00000001000141cc test_kernel`int boost::detail::do_invoke<boost::shared_ptr<boost::detail::translator_holder_base>, boost::function<int ()>>(tr=0x0000000100325810, F=0x000000016fdfe4b8) at execution_monitor.ipp:329:30 [opt] [inlined]
frame [#13](/bitcoin-bitcoin/13/): 0x00000001000141b0 test_kernel`boost::execution_monitor::catch_signals(this=0x00000001003257f8, F=0x000000016fdfe4b8) at execution_monitor.ipp:931:16 [opt]
frame [#14](/bitcoin-bitcoin/14/): 0x0000000100014460 test_kernel`boost::execution_monitor::execute(this=0x00000001003257f8, F=0x000000016fdfe4b8) at execution_monitor.ipp:1329:16 [opt]
frame [#15](/bitcoin-bitcoin/15/): 0x000000010000fe0c test_kernel`boost::execution_monitor::vexecute(this=0x00000001003257f8, F=0x0000000100a63218) at execution_monitor.ipp:1425:5 [opt] [inlined]
frame [#16](/bitcoin-bitcoin/16/): 0x000000010000fdf0 test_kernel`boost::unit_test::unit_test_monitor_t::execute_and_translate(this=0x00000001003257f8, func=0x0000000100a63218, timeout_microseconds=0) at unit_test_monitor.ipp:49:9 [opt]
frame [#17](/bitcoin-bitcoin/17/): 0x0000000100011200 test_kernel`boost::unit_test::framework::state::execute_test_tree(this=<unavailable>, tu_id=<unavailable>, timeout_microseconds=0, p_random_generator=0x000000016fdfe750) at framework.ipp:815:44 [opt]
frame [#18](/bitcoin-bitcoin/18/): 0x0000000100011340 test_kernel`boost::unit_test::framework::state::execute_test_tree(this=<unavailable>, tu_id=<unavailable>, timeout_microseconds=0, p_random_generator=0x0000000000000000) at framework.ipp:740:54 [opt]
frame [#19](/bitcoin-bitcoin/19/): 0x000000010000efd4 test_kernel`boost::unit_test::framework::run(id=1, continue_test=<unavailable>) at framework.ipp:1722:29 [opt]
frame [#20](/bitcoin-bitcoin/20/): 0x000000010001f220 test_kernel`boost::unit_test::unit_test_main(init_func=<unavailable>, argc=<unavailable>, argv=<unavailable>) at unit_test_main.ipp:250:9 [opt]
frame [#21](/bitcoin-bitcoin/21/): 0x000000018429bda4 dyld`start + 6992
Idea for a fix
Document the null return and add null guards to the entry accessors.
Related
Environment
Bitcoin Core ddb94fd3e (master, 2026-05-14) macOS 26.4.1 arm64 Apple clang version 21.0.0 (clang-2100.0.123.102)
cmake -B build -DBUILD_TESTS=ON -DBUILD_KERNEL_LIB=ON -DENABLE_IPC=OFF -DCMAKE_BUILD_TYPE=Debug (no extra patches).
Additional info
Manually discovered in an package I am building using libbitcoinkernel bindings for another language. I then used AI to create the repro-kernel-best-entry-null-after-wipe test case based on that.
Assisted-by: Claude Opus 4.7