From 40f9d8690e8d81e52809c0dac7aeb5889e5e7d21 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 13:51:45 +0000 Subject: [PATCH 1/2] fix(port): authorize privileged IPC senders by kernel-validated PID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The main process accepts a set of privileged-operation messages from its peers — conf_store, modules, socket bind, socket unlink, access log open, cert / script store fetch. Several of these handlers authorize the sender by comparing msg->port_msg.pid to an expected process's PID. But port_msg.pid is written by the sender at src/nxt_port_socket.c (msg.port_msg.pid = nxt_pid), so a compromised worker can forge it and pose as any peer. Spoofable handlers reached without authorization: - conf_store: rewrites the persistent configuration store; arbitrary config on the next reload is root code execution. - access_log: opens a file as root with a sender-supplied path; LPE via /etc/passwd or any other privileged path. - socket_unlink: arbitrary unlink as root. - socket: binds a privileged listening socket on the sender's behalf. - cert_get / script_get: pull arbitrary certificate / script material out of main on behalf of the spoofed identity. - modules: directs main to interpret a buffer of "discovered" module descriptors. socketpair() already enables SO_PASSCRED on every Unit IPC pair, and the recv path stashes the kernel-stamped sender PID into msg->cmsg_pid. The existing nxt_recv_msg_cmsg_pid(msg) helper returns that value (and falls back to port_msg.pid on platforms without SCM_CREDENTIALS / cmsgcred — i.e. neither Linux nor FreeBSD). A handful of handlers already use it (cert_delete, script_delete, cert_ocsp_get, new_port); this commit extends the same shape to the spoofable handlers above. Concretely: - nxt_main_port_modules_handler authz uses cmsg_pid - nxt_main_port_conf_store_handler authz uses cmsg_pid - nxt_main_port_socket_handler port lookup uses cmsg_pid - nxt_main_port_socket_unlink_handler gains a router-only check - nxt_main_port_access_log_handler gains a router-only check - nxt_cert_store_get_handler port lookup uses cmsg_pid - nxt_script_store_get_handler port lookup uses cmsg_pid No behaviour change on the well-formed path: legitimate senders' declared PID equals the kernel-stamped PID by construction. The diagnostics that previously printed msg->port_msg.pid now print the kernel-stamped value, so an attempted spoof shows up as the attacker's real PID in the alert. A deterministic test for the spoof path needs a process that writes a forged port_msg.pid into a socketpair end; that requires either a debug-only test hook or fault injection at the send side. The fix itself is review-verified. --- src/nxt_cert.c | 13 +++++-- src/nxt_main_process.c | 79 ++++++++++++++++++++++++++++++++++++------ src/nxt_script.c | 13 +++++-- 3 files changed, 89 insertions(+), 16 deletions(-) diff --git a/src/nxt_cert.c b/src/nxt_cert.c index 16dc811f9..2c93788dc 100644 --- a/src/nxt_cert.c +++ b/src/nxt_cert.c @@ -1175,12 +1175,19 @@ nxt_cert_store_get_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg) nxt_runtime_t *rt; nxt_port_msg_type_t type; - port = nxt_runtime_port_find(task->thread->runtime, msg->port_msg.pid, + /* + * Look up the sender's port via the kernel-validated PID + * (SCM_CREDENTIALS). msg->port_msg.pid is self-declared, so using + * it would let a compromised worker spoof the controller / router + * and pull arbitrary certificate material out of main. + */ + port = nxt_runtime_port_find(task->thread->runtime, + nxt_recv_msg_cmsg_pid(msg), msg->port_msg.reply_port); if (nxt_slow_path(port == NULL)) { nxt_alert(task, "process port not found (pid %PI, reply_port %d)", - msg->port_msg.pid, msg->port_msg.reply_port); + nxt_recv_msg_cmsg_pid(msg), msg->port_msg.reply_port); return; } @@ -1188,7 +1195,7 @@ nxt_cert_store_get_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg) && port->type != NXT_PROCESS_ROUTER)) { nxt_alert(task, "process %PI cannot store certificates", - msg->port_msg.pid); + nxt_recv_msg_cmsg_pid(msg)); return; } diff --git a/src/nxt_main_process.c b/src/nxt_main_process.c index 8860e4c8c..a4638dd3e 100644 --- a/src/nxt_main_process.c +++ b/src/nxt_main_process.c @@ -1118,7 +1118,14 @@ nxt_main_port_socket_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg) nxt_listening_socket_t ls; u_char message[2048]; - port = nxt_runtime_port_find(task->thread->runtime, msg->port_msg.pid, + /* + * Look up the sender's port via the kernel-validated PID + * (SCM_CREDENTIALS). msg->port_msg.pid is self-declared, so using + * it would let a compromised worker impersonate the router and + * ask main to bind a privileged listening socket. + */ + port = nxt_runtime_port_find(task->thread->runtime, + nxt_recv_msg_cmsg_pid(msg), msg->port_msg.reply_port); if (nxt_slow_path(port == NULL)) { return; @@ -1126,7 +1133,7 @@ nxt_main_port_socket_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg) if (nxt_slow_path(port->type != NXT_PROCESS_ROUTER)) { nxt_alert(task, "process %PI cannot create listener sockets", - msg->port_msg.pid); + nxt_recv_msg_cmsg_pid(msg)); return; } @@ -1333,18 +1340,34 @@ nxt_main_port_socket_unlink_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg) size_t i; nxt_buf_t *b; const char *filename; + nxt_port_t *router_port; nxt_runtime_t *rt; nxt_sockaddr_t *sa; nxt_listen_socket_t *ls; + rt = task->thread->runtime; + + /* + * Privileged unlink of an attacker-controllable path. Only accept + * from the router; identify the sender via SCM_CREDENTIALS so a + * compromised worker cannot spoof the message and have main remove + * arbitrary files. + */ + router_port = rt->port_by_type[NXT_PROCESS_ROUTER]; + if (nxt_slow_path(router_port == NULL + || nxt_recv_msg_cmsg_pid(msg) != router_port->pid)) + { + nxt_alert(task, "process %PI cannot unlink listener sockets", + nxt_recv_msg_cmsg_pid(msg)); + return; + } + b = msg->buf; sa = (nxt_sockaddr_t *) b->mem.pos; filename = sa->u.sockaddr_un.sun_path; unlink(filename); - rt = task->thread->runtime; - for (i = 0; i < rt->listen_sockets->nelts; i++) { const char *name; @@ -1448,8 +1471,17 @@ nxt_main_port_modules_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg) rt = task->thread->runtime; - if (msg->port_msg.pid != rt->port_by_type[NXT_PROCESS_DISCOVERY]->pid) { - nxt_alert(task, "process %PI cannot send modules", msg->port_msg.pid); + /* + * Use the kernel-validated sender PID (SCM_CREDENTIALS) for both + * the authorisation check and the port lookup: msg->port_msg.pid + * is self-declared by the sender and a compromised worker can + * forge it to impersonate discovery / controller / router. + */ + if (nxt_recv_msg_cmsg_pid(msg) + != rt->port_by_type[NXT_PROCESS_DISCOVERY]->pid) + { + nxt_alert(task, "process %PI cannot send modules", + nxt_recv_msg_cmsg_pid(msg)); return; } @@ -1458,7 +1490,8 @@ nxt_main_port_modules_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg) return; } - port = nxt_runtime_port_find(task->thread->runtime, msg->port_msg.pid, + port = nxt_runtime_port_find(task->thread->runtime, + nxt_recv_msg_cmsg_pid(msg), msg->port_msg.reply_port); if (nxt_fast_path(port != NULL)) { @@ -1620,8 +1653,15 @@ nxt_main_port_conf_store_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg) ctl_port = rt->port_by_type[NXT_PROCESS_CONTROLLER]; - if (nxt_slow_path(msg->port_msg.pid != ctl_port->pid)) { - nxt_alert(task, "process %PI cannot store conf", msg->port_msg.pid); + /* + * Use the kernel-validated sender PID (SCM_CREDENTIALS): msg->port_msg.pid + * is self-declared and a compromised worker can forge it to pose as the + * controller and have main rewrite the persistent configuration store — + * arbitrary configuration on the next reload is root code execution. + */ + if (nxt_slow_path(nxt_recv_msg_cmsg_pid(msg) != ctl_port->pid)) { + nxt_alert(task, "process %PI cannot store conf", + nxt_recv_msg_cmsg_pid(msg)); return; } @@ -1728,11 +1768,30 @@ nxt_main_port_access_log_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg) u_char *path; nxt_int_t ret; nxt_file_t file; - nxt_port_t *port; + nxt_port_t *port, *router_port; + nxt_runtime_t *rt; nxt_port_msg_type_t type; nxt_debug(task, "opening access log file"); + rt = task->thread->runtime; + + /* + * Privileged file open as root with an attacker-controllable path. + * Only accept from the router; identify the sender via + * SCM_CREDENTIALS so a compromised worker cannot spoof the message + * and have main create / append to /etc/passwd or any other + * privileged path. + */ + router_port = rt->port_by_type[NXT_PROCESS_ROUTER]; + if (nxt_slow_path(router_port == NULL + || nxt_recv_msg_cmsg_pid(msg) != router_port->pid)) + { + nxt_alert(task, "process %PI cannot open access log", + nxt_recv_msg_cmsg_pid(msg)); + return; + } + path = msg->buf->mem.pos; nxt_memzero(&file, sizeof(nxt_file_t)); diff --git a/src/nxt_script.c b/src/nxt_script.c index 4df011d35..ab680ce83 100644 --- a/src/nxt_script.c +++ b/src/nxt_script.c @@ -541,12 +541,19 @@ nxt_script_store_get_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg) nxt_runtime_t *rt; nxt_port_msg_type_t type; - port = nxt_runtime_port_find(task->thread->runtime, msg->port_msg.pid, + /* + * Look up the sender's port via the kernel-validated PID + * (SCM_CREDENTIALS). msg->port_msg.pid is self-declared, so using + * it would let a compromised worker spoof the controller / router + * and pull arbitrary script material out of main. + */ + port = nxt_runtime_port_find(task->thread->runtime, + nxt_recv_msg_cmsg_pid(msg), msg->port_msg.reply_port); if (nxt_slow_path(port == NULL)) { nxt_alert(task, "process port not found (pid %PI, reply_port %d)", - msg->port_msg.pid, msg->port_msg.reply_port); + nxt_recv_msg_cmsg_pid(msg), msg->port_msg.reply_port); return; } @@ -554,7 +561,7 @@ nxt_script_store_get_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg) && port->type != NXT_PROCESS_ROUTER)) { nxt_alert(task, "process %PI cannot store scripts", - msg->port_msg.pid); + nxt_recv_msg_cmsg_pid(msg)); return; } From 8fd65848d23f8d36fd9e7770377aa629e9836f72 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 14:49:04 +0000 Subject: [PATCH 2/2] fixup(port): NULL-check discovery and controller ports before deref MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Gemini review on PR #29: rt->port_by_type[NXT_PROCESS_DISCOVERY] and rt->port_by_type[NXT_PROCESS_CONTROLLER] are not guaranteed to be non-NULL at every point the corresponding handler can be entered. Discovery exits right after sending its modules message, so a late or duplicated modules packet can arrive after the discovery slot has been cleared. The controller slot is unset during early startup and late teardown. Either case turned the previous direct `->pid` deref into a NULL pointer crash of the privileged main process — a worker DoS that the access check itself failed to gate. Add a NULL guard ahead of the PID comparison in both handlers; on NULL, the alert path runs and the handler returns without dispatching the message. Matches the existing pattern used in the socket_unlink_handler and access_log_handler additions in this PR. --- src/nxt_main_process.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/nxt_main_process.c b/src/nxt_main_process.c index a4638dd3e..fabe76bec 100644 --- a/src/nxt_main_process.c +++ b/src/nxt_main_process.c @@ -1475,10 +1475,14 @@ nxt_main_port_modules_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg) * Use the kernel-validated sender PID (SCM_CREDENTIALS) for both * the authorisation check and the port lookup: msg->port_msg.pid * is self-declared by the sender and a compromised worker can - * forge it to impersonate discovery / controller / router. + * forge it to impersonate discovery / controller / router. Guard + * against a NULL discovery slot — discovery exits after sending + * the modules message, so a late or duplicated message can arrive + * after rt->port_by_type[DISCOVERY] has been cleared. */ - if (nxt_recv_msg_cmsg_pid(msg) - != rt->port_by_type[NXT_PROCESS_DISCOVERY]->pid) + if (nxt_slow_path(rt->port_by_type[NXT_PROCESS_DISCOVERY] == NULL + || nxt_recv_msg_cmsg_pid(msg) + != rt->port_by_type[NXT_PROCESS_DISCOVERY]->pid)) { nxt_alert(task, "process %PI cannot send modules", nxt_recv_msg_cmsg_pid(msg)); @@ -1658,8 +1662,12 @@ nxt_main_port_conf_store_handler(nxt_task_t *task, nxt_port_recv_msg_t *msg) * is self-declared and a compromised worker can forge it to pose as the * controller and have main rewrite the persistent configuration store — * arbitrary configuration on the next reload is root code execution. + * The NULL guard covers the early-startup / late-teardown window in + * which the controller slot is unset. */ - if (nxt_slow_path(nxt_recv_msg_cmsg_pid(msg) != ctl_port->pid)) { + if (nxt_slow_path(ctl_port == NULL + || nxt_recv_msg_cmsg_pid(msg) != ctl_port->pid)) + { nxt_alert(task, "process %PI cannot store conf", nxt_recv_msg_cmsg_pid(msg)); return;