Skip to content

v1.1.2

Choose a tag to compare

@M-Chris M-Chris released this 11 Jul 05:16
· 24 commits to main since this release

@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 with BFINAL=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 on Z_STREAM_END and 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: true fatally
    rejected every session-resumption attempt (missing SSL session-id context);
    resumption now works, and requestCert servers advertise their acceptable
    client CAs (matching Node's tls.Server behavior) so certificate-picking
    clients can respond.
  • Request smuggling / response-splitting hardening — a 0x00 byte in an
    app-supplied response header name slipped through validation (a strchr
    set-terminator match); rejected now.
  • Worker-thread safety — the request/WebSocket registries were
    process-global; with reusePort workers (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 over maxHeadSize. 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=8 offers are declined instead of illegally echoed as 9
    (§7.1.2.1); client_max_window_bits is 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 like not-upgrade no longer match.
  • TLS robustness — a truncated/corrupt certificate in an inline PEM chain
    or ca bundle now fails serve() 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 every reusePort worker (or every process behind one
    address) the same keys and TLS sessions resume across all of them. Same
    layout and semantics as Node's tls.Server ticketKeys; wrong-length input
    throws from serve(). 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_takeover in the negotiated response (RFC 7692 §7.1.1.1);
    clients capping server_max_window_bits below the shared window
    transparently fall back to a per-connection context.
  • Linux arm64 musl — new @morojs/engine-linux-arm64-musl package and CI
    leg: Alpine containers on AWS Graviton / Apple-Silicon Docker now load the
    native engine instead of silently falling back to node: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 an x86-64-v2 baseline 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=8 get 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.