From ea09f244756d7c85dae06c73716891385315eb48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Sun, 20 Sep 2026 19:18:33 -0400 Subject: [PATCH 1/4] bitcoin: a pull that has overrun must stay overrun --- bitcoin/block.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bitcoin/block.c b/bitcoin/block.c index 6838f2c39344..55cf93eb79ac 100644 --- a/bitcoin/block.c +++ b/bitcoin/block.c @@ -1,5 +1,4 @@ #include "config.h" -#include #include #include #include @@ -11,7 +10,9 @@ static const u8 *pull(const u8 **cursor, size_t *max, void *copy, size_t n) { const u8 *p = *cursor; - if (*max < n) { + /* Once a pull has overrun, the cursor stays NULL: a later zero-length + * pull must not resume from it. */ + if (!p || *max < n) { *cursor = NULL; *max = 0; /* Just make sure we don't leak uninitialized mem! */ @@ -21,7 +22,6 @@ static const u8 *pull(const u8 **cursor, size_t *max, void *copy, size_t n) } *cursor += n; *max -= n; - assert(p); if (copy) memcpy(copy, p, n); return memcheck(p, n); From 391f490d015825873bc5e7676dd45b3aec927785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Thu, 24 Sep 2026 14:33:24 -0400 Subject: [PATCH 2/4] bitcoin: free the decoded block when parsing it fails --- bitcoin/block.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitcoin/block.c b/bitcoin/block.c index 55cf93eb79ac..b9ed2664f402 100644 --- a/bitcoin/block.c +++ b/bitcoin/block.c @@ -155,7 +155,7 @@ bitcoin_block_from_hex(const tal_t *ctx, const struct chainparams *chainparams, /* De-hex the array. */ len = hex_data_size(hexlen); - p = linear_tx = tal_arr(ctx, u8, len); + p = linear_tx = tal_arr(b, u8, len); if (!hex_decode(hex, hexlen, linear_tx, len)) return tal_free(b); From e7b4edb3ec3250eb11453646002e27caed06e33f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Tue, 22 Sep 2026 15:50:17 -0400 Subject: [PATCH 3/4] bitcoin: hash a block's bytes only once pull has checked them --- bitcoin/block.c | 37 ++++++++++++----------- bitcoin/test/run-bitcoin_block_from_hex.c | 18 +++++++++++ 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/bitcoin/block.c b/bitcoin/block.c index b9ed2664f402..ba5b7b4ed228 100644 --- a/bitcoin/block.c +++ b/bitcoin/block.c @@ -60,6 +60,16 @@ static void sha256_varint(struct sha256_ctx *ctx, u64 val) sha256_update(ctx, vt, vtlen); } +/* Hashing before the pull reads past the end of a truncated block, so let + * pull() say whether the bytes are there. */ +static void pull_and_hash(const u8 **cursor, size_t *len, + struct sha256_ctx *shactx, size_t n) +{ + const u8 *p = pull(cursor, len, NULL, n); + if (p) + sha256_update(shactx, p, n); +} + static void bitcoin_block_pull_dynafed_params(const u8 **cursor, size_t *len, struct sha256_ctx *shactx) { u8 type; @@ -73,40 +83,33 @@ static void bitcoin_block_pull_dynafed_params(const u8 **cursor, size_t *len, st /* "scriptPubKey" used for block signing */ l1 = pull_varint(cursor, len); sha256_varint(shactx, l1); - sha256_update(shactx, *cursor, l1); - pull(cursor, len, NULL, l1); + pull_and_hash(cursor, len, shactx, l1); /* signblock_witness_limit */ - sha256_update(shactx, *cursor, 4); - pull(cursor, len, NULL, 4); + pull_and_hash(cursor, len, shactx, 4); /* Skip elided_root */ - sha256_update(shactx, *cursor, 32); - pull(cursor, len, NULL, 32); + pull_and_hash(cursor, len, shactx, 32); break; case DYNAFED_PARAMS_FULL: /* "scriptPubKey" used for block signing */ l1 = pull_varint(cursor, len); sha256_varint(shactx, l1); - sha256_update(shactx, *cursor, l1); - pull(cursor, len, NULL, l1); + pull_and_hash(cursor, len, shactx, l1); /* signblock_witness_limit */ - sha256_update(shactx, *cursor, 4); - pull(cursor, len, NULL, 4); + pull_and_hash(cursor, len, shactx, 4); /* fedpeg_program */ l1 = pull_varint(cursor, len); sha256_varint(shactx, l1); - sha256_update(shactx, *cursor, l1); - pull(cursor, len, NULL, l1); + pull_and_hash(cursor, len, shactx, l1); /* fedpegscript */ l1 = pull_varint(cursor, len); sha256_varint(shactx, l1); - sha256_update(shactx, *cursor, l1); - pull(cursor, len, NULL, l1); + pull_and_hash(cursor, len, shactx, l1); /* extension space */ l2 = pull_varint(cursor, len); @@ -114,8 +117,7 @@ static void bitcoin_block_pull_dynafed_params(const u8 **cursor, size_t *len, st for (size_t i = 0; i < l2; i++) { l1 = pull_varint(cursor, len); sha256_varint(shactx, l1); - sha256_update(shactx, *cursor, l1); - pull(cursor, len, NULL, l1); + pull_and_hash(cursor, len, shactx, l1); } break; } @@ -187,8 +189,7 @@ bitcoin_block_from_hex(const tal_t *ctx, const struct chainparams *chainparams, /* elemens_header.challenge */ templen = pull_varint(&p, &len); sha256_varint(&shactx, templen); - sha256_update(&shactx, p, templen); - pull(&p, &len, NULL, templen); + pull_and_hash(&p, &len, &shactx, templen); /* elements_header.solution. Not hashed since it'd be * a circular dependency. */ diff --git a/bitcoin/test/run-bitcoin_block_from_hex.c b/bitcoin/test/run-bitcoin_block_from_hex.c index 94b26ad5d05f..fefd44fe9879 100644 --- a/bitcoin/test/run-bitcoin_block_from_hex.c +++ b/bitcoin/test/run-bitcoin_block_from_hex.c @@ -60,6 +60,16 @@ static const char block[] = "ac0eb82500000000001976a914e05655a7d90b01ba874d81beff57ee09610ca" "3ce88ac00000000"; +/* Elements header whose challenge varint promises 80 bytes with none + * left in the block. */ +static const char elements_short_challenge[] = + "0000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000afdc6a5c6400000050"; + +/* Dynafed header whose scriptPubKey varint promises 80 bytes with none + * left in the block. */ +static const char elements_short_dynafed[] = + "000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000afdc6a5c640000000150"; + STRUCTEQ_DEF(sha256_double, 0, sha); int main(int argc, const char *argv[]) @@ -70,6 +80,14 @@ int main(int argc, const char *argv[]) struct bitcoin_block *b; common_setup(argv[0]); + chainparams = chainparams_for_network("liquid-regtest"); + assert(!bitcoin_block_from_hex(NULL, chainparams, + elements_short_challenge, + strlen(elements_short_challenge))); + assert(!bitcoin_block_from_hex(NULL, chainparams, + elements_short_dynafed, + strlen(elements_short_dynafed))); + chainparams = chainparams_for_network("bitcoin"); b = bitcoin_block_from_hex(NULL, chainparams, block, strlen(block)); From acfb978a4f5c7faa5d855f54216bc2ef60aca7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Sat, 26 Sep 2026 20:42:49 -0400 Subject: [PATCH 4/4] bitcoin: reject a block whose tx count or loops outrun its bytes --- bitcoin/block.c | 9 +++++++-- bitcoin/test/run-bitcoin_block_from_hex.c | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/bitcoin/block.c b/bitcoin/block.c index ba5b7b4ed228..9afee45df726 100644 --- a/bitcoin/block.c +++ b/bitcoin/block.c @@ -114,7 +114,7 @@ static void bitcoin_block_pull_dynafed_params(const u8 **cursor, size_t *len, st /* extension space */ l2 = pull_varint(cursor, len); sha256_varint(shactx, l2); - for (size_t i = 0; i < l2; i++) { + for (size_t i = 0; i < l2 && *cursor; i++) { l1 = pull_varint(cursor, len); sha256_varint(shactx, l1); pull_and_hash(cursor, len, shactx, l1); @@ -130,7 +130,7 @@ static void bitcoin_block_pull_dynafed_details(const u8 **cursor, size_t *len, s /* Consume the signblock_witness */ u64 numwitnesses = pull_varint(cursor, len); - for (size_t i=0; ihdr.hash.shad); num = pull_varint(&p, &len); + /* Every transaction takes at least one byte */ + if (num > len) + return tal_free(b); b->tx = tal_arr(b, struct bitcoin_tx *, num); b->txids = tal_arr(b, struct bitcoin_txid, num); for (i = 0; i < num; i++) { b->tx[i] = pull_bitcoin_tx_only(b->tx, &p, &len); + if (!b->tx[i]) + return tal_free(b); b->tx[i]->chainparams = chainparams; bitcoin_txid(b->tx[i], &b->txids[i]); } diff --git a/bitcoin/test/run-bitcoin_block_from_hex.c b/bitcoin/test/run-bitcoin_block_from_hex.c index fefd44fe9879..3e2a6228912a 100644 --- a/bitcoin/test/run-bitcoin_block_from_hex.c +++ b/bitcoin/test/run-bitcoin_block_from_hex.c @@ -70,6 +70,19 @@ static const char elements_short_challenge[] = static const char elements_short_dynafed[] = "000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000afdc6a5c640000000150"; +/* Block whose only transaction does not parse. */ +static const char bad_tx[] = + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001ff"; + +/* Block claiming far more transactions than it has bytes. */ +static const char huge_tx_count[] = + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffff7f"; + +/* Dynafed header whose extension space claims far more entries than it + * has bytes. */ +static const char elements_huge_extension[] = + "000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000ffffffffffffffff7f"; + STRUCTEQ_DEF(sha256_double, 0, sha); int main(int argc, const char *argv[]) @@ -87,8 +100,16 @@ int main(int argc, const char *argv[]) assert(!bitcoin_block_from_hex(NULL, chainparams, elements_short_dynafed, strlen(elements_short_dynafed))); + assert(!bitcoin_block_from_hex(NULL, chainparams, + elements_huge_extension, + strlen(elements_huge_extension))); chainparams = chainparams_for_network("bitcoin"); + assert(!bitcoin_block_from_hex(NULL, chainparams, + bad_tx, strlen(bad_tx))); + assert(!bitcoin_block_from_hex(NULL, chainparams, + huge_tx_count, strlen(huge_tx_count))); + b = bitcoin_block_from_hex(NULL, chainparams, block, strlen(block));