Skip to content

control: remove unprivileged socket#680

Open
rsmarples wants to merge 8 commits into
masterfrom
control
Open

control: remove unprivileged socket#680
rsmarples wants to merge 8 commits into
masterfrom
control

Conversation

@rsmarples

Copy link
Copy Markdown
Member

Instead workout credentials from the connected socket by getpeereid(2).
Remove the limit of only one connected control client.

This needs some testing and is only expected to compile on the BSDs right now.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f603c9dd-29c0-4b0f-a36c-0cb9d4e7ba2a

📥 Commits

Reviewing files that changed from the base of the PR and between f836607 and ec2065b.

📒 Files selected for processing (1)
  • src/control.c
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/control.c

Walkthrough

This PR removes the separate unprivileged control socket, uses peer credentials for privilege classification, changes control messaging to recvmsg()/msghdr, and updates privsep forwarding, root privilege IPC, socket consumers, and controller lifecycle handling.

Changes

Unified control socket and privsep privilege flow

Layer / File(s) Summary
Control and privsep interfaces
src/control.h, src/privsep.h, src/privsep-control.h, src/privsep-root.h
Control flags and APIs now use FD_PRIV, msghdr-based messaging, and the PS_USER_ISPRIV command.
Single-socket reception and privilege classification
src/control.c
Control reception extracts peer credentials, resolves privilege locally or through privsep root IPC, parses commands with control_recvmsg, and reuses control-fd entries.
Socket lifecycle and command consumers
src/control.c, src/dhcpcd.c, src/dhcpcd.h, src/dhcpcd.8.in, src/defs.h
Unprivileged socket state and paths are removed; socket creation, callers, authorization, response framing, documentation, and version metadata are updated.
Privsep control proxy forwarding
src/privsep-control.c
Privsep dispatch creates per-message control fds, forwards command/listen data to matching clients, and sends messages through ps_ctl_sendmsg.
Root-side privilege IPC and controller lifecycle
src/privsep-root.c, src/privsep.c
The root process handles PS_USER_ISPRIV requests, and controller-specific cleanup preserves root resources.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Control as control.c
  participant Root as privsep-root.c
  participant Proxy as privsep-control.c

  Client->>Control: send control command
  Control->>Control: recvmsg and extract uid/gid
  Control->>Root: ps_root_user_ispriv(uid, gid)
  Root-->>Control: privilege result
  Control->>Proxy: ps_ctl_sendmsg(msghdr)
  Proxy-->>Client: forward response or listen data
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: removing the unprivileged socket.
Description check ✅ Passed The description is related to the changeset and mentions credential handling and removing the single-client limit.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch control

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/dhcpcd.c (1)

2570-2576: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Duplicate "spawned manager process" log.

The same logdebugx(...) now appears both before the start_manager: label (line 2572) and after it (line 2576). On the daemonise fall-through path both fire, logging the manager PID twice; the goto start_manager path only hits 2576. Drop the pre-label copy so both paths log exactly once.

🩹 Remove the duplicate
 	/* We have now forked, setsid, forked once more.
 	 * From this point on, we are the controlling daemon. */
-	logdebugx("spawned manager process on PID %d", (int)getpid());
 
 start_manager:
 
 	logdebugx("spawned manager process on PID %d", (int)getpid());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dhcpcd.c` around lines 2570 - 2576, The manager startup debug message is
duplicated around the start_manager label in dhcpcd.c, causing the daemonise
fall-through path to log the same PID twice. Remove the pre-label logdebugx call
and keep the single logdebugx("spawned manager process on PID %d", ...) after
start_manager so both the fall-through and goto start_manager paths emit the
message exactly once.
🧹 Nitpick comments (4)
src/privsep-root.c (1)

328-338: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant reassignment of p.

p is already initialized to data at Line 328, so the p = data; at Line 335 is redundant. Minor cleanup.

♻️ Cleanup
-	uint8_t *p = data;
+	uint8_t *p;
 
 	if (len != sizeof(uid) + sizeof(gid)) {
 		errno = EINVAL;
 		return -1;
 	}
 
 	p = data;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/privsep-root.c` around lines 328 - 338, Remove the redundant reassignment
of the local pointer in the privilege-separation parsing logic: in the function
that reads `uid` and `gid` from `data`, `p` is already initialized before the
length check, so the later `p = data;` is unnecessary. Keep the existing
`memcpy` flow intact and simply delete the duplicate assignment while preserving
the `uid`/`gid` extraction behavior.
src/privsep-control.c (1)

40-42: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused PS_CTL_FLAGS_PRIV macro
PS_CTL_FLAGS_PRIV is never referenced; privilege is already carried by PS_CTL_PRIV/PS_CTL in ps_ctl_dispatch, so this macro can be dropped.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/privsep-control.c` around lines 40 - 42, Remove the unused
PS_CTL_FLAGS_PRIV macro from privsep-control.c and keep the PS_CTL_FD helper
intact. Verify that ps_ctl_dispatch and the existing PS_CTL_PRIV/PS_CTL macros
still provide the privilege handling needed, and delete any related dead
definition so the header/source only contains symbols that are actually
referenced.
src/control.c (2)

453-453: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

listen() backlog uses sizeof(ctx->control_fds).

ctx->control_fds is a TAILQ head, so sizeof yields the struct size (~16), not a meaningful connection backlog. It works but is misleading; consider a named constant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/control.c` at line 453, The backlog passed to listen() in the control
socket setup uses sizeof(ctx->control_fds), which is just the TAILQ head size
and not a meaningful queue limit. Update the listen() call in the control
initialization path to use a clear named constant or defined backlog value
instead of sizeof(ctx->control_fds), keeping the existing fd/control_fds logic
unchanged.

434-454: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Use the SOCK_MODE macro and confirm the world-writable socket is intended.

SOCK_MODE is defined at line 434 (same value, 0666) but control_start1() chmods with the literal 0666, leaving the macro unused. Static analysis also flags the world-writable mode (CWE-732). Under the new credential model any user may connect and privilege is enforced via getpeereid/FD_PRIV, so world-writable is plausibly intentional — but then the ctx->control_group chown no longer restricts who can connect. Please confirm 0666 is desired rather than 0660 + control_group, and either way replace the literal with the macro.

♻️ Use the defined macro
 	if (bind(fd, (struct sockaddr *)&sa, len) == -1 ||
-	    chmod(sa.sun_path, 0666) == -1 ||
+	    chmod(sa.sun_path, SOCK_MODE) == -1 ||
 	    (ctx->control_group &&
 		chown(sa.sun_path, geteuid(), ctx->control_group) == -1) ||
 	    listen(fd, sizeof(ctx->control_fds)) == -1)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/control.c` around lines 434 - 454, In control_start1(), replace the
hardcoded chmod mode with the existing SOCK_MODE macro so the defined socket
permissions are used consistently. Also verify the intended access model for the
control socket: if world-writable access is deliberate under the new
credential/FD_PRIV checks, keep SOCK_MODE and leave a note or related
adjustment; otherwise tighten the mode to a group-only setting and ensure
ctx->control_group still meaningfully restricts access.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/control.c`:
- Around line 642-676: In control_user_ispriv(), treat an unknown uid from
getpwuid() as non-privileged instead of returning -1, so control_handle_data()
does not drop read-only clients whose user is missing from the passwd database.
Update the early return path in control_user_ispriv() to return 0 for this case,
and make sure any error logging in the caller does not rely on stale errno from
getpwuid() when no real error occurred.

In `@src/privsep-control.c`:
- Around line 111-123: The PS_CTL_PRIV/PS_CTL handling in privsep control is
incorrectly closing the shared proxy work fd when control_recvmsg() fails.
Update the control_new()/control_free() flow so the wrapper is dropped without
closing ctx->ps_ctl->psp_work_fd on the error path, and keep the underlying
shared fd registered for later dispatches; use the PS_CTL_PRIV/PS_CTL case block
and the control_recvmsg() cleanup branch as the places to adjust.

---

Outside diff comments:
In `@src/dhcpcd.c`:
- Around line 2570-2576: The manager startup debug message is duplicated around
the start_manager label in dhcpcd.c, causing the daemonise fall-through path to
log the same PID twice. Remove the pre-label logdebugx call and keep the single
logdebugx("spawned manager process on PID %d", ...) after start_manager so both
the fall-through and goto start_manager paths emit the message exactly once.

---

Nitpick comments:
In `@src/control.c`:
- Line 453: The backlog passed to listen() in the control socket setup uses
sizeof(ctx->control_fds), which is just the TAILQ head size and not a meaningful
queue limit. Update the listen() call in the control initialization path to use
a clear named constant or defined backlog value instead of
sizeof(ctx->control_fds), keeping the existing fd/control_fds logic unchanged.
- Around line 434-454: In control_start1(), replace the hardcoded chmod mode
with the existing SOCK_MODE macro so the defined socket permissions are used
consistently. Also verify the intended access model for the control socket: if
world-writable access is deliberate under the new credential/FD_PRIV checks,
keep SOCK_MODE and leave a note or related adjustment; otherwise tighten the
mode to a group-only setting and ensure ctx->control_group still meaningfully
restricts access.

In `@src/privsep-control.c`:
- Around line 40-42: Remove the unused PS_CTL_FLAGS_PRIV macro from
privsep-control.c and keep the PS_CTL_FD helper intact. Verify that
ps_ctl_dispatch and the existing PS_CTL_PRIV/PS_CTL macros still provide the
privilege handling needed, and delete any related dead definition so the
header/source only contains symbols that are actually referenced.

In `@src/privsep-root.c`:
- Around line 328-338: Remove the redundant reassignment of the local pointer in
the privilege-separation parsing logic: in the function that reads `uid` and
`gid` from `data`, `p` is already initialized before the length check, so the
later `p = data;` is unnecessary. Keep the existing `memcpy` flow intact and
simply delete the duplicate assignment while preserving the `uid`/`gid`
extraction behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0f1cec4a-8cc9-4b2c-99f5-c3d964783ef1

📥 Commits

Reviewing files that changed from the base of the PR and between 8b31291 and 03a1d7b.

📒 Files selected for processing (11)
  • src/control.c
  • src/control.h
  • src/dhcpcd.8.in
  • src/dhcpcd.c
  • src/dhcpcd.h
  • src/privsep-control.c
  • src/privsep-control.h
  • src/privsep-root.c
  • src/privsep-root.h
  • src/privsep.c
  • src/privsep.h

Comment thread src/control.c
Comment thread src/privsep-control.c
rsmarples added 3 commits July 5, 2026 16:20
Instead workout credentials from the connected socket
by getpeereid(2).
Remove the limit of only one connected control client.

This needs some testing and is only expected to compile on the BSDs
right now.
Add --isprivileged to return either true or false so the
caller can work this out as we now just have the one socket.
Bump version for this.

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/privsep-control.c`:
- Around line 178-181: Update the recvmsg error handling around msglen in the
control proxy to detect EAGAIN, EWOULDBLOCK, and EINTR and return to the event
loop without calling eloop_exit for those recoverable failures. Preserve the
existing log-and-exit behavior for all other recvmsg errors.
- Around line 259-262: Update the socketpair creation in the privsep control
setup to preserve non-blocking behavior for both work_fd and listen_fd by
restoring the non-blocking socket flag alongside SOCK_CLOEXEC, unless
xsocketpair or the target platform already guarantees it. Keep both descriptors
suitable for use by the eloop event loop.
- Line 97: Replace the unbounded strcmp check for "--isprivileged" in the
control request handling branch with a length-bounded comparison using the
received len value, matching the surrounding bounded-payload logic and
preventing reads beyond the client buffer.
- Around line 211-230: Update the control-message reads in the receive path
around ps_ctl_listen() so they handle partial length-prefix and payload reads
without stalling the event loop: preserve non-blocking socket setup in
ps_ctl_start() using SOCK_CXNB, or replace these reads with recv(...,
MSG_WAITALL) while retaining appropriate EOF/error handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 33712ca1-d250-4d7c-b5ba-e2bded8f15d6

📥 Commits

Reviewing files that changed from the base of the PR and between 8699713 and 90b239e.

📒 Files selected for processing (5)
  • src/control.c
  • src/control.h
  • src/defs.h
  • src/dhcpcd.c
  • src/privsep-control.c
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/control.c

Comment thread src/privsep-control.c Outdated
Comment thread src/privsep-control.c
Comment on lines +178 to +181
if (msglen == -1) {
logerr("%s: read", __func__);
eloop_exit(ctx->eloop, EXIT_FAILURE);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle non-fatal errors from recvmsg.

If recvmsg fails with EAGAIN, EWOULDBLOCK, or EINTR, terminating the control proxy process via eloop_exit is overly aggressive. This is especially problematic if the socket is reverted to non-blocking mode, where EAGAIN is expected.

Consider ignoring these recoverable errors so the event loop can retry.

🛠️ Proposed fix
 	if (msglen == -1) {
+		if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
+			return;
 		logerr("%s: read", __func__);
 		eloop_exit(ctx->eloop, EXIT_FAILURE);
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (msglen == -1) {
logerr("%s: read", __func__);
eloop_exit(ctx->eloop, EXIT_FAILURE);
}
if (msglen == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
return;
logerr("%s: read", __func__);
eloop_exit(ctx->eloop, EXIT_FAILURE);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/privsep-control.c` around lines 178 - 181, Update the recvmsg error
handling around msglen in the control proxy to detect EAGAIN, EWOULDBLOCK, and
EINTR and return to the event loop without calling eloop_exit for those
recoverable failures. Preserve the existing log-and-exit behavior for all other
recvmsg errors.

Comment thread src/privsep-control.c Outdated
Comment thread src/privsep-control.c
Comment on lines +259 to +262
if (xsocketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, work_fd) ==
-1 ||
xsocketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, listen_fd) ==
-1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Retain non-blocking behavior for event-loop file descriptors.

Replacing SOCK_CXNB with SOCK_STREAM | SOCK_CLOEXEC drops the SOCK_NONBLOCK flag (assuming SOCK_CXNB provided it). Since work_fd and listen_fd are added to the eloop (lines 292 and 299), they should remain non-blocking to prevent any read() or recvmsg() calls from stalling the single-threaded event loop.

Unless SOCK_CLOEXEC natively implies non-blocking on this target, please restore the non-blocking flag.

🛠️ Proposed fix
-	if (xsocketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, work_fd) ==
-		-1 ||
-	    xsocketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, listen_fd) ==
-		-1)
+	if (xsocketpair(AF_UNIX, SOCK_STREAM | SOCK_CXNB, 0, work_fd) ==
+		-1 ||
+	    xsocketpair(AF_UNIX, SOCK_STREAM | SOCK_CXNB, 0, listen_fd) ==
+		-1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (xsocketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, work_fd) ==
-1 ||
xsocketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, listen_fd) ==
-1)
if (xsocketpair(AF_UNIX, SOCK_STREAM | SOCK_CXNB, 0, work_fd) ==
-1 ||
xsocketpair(AF_UNIX, SOCK_STREAM | SOCK_CXNB, 0, listen_fd) ==
-1)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/privsep-control.c` around lines 259 - 262, Update the socketpair creation
in the privsep control setup to preserve non-blocking behavior for both work_fd
and listen_fd by restoring the non-blocking socket flag alongside SOCK_CLOEXEC,
unless xsocketpair or the target platform already guarantees it. Keep both
descriptors suitable for use by the eloop event loop.

@rsmarples

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

1 participant