Skip to content

[Bug]signed integer overflow in yajl_parse_integer (CWE-190) #262

Description

@1820893135-pixel

Summary

yajl_parse_integer() has a signed integer overflow when parsing overly long integers. The function accumulates digits with ret *= 10, but its MAX_VALUE_TO_MULTIPLY bounds check happens before ret *= 10. When the intermediate value is exactly equal to MAX_VALUE_TO_MULTIPLY (= LLONG_MAX / 10 rounded up = 922337203685477587), the next iteration performs ret *= 10 first, which already overflows LLONG_MAX (9223372036854775807); only then is the range check done. UBSan catches it (signed integer overflow: 922337203685477587 * 10 cannot be represented). Because YAJL handles this with errno = ERANGE + a clamped return, the overflowed value itself is "corrected" to LLONG_MAX/LLONG_MIN, so the practical impact is usually low severity — but it can escalate where UBSan halt-on-error is enabled or where callers use the integer value for subsequent array indexing/allocation sizes.

  • Affected versions: YAJL 2.1.1 / HEAD 5e3a785 (2015-09-24)
  • Severity: Low (non-fatal under UBSan recover mode; downstream misuse cannot be ruled out)
  • CWE: CWE-190 (Integer Overflow or Wraparound)

Detail

Affected code

The bug lives in src/yajl_parser.c:

/* src/yajl_parser.c:36-57 */
#define MAX_VALUE_TO_MULTIPLY ((LLONG_MAX / 10) + (LLONG_MAX % 10))

 /* same semantics as strtol */
long long
yajl_parse_integer(const unsigned char *number, unsigned int length)
{
    long long ret  = 0;
    long sign = 1;
    const unsigned char *pos = number;
    if (*pos == '-') { pos++; sign = -1; }
    if (*pos == '+') { pos++; }

    while (pos < number + length) {
        if ( ret > MAX_VALUE_TO_MULTIPLY ) {     /* check 1: before the multiply */
            errno = ERANGE;
            return sign == 1 ? LLONG_MAX : LLONG_MIN;
        }
        ret *= 10;                               /* <-- overflow point */
        if (LLONG_MAX - ret < (*pos - '0')) {    /* check 2: after the multiply */
            errno = ERANGE;
            return sign == 1 ? LLONG_MAX : LLONG_MIN;
        }
        if (*pos < '0' || *pos > '9') {
            errno = ERANGE;
            return sign == 1 ? LLONG_MAX : LLONG_MIN;
        }
        ret += (*pos++ - '0');
    }

    return sign * ret;
}

Root cause: check 1 uses strict ret > MAX_VALUE_TO_MULTIPLY. When ret == MAX_VALUE_TO_MULTIPLY (i.e. 922337203685477587), check 1 does not intercept and ret *= 10 executes directly:

ret = 922337203685477587
ret * 10 = 9223372036854775870
LLONG_MAX = 9223372036854775807

9223372036854775870 > 9223372036854775807, so the signed multiply overflows, triggering UBSan's signed-integer-overflow. Check 2 later returns ERANGE because LLONG_MAX - ret is negative, but the overflow has already occurred — an abort under -fno-sanitize-recover=all, and undefined behavior per the C standard regardless.

Call chain:

yajl_parse_integer (yajl_parser.c:49)
  └─ yajl_do_parse (yajl_parser.c:287)
       └─ yajl_parse (yajl.c:130)
            └─ yajl_tree_parse / yajl_parse user entry point

POC

Triggering input (22 bytes, cfg 0x40 routes through the DOM path, 9223372036854775870 has 19 digits):

# Input bytes (hex): 40 5b 39 32 32 33 33 37 32 30 33 36 38 35 34 37 37 35 38 37 30 5d
# i.e. @[9223372036854775870]

# Repro command (UBSan halt-on-error)
UBSAN_OPTIONS=halt_on_error=1 ./yajl_fuzzer -runs=0 verify/repro_integer_overflow.bin

Pure API repro:

#include <yajl/yajl_parser.h>
int main(void) {
    const unsigned char *num = (const unsigned char *)"9223372036854775870";
    errno = 0;
    long long v = yajl_parse_integer(num, 19);
    /* triggers signed integer overflow, reported by UBSan */
    printf("%lld\n", v);
    return 0;
}

Trigger result

runtime error: signed integer overflow: 922337203685477587 * 10 cannot be represented in type 'long long'

# with halt_on_error=1 the process terminates with a non-zero exit code
  • Replay exit code (recover mode): 0 (UBSan recovers by default, non-fatal)
  • Replay exit code (halt_on_error): non-zero, process terminates
  • Deterministic: yes

Suggested fix

  1. Move the check before ret *= 10 and switch to divide-then-verify (divide before multiply) to eliminate the overflow entirely:
while (pos < number + length) {
    if (*pos < '0' || *pos > '9') {
        errno = ERANGE;
        return sign == 1 ? LLONG_MAX : LLONG_MIN;
    }
    if (ret > (LLONG_MAX - (*pos - '0')) / 10) {  /* full pre-check before the multiply */
        errno = ERANGE;
        return sign == 1 ? LLONG_MAX : LLONG_MIN;
    }
    ret = ret * 10 + (*pos++ - '0');
}
  1. Simply reuse strtoll (matches the semantics claimed in the comment; the standard implementation already handles the boundary correctly):
#include <stdlib.h>
#include <errno.h>

long long
yajl_parse_integer(const unsigned char *number, unsigned int length)
{
    char buf[32];
    if (length >= sizeof(buf)) { errno = ERANGE; return LLONG_MAX; }
    memcpy(buf, number, length);
    buf[length] = '\0';
    return strtoll(buf, NULL, 10);
}
  1. If the loop accumulation must stay, at minimum tighten the boundary from ret > MAX_VALUE_TO_MULTIPLY to ret > (LLONG_MAX - 9) / 10, checked before ret *= 10.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions