Skip to content

Zstd.write_skippable_frame reads out of bounds (heap-buffer-overflow) in rb_str_new #136

Description

@Watson1978

Zstd.write_skippable_frame(input, skippable) triggers a heap-buffer-overflow (out-of-bounds read) detected by AddressSanitizer. In skippable_frame.c the output buffer is initialized by copying dst_size bytes out of input_data, but input_data is only input_size bytes long, and dst_size = input_size + ZSTD_SKIPPABLEHEADERSIZE + skip_size > input_size.

Affected code

ext/zstdruby/skippable_frame.c:26

size_t dst_size = input_size + ZSTD_SKIPPABLEHEADERSIZE + skip_size;
VALUE output = rb_str_new(input_data, dst_size);  // reads dst_size bytes from an input_size-byte buffer

Environment

  • OS: Linux 7.1.1-2
  • Ruby: ruby 4.0.5
  • Compiler: gcc 16.1.1

Reproduction

Build the extension with ASan and run under LD_PRELOAD'd libasan (Ruby itself is not ASan-instrumented, and extconf.rb assigns $CFLAGS, so append the flags inside extconf.rb before create_makefile):

diff --git a/ext/zstdruby/extconf.rb b/ext/zstdruby/extconf.rb
index f963367..bfe8fa6 100644
--- a/ext/zstdruby/extconf.rb
+++ b/ext/zstdruby/extconf.rb
@@ -54,4 +54,7 @@ def ext_export_filename
 # add folder, where compiler can search source files
 $VPATH << "$(srcdir)"
 
+$CFLAGS  << ' -fsanitize=address -fno-omit-frame-pointer -g -O0'
+$LDFLAGS << ' -fsanitize=address'
+
 create_makefile("zstd-ruby/zstdruby")
# repro.rb
require 'zstd-ruby'

payload = "A" * 1024
Zstd.write_skippable_frame(payload, "sample data")

puts "reached end without ASan error"
$ rake install:local
$ LD_PRELOAD=$(gcc -print-file-name=libasan.so) ASAN_OPTIONS=detect_leaks=0 ruby repro.rb
=================================================================
==17416==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7cec25634581 at pc 0x7f5c2755d772 bp 0x7ffc0b087190 sp 0x7ffc0b086938
READ of size 1043 at 0x7cec25634581 thread T0
    #0 0x7f5c2755d771 in memcpy (/usr/lib/gcc/x86_64-pc-linux-gnu/16/../../../../lib/libasan.so+0x15d771) (BuildId: b3c854c755b60b866f8e124222a7a865d450079f)
    #1 0x7f5c26d0253e in memcpy /usr/include/bits/string_fortified.h:29
    #2 0x7f5c26d0253e in ruby_nonempty_memcpy include/ruby/internal/memory.h:759
    #3 0x7f5c26d0253e in ruby_nonempty_memcpy include/ruby/internal/memory.h:756
    #4 0x7f5c26d0253e in str_enc_new /tmp/ruby-build.20260520095325.246649.XiGeNt/ruby-4.0.5/string.c:1089
    #5 0x7b5c06ec26ed in rb_write_skippable_frame /home/watson/.rbenv/versions/4.0.5/lib/ruby/gems/4.0.0/gems/zstd-ruby-2.0.6/ext/zstdruby/skippable_frame.c:26
    #6 0x7f5c26d8c50b in vm_call_cfunc_with_frame_ /tmp/ruby-build.20260520095325.246649.XiGeNt/ruby-4.0.5/vm_insnhelper.c:3901
    #7 0x7f5c26da465c in vm_sendish /tmp/ruby-build.20260520095325.246649.XiGeNt/ruby-4.0.5/vm_insnhelper.c:6123
    #8 0x7f5c26da465c in vm_exec_core /tmp/ruby-build.20260520095325.246649.XiGeNt/ruby-4.0.5/insns.def:904
    #9 0x7f5c26daa8e9 in vm_exec_loop /tmp/ruby-build.20260520095325.246649.XiGeNt/ruby-4.0.5/vm.c:2825
    #10 0x7f5c26daa8e9 in rb_vm_exec /tmp/ruby-build.20260520095325.246649.XiGeNt/ruby-4.0.5/vm.c:2804
    #11 0x7f5c26b68b58 in rb_ec_exec_node /tmp/ruby-build.20260520095325.246649.XiGeNt/ruby-4.0.5/eval.c:283
    #12 0x7f5c26b6c8da in ruby_run_node /tmp/ruby-build.20260520095325.246649.XiGeNt/ruby-4.0.5/eval.c:321
    #13 0x56206a66d0ff in rb_main main.c:42
    #14 0x56206a66d0ff in main main.c:62
    #15 0x7f5c26627c8d  (/usr/lib/libc.so.6+0x27c8d) (BuildId: e40d0fb39fa49848e0042984207f7c80fdbc8c7e)
    #16 0x7f5c26627dca in __libc_start_main (/usr/lib/libc.so.6+0x27dca) (BuildId: e40d0fb39fa49848e0042984207f7c80fdbc8c7e)
    #17 0x56206a66d144 in _start (/home/watson/.rbenv/versions/4.0.5/bin/ruby+0x1144) (BuildId: 9c4c4eb747648adb3b4c85bd02313989f516f550)

0x7cec25634581 is located 0 bytes after 1025-byte region [0x7cec25634180,0x7cec25634581)
allocated by thread T0 here:
    #0 0x7f5c27560181 in malloc (/usr/lib/gcc/x86_64-pc-linux-gnu/16/../../../../lib/libasan.so+0x160181) (BuildId: b3c854c755b60b866f8e124222a7a865d450079f)
    #1 0x7f5c26b957f2 in rb_gc_impl_malloc gc/default/default.c:8294
    #2 0x7f5c26b957f2 in ruby_xmalloc_body /tmp/ruby-build.20260520095325.246649.XiGeNt/ruby-4.0.5/gc.c:5144
    #3 0x7f5c26b957f2 in ruby_xmalloc /tmp/ruby-build.20260520095325.246649.XiGeNt/ruby-4.0.5/gc.c:5126

SUMMARY: AddressSanitizer: heap-buffer-overflow /usr/include/bits/string_fortified.h:29 in memcpy
Shadow bytes around the buggy address:
  0x7cec25634300: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7cec25634380: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7cec25634400: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7cec25634480: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7cec25634500: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x7cec25634580:[01]fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7cec25634600: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7cec25634680: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7cec25634700: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7cec25634780: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7cec25634800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==17416==ABORTING

Suggested fix

The buffer is fully overwritten by ZSTD_writeSkippableFrame afterward, so it does not need to be initialized from input_data:

-  VALUE output = rb_str_new(input_data, dst_size);
+  VALUE output = rb_str_new(NULL, dst_size);

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions