Unit 1.36.0 Released§
FreeUnit 1.36.0 is a security and correctness hardening release. It closes a
set of local privilege-boundary and memory-safety issues (covered by two
security advisories), adds a graceful SIGQUIT stop that drains in-flight
requests before exit, and upgrades the WebAssembly runtime.
Security hardening
- Config validation: reject empty values and embedded NUL bytes in
configuration strings later used as NUL-terminated C strings (
user,group,working_directory,stdout/stderr,executable, the per-language application targets,access_log, the TLS certificate, the njsjs_module, and PHPoptions.file) and in pathname unix socket addresses — closing a silent-truncation target confusion. - Static serving: reject embedded NUL bytes in templated
shareandchrootpaths after variable resolution, beforeopen()/openat2(). - Isolation: mount rootfs automount destinations through
/proc/self/fdon theopenat2-validated descriptor, closing a symlink-swap TOCTOU that could escape the rootfs. - Proxy: treat a duplicate upstream
Content-Lengthas inconsistent (drop both, read to EOF) to prevent a response-smuggling desync, and keep downstream keep-alive disabled afterward so an HTTP/1.1 client cannot reuse the connection past the re-framed response. - libunit: bound the request header-fields region against the shared
memory message size, validate serialized pointers and cached header
indexes, and range-check the Java
InputStream.read()binding — preventing out-of-bounds access on malformed requests. - Controller: fix a debug-build crash and an accepted-fd leak on control-connection close and failure paths.
- OpenTelemetry: restart a malformed W3C
traceparentinstead of failing the request with HTTP 500, and zero-initialize injected trace fields (garbage flag bits could otherwise drop the header at random). - Go module: fix a libunit port-fd double-close race.
Two advisories were published with this release (CVEs pending):
- GHSA-768w-qrh2-jjvh — control-socket peer-authentication bypass, cgroup-namespace TOCTOU, and mount-destination TOCTOU (CVSS 8.8).
- GHSA-g6v8-r577-76jm — WebSocket out-of-bounds read and cross-frame data disclosure (CVSS 7.5).
Both are fixed in 1.36.0 and backported to the 1.34.x LTS line.
Graceful shutdown
SIGQUITnow performs a graceful stop: application workers drain their in-flight requests before exiting.SIGTERMremains the fast exit.
Dependencies
- wasm-wasi-component wasmtime crates upgraded 35.0.0 → 36.0.12, clearing the sandbox-escape advisories.
Full Changelog§
Changes with FreeUnit 1.36.0 16 Jul 2026
*) Bugfix: the controller crashed in debug builds when a control-socket
connection closed after the active-connection tracking change, and
leaked the accepted file descriptor when a control connection failed
during initialization or body allocation; connection tracking and
teardown now route through the engine's tracking macros and the
close path.
*) Bugfix: ignore a malformed incoming "traceparent" header and restart
the trace per W3C Trace Context instead of rejecting the request
with HTTP 500; the malformed header is dropped instead of being
forwarded alongside the restarted one, and context accepted from a
valid duplicate is preserved. Values are now validated in full
(segment lengths, lowercase hex, no all-zero ids, no "ff" version),
so content-invalid values restart the trace instead of being
re-emitted verbatim. An inbound "tracestate" is dropped together
with the rejected context (kept when the traceparent is inherited).
The injected "traceparent" fields are now zero-initialized; garbage
flag bits could get the header randomly dropped from proxied
requests and responses.
*) Bugfix: reject empty values and embedded NUL bytes in configuration
strings that are later used as NUL-terminated C strings ("user",
"group", "working_directory", "stdout", "stderr", "executable", and
the per-language application targets) and in pathname unix socket
addresses, closing a silent-truncation target confusion.
*) Bugfix: reject empty values and embedded NUL bytes in the remaining
configuration strings used as NUL-terminated C strings outside
application options: the "access_log" path (string and object
forms), the TLS "certificate" and njs "js_module" store names, and
the PHP "options.file" php.ini path.
*) Bugfix: reject an embedded NUL byte in a templated static-serving
path ("share" and "chroot") after it is resolved from
request-controlled variables and before it reaches open()/openat2(),
preventing a silent truncation that could serve a different file
than the resolved path.
*) Bugfix: mount rootfs automount destinations onto the openat2
validated descriptor via "/proc/self/fd" instead of re-resolving the
path, closing a check-to-use window in which a destination component
could be swapped for a symlink escaping the rootfs.
*) Bugfix: treat duplicate upstream "Content-Length" headers in a
proxied response as inconsistent — both headers are dropped and the
body is read to EOF instead of trusting either length, preventing a
response-smuggling desynchronization.
*) Bugfix: keep downstream keepalive disabled after a duplicate or invalid
upstream "Content-Length"; a clean upstream close no longer re-enables
it, so an HTTP/1.1 client cannot reuse the connection past the re-framed
response. The complete body still ends with its terminal chunk — body
truncation is now tracked separately from the inconsistent flag.
*) Bugfix: bound the request-header fields region against the shared
memory message size in libunit, validate every field name/value
serialized pointer and the cached header-field indexes before use,
and range-check off/len in the Java InputStream read() binding,
preventing out-of-bounds access on a malformed request.
*) Bugfix: fix a libunit port-fd double-close race in the Go language
module: getUnixConn() wrapped the raw shared-port descriptor (which
net.FileConn dups) and a deferred Close() closed the original
descriptor number while libunit still owned it, an intermittent
double-close that raised an EBADF alert or, worse, closed an
unrelated reused live descriptor. Go now dup(2)s the descriptor and
wraps only the dup, so libunit remains the sole owner and closer;
libunit additionally holds a port reference across the out-of-mutex
add_port callback window so a concurrent destroy cannot free the
port mid-callback.
*) Feature: SIGQUIT now performs a graceful stop — application workers
drain in-flight requests before exiting; SIGTERM remains the fast
exit that drops them.
*) Change: upgrade the wasm-wasi-component wasmtime crates from 35.0.0
to 36.0.12, clearing the wasmtime sandbox-escape advisories, with the
accompanying WasiView/WasiHttpView source migration.
*) Change: upgrade contrib njs to 1.0.0.
*) Feature: two-phase listener close — reconfiguration disarms a removed
listener first and releases its descriptor only after pending accept
references drain, instead of resetting them.
*) Change: the event engine now tracks in-flight connections on a
dedicated active connections queue, symmetric with idle tracking —
groundwork for graceful connection draining; connection accounting
in the "/status" API is exact on all close paths.
*) Change: broaden the regression test suite — proxy Content-Length and
chunked-response relay framing, OpenTelemetry 5xx tracing and inbound
traceparent handling, and Java ServletInputStream bounds — and run
the C unit-test suite in CI, which was previously compiled but never
executed.
*) Bugfix: authorize privileged IPC message types by the kernel-validated
sender PID (SCM_CREDENTIALS), close any received descriptors on every
rejected privileged message, and cap port queue item size at the slot
data bound.
*) Bugfix: reject control characters (CR, LF, NUL, DEL) in "set_headers"
names and values at configuration load, including the literal
segments of templated values.
*) Bugfix: handle buffer-allocation failure in the port read handlers
with an orderly teardown instead of a NULL-pointer dereference, route
a connection write error after a partial send to the error handler
immediately, and free the timer batching array on engine teardown.
*) Feature: upgrade the OpenTelemetry Rust crates from 0.24 to 0.32 and
rewrite the OTLP trace exporter on the stable, dedicated-thread batch
span processor (blocking reqwest client, no tokio runtime).
*) Feature: W3C traceparent header inheritance now correctly propagates
the upstream sampling decision; the sampler wraps TraceIdRatioBased
inside ParentBased so that remote parent trace flags are honoured.
*) Feature: add fake_otlp mock OTLP collector (test/fake_otlp/) and the
test_otel.py test suite covering span export, traceparent
propagation, sampling, and configuration validation over both the
OTLP/HTTP and OTLP/gRPC transports.
*) Feature: reintroduce OTLP/gRPC trace export. Both transports are
compiled into every "--otel" build and the
"settings/telemetry/protocol" option selects "http" (OTLP/HTTP) or
"grpc" (OTLP/gRPC) at runtime, as in upstream Unit. gRPC uses tonic
over a small tokio runtime; v1 is plaintext h2c only (no TLS to the
collector, like the HTTP path).
*) Change: rename the default OTel service name from "NGINX Unit" to
"FreeUnit".
*) Bugfix: add validation bounds for "batch_size" (1-65536) and
"sampling_ratio" (0-1) in the OpenTelemetry configuration; previously
invalid values were silently rejected without an error message.
*) Bugfix: replace eprintln! error output in the OTel Rust layer with
the C log callback (nxt_otel_log_cb), so errors flow through the
standard Unit logging infrastructure.
*) Bugfix: remove unreachable Protocol::HttpJson dead-code arm from the
OTel exporter; only HttpBinary was ever selected.
*) Bugfix: fix a rootfs automount reload race: sever the worker's mount
namespace propagation (MS_REC|MS_PRIVATE) before mounting and let a
per-worker automount be reaped with the namespace instead of being
unmounted from the host, so a configuration reload no longer races the
previous worker generation's teardown against the new prototype's
procfs mount (freeunitorg/freeunit#83).
*) Bugfix: validate that a loaded TLS private key matches its certificate
at config time, and guard the wildcard-name SAN matcher against a
zero-length SAN entry.
*) Bugfix: correct IPv4 /32 CIDR fallthrough, symmetric URI/pattern
decoding, the PCRE2 match-data ovector size, and short port-range
parsing in HTTP routing; document the case-insensitive host matcher
and the capset stance.
*) Bugfix: tighten file-descriptor and CLOEXEC lifetime — CLOEXEC-protect
accepted sockets and pipe ends, close the pipe end on
nxt_fd_nonblocking() failure and the source fd on compression mmap
failure, and narrow the accept4() fallback to ENOSYS.
*) Bugfix: tighten WebSocket frame-bound checks — reject truncated
extended-length frames in libunit, validate the 64-bit extended-length
MSB (RFC 6455), fix a no-op frame-size decrement that could copy bytes
beyond the declared payload, bound the Java sendWsFrame JNI arguments,
and guard pending_payload_len overflow in the Python ASGI handler.
*) Bugfix: bounds-check peer-supplied shared-memory offsets — range-check
chunk_id and chunk_id+nchunks in incoming mmap messages, close a
lookup/dereference window on the incoming-mmap handler, reject
response-buffer size overflow, and validate request sptr offsets
before use.
*) Bugfix: bounds-check app-supplied arguments across the language
bindings (PHP header skip / realpath / PATH_INFO; Python WSGI and ASGI
checks; Perl ERRSV scrub; Java InputStream.readLine off/len; WASM
guest offsets).
*) Bugfix: tighten isolation boundaries — require a matching peer UID
(root or Unit's own effective UID) on the unix control socket, so the
"control-user", "control-group", and "control-mode" permissions are
defense-in-depth rather than a means to delegate control-API access;
resolve mount destinations with openat2(RESOLVE_BENEATH); and resolve
relative cgroup paths against the child's /proc/<pid>/cgroup.
*) Bugfix: reject embedded NUL bytes in the "rootfs" and cgroup "path"
isolation options, and validate "rootfs" as an absolute path other
than "/" at configuration time.
*) Bugfix: fix an ineffective truncation guard when building the
cgroup.procs pathname: snprintf()'s return value overwrote the
original directory length before the bounds comparison, so a long
cgroup directory path could overflow the check. The append now
preserves the original length and fails with ENAMETOOLONG when
"/cgroup.procs" plus the trailing NUL cannot fit.
*) Bugfix: fail cgroup setup when the per-process pool allocation that
caches the created directory for cleanup fails; the directory is now
removed and setup errors out, rather than moving the process into a
cgroup that could never be cleaned up — a cgroup directory leak under
memory pressure.
*) Bugfix: reject a "rootfs" that lexically resolves to "/" (such as
"/.", "/..", or "/foo/.."); chroot("/") is a no-op and would silently
defeat rootfs isolation.
*) Bugfix: cap JSON parser depth and element counts in the controller,
scrub PHP TrueAsync exception state before the prototype fork, and bind
the Ruby rack.input / rack.errors handles to their originating request.
*) Bugfix: bound proxy Content-Length and URI/string helpers — truncate a
proxied response body that exceeds its Content-Length and close the
connection, flag invalid or oversized upstream Content-Length, fix an
nxt_is_complex_uri_encoded() off-by-one, and reject over-long lengths
in nxt_rmemstrn().
*) Feature: enforce the runtime/OS end-of-life policy automatically — the
unit-eol-check tool now fails when a shipped variant has outlived its
support window (supported_until past EOL + grace), and a new
eol-check GitHub workflow validates pkg/eol.json against endoflife.date
on every relevant pull request and weekly on a schedule.