Description: io_ungetc uses a 16-bit signed 'buf->len' to track the current buffer length but only checks the length of the input string, not the cumulative total. Repeated IO#ungetc calls cause 'buf->len' to overflow past SHRT_MAX into a negative value, resulting in memmove being invoked with a negative size parameter.
Trigger conditions: Repeated ungetc operations (e.g., pushing 1024-byte strings) until the cumulative buffer length exceeds SHRT_MAX (~32767) cause the signed short 'buf->len' to wrap to a negative value, triggering the crash.
Harness name: mruby_fuzzer
Crashing input:
r, w = IO.pipe
w.write("test data")
w.close
# Try multiple large ungetc operations that might cause buffer issues
1000.times do |i|
begin
r.ungetc("X" * 1024)
rescue => e
puts "ungetc error at #{i}: #{e}"
break
end
end
r.close
Crash output:
+ FUZZER=mruby_fuzzer
+ shift
+ '[' '!' -v TESTCASE ']'
+ TESTCASE=/testcase
+ '[' '!' -f /testcase ']'
+ export RUN_FUZZER_MODE=interactive
+ RUN_FUZZER_MODE=interactive
+ export FUZZING_ENGINE=libfuzzer
+ FUZZING_ENGINE=libfuzzer
+ export SKIP_SEED_CORPUS=1
+ SKIP_SEED_CORPUS=1
+ run_fuzzer mruby_fuzzer /testcase
sysctl: setting key "vm.mmap_rnd_bits", ignoring: Read-only file system
Dictionary: 102 entries
/out/mruby_fuzzer: Running 1 inputs 1 time(s) each.
Running: /testcase
=================================================================
==43==ERROR: AddressSanitizer: negative-size-param: (size=-32768)
SCARINESS: 10 (negative-size-param)
Stack Frame #0 in __asan_memmove /src/llvm-project/compiler-rt/lib/asan/asan_interceptors_memintrinsics.cpp:71:3
Stack Frame #1 in io_ungetc io.c
Stack Frame #2 in mrb_vm_exec (/out/mruby_fuzzer+0x2e9297)
Stack Frame #3 in mrb_vm_run (/out/mruby_fuzzer+0x2d18e2)
Stack Frame #4 in mrb_top_run (/out/mruby_fuzzer+0x32d589)
Stack Frame #5 in mrb_load_exec (/out/mruby_fuzzer+0x3c61c5)
Stack Frame #6 in mrb_load_nstring_cxt (/out/mruby_fuzzer+0x3c712c)
Stack Frame #7 in mrb_load_string_cxt (/out/mruby_fuzzer+0x3c7260)
Stack Frame #8 in mrb_load_string (/out/mruby_fuzzer+0x3c72d8)
Stack Frame #9 in LLVMFuzzerTestOneInput (/out/mruby_fuzzer+0x2a7c5b)
Stack Frame #10 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:614:13
Stack Frame #11 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:327:6
Stack Frame #12 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:862:9
Stack Frame #13 in main /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerMain.cpp:20:10
Stack Frame #14 in __libc_start_main /build/glibc-B3wQXB/glibc-2.31/csu/../csu/libc-start.c:308:16
Stack Frame #15 in _start (/out/mruby_fuzzer+0x13fb0d)
DEDUP_TOKEN: __asan_memmove--io_ungetc--mrb_vm_exec--mrb_vm_run--mrb_top_run
0x52d000000404 is located 4 bytes inside of 32772-byte region [0x52d000000400,0x52d000008404)
allocated by thread T0 here:
Stack Frame #0 in realloc /src/llvm-project/compiler-rt/lib/asan/asan_malloc_linux.cpp:82:3
Stack Frame #1 in mrb_basic_alloc_func (/out/mruby_fuzzer+0x2a9955)
Stack Frame #2 in mrb_realloc_simple (/out/mruby_fuzzer+0x41ecc0)
Stack Frame #3 in mrb_realloc (/out/mruby_fuzzer+0x41f417)
Stack Frame #4 in io_ungetc io.c
Stack Frame #5 in mrb_vm_exec (/out/mruby_fuzzer+0x2e9297)
Stack Frame #6 in mrb_vm_run (/out/mruby_fuzzer+0x2d18e2)
Stack Frame #7 in mrb_top_run (/out/mruby_fuzzer+0x32d589)
Stack Frame #8 in mrb_load_exec (/out/mruby_fuzzer+0x3c61c5)
Stack Frame #9 in mrb_load_nstring_cxt (/out/mruby_fuzzer+0x3c712c)
Stack Frame #10 in mrb_load_string_cxt (/out/mruby_fuzzer+0x3c7260)
Stack Frame #11 in mrb_load_string (/out/mruby_fuzzer+0x3c72d8)
Stack Frame #12 in LLVMFuzzerTestOneInput (/out/mruby_fuzzer+0x2a7c5b)
Stack Frame #13 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:614:13
Stack Frame #14 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:327:6
Stack Frame #15 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:862:9
Stack Frame #16 in main /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerMain.cpp:20:10
Stack Frame #17 in __libc_start_main /build/glibc-B3wQXB/glibc-2.31/csu/../csu/libc-start.c:308:16
DEDUP_TOKEN: __interceptor_realloc--mrb_basic_alloc_func--mrb_realloc_simple--mrb_realloc--io_ungetc
SUMMARY: AddressSanitizer: negative-size-param io.c in io_ungetc
==43==ABORTING
/out/mruby_fuzzer -rss_limit_mb=2560 -timeout=25 /testcase -dict=mruby.dict -only_ascii=1 < /dev/null
Patch:
--- a/mrbgems/mruby-io/src/io.c
+++ b/mrbgems/mruby-io/src/io.c
@@ -1552,6 +1552,9 @@
mrb_get_args(mrb, "S", &str);
mrb_int len = RSTRING_LEN(str);
+ if (len > SHRT_MAX - buf->len) {
+ mrb_raise(mrb, E_ARGUMENT_ERROR, "ungetc buffer would overflow");
+ }
if (len > SHRT_MAX) {
mrb_raise(mrb, E_ARGUMENT_ERROR, "string too long to ungetc");
}