Skip to content

Commit 6b98af4

Browse files
ksssclaude
andcommitted
Reject start_pos > end_pos in parser/lexer entrypoints
`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>
1 parent ac0d8bf commit 6b98af4

4 files changed

Lines changed: 31 additions & 4 deletions

File tree

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ gem "net-smtp"
4848
gem 'csv'
4949
gem 'ostruct'
5050
gem 'pstore'
51+
gem "timeout"
5152

5253
group :minitest do
5354
gem "minitest"

Gemfile.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ DEPENDENCIES
231231
steep!
232232
tempfile
233233
test-unit
234+
timeout
234235

235236
BUNDLED WITH
236237
4.0.1

ext/rbs_extension/main.c

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,17 @@ static VALUE parse_type_try(VALUE a) {
145145
return rbs_struct_to_ruby_value(ctx, type);
146146
}
147147

148-
static rbs_lexer_t *alloc_lexer_from_buffer(rbs_allocator_t *allocator, VALUE string, rb_encoding *encoding, int start_pos, int end_pos) {
148+
static void validate_position_range(int start_pos, int end_pos) {
149149
if (start_pos < 0 || end_pos < 0) {
150150
rb_raise(rb_eArgError, "negative position range: %d...%d", start_pos, end_pos);
151151
}
152+
if (start_pos > end_pos) {
153+
rb_raise(rb_eArgError, "invalid position range: %d...%d", start_pos, end_pos);
154+
}
155+
}
156+
157+
static rbs_lexer_t *alloc_lexer_from_buffer(rbs_allocator_t *allocator, VALUE string, rb_encoding *encoding, int start_pos, int end_pos) {
158+
validate_position_range(start_pos, end_pos);
152159

153160
const char *encoding_name = rb_enc_name(encoding);
154161

@@ -162,9 +169,7 @@ static rbs_lexer_t *alloc_lexer_from_buffer(rbs_allocator_t *allocator, VALUE st
162169
}
163170

164171
static rbs_parser_t *alloc_parser_from_buffer(VALUE buffer, int start_pos, int end_pos) {
165-
if (start_pos < 0 || end_pos < 0) {
166-
rb_raise(rb_eArgError, "negative position range: %d...%d", start_pos, end_pos);
167-
}
172+
validate_position_range(start_pos, end_pos);
168173

169174
VALUE string = rb_funcall(buffer, rb_intern("content"), 0);
170175
StringValue(string);

test/rbs/parser_test.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require "test_helper"
2+
require "timeout"
23

34
class RBS::ParserTest < Test::Unit::TestCase
45
def buffer(source)
@@ -1028,4 +1029,23 @@ class Foo[T < Integer] < Bar # Comment
10281029
assert_equal [:tTRIVIA, "\n", 56...57], tokens.shift.then { |t| [t[0], t[1].source, t[1].range] }
10291030
assert_equal [:pEOF, '', 57...57], tokens.shift.then { |t| [t[0], t[1].source, t[1].range] }
10301031
end
1032+
1033+
def test_invalid_position_range_raises
1034+
# Regression: start_pos > end_pos used to cause an infinite loop in the lexer.
1035+
Timeout.timeout(5) do
1036+
assert_raises(ArgumentError) do
1037+
RBS::Parser._parse_signature(buffer(""), 1, 0)
1038+
end
1039+
end
1040+
end
1041+
1042+
def test_invalid_byte_range_in_parse_type_raises
1043+
# Regression: parse_type's byte_range: keyword reaches _parse_type directly,
1044+
# which used to hang on reversed ranges.
1045+
Timeout.timeout(5) do
1046+
assert_raises(ArgumentError) do
1047+
RBS::Parser.parse_type("", byte_range: 1..0)
1048+
end
1049+
end
1050+
end
10311051
end

0 commit comments

Comments
 (0)