Skip to content

Reject reversed position ranges in parser/lexer entrypoints#2974

Open
ksss wants to merge 1 commit into
ruby:masterfrom
ksss:ksss/fix-invalid-position-range
Open

Reject reversed position ranges in parser/lexer entrypoints#2974
ksss wants to merge 1 commit into
ruby:masterfrom
ksss:ksss/fix-invalid-position-range

Conversation

@ksss
Copy link
Copy Markdown
Collaborator

@ksss ksss commented May 25, 2026

Summary

alloc_parser_from_buffer and alloc_lexer_from_buffer already rejected negative positions but accepted ranges where start_pos > end_pos. Such a range made the lexer's main loop condition (current.byte_pos == end_pos) unsatisfiable: current advances past end_pos and the equality is never reached, producing an infinite loop in the C extension with the GVL held (SIGINT ineffective).

Reproducer

RBS::Parser.parse_type("", byte_range: 1..0)
# hangs forever; only `kill -9` stops the process

The same hang is reachable from RBS::Parser.parse_method_type via its byte_range: keyword, and from the low-level _parse_signature / _parse_type / _parse_method_type directly with reversed start_pos / end_pos arguments. The _lex entrypoint always passes start_pos = 0, so the new guard on the lexer side is defensive rather than a present-day bug fix.

Fix

Add a reversed-range guard alongside the existing negative-position check, and extract both into a validate_position_range helper shared by alloc_lexer_from_buffer and alloc_parser_from_buffer:

static void validate_position_range(int start_pos, int end_pos) {
    if (start_pos < 0 || end_pos < 0) {
        rb_raise(rb_eArgError, "negative position range: %d...%d", start_pos, end_pos);
    }
    if (start_pos > end_pos) {
        rb_raise(rb_eArgError, "invalid position range: %d...%d", start_pos, end_pos);
    }
}

Reversed ranges now raise ArgumentError immediately instead of reaching the lexer.


This PR description was written by Claude Code.

`alloc_parser_from_buffer` and `alloc_lexer_from_buffer` in the C
extension already rejected negative positions, but accepted ranges
where `start_pos > end_pos`. The lexer's main loop condition is
`current.byte_pos == end_pos`, so a reversed range never terminates:
`current` advances past `end_pos` and the equality is never satisfied,
producing an infinite loop with the GVL held (SIGINT ineffective).

Add the new guard alongside the existing negative-position check and
extract both into a `validate_position_range` helper shared by both
entrypoints.

Minimal reproducer (public API, used to hang indefinitely):

  RBS::Parser.parse_type("", byte_range: 1..0)

Found by fuzzing the parser entry points with randomized position
arguments.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ksss ksss added this to the RBS 4.1 milestone May 25, 2026
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.

1 participant