fix(port): authorize privileged IPC senders by kernel-validated PID#29
fix(port): authorize privileged IPC senders by kernel-validated PID#29andypost wants to merge 2 commits into
Conversation
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.
There was a problem hiding this comment.
Code Review
This pull request enhances security by replacing self-declared process IDs with kernel-validated PIDs across several message handlers to prevent spoofing by compromised worker processes. The review feedback correctly identifies two potential NULL pointer dereferences in src/nxt_main_process.c where the discovery and controller ports are accessed without prior NULL checks, which could lead to main process crashes.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| 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; | ||
| } |
There was a problem hiding this comment.
If the discovery process has exited or is not running, rt->port_by_type[NXT_PROCESS_DISCOVERY] can be NULL. Accessing its pid field directly will cause a NULL pointer dereference and crash the main process.
Add a NULL check for the discovery port before comparing its 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));
return;
}| 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; | ||
| } |
There was a problem hiding this comment.
If the controller process has not started yet or has exited, ctl_port can be NULL. Accessing ctl_port->pid without a prior NULL check will result in a NULL pointer dereference and crash the main process.
Add a NULL check for ctl_port before dereferencing it, similar to how router_port is checked in other handlers.
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;
}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.
Summary
The main process accepts several privileged-operation IPC messages
(
conf_store,modules,socket,socket_unlink,access_log,cert_get,script_get). Most of the receiving handlers authorizethe sender by comparing
msg->port_msg.pidto an expected process'sPID. But
port_msg.pidis written by the sender atsrc/nxt_port_socket.c(msg.port_msg.pid = nxt_pid), so acompromised worker can forge it and impersonate any peer. Concrete
chains reachable today:
conf_storeaccess_log/etc/passwdor any other privileged path.socket_unlinksocketcert_get/script_getmodulessocketpair()already enablesSO_PASSCREDon every IPC pair and therecv 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.pidonly on platforms withoutSCM_CREDENTIALS/cmsgcred, i.e. neither Linux nor FreeBSD). Ahandful of handlers already use it (
cert_delete,script_delete,cert_ocsp_get,new_port); this PR extends the same shape to thespoofable handlers.
Files
src/nxt_main_process.cmodules_handler,conf_store_handler,socket_handlerswitch authz tocmsg_pid.socket_unlink_handlerandaccess_log_handlergain a router-onlycmsg_pidcheck.src/nxt_cert.ccert_store_get_handlerport lookup usescmsg_pid.src/nxt_script.cscript_store_get_handlerport lookup usescmsg_pid.No behaviour change on the well-formed path: a legitimate sender's
declared PID equals the kernel-stamped PID by construction. An
attempted spoof shows up as the attacker's real PID in the alert,
which is more useful for forensics than the previous self-declared
value.
Tests
A deterministic regression test for the spoof path needs a process
that writes a forged
port_msg.pidinto a socketpair end; thatrequires either a debug-only test hook or fault injection at the send
side, tracked separately.
Independence
Does not depend on PRs #27, #28, or any of the in-flight audit PRs.
Touches three core files, no protocol or config-surface changes, no
public-API changes.
Out of scope
A comprehensive audit of every remaining handler to confirm they each
authorize via
cmsg_pidis queued as a follow-up; this PR closes thehandlers whose spoofable check is on the critical path.
Generated by Claude Code