Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,52 @@ Changes with FreeUnit 1.35.6 25 Jun 2026
*) Bugfix: remove unreachable Protocol::HttpJson dead-code arm from the
OTel exporter; only HttpBinary was ever selected.

*) 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 on
the unix control socket, resolve mount destinations with
openat2(RESOLVE_BENEATH), and resolve relative cgroup paths against the
child's /proc/<pid>/cgroup.

*) 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().


Changes with FreeUnit 1.35.5 29 May 2026

Expand Down
18 changes: 18 additions & 0 deletions auto/files
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,24 @@ nxt_feature_test="#include <fcntl.h>
. auto/feature


# pipe2(), Linux 2.6.27/glibc 2.9, FreeBSD 10.0, NetBSD 6.0, OpenBSD 5.7.

nxt_feature="pipe2()"
nxt_feature_name=NXT_HAVE_PIPE2
nxt_feature_run=
nxt_feature_incs=
nxt_feature_libs=
nxt_feature_test="#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>

int main(void) {
int pp[2];
return pipe2(pp, O_CLOEXEC);
}"
. auto/feature


nxt_feature="openat2()"
nxt_feature_name=NXT_HAVE_OPENAT2
nxt_feature_run=
Expand Down
15 changes: 15 additions & 0 deletions src/java/nxt_jni_InputStream.c
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,27 @@ static jint JNICALL
nxt_java_InputStream_readLine(JNIEnv *env, jclass cls,
jlong req_info_ptr, jarray out, jint off, jint len)
{
jsize array_len;
uint8_t *data;
ssize_t res;
nxt_unit_request_info_t *req;

req = nxt_jlong2ptr(req_info_ptr);

/*
* Validate (off, len) against the array bounds before handing
* GetPrimitiveArrayCritical's pointer + an attacker-controlled
* offset to nxt_unit_request_read(). Without this, a malicious
* caller can drive an OOB write of up to len bytes past the
* array's heap allocation.
*/
array_len = (*env)->GetArrayLength(env, out);
if (off < 0 || len < 0 || off > array_len || len > array_len - off) {
nxt_java_throw_IllegalStateException(env,
"InputStream.readLine: off/len out of bounds");
return -1;
}
Comment on lines +107 to +112

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The out array parameter is not checked for NULL before calling GetArrayLength. If a Java caller passes null, this will cause a JVM crash. A defensive NULL check should be added first.

    if (out == NULL) {
        nxt_java_throw_IllegalStateException(env,
            "InputStream.readLine: output array is null");
        return -1;
    }

    array_len = (*env)->GetArrayLength(env, out);
    if (off < 0 || len < 0 || off > array_len || len > array_len - off) {
        nxt_java_throw_IllegalStateException(env,
            "InputStream.readLine: off/len out of bounds");
        return -1;
    }


data = (*env)->GetPrimitiveArrayCritical(env, out, NULL);

res = nxt_unit_request_readline_size(req, len);
Expand Down
23 changes: 23 additions & 0 deletions src/java/nxt_jni_Request.c
Original file line number Diff line number Diff line change
Expand Up @@ -731,12 +731,23 @@ static void JNICALL
nxt_java_Request_sendWsFrameBuf(JNIEnv *env, jclass cls,
jlong req_info_ptr, jobject buf, jint pos, jint len, jbyte opCode, jboolean last)
{
jlong cap;
nxt_unit_request_info_t *req;

req = nxt_jlong2ptr(req_info_ptr);
uint8_t *b = (*env)->GetDirectBufferAddress(env, buf);

if (b != NULL) {
cap = (*env)->GetDirectBufferCapacity(env, buf);
if (pos < 0 || len < 0 || cap < 0
|| (jlong) pos > cap
|| (jlong) len > cap - (jlong) pos)
{
nxt_java_throw_IllegalStateException(env,
"sendWsFrame: pos/len out of buffer capacity");
return;
}

nxt_unit_websocket_send(req, opCode, last, b + pos, len);

} else {
Expand All @@ -749,9 +760,21 @@ static void JNICALL
nxt_java_Request_sendWsFrameArr(JNIEnv *env, jclass cls,
jlong req_info_ptr, jarray arr, jint pos, jint len, jbyte opCode, jboolean last)
{
jsize cap;
nxt_unit_request_info_t *req;

req = nxt_jlong2ptr(req_info_ptr);

cap = (*env)->GetArrayLength(env, arr);
if (pos < 0 || len < 0
|| pos > cap
|| len > cap - pos)
{
nxt_java_throw_IllegalStateException(env,
"sendWsFrame: pos/len out of array length");
return;
}
Comment on lines +768 to +776

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The arr parameter is not checked for NULL before calling GetArrayLength. If a Java caller passes null, this will cause a JVM crash. A defensive NULL check should be added first.

    if (arr == NULL) {
        nxt_java_throw_IllegalStateException(env,
            "sendWsFrame: array is null");
        return;
    }

    cap = (*env)->GetArrayLength(env, arr);
    if (pos < 0 || len < 0
        || pos > cap
        || len > cap - pos)
    {
        nxt_java_throw_IllegalStateException(env,
            "sendWsFrame: pos/len out of array length");
        return;
    }


uint8_t *b = (*env)->GetPrimitiveArrayCritical(env, arr, NULL);

if (b != NULL) {
Expand Down
12 changes: 12 additions & 0 deletions src/nxt_capability.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
* Copyright (C) NGINX, Inc.
*/

/*
* NOTE: this module currently exposes capability *detection*
* (capget), used by nxt_isolation.c and the credential machinery to
* decide whether a non-root build can honor setuid/setgid in the
* "user"/"group" config keys. Capabilities are NOT dropped
* programmatically via capset(2): the privilege barrier for app
* processes is the existing setuid + PR_SET_NO_NEW_PRIVS dance in
* nxt_credential.c / nxt_process.c. Operators expecting an explicit
* capability-drop step should not assume one exists; isolation in
* Unit relies on uid/namespace/seccomp boundaries, not capset.
*/

#include <nxt_main.h>

#if (NXT_HAVE_LINUX_CAPABILITY)
Expand Down
81 changes: 71 additions & 10 deletions src/nxt_cgroup.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@


static int nxt_mk_cgpath_relative(nxt_task_t *task, const char *dir,
char *cgpath);
char *cgpath, nxt_pid_t pid);
static nxt_int_t nxt_mk_cgpath(nxt_task_t *task, const char *dir,
char *cgpath);
char *cgpath, nxt_pid_t pid);


nxt_int_t
Expand All @@ -29,7 +29,16 @@ nxt_cgroup_proc_add(nxt_task_t *task, nxt_process_t *process)
return NXT_OK;
}

ret = nxt_mk_cgpath(task, process->isolation.cgroup.path, cgprocs);
/*
* Resolve the cgroup path against /proc/<child>/cgroup rather than
* /proc/self/cgroup: the parent's cgroup view may differ from the
* just-forked child's, particularly when CLONE_NEWCGROUP is in play
* and the configured path is relative. Reading the child's own
* /proc entry avoids a TOCTOU where the parent moves between cgroups
* after fork() but before this write.
*/
ret = nxt_mk_cgpath(task, process->isolation.cgroup.path, cgprocs,
process->pid);
if (nxt_slow_path(ret == NXT_ERROR)) {
return NXT_ERROR;
}
Expand All @@ -41,6 +50,18 @@ nxt_cgroup_proc_add(nxt_task_t *task, nxt_process_t *process)

len = strlen(cgprocs);

/*
* Stash the resolved directory before appending "/cgroup.procs" so
* nxt_cgroup_cleanup() can rmdir without re-reading /proc/<pid>/cgroup,
* which is gone once the child has exited.
*/
process->isolation.cgroup.resolved_path = nxt_mp_alloc(process->mem_pool,
len + 1);
if (nxt_fast_path(process->isolation.cgroup.resolved_path != NULL)) {
nxt_memcpy(process->isolation.cgroup.resolved_path, cgprocs, len);
process->isolation.cgroup.resolved_path[len] = '\0';
}

len = snprintf(cgprocs + len, NXT_MAX_PATH_LEN - len, "/cgroup.procs");
if (nxt_slow_path(len >= NXT_MAX_PATH_LEN - len)) {
nxt_errno = ENAMETOOLONG;
Expand Down Expand Up @@ -71,35 +92,72 @@ nxt_cgroup_cleanup(nxt_task_t *task, const nxt_process_t *process)
char cgroot[NXT_MAX_PATH_LEN], cgpath[NXT_MAX_PATH_LEN];
nxt_int_t ret;

ret = nxt_mk_cgpath(task, "", cgroot);
/*
* cgroot is the parent process's own cgroup directory; we must not
* rmdir it. Resolved against /proc/self/cgroup (pid=0): the child
* is gone by the time cleanup runs, so /proc/<child_pid>/cgroup no
* longer exists. The TOCTOU concern that motivated using the
* child's view in nxt_cgroup_proc_add() does not apply at cleanup
* — we just need a stop boundary, and rmdir on the parent's own
* cgroup will fail anyway because it is non-empty.
*/
ret = nxt_mk_cgpath(task, "", cgroot, 0);
if (nxt_slow_path(ret == NXT_ERROR)) {
return;
}

ret = nxt_mk_cgpath(task, process->isolation.cgroup.path, cgpath);
if (nxt_slow_path(ret == NXT_ERROR)) {
/*
* Use the resolved path cached by nxt_cgroup_proc_add(); falling
* back to /proc/<pid>/cgroup here would fail with ENOENT. If the
* cache was missed (e.g. mp_alloc failure during add), there is
* nothing to clean up that we can address safely — bail out.
*/
if (process->isolation.cgroup.resolved_path == NULL) {
return;
}

ret = snprintf(cgpath, sizeof(cgpath), "%s",
process->isolation.cgroup.resolved_path);
if (nxt_slow_path(ret < 0 || (size_t) ret >= sizeof(cgpath))) {
return;
}

while (*cgpath != '\0' && strcmp(cgroot, cgpath) != 0) {
rmdir(cgpath);
ptr = strrchr(cgpath, '/');
if (ptr == NULL) {
break;
}
*ptr = '\0';
}
}


static int
nxt_mk_cgpath_relative(nxt_task_t *task, const char *dir, char *cgpath)
nxt_mk_cgpath_relative(nxt_task_t *task, const char *dir, char *cgpath,
nxt_pid_t pid)
{
int i, len;
char *buf, *ptr;
FILE *fp;
size_t size;
ssize_t nread;
nxt_bool_t found;
char procpath[NXT_MAX_PATH_LEN];

if (pid > 0) {
len = snprintf(procpath, sizeof(procpath), "/proc/%d/cgroup",
(int) pid);
if (len < 0 || (size_t) len >= sizeof(procpath)) {
nxt_errno = ENAMETOOLONG;
return -1;
}
} else {
nxt_memcpy(procpath, "/proc/self/cgroup",
sizeof("/proc/self/cgroup"));
}

fp = nxt_file_fopen(task, "/proc/self/cgroup", "re");
fp = nxt_file_fopen(task, procpath, "re");
if (nxt_slow_path(fp == NULL)) {
return -1;
}
Expand Down Expand Up @@ -145,7 +203,7 @@ nxt_mk_cgpath_relative(nxt_task_t *task, const char *dir, char *cgpath)


static nxt_int_t
nxt_mk_cgpath(nxt_task_t *task, const char *dir, char *cgpath)
nxt_mk_cgpath(nxt_task_t *task, const char *dir, char *cgpath, nxt_pid_t pid)
{
int len;

Expand All @@ -154,9 +212,12 @@ nxt_mk_cgpath(nxt_task_t *task, const char *dir, char *cgpath)
* the cgroup path include the main unit processes cgroup. I.e
*
* NXT_CGROUP_ROOT/<main process cgroup>/<cgroup path>
*
* pid: read /proc/<pid>/cgroup so the path reflects the just-forked
* child's cgroup view (or 0 to fall back to /proc/self/cgroup).
*/
if (dir[0] != '/') {
len = nxt_mk_cgpath_relative(task, dir, cgpath);
len = nxt_mk_cgpath_relative(task, dir, cgpath, pid);
} else {
len = snprintf(cgpath, NXT_MAX_PATH_LEN, NXT_CGROUP_ROOT "%s", dir);
}
Expand Down
Loading
Loading