Repository navigation
v0.91: TLS-to-Cloudflare large-response fix
v0.91 closes the long-standing TLS-to-Cloudflare hang on large
responses. With this fix, https.get works against ~1 MB Cloudflare-
fronted endpoints (including www.cloudflare.com and the npm
registry), unblocking registry-based npm install <package-name>
on PPC under ionpower-node — alongside the local-tarball install
path that landed in v0.90.
Driven by docs/sessions/2026-05-11-session-1-node-10-parity-pass-5/notes.md.
Summary
The remaining v0.90 TLS gap — "handshake completes; large
Cloudflare responses hang or eventually SSL_read: bad record mac" — turned out to be the JS TLS pump silently dropping bytes
when our 32 KB BIO pair filled mid-read. The fix is a 1-wave
JS-only change to _TLSSocket.prototype._onRawData: instead of
breaking the bioWrite loop on a 0-return, drain via SSL_read to
free space in the BIO ring buffer, then retry the unwritten tail.
Tests: 469 (v0.90) → 470 (v0.91, +1 new). G3 + G4 + G5 triad-
builds clean. All existing TLS smokes (tls_smoke,
https_get_smoke, https_server_smoke, wss_smoke,
axios_smoke, node_fetch_smoke) continue to pass.
What's new (pass 5)
_TLSSocket._onRawData drains BIO before retrying on stall
tls.cpp provisions the network-side BIO via BIO_new_bio_pair
with a fixed 32 KB ring buffer. Unlike BIO_s_mem (which grows on
demand), the BIO pair returns 0 from BIO_write once the ring is
full and the caller is expected to drain via SSL_read before
retrying. When _Socket.readFd delivers a single chunk larger
than 32 KB (which is routine for Cloudflare responses — a Cloudflare
edge often coalesces ~40-50 KB into one TCP read once warmed up),
the pump's bioWrite loop wrote what it could and silently
dropped the remaining bytes via if (n <= 0) break;. The next
TCP read brought the bytes that came AFTER the dropped tail; we
fed them to the BIO and SSL_read saw records with non-contiguous
ciphertext — AEAD tag verification failed → error:1408F119: SSL routines:ssl3_get_record:decryption failed or bad record mac on
the next record (consistently observed around 50 KB plaintext).
Fix: when bioWrite returns 0, drive _pumpRead (post-handshake)
or _driveHandshake (pre-handshake) to drain plaintext via
SSL_read — this frees BIO space — then _flushOutgoing (in case
SSL_read emitted any KeyUpdate-style records), then retry the
unwritten tail. Cap at 64 stalls as a runaway guard (a real 1 MB
workload hits 2 stalls).
Verified end-to-end:
- Raw TLS to
www.cloudflare.com: 981 KB delivered, TLS end
clean. https.get('https://www.cloudflare.com/'): STATUS 200,
chunked body 978 KB parsed cleanly.https.get('https://registry.npmjs.org/<pkg>'): STATUS 200,
content-length-bounded body delivered.
Smoke: test/tls_cloudflare_smoke.js
— issues https.get('https://www.cloudflare.com/'), asserts
status 200, asserts body >= 200 KB.
demos/npm-install/
New demo: real npm-6 invoking against a vendored mri-1.2.0
tarball, end-to-end install + require + sanity-check. Companion
to the existing hand-rolled demos/npm-fetch/. See
demos/npm-install/README.md.
Note on smoke flakiness vs fix correctness
The new tls_cloudflare_smoke.js is shaped to distinguish a real
regression (SSL_read fails with "bad record mac" or similar —
fail hard, no retry) from a transient Cloudflare-edge issue
(timeout with no bytes flowing — SKIP and exit 0). Manual trials
during this pass showed the live www.cloudflare.com edge is
sometimes slow to start replying from certain source IPs / source
networks. The fix itself is correct: when a connection DOES
succeed, the full ~1 MB body downloads cleanly. The SKIP path
keeps CI from blocking on Cloudflare's edge unreliability.
What's still pending
process.getuid/getgid/geteuid/getegid. Tiny
POSIX wrappers. Not blocking npm install on Tiger as a normal
user (tar'spreserveOwnergate short-circuits to false when
process.getuidis absent — which is the correct non-root
behavior), but some lifecycle scripts may want them. Deferred
to v0.92.JS_ReportError → ThrowFsErrorin remainingfs.cpp
sites (writeFileSync, appendFileSync, copyFileSync, chmodSync).
Cosmetic; the error shape is wrong but rarely caught.fs.promises.read/fs.promises.write{bytesRead, buffer}return shape. Not hit by npm-6.14.18 install path.- ES modules first-class. Needs SM 60+; long-term.
Compatibility
Same SpiderMonkey, same OpenSSL, same JIT. Existing v0.90 scripts
run unchanged. The TLS fix is strictly additive — pre-fix
behavior was "drop bytes, fail with bad record mac on next
record"; post-fix is "drain, retry, succeed".
Tarballs
ionpower-node-0.91-g3-ppc.tar.gz— G3 (PPC 750)ionpower-node-0.91-g4-ppc.tar.gz— G4 (PPC 7450)ionpower-node-0.91-g5-ppc.tar.gz— G5 (PPC 970)
Each pairs with the matching mozjs-45-ionpower-{g3,g4,g5}.tar.gz
from the v0.87 release
(SpiderMonkey hasn't changed since then; reuse those).