Skip to content

negative memcpy size in io_gets #6646

Description

@tim-becker

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();
       }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions