From 770ccaa6fb2425f560dd8e69b5f617a5ca2cf7cd Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 2 Jun 2026 08:19:07 +0900 Subject: [PATCH] [ruby/psych] 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. https://github.com/ruby/psych/commit/99ecd94560 Co-Authored-By: Claude Opus 4.8 --- ext/psych/psych_parser.c | 11 +++++++++-- test/psych/test_parser.rb | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/ext/psych/psych_parser.c b/ext/psych/psych_parser.c index d973496284f..41637713930 100644 --- a/ext/psych/psych_parser.c +++ b/ext/psych/psych_parser.c @@ -33,8 +33,15 @@ static int io_reader(void * data, unsigned char *buf, size_t size, size_t *read) if(! NIL_P(string)) { void * str = (void *)StringValuePtr(string); - *read = (size_t)RSTRING_LEN(string); - memcpy(buf, str, *read); + size_t len = (size_t)RSTRING_LEN(string); + + /* IO#read(size) is documented to return at most `size` bytes, but a + * misbehaving IO-like object may return more. Clamp the copy to the + * buffer libyaml gave us to avoid writing past its end. */ + if(len > size) len = size; + + *read = len; + memcpy(buf, str, len); } return 1; diff --git a/test/psych/test_parser.rb b/test/psych/test_parser.rb index c1e0abb89d0..b5db4e96151 100644 --- a/test/psych/test_parser.rb +++ b/test/psych/test_parser.rb @@ -198,6 +198,29 @@ module Psych assert_called :end_stream end + def test_parse_io_returns_more_bytes_than_requested + # An IO-like source whose #read returns more bytes than the size it was + # asked for must not overflow libyaml's read buffer. + io = Object.new + def io.external_encoding; Encoding::UTF_8 end + def io.read len + return nil if @done + @done = true + "--- a\n" + ("#" * (len + (1 << 20))) + end + + # CRuby clamps the over-read and parses; JRuby's parser rejects the + # over-reading IO with an IOError. Either way there is no overflow. + begin + @parser.parse io + rescue IOError + return + end + assert_called :start_stream + assert_called :scalar + assert_called :end_stream + end + def test_syntax_error assert_raise(Psych::SyntaxError) do @parser.parse("---\n\"foo\"\n\"bar\"\n")