Skip to content

Bound the leading-zero skip in pm_integer_parse - #4193

Merged
kddnewton merged 1 commit into
ruby:mainfrom
matz:fix-integer-parse-bounds
Aug 1, 2026
Merged

Bound the leading-zero skip in pm_integer_parse#4193
kddnewton merged 1 commit into
ruby:mainfrom
matz:fix-integer-parse-bounds

Conversation

@matz

@matz matz commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Problem

pm_integer_parse() skips leading zeros in the PM_INTEGER_BASE_DEFAULT branch without checking end:

case PM_INTEGER_BASE_DEFAULT:
    while (*start == '0') start++; // 01 -> 1

For an all-zero input this reads one byte past the range. The if (start >= end) return; below it comes too late; the loop has already dereferenced the byte at end.

pm_float_node_rational_create() reaches it. It allocates length bytes for the mantissa, writes only length - 1 of them because the decimal point is squeezed out, and passes end = digits + length - 1. So the byte the skip lands on is not merely one-past-the-end, it is uninitialized heap:

$ valgrind ./parse +0.0r
Conditional jump or move depends on uninitialised value(s)
   at pm_integer_parse (integer.c:506)
   by pm_float_node_rational_create (prism.c:3963)
   by parse_expression_prefix (prism.c:17898)

A leading + is needed to make the mantissa buffer long enough for the skip to reach the uninitialized byte, so +0.0r reports while 0.0r does not.

Fix

Bound the loop, matching what PM_INTEGER_BASE_DECIMAL and PM_INTEGER_BASE_UNKNOWN already do with (end - start) > 1.

The parsed value does not change: start >= end returns 0 either way. I confirmed the same harness reports the read before the patch and is clean after, with value=0 in both cases.

Found by an mruby fuzzer testcase.

The PM_INTEGER_BASE_DEFAULT branch skips leading zeros without checking
`end`, so it reads one byte past the range for an all-zero input.

pm_float_node_rational_create() reaches it: it allocates `length` bytes
for the mantissa, writes only `length - 1` of them because the decimal
point is squeezed out, and passes end = digits + length - 1. Parsing
`+0.0r` then walks the skip onto the byte that was never written.

    $ valgrind ./parse +0.0r
    Conditional jump or move depends on uninitialised value(s)
       at pm_integer_parse (integer.c:506)
       by pm_float_node_rational_create (prism.c:3963)
       by parse_expression_prefix (prism.c:17898)

The value parsed is unaffected, since `start >= end` returns 0 either
way. The other bases already guard with `(end - start) > 1`.

Found by an mruby fuzzer testcase.
@kddnewton
kddnewton merged commit f9629ee into ruby:main Aug 1, 2026
110 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants