Skip to content

Commit 99ecd94

Browse files
hsbtclaude
andcommitted
Clamp io_reader copy to libyaml's buffer size
io_reader trusted IO#read to honour its size argument and copied RSTRING_LEN(string) bytes into libyaml's fixed-capacity buffer. An IO-like object whose #read over-returns wrote past the buffer end, a heap out-of-bounds write reachable from Psych.load/safe_load/parse. Clamp the copied length to the requested size. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7a73514 commit 99ecd94

2 files changed

Lines changed: 32 additions & 2 deletions

File tree

ext/psych/psych_parser.c

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,15 @@ static int io_reader(void * data, unsigned char *buf, size_t size, size_t *read)
3333

3434
if(! NIL_P(string)) {
3535
void * str = (void *)StringValuePtr(string);
36-
*read = (size_t)RSTRING_LEN(string);
37-
memcpy(buf, str, *read);
36+
size_t len = (size_t)RSTRING_LEN(string);
37+
38+
/* IO#read(size) is documented to return at most `size` bytes, but a
39+
* misbehaving IO-like object may return more. Clamp the copy to the
40+
* buffer libyaml gave us to avoid writing past its end. */
41+
if(len > size) len = size;
42+
43+
*read = len;
44+
memcpy(buf, str, len);
3845
}
3946

4047
return 1;

test/psych/test_parser.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,29 @@ def test_parse_io
198198
assert_called :end_stream
199199
end
200200

201+
def test_parse_io_returns_more_bytes_than_requested
202+
# An IO-like source whose #read returns more bytes than the size it was
203+
# asked for must not overflow libyaml's read buffer.
204+
io = Object.new
205+
def io.external_encoding; Encoding::UTF_8 end
206+
def io.read len
207+
return nil if @done
208+
@done = true
209+
"--- a\n" + ("#" * (len + (1 << 20)))
210+
end
211+
212+
# CRuby clamps the over-read and parses; JRuby's parser rejects the
213+
# over-reading IO with an IOError. Either way there is no overflow.
214+
begin
215+
@parser.parse io
216+
rescue IOError
217+
return
218+
end
219+
assert_called :start_stream
220+
assert_called :scalar
221+
assert_called :end_stream
222+
end
223+
201224
def test_syntax_error
202225
assert_raise(Psych::SyntaxError) do
203226
@parser.parse("---\n\"foo\"\n\"bar\"\n")

0 commit comments

Comments
 (0)