Skip to content
Open
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
8 changes: 8 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ Changes with FreeUnit 1.35.4 xx xxx 2026
*) Bugfix: replace removed cgi module with email.parser in Python upload
test fixture; fixes test suite compatibility with Python 3.13.

*) Bugfix: tighten file-descriptor and CLOEXEC lifetime across the
accept(), pipe(), and static-file compression paths; accepted
client sockets and pipe ends are now CLOEXEC-protected so they
cannot leak into spawned application processes, pipe creation
no longer leaks one end on nxt_fd_nonblocking() failure, the
compression mmap failure paths close the source file fd, and the
accept4() availability probe falls back only on ENOSYS.

Changes with FreeUnit 1.35.3 07 Apr 2026

*) Feature: migrate contrib package mirror from packages.nginx.org to
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
12 changes: 12 additions & 0 deletions src/nxt_conn_accept.c
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,18 @@ nxt_conn_io_accept(nxt_task_t *task, void *obj, void *data)
return;
}

/*
* Set FD_CLOEXEC so the accepted client socket does not leak into
* spawned application processes (the accept4()-based path uses
* SOCK_CLOEXEC instead).
*/
if (nxt_slow_path(fcntl(s, F_SETFD, FD_CLOEXEC) == -1)) {
nxt_alert(task, "fcntl(%d, F_SETFD, FD_CLOEXEC) failed %E", s,
nxt_errno);
nxt_socket_close(task, s);
return;
}

c->socket.fd = s;

#if (NXT_LINUX)
Expand Down
16 changes: 11 additions & 5 deletions src/nxt_epoll_engine.c
Original file line number Diff line number Diff line change
Expand Up @@ -294,14 +294,20 @@ nxt_epoll_test_accept4(nxt_event_engine_t *engine, nxt_conn_io_t *io)

#if (NXT_HAVE_ACCEPT4)

/*
* Probe accept4() availability. The call is expected to fail
* (fd is -1) -- only ENOSYS indicates the syscall is missing
* and warrants a fallback to plain accept(). Any other errno
* (EBADF on most kernels) means accept4() is supported.
*/
(void) accept4(-1, NULL, NULL, SOCK_NONBLOCK);

if (nxt_errno != NXT_ENOSYS) {
handler = nxt_epoll_conn_io_accept4;

} else {
if (nxt_errno == NXT_ENOSYS) {
nxt_log(&engine->task, NXT_LOG_INFO, "accept4() failed %E",
NXT_ENOSYS);

} else {
handler = nxt_epoll_conn_io_accept4;
}

#endif
Expand Down Expand Up @@ -1025,7 +1031,7 @@ nxt_epoll_conn_io_accept4(nxt_task_t *task, void *obj, void *data)
* The returned socklen is ignored here,
* see comment in nxt_conn_io_accept().
*/
s = accept4(lev->socket.fd, sa, &socklen, SOCK_NONBLOCK);
s = accept4(lev->socket.fd, sa, &socklen, SOCK_NONBLOCK | SOCK_CLOEXEC);

if (s != -1) {
c->socket.fd = s;
Expand Down
22 changes: 22 additions & 0 deletions src/nxt_file.c
Original file line number Diff line number Diff line change
Expand Up @@ -725,22 +725,44 @@ nxt_int_t
nxt_pipe_create(nxt_task_t *task, nxt_fd_t *pp, nxt_bool_t nbread,
nxt_bool_t nbwrite)
{
#if (NXT_HAVE_PIPE2)
if (pipe2(pp, O_CLOEXEC) != 0) {
nxt_alert(task, "pipe2() failed %E", nxt_errno);

return NXT_ERROR;
}
#else
if (pipe(pp) != 0) {
nxt_alert(task, "pipe() failed %E", nxt_errno);

return NXT_ERROR;
}

/*
* Set FD_CLOEXEC on both ends so that pipe fds do not leak across
* exec() into spawned application processes.
*/
if (fcntl(pp[0], F_SETFD, FD_CLOEXEC) == -1
|| fcntl(pp[1], F_SETFD, FD_CLOEXEC) == -1)
{
nxt_alert(task, "fcntl(F_SETFD, FD_CLOEXEC) failed %E", nxt_errno);
nxt_pipe_close(task, pp);
return NXT_ERROR;
}
#endif

nxt_debug(task, "pipe(): %FD:%FD", pp[0], pp[1]);

if (nbread) {
if (nxt_fd_nonblocking(task, pp[0]) != NXT_OK) {
nxt_pipe_close(task, pp);
return NXT_ERROR;
}
}

if (nbwrite) {
if (nxt_fd_nonblocking(task, pp[1]) != NXT_OK) {
nxt_pipe_close(task, pp);
return NXT_ERROR;
}
}
Expand Down
12 changes: 12 additions & 0 deletions src/nxt_http_compression.c
Original file line number Diff line number Diff line change
Expand Up @@ -274,20 +274,28 @@ nxt_http_comp_compress_static_response(nxt_task_t *task, nxt_http_request_t *r,
nxt_alert(task, "ftruncate(%d<%s>, %uz) failed %E",
tfile.fd, tmp_path, out_size, nxt_errno);
nxt_file_close(task, &tfile);
nxt_file_close(task, *f);
*f = NULL;
return NXT_ERROR;
}

in = nxt_mem_mmap(NULL, in_size, PROT_READ, MAP_SHARED, (*f)->fd, 0);
if (nxt_slow_path(in == MAP_FAILED)) {
nxt_alert(task, "mmap(%uz) of source failed %E", in_size, nxt_errno);
nxt_file_close(task, &tfile);
nxt_file_close(task, *f);
*f = NULL;
Comment on lines +286 to +287
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The fix for the source file descriptor leak (V11) is incomplete and introduces an API inconsistency.

  1. Incomplete Leak Fix: While *f is now closed on mmap failures, it is still leaked on other error paths in this function, such as ftruncate failures (lines 273 and 328) and compression failure (line 313). Since the function takes ownership of the file descriptor (as evidenced by it being closed on success at line 336), it should be closed on all error paths to prevent resource exhaustion.
  2. Inconsistent Pointer Handling: Setting *f = NULL on failure (lines 285 and 296) is inconsistent with the success path where the pointer *f remains valid while the underlying struct is updated (**f = tfile). This inconsistency may lead to null pointer dereferences in the caller if it attempts to access the file object after a failure, especially if it expects to fall back to uncompressed serving or perform its own cleanup.

return NXT_ERROR;
}

out = nxt_mem_mmap(NULL, out_size, PROT_READ|PROT_WRITE, MAP_SHARED,
tfile.fd, 0);
if (nxt_slow_path(out == MAP_FAILED)) {
nxt_alert(task, "mmap(%uz) of temp failed %E", out_size, nxt_errno);
nxt_mem_munmap(in, in_size);
nxt_file_close(task, &tfile);
nxt_file_close(task, *f);
*f = NULL;
return NXT_ERROR;
}

Expand All @@ -308,6 +316,8 @@ nxt_http_comp_compress_static_response(nxt_task_t *task, nxt_http_request_t *r,
nxt_file_close(task, &tfile);
nxt_mem_munmap(in, in_size);
nxt_mem_munmap(out, out_size);
nxt_file_close(task, *f);
*f = NULL;
return NXT_ERROR;
}

Expand All @@ -324,6 +334,8 @@ nxt_http_comp_compress_static_response(nxt_task_t *task, nxt_http_request_t *r,
nxt_alert(task, "ftruncate(%d<%s>, %uz) failed %E",
tfile.fd, tmp_path, *out_total, nxt_errno);
nxt_file_close(task, &tfile);
nxt_file_close(task, *f);
*f = NULL;
return NXT_ERROR;
}

Expand Down
11 changes: 11 additions & 0 deletions src/nxt_kqueue_engine.c
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,17 @@ nxt_kqueue_conn_io_accept(nxt_task_t *task, void *obj, void *data)
s = accept(lev->socket.fd, sa, &socklen);

if (s != -1) {
/*
* Set FD_CLOEXEC so the accepted client socket does not leak
* into spawned application processes.
*/
if (nxt_slow_path(fcntl(s, F_SETFD, FD_CLOEXEC) == -1)) {
nxt_alert(task, "fcntl(%d, F_SETFD, FD_CLOEXEC) failed %E", s,
nxt_errno);
nxt_socket_close(task, s);
return;
}

c->socket.fd = s;

nxt_debug(task, "accept(%d): %d", lev->socket.fd, s);
Expand Down
Loading