Skip to content

Releases: faratech/httpjet

httpjet v0.0.5

Choose a tag to compare

@faratech faratech released this 12 Oct 20:38

httpjet 0.0.5 — whole-codebase audit remediation

This release fixes the 15 findings of a whole-codebase bug sweep (2026-10-12: four parallel reviewers across the transport, page-cache, LSAPI/rewrite, and proxy/ACL/TLS surfaces, every finding hand-verified in source and the worst one reproduced dynamically). Every fix ships with a regression test. Dependencies are refreshed to their latest releases; the toolchain stays at Rust 1.99.

Availability (high)

  • Rewrite [S=n] infinite loop. A rule such as RewriteRule ^x$ - [S=18446744073709551615] wrapped the evaluator's rule index back onto itself in release builds, pinning a worker core forever per matching request. Skip advance now saturates past the end of the ruleset.
  • Unbounded HTTP/2 upstream handshake in the proxy. The h2:///h2s:// handshake could stall indefinitely at the IO layer, pinning a permit from the process-wide dial budget (64) plus an upstream maxConns slot. It now runs under the connect budget, mirroring the health-check probe.
  • HTTP/2 reset without teardown. After the server sent RST_STREAM for a body-pending stream (zero WINDOW_UPDATE increment, self-referential PRIORITY), a later DATA … END_STREAM still completed and dispatched the already-reset request. The reset path now tears down the receiving stream and returns its buffered-body accounting.

Proxy correctness

  • WebSocket targets enforce maxConns. Default (non-load-balanced) WebSocket upstreams acquired no concurrency permit at all; the long-lived relay was bounded only by global client limits. A permit is now acquired and held for the relay's lifetime.
  • Failover survives config reloads. Retained upstreams kept writing dial failures to the previous generation's cooldown counter, so after a reload the new pool never saw the primary as bad and fast-failed instead of failing over. Cooldowns are carried into the retained generation and handles are rebound on selection.

Request handling and PHP trust boundary

  • Forwarding-header synonyms. The blocklist for proxy-asserted identity headers was an exact-name list; X-Client-IP, X-Forwarded-Protocol, Proxy-Client-IP, and similar synonyms from untrusted clients reached PHP as spoofable $_SERVER values. Matching is now class-based (the *-client-ip, x-forwarded-*, and *-real-ip families, dash/underscore-folded) and fails closed.
  • CGI env overrides. [E=…] could replace server-computed REQUEST_METHOD, REQUEST_URI, PATH_INFO, CONTENT_*, GATEWAY_INTERFACE, and REQUEST_TIME*; all are now on the override deny-list.
  • WAF fail-open no longer corrupts requests. A sidecar timeout mid-body-collection used to forward an emptied body downstream; the body is now collected and restored before the timed inspection exchange.

HTTP/3, page cache, and LSAPI lifecycle

  • H3 request targets. A network-path :path (//evil.example/x) minted a URI authority diverging from the routed Host; rejected. QPACK field sections with a nonzero Required Insert Count or Delta Base are rejected (the dynamic table is advertised at capacity 0).
  • Purge completeness. /__hj_cache_purge honors every X-LiteSpeed-Purge header line instead of only the first, and the tag-purge version sweep can no longer unlink a live re-stored page's body file. enablePostCache now warns (config lint + startup log) instead of being silently inert.
  • LSAPI lifecycle. Restart backoff is cancellable on shutdown, registry drain has a wall-clock bound so a mid-start pool cannot be orphaned, and a jail chroot configured without credentials is a hard spawn error rather than a silent skip.

Dependencies

Cargo.lock refreshed to the latest semver-compatible releases (aws-lc-rs 1.18.2, zstd 0.14.2, serde_json 1.0.154, tokio-util 0.7.20, OpenTelemetry 0.33.1, proptest 1.12, and more); all manifests already required the latest majors. cargo audit is clean and the full test suite (2,028 tests) passes.

Production deployments should follow the usual path: scripts/build-pgo.sh, then scripts/deploy-lscache.sh and the cache gate scripts.

httpjet v0.0.4

Choose a tag to compare

@faratech faratech released this 07 Oct 03:03

httpjet 0.0.4 — security release

This release fixes the findings of a whole-codebase security review (2026-10-06) and moves every dependency and the toolchain to their latest versions. Upgrading is recommended for anyone serving HTTP/3.

HTTP/3 (high)

  • Connection-budget exhaustion before address validation. A new QUIC connection used to take a permit from the connection budget shared with TCP before the client's address was validated, so spoofed Initials could starve TCP accepts. The budget is now charged when the handshake completes. Once 64 handshakes are pending on a core, Initials from unvalidated addresses get a stateless Retry. Handshakes are capped at 256 per core and time out after 10 s.
  • quinn-proto 0.11.19 (GHSA-53rm-773f-4q8c: one MAX_STREAM_DATA frame could open 2^40 streams). The request-stream accept loop also enforces its own bound, independent of quinn: at most 256 new request streams per connection drive and 512 tracked requests per connection, otherwise the connection closes with H3_EXCESSIVE_LOAD. The same quinn release fixes a client-side Retry issue (GHSA-wppq-2f6r-wfvm) that does not affect httpjet's server role. Thanks to the quinn maintainers for the fixes.

Request handling

  • Rewrite targets are decoded once. An internal rewrite target is percent-decoded and normalized once, matching Apache's internal-redirect semantics. Previously, destination-scoped policy (Header directives, body limits, static contexts) judged the still-encoded path while the static handler decoded and served it.
  • Client headers reaching PHP/CGI follow one rule. This applies to the LSAPI env table, the LSAPI header block and FastCGI params. Underscore-named headers (which lsphp mapped to HTTP_* through the header block), Proxy, and forwarding-authority headers (CF-Connecting-IP, X-Forwarded-*, X-Real-IP, True-Client-IP, Forwarded) from untrusted peers no longer reach the application.
  • HTTP/1.1 request targets and Host. RFC 9112 target forms and a valid Host are required. An absolute-form authority replaces Host. CONNECT and heads with bare-LF line endings are refused before dispatch.
  • Basic auth.
    • AuthUserFile must open inside the docroot, checked on the opened descriptor. Use --auth-user-file-roots for other locations.
    • Unknown users pay the same bcrypt cost as known ones.
    • bcrypt cost is capped at 14.
    • Plaintext htpasswd entries no longer verify, as in Apache on Unix.
    • An unusable user file returns 500.

Control surfaces

  • /__hj_cache_purge accepts only POST (405 + Allow otherwise). /__hj_cache_ready accepts only GET/HEAD. check warns that unix-socket listeners expose both.
  • The page-cache integrity key is opened through a validated path. httpjet_pagecache_integrity_active reports its state, and --page-cache-integrity-required refuses to run the file tier without it.
  • SIGHUP reloads time out instead of waiting indefinitely. Config files are size-capped. Changes to proxyProtocol, proxyProtocolPeers or a unix-socket path are classified as restart-required.
  • New --profile-token-file. The metrics listener is capped at 64 concurrent connections. The lsphp control socket is created through a rename, so it never exists with looser permissions.

Dependencies and tooling

  • Dependencies are refreshed to their latest releases. The vendored forks moved to socket2 0.6, mio 1 and thiserror 2. The unmaintained rustls-pemfile and fxhash were replaced. cargo audit --deny warnings is clean.
  • Rust 1.99.0 is now the minimum and CI toolchain.
  • New daily Dependency advisories workflow. It also watches transport-critical crates for releases that advisory databases have not caught up with yet.
  • New clippy panic-site ratchet for the request-serving crates, since release builds use panic = "abort".
  • New fuzz targets: qpack_decode, proxy_protocol_parse, lscache_control.

Upgrade notes

  • HTTP/1.1 clients must send a valid Host. Absolute-form requests have their authority used as Host.
  • Operators keeping htpasswd files outside the docroot need --auth-user-file-roots. Plaintext htpasswd entries must be re-hashed with bcrypt (htpasswd -B).
  • Local purge scripts must use POST.

httpjet v0.0.3

Choose a tag to compare

@faratech faratech released this 27 Sep 04:10

httpjet v0.0.3 — security and hot-path fixes

This release fixes several ways static or precompressed file serving could expose script source, blocks proxied responses from setting internal cache controls, and restores local purge handling when a rewrite proxy route is disabled. It also tightens the page-cache file tier and reduces work in HTTP/3 deadlines, cache hits and startup, PHP suffix routing, LSAPI dispatch, and client throttling.

The page-cache file tier now opens files beneath a pinned root with openat2 where available and uses a secure descriptor walk elsewhere. The new HJPC v3 format discards older v2 cache entries once at startup; expect a cold cache immediately after upgrading if the persistent file tier is enabled.

HTTP/3 load clients are included in the public source so the benchmark suite can exercise QUIC even without an HTTP/3-capable h2load. The standalone fuzz lockfile is updated for 0.0.3.

The deployed x86_64 PGO build passed config, cache-isolation, purge, HTTP/2, HTTP/3, live member, and restart-persistence checks. Paired old/new loopback benchmarks completed with identical bytes and zero request failures. Throughput did not show a repeatable slowdown, although host I/O contention made the measurements too variable to resolve small differences reliably. Public CI, protocol interoperability, and bounded fuzzing were run against this source snapshot.

httpjet v0.0.2

Choose a tag to compare

@faratech faratech released this 23 Sep 04:24

httpjet v0.0.2 — September 2026: correctness fixes, page-cache efficiency, lower per-connection cost

This release carries everything since v0.0.1 (September 17): a review round of correctness and
hardening fixes, page-cache CPU work, progressive streaming for cacheable misses, lower
per-connection memory, and a dependency refresh. New features are opt-in; defaults that changed
are listed under Behavior changes below.

Measured figures come from one production configuration (a CDN-fronted forum plus two
template-driven sites) or from A/B runs against the previous build, and will vary with workload.

Fixes

  • Rewrite [P] targets: the loopback screen now pins the dial. The screen resolved the target
    name once, then the proxy dialed by name and resolved again, so a DNS answer that changed between
    the two (rebinding) could still reach loopback. The dial now uses the exact address that was
    screened. IP-literal targets skip the resolver entirely.
  • CGI environment.
    • SSL_CLIENT_* variables can no longer be overridden by SetEnvIf/[E=]; they are denied
      like the other server-computed variables.
    • A user verified by a scoped Basic-auth realm reaches the app as REMOTE_USER/AUTH_TYPE again.
    • The page cache's stale-while-revalidate refresh marker (HJ_CACHE_REFRESH) reaches the app
      again, so an application-level cache can no longer answer a background refresh with its own
      aging copy.
    • v0.0.1's environment deny-list had dropped both of these.
  • .htaccess.
    • An unreadable file is still deny-all (fail closed), but the deny now heals as soon as the file
      is readable again. It used to persist until the file's mtime changed, and chmod doesn't
      change mtime.
    • The per-directory walk stops at the first directory that does not exist, so front-controller
      URLs no longer fill the cache with negative entries. It used to saturate at 16,384 entries and
      stop memoizing new directories.
  • HTTP/2.
    • WINDOW_UPDATE credit that arrived for an already-finished stream was parked indefinitely, and
      on long-lived connections could exhaust the table that holds pre-response credit.
    • A stream reset by the body deadline now accepts late trailers without a connection-wide GOAWAY.
    • Receive-side WINDOW_UPDATEs are batched at half the window instead of sent twice per DATA frame.
  • Stale-if-error during a PHP outage. When the PHP pool stopped answering, a failed request first
    rendered a PHP-handled ErrorDocument through the same failed pool, and only then served the
    retained stale copy. That doubled the wait: in our deployment 120 s, past a typical CDN origin
    timeout. A generated 502/503/504 from a PHP dispatch now skips PHP-handled error documents, so the
    stale copy (or the built-in error page) arrives after one backend timeout.
  • Page-cache admission counted twice. A cookieless request probed the cache on the on-core fast
    path and again in dispatch, and both recorded an admission sighting. As a result
    --page-cache-admit-threshold 2 behaved like 1, and miss counts were doubled. One request is now
    one sighting.

Page cache: less CPU for the same content

  • Store-time dictionary compression defaults to zstd level 12 instead of 15. In our A/B that
    was 2.7-2.9x faster for 1-2% larger stored bodies.
    • New --page-cache-dict-level vhost=level,... sets a level per vhost.
    • Encoders are pooled per dictionary.
    • In our deployment, dictionary encoding had been the largest share of httpjet's own CPU.
  • Fewer encodes.
    • A capsule render stored under two keys is encoded once.
    • Incompressible content types and tiny bodies are never encoded.
    • The skip reasons are exported as httpjet_pagecache_dict_recompress_skipped_total{reason}.
  • Cheaper bookkeeping.
    • Entry metadata is compressed at zstd level 6 instead of 19.
    • Weak ETags are hashed a word at a time.
    • A decoded body is built once per hit.
    • Store-time finalize paths no longer pin decoded metadata in RAM.
    • Body collectors are presized from Content-Length.
    • Streaming zstd compression contexts are reused across responses.
  • Miss responses carry the stored entry's weak ETag, so a CDN can revalidate them with
    If-None-Match.
  • New observability: precompressed-variant fill/serve counters per codec, and sampled phase
    histograms for store and body collection.

New opt-in features

  • --page-cache-stream-fill-vhosts <vhosts>: progressive miss streaming.
    • For templates that print without a Content-Length, a cacheable miss is forwarded to the client
      as the backend writes it, then stored once the backend finishes cleanly. It used to be
      buffered whole before the first byte went out.
    • The transport sends the head with the first byte, and the streamed compressor flushes its
      first block when the backend stalls.
    • A backend error, a client disconnect, or a body over the object cap stores nothing.
    • Single-flight followers still wake to a stored entry.
    • In our deployment it moved a progressive site's miss time-to-first-byte from ~9 ms to ~6 ms.
  • --page-cache-opt-in-status 303,404,410 + --page-cache-303-prefixes <prefixes>. Cache these
    statuses only on an explicit X-LiteSpeed-Cache-Control: public, with the TTL capped at 300 s and
    no stale windows. A 303 is cached only under a listed path prefix.
  • --trusted-proxy-idle-secs N + --trusted-proxy-idle-max-conns N. A longer idle window for
    connections whose peer presented a verified client certificate, such as a CDN's authenticated
    origin pulls, which never resume TLS. At 600 s this cut full TLS handshakes per request by ~55%
    in our deployment. It falls back to the normal floor above the connection cap.
  • --tls-session-tickets N (default 2, unchanged behavior). 0 stops issuing TLS 1.3 tickets
    and shrinks the session cache to TLS 1.2 session IDs, for deployments whose peers never resume.
    It measured ~5% less server CPU per handshake.
  • HTTPJET_LOG_DIR=<dir> redirects every log file an instance opens (unified and per-vhost). It
    is meant for test instances that load a production config.

Lower per-connection cost

  • Idle TLS connections hold no write buffer. A flushed 64 KiB ciphertext buffer returns to a
    per-thread pool (cap 32) and the next write takes a warm one, so a response costs no allocation or
    page fault. In a 2,000-connection A/B this used 19-30% less memory per idle TLS connection, with
    throughput unchanged within noise.
  • HTTP/2 per-connection stream maps start empty instead of being pre-sized at accept.
  • TLS parameters for a repeat client certificate are memoized instead of re-parsed on every
    handshake.

Behavior changes to check when upgrading

  • --page-cache-admit-threshold now means what it says (see the admission fix above). If you
    set 2 while it behaved like 1, set 1 to keep the previous store rate.
  • The connection-close mimalloc collect uses --mimalloc-trim-threshold-mib (default 768)
    instead of a hard 384 MiB cap. A large page cache keeps a healthy process above 384 MiB, which
    made every thread force-collect every 30 s. Size the flag above your healthy peak RSS; in our
    deployment minor page faults fell from ~6,600/s to ~400/s.
  • Default store-time dictionary level is 12 (was 15). Bodies stored at 15 still decode.
  • HTTP/2 receive-side WINDOW_UPDATE batching changes wire behavior (h2spec 2.6.0: 146/146 over TLS).

Public repository and CI

  • CI and the interoperability workflow now install Rust 1.98.1. They had still pinned 1.97.0, below
    the workspace floor, so scheduled runs stopped at the toolchain check.
  • The helper scripts the workflows call are now published: ACME and OCSP acceptance, h2spec,
    bounded fuzzing, the OCI smoke test, and two HTTP/3 test clients.
  • fuzz/Cargo.lock is current, and the OCI example builds on rust:1.98-bookworm.
  • The OCI smoke test works on non-root runners.

Dependencies

  • opentelemetry 0.33 (optional otel feature), plus cc, hyper-rustls, rand, and unicode-ident.
  • Built and tested with Rust 1.98.1.

httpjet v0.0.1

Choose a tag to compare

@faratech faratech released this 17 Sep 00:28

httpjet v0.0.1 — September 2026: security hardening round + new subsystems

First tagged release, adopting semantic versioning.

This sync carries everything since the previous public snapshot (September 6): a set of new
opt-in subsystems, a transactional zero-downtime reload architecture, and a whole-codebase
security review whose twelve fixes are the centerpiece of this release. As always, every
subsystem below is opt-in — the default configuration serves exactly as before.

Security hardening round (whole-codebase review)

Five parallel review slices covered the request pipeline, transports, TLS and unsafe code, trust
and access control, backend gateways, cache and static serving, control surfaces, process
supervision, ACME, logging, and ops packaging. Twelve fixes shipped, all fail-closed:

Transport hardening

  • HTTP/2: frame and request-body deadlines on the production loop. The production (monoio)
    h2 loop never enforced frame_read_timeout/request_body_timeout — those deadlines existed
    only in a code path that doesn't run — so a peer trickling one byte per idle window could pin a
    connection, its header block, and its body buffers indefinitely. The production loop now
    mirrors the tokio-loop bookkeeping: partial frames must complete within the frame deadline
    (GOAWAY PROTOCOL_ERROR otherwise), and requests with decoded headers but no END_STREAM are
    reset with body accounting released after the body deadline.
  • HTTP/1.1: stalled sendfile is bounded. A zero-window reader used to cause ~10k
    sendfile(2) syscalls/s per connection on a pinned worker core, forever. Retries back off
    exponentially (100 µs → 12.8 ms, reset on progress) and a transfer with zero progress for 60 s
    closes the connection.
  • HTTP/1.1: duplicate identical Content-Length lines collapse to one on the forwarded
    header map, so a lenient upstream cannot re-parse two lines differently.
  • CPU pinning respects CPU_SETSIZE — available_parallelism() can exceed the fixed
    1024-bit affinity mask on pathological hosts.

Trust boundary

  • PROXY protocol: claimed loopback/unspecified sources are refused at the parser. The
    pp-claimed source was applied before the mandatory client-certificate gate, so on a
    pp-enabled public listener a direct client could claim 127.0.0.1 and be treated as a trusted
    internal peer: skipping the client certificate, reaching the loopback-gated cache purge/ready
    endpoints, and bypassing the plaintext mTLS-gate redirect. Such headers are malformed in both
    v1 and v2 (v4-mapped forms included) and the connection is closed. Real load balancers never
    relay loopback client addresses.
  • Rewrite [P] targets are screened away from the local trust plane. [P] URLs are
    request-influenceable (.htaccess authors; $N backrefs in the authority), and a same-host
    dial presents a loopback raw peer — exactly what the purge/ready endpoints, the metrics
    listener, and the admin listeners gate on. Rewrite-driven targets refuse unix:/uds:
    transports outright and resolve the authority (bounded 3 s) to reject loopback, unspecified,
    and link-local addresses — including /etc/hosts pins, which bypass literal screens.
    Config-declared proxy targets remain operator-trusted and unscreened.
  • CGI environment: directory-authored overrides of server-computed variables are denied
    (shared by LSAPI and FastCGI). Rewrite [E=...]/SetEnvIf entries could previously re-point
    SCRIPT_FILENAME at an arbitrary readable file or forge REMOTE_ADDR/HTTPS/SERVER_*.
    SCRIPT_NAME, QUERY_STRING, REDIRECT_* (front-controller and ErrorDocument contracts) and
    HTTP_* mirrors remain overridable.

Fail-closed defaults

  • .htaccess: unreadable or non-UTF-8 access files no longer degrade to "no rules." A
    present-but-unreadable file, or a single non-UTF-8 byte, memoized the directory as unprotected
    until the mtime changed. Non-UTF-8 files decode lossily (rules survive); any other read error
    yields the deny-all sentinel, self-healing on mtime change — matching the existing parse-error
    behavior.
  • lsphp: never-root is enforced on every credential path, and a setuid/setgid lsphp binary
    refuses the spawn outright (after the supervisor's credential drop, executing such a binary
    yields workers whose effective uid is root).
  • Isolation modes: out-of-range setUIDMode/chrootMode values fail the vhost load instead
    of silently disabling isolation.

Cache integrity

  • The page cache varies stored entries on Origin when the .htaccess chain reads it. An
    origin-reflecting chain (SetEnvIf / RewriteCond %{HTTP:Origin}, typically for a reflected
    Access-Control-Allow-Origin) produces origin-conditional responses, but the cache stored
    whatever headers the first storer's render produced — every later visitor received that
    request's CORS verdict. Origin now folds into the cache-key vary discriminant (bounded hash)
    across lookup, store, stale-if-error, and capsule keys. Absent/empty Origin keeps the
    historic base key, so entries persisted before this change stay hit-able; origin-bearing
    requests (browser fetch/XHR) re-render once per URL per origin after upgrading.

Admin write API and packaging

  • useIpInProxyHeader and the whole <security> block (trusted-CIDR access rules,
    followSymlink, geo/ASN lists) are restart-required rather than silently publishable as
    live "application" updates; unix-socket ext-processor addresses are authorized against
    --admin-resource-root.
  • The OCI seccomp profile gates ptrace/process_vm_* on CAP_SYS_PTRACE; the page-cache
    integrity key is created O_CREAT|O_EXCL at mode 0600 (no umask window, no symlink redirect).

New subsystems since the previous sync (all opt-in)

  • FastCGI gateway — bounded wire protocol, connection pool, CGI response parsing, streaming
    handler, and suffix routing.
  • WAF sidecar — request inspection before cache and backends with bounded aggregate memory;
    when configured, every request funnels through inspection (fast paths decline).
  • ACME client — durable HTTP-01 issuance/renewal with TLS activation, plus scoped DNS-01
    wildcard issuance with durable cleanup.
  • OCSP stapling — opt-in validated staple lifecycle with fail-closed "required" mode.
  • Admin APIs — read-only loopback introspection (GET /v1/status) and an opt-in write
    transaction API: token-file auth, bounded control requests, JSON config submissions validated
    against operator-declared resource roots, and revision-checked publications that leave the
    current configuration serving on any failure.
  • Transactional zero-downtime reload — configuration changes are classified; TLS/cert and
    trust-policy changes flow through prepared same-socket TCP successors with kernel-verified
    socket-identity handoff, raced-accept transfer, activation gating, and generation-pinned
    connection views. Failed candidates leave the currently serving generation untouched.
  • Reverse-proxy load-balancing groups — weighted selection, strict group configuration
    (canonical unix syntax, validated ports), bounded active health probes, and generation-scoped
    pool lifecycle.
  • OpenTelemetry — bounded OTLP trace/metric export with an explicit trust policy for inbound
    trace parents; spans cover H1/H2/H3, WebSocket relays, LSAPI dispatch, and cache/rewrite
    stages; export never blocks request handling.
  • kTLS — auto mode gated on real NIC TLS-hardware offload; H1 file bodies send via sendfile
    on plaintext and kTLS connections.
  • Logging — structured JSON error events with credential redaction, an optional syslog tap,
    and a shared parser corpus replay.
  • Bounded request decompression for Content-Encoding request bodies, and a hardened OCI
    packaging example.

Dependency refresh

All dependencies bumped to their latest releases, including major versions: rustls
0.23.45 (closes RUSTSEC-2026-0285)
, tokio 1.53.1, quinn 0.11.12, sha2 0.11, hmac 0.13,
zstd 0.14, brotli 9, base64 0.23, bcrypt 0.19.3. The vendored monoio fork was ported to
io-uring 0.7.15 (upstream monoio 0.2.4 remains the latest release): the fork's multishot
accept machinery re-applies unchanged on the 0.7 Builder<S, C> defaults, and the dependency
graph dedups to a single io-uring version. The minimum supported Rust is now 1.98.

Upgrade notes

  • PROXY-protocol listeners: connections whose PROXY header claims a loopback or unspecified
    source are now closed by design. If a fronting load balancer sends such headers, fix the LB.
  • Rewrite [P] rules targeting loopback, link-local, or unix sockets now return 403 — declare
    same-host backends as config proxy contexts instead; operator configuration is trusted,
    rewrite-driven targets are not.
  • [E=SCRIPT_FILENAME]-style env overrides of server-computed CGI variables are ignored;
    front-controller rewrites using SCRIPT_NAME/QUERY_STRING are unaffected.
  • setUIDMode/chrootMode values outside 0–2 abort the affected vhost config load instead of
    silently disabling isolation.
  • Everything else is behavior-identical for conformant clients; new subsystems activate only via
    their flags.

Validation

  • Full workspace test suite (16 crates, ~1,500 tests) green; new regression tests cover every
    security fix, including io_uring-driver tests that drive the real h2 deadline paths.
  • fast-memo parity gate: 192-case byte-identity matrix on a real 800-line .htaccess chain —
    zero differences.
  • Page-cache cross-URL, cross-scheme, and cross-host gates green; file-tier persistence gate
    restored 148k entries across a restart; private-tier session-isolation gate green.
  • A live probe on an origin-reflecting chain confirms per-origin CORS caching: a no-Origin
    request no longer replays the first storer's Access-Control-Allow-Origin.

Co-Authored-By: Claude Code noreply@anthropic.com