Puma MiniSSL corrupts TLS records under concurrency on TruffleRuby (bad record mac / truncated responses)
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)
# 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] }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
doneExpected
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 chunkedeach) → 0 failures on TruffleRuby. The bug is proportional to write count × concurrency. - A global mutex around
MiniSSL::Socket#writereduces but does not eliminate it (suggesting concurrent read/other ops on the SSL session are also involved). - Suspected cause:
MiniSSLnative SSL state is not safe for concurrent access across Puma worker threads; CRuby's GVL masks this, TruffleRuby's real parallelism exposes it.
Source: puma/puma