kernel: Allow null arguments for serialized data
What changed, and why it matters
This change loosens a strict rule in the Bitcoin Core kernel library so that callers can safely pass an empty data buffer even if its internal memory pointer happens to be null. Previously the library declared that certain pointers could never be null, which could cause crashes or undefined behavior when empty vectors produced null pointers. The patch now checks for the one truly invalid case—a null pointer paired with a non-zero length—and otherwise accepts null pointers when the length is zero.
Treat as a defensive hardening fix. Review downstream language bindings and callers to ensure they do not rely on the removed non-null guarantee, and verify that the new null-with-nonzero-length checks are consistently applied to any other serialized-data entry points.
Security signals we found
Removal of non-null contract annotations due to implementation-defined null pointers on empty spans
Addition of null-pointer-with-nonzero-length guards in three C API entry points
New unit tests covering null pointer + non-zero length and empty serialized inputs
Potential for undefined behavior or crashes if a null pointer with non-zero length were dereferenced
Evidence from the diff
The commit removes BITCOINKERNEL_ARG_NONNULL(1) annotations from btck_transaction_create, btck_script_pubkey_create, and btck_block_create, because std::span constructed from an empty std::vector may yield a null data() pointer depending on the standard library implementation. It adds explicit guards that return nullptr (or let the existing deserialization path handle the empty span) only when the pointer is null and the length is non-zero. Tests are added/relocated to verify that invalid, empty, and null-with-nonzero-length spans throw std::runtime_error as expected.
Changed components
src/kernel/bitcoinkernel.cppsrc/kernel/bitcoinkernel.hsrc/test/kernel/test_kernel.cpplibbitcoinkernel C APIInspect captured patch +28 / −18
diff --git a/src/kernel/bitcoinkernel.cpp b/src/kernel/bitcoinkernel.cpp
index 240255bb..0b9ea513 100644
--- a/src/kernel/bitcoinkernel.cpp
+++ b/src/kernel/bitcoinkernel.cpp
@@ -497,6 +497,9 @@ struct btck_Txid: Handle<btck_Txid, Txid> {};
btck_Transaction* btck_transaction_create(const void* raw_transaction, size_t raw_transaction_len)
{
+ if (raw_transaction == nullptr && raw_transaction_len != 0) {
+ return nullptr;
+ }
try {
DataStream stream{std::span{reinterpret_cast<const std::byte*>(raw_transaction), raw_transaction_len}};
return btck_Transaction::create(std::make_shared<const CTransaction>(deserialize, TX_WITH_WITNESS, stream));
@@ -556,6 +559,9 @@ void btck_transaction_destroy(btck_Transaction* transaction)
btck_ScriptPubkey* btck_script_pubkey_create(const void* script_pubkey, size_t script_pubkey_len)
{
+ if (script_pubkey == nullptr && script_pubkey_len != 0) {
+ return nullptr;
+ }
auto data = std::span{reinterpret_cast<const uint8_t*>(script_pubkey), script_pubkey_len};
return btck_ScriptPubkey::create(data.begin(), data.end());
}
@@ -1033,6 +1039,9 @@ int btck_chainstate_manager_import_blocks(btck_ChainstateManager* chainman, cons
btck_Block* btck_block_create(const void* raw_block, size_t raw_block_length)
{
+ if (raw_block == nullptr && raw_block_length != 0) {
+ return nullptr;
+ }
auto block{std::make_shared<CBlock>()};
DataStream stream{std::span{reinterpret_cast<const std::byte*>(raw_block), raw_block_length}};
diff --git a/src/kernel/bitcoinkernel.h b/src/kernel/bitcoinkernel.h
index 4c94b59f..93e160cd 100644
--- a/src/kernel/bitcoinkernel.h
+++ b/src/kernel/bitcoinkernel.h
@@ -469,12 +469,12 @@ typedef uint8_t btck_ChainType;
/**
* @brief Create a new transaction from the serialized data.
*
- * @param[in] raw_transaction Non-null.
+ * @param[in] raw_transaction Serialized transaction.
* @param[in] raw_transaction_len Length of the serialized transaction.
* @return The transaction, or null on error.
*/
BITCOINKERNEL_API btck_Transaction* BITCOINKERNEL_WARN_UNUSED_RESULT btck_transaction_create(
- const void* raw_transaction, size_t raw_transaction_len) BITCOINKERNEL_ARG_NONNULL(1);
+ const void* raw_transaction, size_t raw_transaction_len);
/**
* @brief Copy a transaction. Transactions are reference counted, so this just
@@ -567,12 +567,12 @@ BITCOINKERNEL_API void btck_transaction_destroy(btck_Transaction* transaction);
/**
* @brief Create a script pubkey from serialized data.
- * @param[in] script_pubkey Non-null.
+ * @param[in] script_pubkey Serialized script pubkey.
* @param[in] script_pubkey_len Length of the script pubkey data.
* @return The script pubkey.
*/
BITCOINKERNEL_API btck_ScriptPubkey* BITCOINKERNEL_WARN_UNUSED_RESULT btck_script_pubkey_create(
- const void* script_pubkey, size_t script_pubkey_len) BITCOINKERNEL_ARG_NONNULL(1);
+ const void* script_pubkey, size_t script_pubkey_len);
/**
* @brief Copy a script pubkey.
@@ -1111,12 +1111,12 @@ BITCOINKERNEL_API btck_Block* BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_read(
/**
* @brief Parse a serialized raw block into a new block object.
*
- * @param[in] raw_block Non-null, serialized block.
+ * @param[in] raw_block Serialized block.
* @param[in] raw_block_len Length of the serialized block.
* @return The allocated block, or null on error.
*/
BITCOINKERNEL_API btck_Block* BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_create(
- const void* raw_block, size_t raw_block_len) BITCOINKERNEL_ARG_NONNULL(1);
+ const void* raw_block, size_t raw_block_len);
/**
* @brief Copy a block. Blocks are reference counted, so this just increments
diff --git a/src/test/kernel/test_kernel.cpp b/src/test/kernel/test_kernel.cpp
index e1c9376d..75c9e466 100644
--- a/src/test/kernel/test_kernel.cpp
+++ b/src/test/kernel/test_kernel.cpp
@@ -14,7 +14,6 @@
#include <cstdint>
#include <cstdlib>
#include <filesystem>
-#include <format>
#include <iostream>
#include <memory>
#include <optional>
@@ -395,6 +394,12 @@ BOOST_AUTO_TEST_CASE(btck_transaction_tests)
auto tx2{Transaction{tx_data_2}};
CheckHandle(tx, tx2);
+ auto invalid_data = hex_string_to_byte_vec("012300");
+ BOOST_CHECK_THROW(Transaction{invalid_data}, std::runtime_error);
+ auto empty_data = hex_string_to_byte_vec("");
+ BOOST_CHECK_THROW(Transaction{empty_data}, std::runtime_error);
+ BOOST_CHECK_THROW(Transaction{std::span<std::byte>(static_cast<std::byte*>(nullptr), 2)}, std::runtime_error);
+
BOOST_CHECK_EQUAL(tx.CountOutputs(), 2);
BOOST_CHECK_EQUAL(tx.CountInputs(), 1);
auto broken_tx_data{std::span<std::byte>{tx_data.begin(), tx_data.begin() + 10}};
@@ -469,6 +474,8 @@ BOOST_AUTO_TEST_CASE(btck_script_pubkey)
ScriptPubkey script{script_data};
ScriptPubkey script2{script_data_2};
CheckHandle(script, script2);
+
+ BOOST_CHECK_THROW(ScriptPubkey{std::span<std::byte>(static_cast<std::byte*>(nullptr), 2)}, std::runtime_error);
}
BOOST_AUTO_TEST_CASE(btck_transaction_output)
@@ -581,6 +588,11 @@ BOOST_AUTO_TEST_CASE(btck_block)
CheckHandle(block, block_100);
Block block_tx{hex_string_to_byte_vec(REGTEST_BLOCK_DATA[205])};
CheckRange(block_tx.Transactions(), block_tx.CountTransactions());
+ auto invalid_data = hex_string_to_byte_vec("012300");
+ BOOST_CHECK_THROW(Block{invalid_data}, std::runtime_error);
+ auto empty_data = hex_string_to_byte_vec("");
+ BOOST_CHECK_THROW(Block{empty_data}, std::runtime_error);
+ BOOST_CHECK_THROW(Block{std::span<std::byte>(static_cast<std::byte*>(nullptr), 2)}, std::runtime_error);
}
Context create_context(std::shared_ptr<TestKernelNotifications> notifications, ChainType chain_type, std::shared_ptr<TestValidationInterface> validation_interface = nullptr)
@@ -713,17 +725,6 @@ void chainman_mainnet_validation_test(TestDirectory& test_directory)
auto context{create_context(notifications, ChainType::MAINNET, validation_interface)};
auto chainman{create_chainman(test_directory, false, false, false, false, context)};
- {
- // Process an invalid block
- auto raw_block = hex_string_to_byte_vec("012300");
- BOOST_CHECK_THROW(Block{raw_block}, std::runtime_error);
- }
- {
- // Process an empty block
- auto raw_block = hex_string_to_byte_vec("");
- BOOST_CHECK_THROW(Block{raw_block}, std::runtime_error);
- }
-
// mainnet block 1
auto raw_block = hex_string_to_byte_vec("010000006fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000982051fd1e4ba744bbbe680e1fee14677ba1a3c3540bf7b1cdb606e857233e0e61bc6649ffff001d01e362990101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000");
Block block{raw_block};
Why this scored 34/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.