v1.1.2
@morojs/engine 1.2.0
Hardening + feature release from a full engine audit: every finding was
verified against source before fixing, and the two most serious bugs were
reproduced before and after the fix. New opt-in API (TLS ticket keys, shared
WebSocket compressor), one new platform, and measurable WebSocket throughput
gains. Backwards-compatible: minor bump.
Fixed — correctness & security
- permessage-deflate silent corruption (critical) — a compressed message
whose DEFLATE stream ended withBFINAL=1(explicitly permitted by
RFC 7692 §7.2.3.6) left the shared inflate stream finished; every later
compressed message on that connection decoded as an empty payload with a
success status. The stream now resets onZ_STREAM_ENDand keeps consuming
(multiple back-to-back streams in one message decode correctly), and the
flush tail is skipped when the stream already ended. Reproduced, fixed,
regression-tested. - mTLS session resumption — servers with
requestCert: truefatally
rejected every session-resumption attempt (missing SSL session-id context);
resumption now works, andrequestCertservers advertise their acceptable
client CAs (matching Node'stls.Serverbehavior) so certificate-picking
clients can respond. - Request smuggling / response-splitting hardening — a
0x00byte in an
app-supplied response header name slipped through validation (astrchr
set-terminator match); rejected now. - Worker-thread safety — the request/WebSocket registries were
process-global; withreusePortworkers (an advertised pattern) two loops
could race on them. All registries are now per-thread. - Spurious 431 on pipelined bodies — the head-size guard counted buffered
body bytes when a fully-buffered pipelined request replayed through one
parse call, rejecting valid requests with bodies overmaxHeadSize. Head
size is now measured on head bytes only (exact old boundary semantics kept). - WebSocket / permessage-deflate conformance — RSV1 on a control frame now
fails the connection when compression is negotiated (RFC 7692 §6.1);
*_max_window_bits=8offers are declined instead of illegally echoed as 9
(§7.1.2.1);client_max_window_bitsis never emitted unless the client
offered it (§7.1.2.2); extension-parameter parsing no longer has an integer
overflow on hostile digit strings. Connection: UPGRADE(any token case) now upgrades; non-token
substrings likenot-upgradeno longer match.- TLS robustness — a truncated/corrupt certificate in an inline PEM chain
orcabundle now failsserve()loudly instead of booting a broken chain;
writes ≥ 2 GiB no longer kill the connection on an integer cast; allocation
failure during session setup sheds the connection instead of crashing.
New
ssl.ticketKeys— Node-compatible 48-byte session-ticket key
pass-through. Give everyreusePortworker (or every process behind one
address) the same keys and TLS sessions resume across all of them. Same
layout and semantics as Node'stls.ServerticketKeys; wrong-length input
throws fromserve(). Key generation/rotation stays the caller's job.wsDeflate.sharedCompressor(opt-in) — one server-owned deflate stream
(reset per message) instead of ~262 KB of deflate state per connection;
per-connection contexts become inflate-only. Forces
server_no_context_takeoverin the negotiated response (RFC 7692 §7.1.1.1);
clients cappingserver_max_window_bitsbelow the shared window
transparently fall back to a per-connection context.- Linux arm64 musl — new
@morojs/engine-linux-arm64-muslpackage and CI
leg: Alpine containers on AWS Graviton / Apple-Silicon Docker now load the
native engine instead of silently falling back tonode:http.
Performance
- WebSocket receive: masked payloads are unmasked with word-wide XOR (was
byte-at-a-time behind a redundant zero-fill), and complete single-frame
messages — the common case — are now unmasked in place in the wire buffer
and delivered zero-copy. Measured: +23% echo throughput at 16 KB payloads
(17.9k → 22.1k msg/s on the reference machine). - Text messages fail fast: UTF-8 is validated incrementally per chunk, so
an invalid 16 MiB text message dies at its first bad byte instead of after
full buffering (close 1007 either way). - Response path: string/Buffer bodies cross the JS boundary with one less
full copy per respond/write/end/wsSend; WebSocket sends reuse the
per-connection scratch buffer; TLS output drains in one copy instead of
16 KB hops. - Memory: parser and message buffers no longer pin huge capacities to idle
keep-alive connections after a large request/message. - Builds: release binaries are compiled with
-fno-exceptions -fno-rtti
and anx86-64-v2baseline on x64 targets.
Behavior changes to note
- Corrupt PEM chain/CA material now throws from
serve()(previously booted
with a broken chain). - Clients offering
*_max_window_bits=8get compression declined (previously
a non-conforming response; conforming clients had to fail the handshake). - HTTP throughput is unchanged (~97k req/s reference, no regression).
Verification
- Full wire-test matrix green: HTTP conformance + edge + hardening +
regression, WebSocket, limits, TLS + TLS-hardening, permessage-deflate —
134 tests, plus 546 C++ unit checks (all also clean under ASan/UBSan). - All four fuzz targets (HTTP parser, WebSocket parser, permessage-deflate,
TLS transport — the TLS harness now completes a real in-memory handshake and
fuzzes the established state) run crash-free. - CI matrix extended: Node 23 smoke, Node 26 integration, macOS conformance,
linux-arm64-musl build/smoke legs.
Platform/ABI matrix: macOS arm64/x64, Linux glibc x64/arm64, Linux musl
x64/arm64, Windows x64 × Node 20–26, prebuilt with npm provenance.