Skip to content

fix(port): authorize privileged IPC senders by kernel-validated PID#29

Draft
andypost wants to merge 2 commits into
pre-1.35.6from
fix/port-sender-cmsg-pid
Draft

fix(port): authorize privileged IPC senders by kernel-validated PID#29
andypost wants to merge 2 commits into
pre-1.35.6from
fix/port-sender-cmsg-pid

Conversation

@andypost

Copy link
Copy Markdown
Owner

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 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 impersonate any peer. Concrete
chains reachable today:

Handler Effect on successful spoof
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.
modules Directs main to interpret a buffer of "discovered" module descriptors.

socketpair() already enables SO_PASSCRED on every 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 only 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 PR extends the same shape to the
spoofable handlers.

Files

File Change
src/nxt_main_process.c modules_handler, conf_store_handler, socket_handler switch authz to cmsg_pid. socket_unlink_handler and access_log_handler gain a router-only cmsg_pid check.
src/nxt_cert.c cert_store_get_handler port lookup uses cmsg_pid.
src/nxt_script.c script_store_get_handler port lookup uses cmsg_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

./configure --debug --tests --openssl
make -j$(nproc)                                  # clean build
build/sbin/unitd ...                             # starts; new alert
                                                 # strings linked

A deterministic regression 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, 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_pid is queued as a follow-up; this PR closes the
handlers whose spoofable check is on the critical path.


Generated by Claude Code

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment thread src/nxt_main_process.c Outdated
Comment on lines 1480 to 1486
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;
}

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

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;
    }

Comment thread src/nxt_main_process.c Outdated
Comment on lines 1662 to 1666
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;
}

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

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants