#3975·puma

Puma MiniSSL corrupts TLS records under concurrency on TruffleRuby (bad record mac / truncated responses)

Author: WilliamGusmanovCreated Jul 13, 2026Updated Jul 13, 2026

Original issue: https://github.com/truffleruby/truffleruby/issues/4371 is suspected to be an issue with Puma.

Summary

Serving large HTTPS responses concurrently through Puma's MiniSSL (its TLS C-extension) produces corrupted TLS records on TruffleRuby. Clients get SSL_read failures / truncated bodies; Puma sometimes logs Puma::MiniSSL::SSLError: OpenSSL error: error:0A0003FC:SSL routines::ssl/tls alert bad record mac. The same code is 100% reliable on CRuby (MRI). It reproduces on TruffleRuby 25.0.0 and 33.0.1, and with Puma 6.6.1 and 8.0.2.

The failure scales with the number of socket writes per response: responses written in many chunks (streamed bodies) fail frequently under concurrency; single-write responses never fail. This is consistent with concurrent SSL_write calls touching shared native SSL state that CRuby's GVL serializes but TruffleRuby (true parallelism) does not.

Environment

  • TruffleRuby: 25.0.0 and 33.0.1 (truffleruby-jvm, like ruby 3.3.7, Oracle GraalVM JVM), linux-aarch64 and linux-amd64
  • Puma: 6.6.1 and 8.0.2 (uses Puma::MiniSSL)
  • Control: MRI CRuby 3.3 — cannot reproduce (0 failures)

Reproducer (self-contained; no framework)

ruby
# config.ru — ~10 MB response delivered in ~160 chunks (multi-write body)
BODY   = 'x' * (10 * 1024 * 1024)
CHUNKS = BODY.scan(/.{1,65536}/m)
class Streamed
  def each; CHUNKS.each { |c| yield c }; end
end
run ->(_env) { [200, { 'content-type' => 'application/octet-stream' }, Streamed.new] }
bash
openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 1 -subj /CN=localhost
gem install puma -v 6.6.1
puma -b 'ssl://0.0.0.0:9292?key=key.pem&cert=cert.pem&verify_mode=none' -t 8:32 config.ru &

# concurrent clients — several will truncate (curl exit 56) instead of returning 10485760 bytes
for r in 1 2 3; do
  for i in $(seq 1 12); do
    curl -sk https://localhost:9292/ -o /dev/null -w '%{size_download}:%{exitcode}\n' &
  done; wait
done

Expected

Every request returns 10485760 bytes, exit 0.

Actual (TruffleRuby)

~14/36 requests truncate at varying offsets with curl exit 56 (SSL_read failure); Puma intermittently logs bad record mac. Identical run on MRI: 0/36 failures.

Notes / narrowing

  • Rewriting the body as a single write ([BODY] instead of the chunked each) → 0 failures on TruffleRuby. The bug is proportional to write count × concurrency.
  • A global mutex around MiniSSL::Socket#write reduces but does not eliminate it (suggesting concurrent read/other ops on the SSL session are also involved).
  • Suspected cause: MiniSSL native SSL state is not safe for concurrent access across Puma worker threads; CRuby's GVL masks this, TruffleRuby's real parallelism exposes it.