Skip to content

Commit 49d5b5f

Browse files
committed
Reject a byte position the lexer cannot start on
`rbs_lexer_new` reaches `start_pos` by stepping one character at a time from the beginning of the buffer, so only the first byte of a character is a position it can stand on. Inside a character the step goes over `start_pos` and lexing quietly begins at the next one. Nothing downstream can tell: the walk keeps `line`, `column` and `char_pos` consistent, so the result points at a real position that simply is not the one asked for. The shift is caught by accident when what it lands on cannot open a token, but an ASCII letter right after the multibyte character parses cleanly in the wrong place -- and #3082 gives the non-ASCII characters tokens of their own, taking even the accident away. Past the end of the buffer there is nothing left to step over, so the walk never finishes: `rbs_skip` stops moving at EOF while the loop waits for a position it will never reach. `parse_type("Integer", byte_range: 20...30)` hangs on master. `rbs_lexer_new` returns `NULL` for a `start_pos` it cannot reach, and the extension turns that into an `ArgumentError` alongside the reversed and negative ranges, since all are the caller's mistake rather than anything about the source text. Starting past the end is plain from the buffer's size, so the extension rejects it before parsing; a `NULL` that comes back after that check can only mean a start inside a character. `end_pos` keeps taking any value: clamping with a large number instead of measuring the buffer is ordinary, and the lexer stops at the end on its own.
1 parent 023fa8a commit 49d5b5f

4 files changed

Lines changed: 98 additions & 14 deletions

File tree

ext/rbs_extension/main.c

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -146,44 +146,66 @@ static VALUE parse_type_try(VALUE a) {
146146
return rbs_struct_to_ruby_value(ctx, type);
147147
}
148148

149-
static void validate_position_range(int start_pos, int end_pos) {
149+
/**
150+
* `end_pos` may point past the end of the buffer: clamping with a large
151+
* number instead of measuring the buffer is ordinary, and the lexer stops at
152+
* the end on its own.
153+
* */
154+
static void validate_position_range(VALUE string, int start_pos, int end_pos) {
150155
if (start_pos < 0 || end_pos < 0) {
151156
rb_raise(rb_eArgError, "negative position range: %d...%d", start_pos, end_pos);
152157
}
153158
if (start_pos > end_pos) {
154159
rb_raise(rb_eArgError, "invalid position range: %d...%d", start_pos, end_pos);
155160
}
161+
162+
long size = RSTRING_LEN(string);
163+
if ((long) start_pos > size) {
164+
rb_raise(rb_eArgError, "position range starts past the end of the buffer: %d...%d, buffer is %ld bytes", start_pos, end_pos, size);
165+
}
156166
}
157167

158168
static rbs_lexer_t *alloc_lexer_from_buffer(rbs_allocator_t *allocator, VALUE string, rb_encoding *encoding, int start_pos, int end_pos) {
159-
validate_position_range(start_pos, end_pos);
169+
validate_position_range(string, start_pos, end_pos);
160170

161171
const char *encoding_name = rb_enc_name(encoding);
162172

163-
return rbs_lexer_new(
173+
rbs_lexer_t *lexer = rbs_lexer_new(
164174
allocator,
165175
rbs_string_from_ruby_string(string),
166176
rbs_encoding_find((const uint8_t *) encoding_name, (const uint8_t *) (encoding_name + strlen(encoding_name))),
167177
start_pos,
168178
end_pos
169179
);
180+
181+
if (lexer == NULL) {
182+
rb_raise(rb_eArgError, "position range starts inside a character: %d...%d", start_pos, end_pos);
183+
}
184+
185+
return lexer;
170186
}
171187

172188
static rbs_parser_t *alloc_parser_from_buffer(VALUE buffer, int start_pos, int end_pos) {
173-
validate_position_range(start_pos, end_pos);
174-
175189
VALUE string = rb_funcall(buffer, rb_intern("content"), 0);
176190
StringValue(string);
177191

192+
validate_position_range(string, start_pos, end_pos);
193+
178194
rb_encoding *encoding = rb_enc_get(string);
179195
const char *encoding_name = rb_enc_name(encoding);
180196

181-
return rbs_parser_new(
197+
rbs_parser_t *parser = rbs_parser_new(
182198
rbs_string_from_ruby_string(string),
183199
rbs_encoding_find((const uint8_t *) encoding_name, (const uint8_t *) (encoding_name + strlen(encoding_name))),
184200
start_pos,
185201
end_pos
186202
);
203+
204+
if (parser == NULL) {
205+
rb_raise(rb_eArgError, "position range starts inside a character: %d...%d", start_pos, end_pos);
206+
}
207+
208+
return parser;
187209
}
188210

189211
static VALUE rbsparser_parse_type(VALUE self, VALUE buffer, VALUE start_pos, VALUE end_pos, VALUE variables, VALUE require_eof, VALUE void_allowed, VALUE self_allowed, VALUE classish_allowed) {

include/rbs/parser.h

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,17 +89,24 @@ RBS_NODISCARD bool rbs_parser_insert_typevar(rbs_parser_t *parser, rbs_constant_
8989
* VALUE string = rb_funcall(buffer, rb_intern("content"), 0);
9090
* rbs_lexer_new(string, 0, 31) // New rbs_lexer_t with buffer content
9191
* ```
92+
*
93+
* Returns `NULL` when `start_pos` is not the first byte of a character in
94+
* `string` -- inside one, or past the end of the buffer. The lexer reaches
95+
* `start_pos` a character at a time, so nowhere else is a position it can
96+
* start from.
9297
* */
93-
rbs_lexer_t *rbs_lexer_new(rbs_allocator_t *, rbs_string_t string, const rbs_encoding_t *encoding, int start_pos, int end_pos);
98+
RBS_NODISCARD rbs_lexer_t *rbs_lexer_new(rbs_allocator_t *, rbs_string_t string, const rbs_encoding_t *encoding, int start_pos, int end_pos);
9499

95100
/**
96101
* Allocate new rbs_parser_t object.
97102
*
98103
* ```
99104
* rbs_parser_new(buffer, string, encoding, 0, 1);
100105
* ```
106+
*
107+
* Returns `NULL` for a `start_pos` that `rbs_lexer_new` rejects.
101108
* */
102-
rbs_parser_t *rbs_parser_new(rbs_string_t string, const rbs_encoding_t *encoding, int start_pos, int end_pos);
109+
RBS_NODISCARD rbs_parser_t *rbs_parser_new(rbs_string_t string, const rbs_encoding_t *encoding, int start_pos, int end_pos);
103110
void rbs_parser_free(rbs_parser_t *parser);
104111

105112
/**

src/parser.c

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3539,12 +3539,18 @@ rbs_lexer_t *rbs_lexer_new(rbs_allocator_t *allocator, rbs_string_t string, cons
35393539
lexer->current_character_bytes = 1;
35403540
}
35413541

3542-
if (start_pos > 0) {
3543-
while (lexer->current.byte_pos < start_pos) {
3544-
rbs_skip(lexer);
3545-
}
3542+
// `rbs_skip` moves a whole character at a time, and moves nothing at all
3543+
// once the input is spent, so this walk can only ever stand on the first
3544+
// byte of a character.
3545+
while (lexer->current.byte_pos < start_pos && lexer->current_code_point != '\0') {
3546+
rbs_skip(lexer);
35463547
}
35473548

3549+
// Stopping anywhere else means `start_pos` is a position the lexer cannot
3550+
// start from: over it, and the walk stepped across a character that
3551+
// straddles it; short of it, and the input ran out first.
3552+
if (lexer->current.byte_pos != start_pos) return NULL;
3553+
35483554
lexer->start = lexer->current;
35493555

35503556
return lexer;
@@ -3554,6 +3560,11 @@ rbs_parser_t *rbs_parser_new(rbs_string_t string, const rbs_encoding_t *encoding
35543560
rbs_allocator_t *allocator = rbs_allocator_init();
35553561

35563562
rbs_lexer_t *lexer = rbs_lexer_new(allocator, string, encoding, start_pos, end_pos);
3563+
if (lexer == NULL) {
3564+
rbs_allocator_free(allocator);
3565+
return NULL;
3566+
}
3567+
35573568
rbs_parser_t *parser = rbs_allocator_alloc(allocator, rbs_parser_t);
35583569

35593570
*parser = (rbs_parser_t) {

test/rbs/type_parsing_test.rb

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,14 +1019,58 @@ def test_parse__range_works
10191019
end
10201020

10211021
def test_parse__byte_range_incorrect
1022-
# We want a better error handling ergonomics, but currently simply raises a syntax error.
1022+
# `"🐕🐈"` is 10 bytes: `"` then two four-byte characters then `"`. Bytes 2
1023+
# to 4 are inside the first of them, and the lexer can only ever stop on a
1024+
# character boundary, so there is no honest answer to give.
10231025

10241026
input = '"🐕🐈"'
10251027

1028+
(2..4).each do |start|
1029+
exn = assert_raises ArgumentError do
1030+
Parser.parse_type(input, byte_range: start...)
1031+
end
1032+
1033+
assert_equal "position range starts inside a character: #{start}...10", exn.message
1034+
end
1035+
1036+
# Byte 5 opens `🐈`, so lexing starts exactly where it was asked to. This
1037+
# is the error the three positions above used to be silently rounded up
1038+
# to.
10261039
exn = assert_raises RBS::ParsingError do
1027-
Parser.parse_type(input, byte_range: 2...)
1040+
Parser.parse_type(input, byte_range: 5...)
10281041
end
10291042

10301043
assert_equal "a.rbs:1:2...1:3: Syntax error: unexpected token for simple type, token=`🐈` (ErrorToken)", exn.message
10311044
end
1045+
1046+
def test_parse__byte_range_starting_past_the_end
1047+
# Reaching `start_pos` means stepping to it, and there is nothing left to
1048+
# step over here, so the lexer used to walk forever.
1049+
exn = assert_raises ArgumentError do
1050+
Parser.parse_type("Integer", byte_range: 20...30)
1051+
end
1052+
1053+
assert_equal "position range starts past the end of the buffer: 20...30, buffer is 7 bytes", exn.message
1054+
1055+
# The end of the buffer itself is a boundary like any other, and an
1056+
# `end_pos` past it is how a caller clamps without measuring.
1057+
assert_nil Parser.parse_type("Integer", byte_range: 7...7)
1058+
assert_equal "Integer", Parser.parse_type("Integer", byte_range: 0...9999).to_s
1059+
end
1060+
1061+
def test_parse__byte_range_incorrect_in_euc_jp
1062+
# In EUC-JP the second byte of a character sits in the same range as a
1063+
# first byte, so decoding at the offset would happily report a character.
1064+
# Only the walk from the start of the buffer knows byte 1 is inside one.
1065+
euc = (+"\xC6\xFCFoo").force_encoding(Encoding::EUC_JP) # 日Foo
1066+
1067+
exn = assert_raises ArgumentError do
1068+
Parser.parse_type(euc, byte_range: 1...)
1069+
end
1070+
1071+
assert_equal "position range starts inside a character: 1...5", exn.message
1072+
1073+
assert_equal RBS::TypeName.parse("Foo"),
1074+
Parser.parse_type(euc, byte_range: 2...).name
1075+
end
10321076
end

0 commit comments

Comments
 (0)