Unit 1.36.1 Released§
FreeUnit 1.36.1 is a maintenance release. It hardens the inter-process message paths against a compromised or malformed peer, fixes a set of HTTP framing and memory-lifetime bugs, makes OpenTelemetry usable in production, raises the TLS build floor to OpenSSL 1.1.1 while adding OpenSSL 4.0 support, and fixes a case where session tickets were silently switched off.
Inter-process message hardening
A worker that is already compromised could previously use the port protocol
to crash or exhaust a privileged process. Shared-memory segment identifiers,
geometry, and chunk ranges arriving from a peer are now validated before use;
a CHANGE_FILE message can no longer claim an out-of-range log file slot;
a short shared-memory object handed over as a port queue is refused where it
is handed over rather than faulting at a later access; and the descriptors a
forged or malformed message leaves behind are closed on every path instead of
accumulating until the receiver runs out.
- A
NEW_PORTannouncing a port that already exists no longer replaces a live port’s queue mapping. - A stale
SCM_RIGHTScontrol message is no longer replayed in libunit. - The router closes the engine port when a worker thread exits, ending a
descriptor leak on every
listen_threadsdecrease.
HTTP
- Chunked messages carrying a trailer section are parsed per RFC 9112 7.1.2 instead of being rejected, and the request body buffer stays alive across framing boundaries.
- An application response that disagrees with itself about its length is rejected rather than desynchronizing the connection.
- Response buffers adopted from the port are released on the error path.
OpenTelemetry
/statusreports span export health as"telemetry": {"spans": {"exported": N, "failed": M}}, present only when telemetry is configured — its absence is how a client tells telemetry is off rather than idle.sampling_rationow saves the work it promises: attribute assembly is skipped for spans the sampler drops, where previously it bought back only the exporter and nothing on the request path.- Spans are no longer lost at the edges of a request’s life, the pipeline is flushed before the router exits, and the exporter is rebuilt only when the telemetry settings actually changed.
http.response.status_codeandhttp.request.body.sizeare emitted as OTLP integers rather than strings, as the semantic conventions specify. Collectors matching these attributes as strings must be adapted. The redundant free-form “Unit Attribute” span event was removed.
TLS
- Session tickets were silently disabled when Unit was built against
OpenSSL 3.x with deprecated APIs turned off: the feature guard tested a
control that is itself deprecated, so the build quietly took the
unsupported branch. The ticket key callback now uses the
EVP_MACAPI. - The TLS code builds against OpenSSL 4.0.
- Building with TLS now requires OpenSSL 1.1.1 or later. The pre-1.1.0 compatibility code is removed. The floor stays at 1.1.1 rather than 3.x because platforms FreeUnit packages for still ship a vendor-patched 1.1.1 — most prominently RHEL 8, which Red Hat maintains into 2029.
Other
- Configuration validation errors now say where they are, with an RFC 6901 JSON Pointer to the member or array element that failed validation — or to the containing object when the failure is an unknown parameter — and a suggestion for a mistyped name.
- The
ifoption of a routematchobject, available since 1.33.0, is now documented and covered by the test suite. - The memory pool allocation cursor is kept aligned by rounding, fixing SIGBUS crashes on 32-bit ARM.
- wasmtime is updated from 36.0.12 to 47.0.2, and the WebAssembly component module no longer permits guest outbound HTTP.
- The Java module bundles Apache Tomcat 9.0.121.
Full Changelog§
Changes with FreeUnit 1.36.1 28 Aug 2026
*) Feature: configuration validation errors now say where they are. A
rejected configuration request carries an RFC 6901 JSON Pointer in
the "path" member of "location": it points at the member whose value
failed validation, or at the containing object when the failure is an
unknown parameter, whose name the message itself already carries; ""
is the document root. On an unknown parameter whose name is a close
and unambiguous typo of a known one, the intended name is reported in
a top-level "suggestion" member. The addition is strictly additive:
the existing response fields and the error message wording are
unchanged, and each new member is omitted where it does not apply.
*) Feature: the /status API reports OpenTelemetry span export health as
"telemetry": {"spans": {"exported": N, "failed": M}} -- the spans the
collector accepted, and the spans whose export failed. Both are
reported because a failure count alone cannot distinguish a healthy
pipeline from one that has never exported anything. The object is
omitted entirely unless Unit was built with OpenTelemetry support,
"/config/settings/telemetry" is set, and the exporter was built
successfully, so its absence is how a client tells telemetry is off
rather than idle. The counters are cumulative since the exporter was
last built, so changing any telemetry setting restarts them, while
re-applying an identical configuration does not.
*) Bugfix: validate the shared memory segment identifier and geometry an
application process supplies in its NXT_PORT_MSG_MMAP message. The
router checked only the pid fields, then used the peer-authored
header id directly as an array index and mapped the descriptor at
whatever length fstat() reported; a compromised or faulty application
could drive an out-of-range write and an oversized mapping.
*) Bugfix: bounds-check the router-supplied chunk identifier and size in
libunit before consuming them, so an out-of-range size can no longer
make every serialized-pointer check in the arrival-time validation
block pass and be read out of bounds in the application process.
*) Bugfix: do not replay a stale SCM_RIGHTS control message in libunit
when the embedder's port_recv callback returns without receiving
anything. The out parameter carrying the control-data length was
stored unconditionally, so a descriptor from an earlier message could
be adopted a second time.
*) Bugfix: check the shared memory buffer allocation when forwarding a
websocket frame. Exhausting the outgoing shared memory crashed the
router with a NULL dereference; every other caller already checked.
*) Bugfix: keep the chunked request body buffer alive across framing
reads. A buffer drained without producing a data slice was handed to
its completion handler while the parser still referenced it, a
use-after-free reachable from an ordinary chunked request.
*) Bugfix: parse the HTTP/1.1 trailer section (RFC 9112 7.1.2) instead
of erroring on the first byte after the terminal chunk. In the proxy
response relay this aborted an already-streaming response and
truncated the body at a racy point; chunked requests carrying a
trailer were rejected the same way. Trailer field names and values
are validated as they are parsed.
*) Bugfix: reject an application response that disagrees with itself
about its own body length. A duplicate Content-Length was neither
detected nor parsed, so every such header was forwarded verbatim and
the body was framed by the advertised length instead of chunked.
*) Bugfix: release the response buffers adopted from the port on the
error paths of the router's response handler, and drain them from the
request output chain when the failure happens after they were linked
into it. Both leaked the chain the handler had taken ownership of.
*) Bugfix: keep the memory pool cursor aligned in nxt_mp_get() by
rounding the requested size up to the maximum alignment rather than
flooring it. The small allocation path is a bump allocator with no
per-allocation alignment step, so any request whose size was not a
multiple of that alignment left the page cursor misaligned, and
allocations from that page kept violating the documented alignment
guarantee until a later size happened to bring the cursor back into
step, which crashed with SIGBUS on strict alignment targets such as
32-bit ARM.
*) Bugfix: serialize the process reference count on the runtime process
mutex and take a reference during cross-thread process lookups. The
count was mutated non-atomically from every engine thread and a
lookup returned an unreferenced process, so a router worker could
operate on an nxt_process_t -- including its shared memory segments
and its mutex -- that the main thread had already torn down.
*) Bugfix: keep the socket and timer tasks of a connection scoped to
that connection, so a work item cannot run against a task belonging
to a different connection.
*) Change: remove the dead chunk-tracking bitmap from the shared memory
segment header. Its trailing fields ran past the header area and
aliased the payload of the segment's first chunk, which was harmless
only because nothing read or wrote them; the field offsets a peer of
either vintage uses are unchanged, so old and new builds still
interoperate. A static assertion now pins the header struct inside
the area reserved for it.
*) Change: the shared memory chunk range check is now a single shared
implementation used by both the router and libunit, with the chunk
count computed so that a peer-supplied size in the top of the 32-bit
range cannot wrap to zero chunks and be accepted.
*) Change: recycle connection structures through a per-engine freelist,
embed the static file context in the request, recycle static file
buffer descriptors through a thread-local freelist, and resolve the
first eight routing variables and the first sixteen request and
response header fields from arrays embedded in the request. Each
avoids or reuses an allocation on a per-connection or per-request hot
path.
*) Change: the WebAssembly component module no longer supports
guest-initiated outbound HTTP. Serving Unit never needed it, and the
feature that provided it linked an entire TLS stack -- rustls,
tokio-rustls, webpki-roots and rustls-webpki -- into the module. A
guest that only imports wasi:http/outgoing-handler, as the
wasi:http/proxy world requires, still instantiates and runs; an
actual outbound call now fails with HttpRequestDenied.
*) Change: update wasmtime from 36.0.12 to 47.0.2 in the WebAssembly
component module, keeping outbound HTTP denied and the TLS stack out
of the dependency graph.
*) Change: the "if" option of a route "match" object, added in 1.33.0,
is now described in the OpenAPI specification and covered by the test
suite. A request matches the step only when the condition is true;
the condition uses the same syntax as the "if" option of the access
log: it is false when it renders to an empty string, "0", "false",
"null", or "undefined", and true otherwise, and a leading "!" negates
the result. Variables test for the presence of a value ("$arg_foo"),
njs template strings express comparisons and regular expressions
("`${uri == '/admin'}`", "`${/^\/admin/.test(uri)}`").
*) Change: build against OpenSSL 4.0, which constified the return types
of the X509 name accessors and deprecated
X509_NAME_get_text_by_NID(); SNI certificate matching and the
"certificates" control API now read the certificate name through
X509_NAME_get_index_by_NID() and the entry accessors instead, and a
new CI job builds and runs the C test suite against OpenSSL 4.0 with
deprecated APIs disabled, so it fails on any new use of a deprecated
function rather than only on a removal.
*) Change: OpenSSL 1.1.1 or later is now required to build with
"--openssl"; configure fails with an explicit message on older
releases. Builds against OpenSSL 1.0.x have not worked since 1.35.2,
when the TLS module started to use OpenSSL_version_num(); the
unreachable pre-1.1.0 compatibility code is removed.
*) Bugfix: close the engine port when a router worker thread exits.
Lowering settings/listen_threads deletes engines, and the thread exit
handler only dropped a port reference, and the runtime held another,
so nothing ever called nxt_port_close(): every deleted engine leaked
its socket pair and its queue descriptor, three per engine, for the
life of the router. The count only ever rose, so a configuration that
raises and lowers the thread count walked the router towards its
descriptor limit.
*) Bugfix: close the descriptors a forged or malformed port message
leaves behind. A message carries up to two descriptors whatever its
type normally uses, and the dispatcher reclaims nothing once a
handler has returned. The main process kept the second descriptor on
the whoami path, the router lost both when a conf data message named
no reply port, and a PROCESS_READY naming an unknown process closed
neither. A peer that is already compromised could exhaust the
descriptor table of the receiving process -- the main process on the
whoami path -- by repeating any of them.
*) Bugfix: bound the log file slot a CHANGE_FILE message claims, and
stop leaking the descriptor it carries. The slot index arrived from a
peer and was used to walk the runtime's log file list without a range
check, and a message whose payload was shorter than the index was
read past its end. The rejection paths now close what the message
attached, and nxt_file_redirect() releases the descriptor when dup2()
fails rather than only on success -- which also ends a leak of one
descriptor per unrotatable log file on every SIGUSR1.
*) Bugfix: a NEW_PORT or PROCESS_READY message carrying a shared-memory
queue shorter than the queue type faulted the receiving process
(main, the prototype, or the router) with SIGBUS on first access; the
object size is now checked before mapping, a short queue is refused
with a warning, and the router no longer leaks the descriptor of a
queue it refuses.
*) Bugfix: a NEW_PORT message announcing a port that already existed, or
one that could not be created, left an uninitialized stack value
behind; the controller dereferenced it unconditionally on every
NEW_PORT, and main and the router relied on a NULL check that could
not catch non-NULL garbage. The result is now cleared explicitly
whenever no port is created.
*) Bugfix: close the file descriptors a NEW_PORT or MMAP message left
behind instead of leaking them. The discovery and application
prototype processes kept every port queue descriptor open for the
life of the process, the main process kept the queue descriptor of
any port it had not just created, the router lost it on ports it does
not map a queue for, the controller never closed it, and the mmap
handler leaked its second descriptor on every path and returned
closing nothing at all when the first was missing; a message type
with no handler also went uncollected. A NEW_PORT for a port that
already has a queue now refuses the new descriptor instead of mapping
it over the live queue and leaking the mapping it replaced, which had
let a repeated announcement re-point an established port at memory of
the sender's choosing.
*) Bugfix: session tickets were silently disabled when Unit was built
against an OpenSSL 3.x library compiled with the deprecated 3.0 API
removed (OPENSSL_NO_DEPRECATED_3_0), because the feature guard tested
for a control macro that is itself declared only under that API and
so was never defined; the session ticket key callback now registers
through SSL_CTX_set_tlsext_ticket_key_evp_cb() and keys an EVP_MAC
context on OpenSSL 3.0 and later, independent of the deprecated
HMAC_CTX API.
*) Bugfix: stop building OpenTelemetry span attributes for spans the
sampler has already dropped. set_attribute() on a non-recording span
discards its argument, but the argument was built first -- the
sprintf round-trips, the pool allocations and two owned strings per
attribute -- and nothing asked whether the span was recording.
Lowering sampling_ratio therefore bought back the exporter and
nothing on the request path. The sampler's verdict is now read once
when the span is created and the attribute stages consult it, which
removes most of telemetry's request-path cost at a low sampling
ratio. Context propagation is deliberately outside that check: the
traceparent must reach the peer and the application whatever the
sampling decision was.
*) Bugfix: do not attach the traceparent to an OpenTelemetry span as a
"Unit Attribute" event. It duplicated the trace and span identifiers
the span context already carries, nothing consumed it, and it was
added twice on a request that took the error path with a trace it had
started itself, because the error path re-runs header propagation.
*) Bugfix: OpenTelemetry spans were lost at the edges of a request's
life. A request that ended before its span was collected -- an error
path, a dropped connection -- left the span allocated and never
exported it; the span is now tied to the memory pool that holds it
and is exported with an error status when that pool is released, so
an abandoned request appears in the trace instead of vanishing.
*) Bugfix: flush the OpenTelemetry span pipeline before the router
exits. Exit and reload discarded the pipeline with up to 4096 spans
still queued, losing every span produced since the last export. The
pipeline is now flushed with a 2 second bound, so a slow or
unreachable collector delays shutdown by at most that long rather
than either hanging it or costing the queued spans.
*) Bugfix: rebuild the OpenTelemetry exporter only when the telemetry
settings have changed. Every configuration change rebuilt it, so
adding a route or changing an application stalled reconfiguration
against an unreachable collector even though it did not touch
"/config/settings/telemetry". The exporter is now rebuilt only when
the endpoint, protocol, sampling ratio or batch size actually differs
from the running one.
*) Change: the OpenTelemetry span attributes "http.response.status_code"
and "http.request.body.size" are now emitted as OTLP integer values,
as the HTTP semantic conventions specify, instead of strings. A
collector or query that matches these attributes as strings must be
adapted; one that reads them as integers, or that relied on the
convention rather than on Unit's previous output, needs no change.
*) Change: the work a traced request does on the request path is
reduced: attribute keys are no longer copied onto the heap per
request, the two attributes above no longer round-trip through
strings, and a span stage now crosses into the tracer once instead of
once per attribute. Together with the sampling fix above, the
telemetry overhead of a request measured 27% lower at
"sampling_ratio" 1.0 and 53% lower at 0.1.
*) Change: the Java module now bundles Apache Tomcat 9.0.121 (from
9.0.110) and ClassGraph 4.8.186 (from 4.8.181). The upstream Tomcat
9.0.121 release fixes CVE-2026-66299 (unbounded buffer for
undelivered WebSocket messages, a memory exhaustion by a slow
client), CVE-2026-73180, CVE-2026-68569, CVE-2026-66422 and
CVE-2026-65927. FreeUnit bundles only the servlet, JSP and EL API
jars, Jasper and the Tomcat utility jars -- not catalina, the
WebSocket implementation or the example webapps -- so the bump keeps
the jars it ships on a supported release.