Skip to content

Conversation

@maflcko
Copy link
Member

@maflcko maflcko commented Dec 19, 2025

Serialization should not behave differently on different architectures. See also the related commit 3789215.

However, on fees.dat file corruption, 32-bit builds may run into an unsigned integer overflow and report the wrong corruption reason, or may even silently continue after the corruption.

This is a bit hard to reproduce, because 32-bit platforms are rare and most of them don't support running the unsigned integer overflow sanitizer. So the possible options to reproduce are:

  • Run on armhf and manually annotate the code to detect the overflow
  • Run on i386 with the integer sanitizer (possibly via podman run -it --rm --platform linux/i386 'debian:trixie')
  • Run the integer sanitizer on any 64-bit platform and manually replace type in the affected line by uint32_t

Afterwards, the steps to reproduce are:

export DEBIAN_FRONTEND=noninteractive && apt update && apt install curl wget htop git vim ccache -y && git clone https://github.com/bitcoin/bitcoin.git  --depth=1 ./b-c && cd b-c && apt install build-essential cmake pkg-config  python3-zmq libzmq3-dev libevent-dev libboost-dev libsqlite3-dev  systemtap-sdt-dev  libcapnp-dev capnproto  libqrencode-dev qt6-tools-dev qt6-l10n-tools qt6-base-dev  clang llvm libc++-dev libc++abi-dev   -y

cmake -B ./bld-cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER='clang' -DCMAKE_CXX_COMPILER='clang++' -DSANITIZERS=undefined,integer,float-divide-by-zero --preset=dev-mode

cmake --build ./bld-cmake --parallel  $(nproc)

curl -fLO 'https://github.com/bitcoin-core/qa-assets/raw/b5ad78e070e4cf36beb415d7b490d948d70ba73f/fuzz_corpora/policy_estimator_io/607473137013139e3676e30ec4b29639e673fa9b'

UBSAN_OPTIONS="suppressions=$(pwd)/test/sanitizer_suppressions/ubsan:print_stacktrace=1:halt_on_error=1:report_error_type=1" FUZZ=policy_estimator_io ./bld-cmake/bin/fuzz ./607473137013139e3676e30ec4b29639e673fa9b 

The output will be something like:

/b-c/src/policy/fees/block_policy_estimator.cpp:448:25: runtime error: unsigned integer overflow: 346685954 * 219 cannot be represented in type 'unsigned int'
    #0 0x5b0b1bbe in TxConfirmStats::Read(AutoFile&, unsigned int) /b-c/bld-cmake/src/./policy/fees/block_policy_estimator.cpp:448:25
    #1 0x5b0b7d3f in CBlockPolicyEstimator::Read(AutoFile&) /b-c/bld-cmake/src/./policy/fees/block_policy_estimator.cpp:1037:29
    #2 0x592a9783 in policy_estimator_io_fuzz_target(std::span<unsigned char const, 4294967295u>) /b-c/bld-cmake/src/test/fuzz/./test/fuzz/policy_estimator_io.cpp:32:32
    #3 0x5896ba8e in void std::__invoke_impl<void, void (*&)(std::span<unsigned char const, 4294967295u>), std::span<unsigned char const, 4294967295u>>(std::__invoke_other, void (*&)(std::span<unsigned char const, 4294967295u>), std::span<unsigned char const, 4294967295u>&&) /usr/lib/gcc/i686-linux-gnu/14/../../../../include/c++/14/bits/invoke.h:61:14
    #4 0x5896b8eb in std::enable_if<is_invocable_r_v<void, void (*&)(std::span<unsigned char const, 4294967295u>), std::span<unsigned char const, 4294967295u>>, void>::type std::__invoke_r<void, void (*&)(std::span<unsigned char const, 4294967295u>), std::span<unsigned char const, 4294967295u>>(void (*&)(std::span<unsigned char const, 4294967295u>), std::span<unsigned char const, 4294967295u>&&) /usr/lib/gcc/i686-linux-gnu/14/../../../../include/c++/14/bits/invoke.h:111:2
    #5 0x5896b44b in std::_Function_handler<void (std::span<unsigned char const, 4294967295u>), void (*)(std::span<unsigned char const, 4294967295u>)>::_M_invoke(std::_Any_data const&, std::span<unsigned char const, 4294967295u>&&) /usr/lib/gcc/i686-linux-gnu/14/../../../../include/c++/14/bits/std_function.h:290:9
    #6 0x59845c95 in std::function<void (std::span<unsigned char const, 4294967295u>)>::operator()(std::span<unsigned char const, 4294967295u>) const /usr/lib/gcc/i686-linux-gnu/14/../../../../include/c++/14/bits/std_function.h:591:9
    #7 0x5983a0da in test_one_input(std::span<unsigned char const, 4294967295u>) /b-c/bld-cmake/src/test/fuzz/util/./test/fuzz/fuzz.cpp:88:5
    #8 0x5983cb80 in main /b-c/bld-cmake/src/test/fuzz/util/./test/fuzz/fuzz.cpp:271:13
    #9 0xf75aecc2  (/lib/i386-linux-gnu/libc.so.6+0x24cc2) (BuildId: 2dc5f2945fad35c1b07d1a5a32520b3c41afaa75)
    #10 0xf75aed87 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x24d87) (BuildId: 2dc5f2945fad35c1b07d1a5a32520b3c41afaa75)
    #11 0x58932db6 in _start (/b-c/bld-cmake/bin/fuzz+0x235ddb6) (BuildId: 7d8d83a77923f14e99c0de64acbc5f5bfc2cce9b)

SUMMARY: UndefinedBehaviorSanitizer: unsigned-integer-overflow /b-c/src/policy/fees/block_policy_estimator.cpp:448:25 

Note: This is marked a "refactor", because the code change does not affect 64-bit builds, and on the still remaining rare 32-bit builds today it is extremely unlikely to happen in production.

@DrahtBot
Copy link
Contributor

DrahtBot commented Dec 19, 2025

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage & Benchmarks

For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/34109.

Reviews

See the guideline for information on the review process.

Type Reviewers
ACK bensig, ismaelsadeeq, luke-jr

If your review is incorrectly listed, please copy-paste <!--meta-tag:bot-skip--> into the comment that the bot should ignore.

@bensig
Copy link
Contributor

bensig commented Jan 5, 2026

ACK fa1d17d

Straightforward fix - using uint64_t instead of size_t prevents potential overflow on 32-bit platforms where size_t is 32-bit.

@maflcko maflcko added this to the 31.0 milestone Jan 5, 2026
@maflcko maflcko requested a review from ismaelsadeeq January 8, 2026 10:00
Copy link
Member

@ismaelsadeeq ismaelsadeeq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK.

Nice catch. It is important to fix this.
However, changing maxConfirms to uint64_t would only prevent the unsigned integer overflow by allowing it to store a very large value (or the modulo of that value).

Let's extend this by adding another commit that validates the scale.
Since we allow reading a corrupted scale, an overflow where the modulo of the multiplication result is stored in maxConfirms (scale* maxPeriods) may go undetected. If that modulo happens to fall within one 1-1008, the in L-454 check would pass, but the resulting scale would be invalid; this is a contrived, rare edge case.

We could be more defensive by requiring that the scale is exactly one of SHORT_SCALE, MED_SCALE, or LONG_SCALE. Otherwise, a corruption that produces a different in-range value would still be accepted as well.

However, for consistency with existing validation (e.g. decay), it likely makes sense to only enforce the upper bound rather than requiring an exact match.

diff --git a/src/policy/fees/block_policy_estimator.cpp b/src/policy/fees/block_policy_estimator.cpp
index 23aecefea2a..03d1c854811 100644
--- a/src/policy/fees/block_policy_estimator.cpp
+++ b/src/policy/fees/block_policy_estimator.cpp
@@ -435,6 +435,10 @@ void TxConfirmStats::Read(AutoFile& filein, size_t numBuckets)
         throw std::runtime_error("Corrupt estimates file. Scale must be non-zero");
     }

+    if (scale > LONG_SCALE) {
+        throw std::runtime_error("Corrupt estimates file. Scale must not exceed the long scale");
+    }
+
     filein >> Using<VectorFormatter<EncodedDoubleFormatter>>(m_feerate_avg);
     if (m_feerate_avg.size() != numBuckets) {
         throw std::runtime_error("Corrupt estimates file. Mismatch in feerate average bucket count");
diff --git a/src/policy/fees/block_policy_estimator.h b/src/policy/fees/block_policy_estimator.h
index ec432dcbf53..a5ade9cd01d 100644
--- a/src/policy/fees/block_policy_estimator.h
+++ b/src/policy/fees/block_policy_estimator.h
@@ -98,6 +98,12 @@ struct FeeCalculation
     unsigned int best_height{0};
 };

+namespace {
+static constexpr unsigned int SHORT_SCALE = 1;
+static constexpr unsigned int MED_SCALE   = 2;
+static constexpr unsigned int LONG_SCALE  = 24;
+}
+
 /** \class CBlockPolicyEstimator
  * The BlockPolicyEstimator is used for estimating the feerate needed
  * for a transaction to be included in a block within a certain number of
@@ -151,13 +157,10 @@ class CBlockPolicyEstimator : public CValidationInterface
 private:
     /** Track confirm delays up to 12 blocks for short horizon */
     static constexpr unsigned int SHORT_BLOCK_PERIODS = 12;
-    static constexpr unsigned int SHORT_SCALE = 1;
     /** Track confirm delays up to 48 blocks for medium horizon */
     static constexpr unsigned int MED_BLOCK_PERIODS = 24;
-    static constexpr unsigned int MED_SCALE = 2;
     /** Track confirm delays up to 1008 blocks for long horizon */
     static constexpr unsigned int LONG_BLOCK_PERIODS = 42;
-    static constexpr unsigned int LONG_SCALE = 24;
     /** Historical estimates that are older than this aren't valid */
     static const unsigned int OLDEST_ESTIMATE_HISTORY = 6 * 1008;

@maflcko
Copy link
Member Author

maflcko commented Jan 8, 2026

However, changing maxConfirms to uint64_t would only prevent the unsigned integer overflow by allowing it to store a very large value (or the modulo of that value).

Let's extend this by adding another commit that validates the scale. Since we allow reading a corrupted scale, an overflow where the modulo of the multiplication result is stored in maxConfirms (scale* maxPeriods) may go undetected. If that modulo happens to fall within one 1-1008, the in L-454 check would pass, but the resulting scale would be invalid; this is a contrived, rare edge case.

Not sure this is relevant to fix. A scale corruption is already extremely unlikely to happen (outside of fuzzing). And the scale corruption along with a file size corruption is impossible on modern filesystems, as they guard the metadata. And for older filesystems it would be cosmically unlikely and found by the next filesystem check.

Recall, that scale is u32, and the code is:

    filein >> Using<VectorFormatter<VectorFormatter<EncodedDoubleFormatter>>>(confAvg);
    maxPeriods = confAvg.size();
    maxConfirms = scale * maxPeriods;

So, for u64 to overflow as a result of multiplication, a massive persisted file would be required (confAvg would have to deserialize correctly first). I don't think any of this can ever happen in practise.

A scale corruption by itself may happen (and does happen in the fuzz target), so a trivial one-line fix for it seemed fine to me.

I am less sure about adding code to further validate the scale: After all, the code here is just writing (and reading) constants. So, either the serialize can be skipped, because it is redundant, brittle and useless. Or, adding guards to it would be wrong, because there is a use-case out there to serialize a not constant scale. Not sure, but it doesn't seem worth it to me to write a larger patch for this.

@ismaelsadeeq
Copy link
Member

It’s fine with me to limit this to the unsigned integer overflow fix.
A follow-up can be opened to further improve this and to validate the scale.
I brought it up as a suggestion for improvement in the review, and the comment can be linked in future discussions.

utACK fa1d17d

@maflcko
Copy link
Member Author

maflcko commented Jan 8, 2026

Yeah, seems fine to track this in a follow-up. Just for reference, if the scale serialization is indeed useless and brittle, it can be removed via:

diff --git a/src/policy/fees/block_policy_estimator.cpp b/src/policy/fees/block_policy_estimator.cpp
index 23aecefea2..8f31b98212 100644
--- a/src/policy/fees/block_policy_estimator.cpp
+++ b/src/policy/fees/block_policy_estimator.cpp
@@ -105,7 +105,7 @@ private:
     double decay;
 
     // Resolution (# of blocks) with which confirmations are tracked
-    unsigned int scale;
+    const unsigned int scale;
 
     // Mempool counts of outstanding transactions
     // For each bucket X, track the number of transactions in the mempool
@@ -430,9 +430,12 @@ void TxConfirmStats::Read(AutoFile& filein, size_t numBuckets)
     if (decay <= 0 || decay >= 1) {
         throw std::runtime_error("Corrupt estimates file. Decay must be between 0 and 1 (non-inclusive)");
     }
-    filein >> scale;
-    if (scale == 0) {
-        throw std::runtime_error("Corrupt estimates file. Scale must be non-zero");
+    {
+    uint32_t dummy;
+    filein >> dummy;
+    if (scale == 0||scale!=dummy) {
+        throw std::runtime_error(strprintf("Corrupt estimates file. Scale must be non-zero and must be equal to %s", dummy));
+    }
     }
 
     filein >> Using<VectorFormatter<EncodedDoubleFormatter>>(m_feerate_avg);

(passes tests, fwiw)

@luke-jr
Copy link
Member

luke-jr commented Jan 14, 2026

163d3e5 fixes also the hypothetical/impractical overflow. Use it if you want.

Also, utACK fa1d17d as an improvement.

@maflcko
Copy link
Member Author

maflcko commented Jan 14, 2026

163d3e5 fixes also the hypothetical/impractical overflow. Use it if you want.

I'll leave as-is for now, because I don't think an additional patch is needed, and the change is also unclear, because three people wrote three different patches for this. So this seems better to leave as-is or for a follow-up.

Whereas this pull request should be trivially correct and already has 3 acks.

@fanquake fanquake merged commit ac76d94 into bitcoin:master Jan 14, 2026
26 checks passed
@maflcko maflcko deleted the 2512-fix-u64 branch January 14, 2026 09:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants