tim-becker · GitHub

Description: The io_gets function accepts a negative limit argument and propagates it unchecked into io_buf_cat, which then calls mrb_str_cat with a negative length. This leads to a negative-size memcpy and abort via AddressSanitizer.

Trigger conditions: A Ruby program calling IO.gets with a negative integer (e.g., r.gets(-1)) on a non-empty IO buffer triggers a negative limit. The code treats the negative limit as n <= buf->len, calls io_buf_cat with n = -1, and crashes in mrb_str_cat due to a negative-size memcpy.

Harness name: mruby_fuzzer

Crashing input:


# exploit io_gets negative length overflow
r,w = IO.pipe
w.write('A'*100 + "\n")
w.close
begin
  r.gets(-1)
rescue => e
end

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=-1)
SCARINESS: 10 (negative-size-param)
Stack Frame #0 in __asan_memcpy /src/llvm-project/compiler-rt/lib/asan/asan_interceptors_memintrinsics.cpp:63:3
Stack Frame #1 in mrb_str_cat (/out/mruby_fuzzer+0x33cbcd)
Stack Frame #2 in io_buf_cat io.c
Stack Frame #3 in io_gets io.c
Stack Frame #4 in mrb_vm_exec (/out/mruby_fuzzer+0x2e9297)
Stack Frame #5 in mrb_vm_run (/out/mruby_fuzzer+0x2d18e2)
Stack Frame #6 in mrb_top_run (/out/mruby_fuzzer+0x32d589)
Stack Frame #7 in mrb_load_exec (/out/mruby_fuzzer+0x3c61c5)
Stack Frame #8 in mrb_load_nstring_cxt (/out/mruby_fuzzer+0x3c712c)
Stack Frame #9 in mrb_load_string_cxt (/out/mruby_fuzzer+0x3c7260)
Stack Frame #10 in mrb_load_string (/out/mruby_fuzzer+0x3c72d8)
Stack Frame #11 in LLVMFuzzerTestOneInput (/out/mruby_fuzzer+0x2a7c5b)
Stack Frame #12 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:614:13
Stack Frame #13 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:327:6
Stack Frame #14 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:862:9
Stack Frame #15 in main /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerMain.cpp:20:10
Stack Frame #16 in __libc_start_main /build/glibc-B3wQXB/glibc-2.31/csu/../csu/libc-start.c:308:16
Stack Frame #17 in _start (/out/mruby_fuzzer+0x13fb0d)
DEDUP_TOKEN: __asan_memcpy--mrb_str_cat--io_buf_cat--io_gets--mrb_vm_exec
0x521000006504 is located 4 bytes inside of 4100-byte region [0x521000006500,0x521000007504)
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 mrb_malloc (/out/mruby_fuzzer+0x41f5c8)
Stack Frame #5 in io_init_buf io.c
Stack Frame #6 in io_s_pipe io.c
Stack Frame #7 in mrb_vm_exec (/out/mruby_fuzzer+0x2e9297)
Stack Frame #8 in mrb_vm_run (/out/mruby_fuzzer+0x2d18e2)
Stack Frame #9 in mrb_top_run (/out/mruby_fuzzer+0x32d589)
Stack Frame #10 in mrb_load_exec (/out/mruby_fuzzer+0x3c61c5)
Stack Frame #11 in mrb_load_nstring_cxt (/out/mruby_fuzzer+0x3c712c)
Stack Frame #12 in mrb_load_string_cxt (/out/mruby_fuzzer+0x3c7260)
Stack Frame #13 in mrb_load_string (/out/mruby_fuzzer+0x3c72d8)
Stack Frame #14 in LLVMFuzzerTestOneInput (/out/mruby_fuzzer+0x2a7c5b)
Stack Frame #15 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerLoop.cpp:614:13
Stack Frame #16 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:327:6
Stack Frame #17 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerDriver.cpp:862:9
Stack Frame #18 in main /src/llvm-project/compiler-rt/lib/fuzzer/FuzzerMain.cpp:20:10
Stack Frame #19 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--mrb_malloc
SUMMARY: AddressSanitizer: negative-size-param (/out/mruby_fuzzer+0x33cbcd) in mrb_str_cat
==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
@@ -1741,6 +1741,10 @@
   mrb_get_args(mrb, "|o?i?", &rs, &rs_given, &limit, &limit_given);
+  if (limit_given && limit < 0) {
+    mrb_raisef(mrb, E_ARGUMENT_ERROR, "negative limit %i given", limit);
+  }
+
   if (limit_given == FALSE) {
     if (rs_given) {
       if (mrb_nil_p(rs)) {
@@ -1748,6 +1752,9 @@
       }
       else if (mrb_integer_p(rs)) {
         limit = mrb_integer(rs);
+        if (limit < 0) {
+          mrb_raisef(mrb, E_ARGUMENT_ERROR, "negative limit %i given", limit);
+        }
         limit_given = TRUE;
         rs = mrb_nil_value();
       }

Read the original on github.com ↗