Skip to content

Global-buffer-overflow in make_float() via large negative exponent (FloatTraits.hpp) #2220

@xD0135

Description

@xD0135

Bug Description

make_float() in FloatTraits.hpp has an unbounded loop index that reads past the end of the powersOfTen array when the absolute value of the exponent exceeds the array size (9 elements for double, 6 for float).

The bug is reachable through normal deserialization when a JSON value contains a decimal number with hundreds or thousands of fractional digits (e.g., "0.000...0001" with 9000+ zeros). The large number of decimal digits produces a huge negative exponent_offset in parseNumber(). While parseNumber() clamps exponents that exceed exponent_max in the positive direction, the negative direction is unclamped — the final exponent += exponent_offset can produce values like -9000, which passes through to make_float() where -e becomes 9000, causing the loop to iterate far beyond the array bounds.

Minimal Reproducer

#include <ArduinoJson.h>
#include <cstdio>

int main() {
    // 500+ fractional digits creates exponent_offset < -500,
    // exceeding the 9-element powersOfTen array (max safe |e| = 511)
    const char* input = "[\"0."
        "00000000000000000000000000000000000000000000000000"
        "00000000000000000000000000000000000000000000000000"
        "00000000000000000000000000000000000000000000000000"
        "00000000000000000000000000000000000000000000000000"
        "00000000000000000000000000000000000000000000000000"
        "00000000000000000000000000000000000000000000000000"
        "00000000000000000000000000000000000000000000000000"
        "00000000000000000000000000000000000000000000000000"
        "00000000000000000000000000000000000000000000000000"
        "00000000000000000000000000000000000000000000000000"
        "1\"]";

    JsonDocument doc;
    deserializeJson(doc, input);

    // Triggers parseNumber() -> make_float() on the string value
    int val = doc[0].as<int>();
    printf("val = %d\n", val);
    return 0;
}

Compile with AddressSanitizer to observe the crash:

clang++ -std=c++17 -fsanitize=address -I src/ repro.cpp -o repro && ./repro

ASAN output (abbreviated):

ERROR: AddressSanitizer: global-buffer-overflow
READ of size 8 at 0x... in make_float<double, long>() FloatTraits.hpp:206

Root Cause Analysis

In src/ArduinoJson/Numbers/FloatTraits.hpp, make_float():

template <typename TFloat, typename TExponent>
inline TFloat make_float(TFloat m, TExponent e) {
    // ...
    for (uint8_t index = 0; e != 0; index++) {  // index unbounded
    if (e & 1)
        m *= powersOfTen[index];                 // OOB when index >= array size
    e >>= 1;
    }
    return m;
}

The positiveBinaryPowersOfTen() / negativeBinaryPowersOfTen() arrays have 9 elements (double) / 6 elements (float). When |e| > 511 (double) or |e| > 63 (float), the loop index exceeds the array bounds.

The caller parseNumber() in parseNumber.hpp has an early-return guard:

if (exponent + exponent_offset > traits::exponent_max) { ... }

But this only catches the positive direction. The negative direction (large exponent_offset from many fractional digits) is not clamped, allowing values like e = -9000 to reach make_float().

Suggested Fix

Either clamp e before entering make_float, or add a defensive bounds check inside the loop:

for (uint8_t index = 0; e != 0; index++) {
    if (index >= traits::kMaxPowersOfTenIndex)  // 9 for double, 6 for float
        return e > 0 ? traits::inf() : TFloat(0);
    if (e & 1)
        m *= powersOfTen[index];
    e >>= 1;
}

Alternatively, add a symmetric negative clamp in parseNumber():

if (-(exponent + exponent_offset) > traits::exponent_max) {
    return Number(is_negative ? -0.0f : 0.0f);
}

Environment

  • ArduinoJson version: 7.4.2
  • Found via: AFL++ fuzzing with AddressSanitizer (-fsanitize=address,undefined)
  • Platform: ESP32-S3 (Arduino Core 3.x / ESP-IDF 5.x) — bug is platform-independent
  • Troubleshooter: N/A — this is a parser bug, not a configuration issue

Impact

On hosted platforms (Linux, macOS) with ASAN, this crashes. Without ASAN, it reads adjacent memory and returns garbage float values. On embedded targets (AVR with PROGMEM, ESP32), it reads past the flash/global data section into adjacent memory. The bug is reachable from any untrusted JSON input containing a string or number with hundreds of fractional digits.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugv5ArduinoJson 5v6ArduinoJson 6v7ArduinoJson 7

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions