diff --git a/CHANGES b/CHANGES index 8ae835854..e90677814 100644 --- a/CHANGES +++ b/CHANGES @@ -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 diff --git a/auto/files b/auto/files index 1fa6ca28f..4912ed802 100644 --- a/auto/files +++ b/auto/files @@ -51,6 +51,24 @@ nxt_feature_test="#include . 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 + #include + + 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= diff --git a/src/nxt_conn_accept.c b/src/nxt_conn_accept.c index e0f09a4d0..7e2576705 100644 --- a/src/nxt_conn_accept.c +++ b/src/nxt_conn_accept.c @@ -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) diff --git a/src/nxt_epoll_engine.c b/src/nxt_epoll_engine.c index d53df1bc4..a4d49ceb0 100644 --- a/src/nxt_epoll_engine.c +++ b/src/nxt_epoll_engine.c @@ -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 @@ -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; diff --git a/src/nxt_file.c b/src/nxt_file.c index 4047d9d7c..37c60aa58 100644 --- a/src/nxt_file.c +++ b/src/nxt_file.c @@ -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; } } diff --git a/src/nxt_http_compression.c b/src/nxt_http_compression.c index 079eceef2..676cb939f 100644 --- a/src/nxt_http_compression.c +++ b/src/nxt_http_compression.c @@ -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; 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; } @@ -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; } @@ -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; } diff --git a/src/nxt_kqueue_engine.c b/src/nxt_kqueue_engine.c index a7a5a29ed..e8e282437 100644 --- a/src/nxt_kqueue_engine.c +++ b/src/nxt_kqueue_engine.c @@ -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);