Skip to content

control: remove unprivileged socket - #680

Open
rsmarples wants to merge 17 commits into
masterfrom
control
Open

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

Conversation

@rsmarples

@rsmarples rsmarples commented Jul 5, 2026

Copy link
Copy Markdown
Member

Instead workout credentials from the connected socket by getpeereid(2).
Add readgroup to dhcpcd.conf which controls who can read interface data from the control socket.
Each control socket command will now return an errno after the command has been processed to indicate the result of the command - for example EPERM.
Remove the limit of only one connected control client.

This is a breaking change for socket consumers.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR removes the separate unprivileged control socket, uses peer credentials and group membership for access classification, changes control messaging to recvmsg()/sendmsg(), and updates privsep forwarding, root IPC, socket lifecycle, configuration, compatibility detection, and process cleanup.

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, src/dhcpcd.h
Control flags, command identifiers, context state, and APIs now support msghdr-based messaging, per-connection capabilities, and group-permission checks.
Single-socket reception and privilege classification
src/control.c
Control reception extracts peer credentials, applies access flags, parses null-separated commands with control_recvmsg, handles unified fd events, and reuses tracked entries.
Socket lifecycle and command consumers
src/control.c, src/dhcpcd.c, src/dhcpcd.8.in, src/dhcpcd.conf.5.in
Socket creation, opening, stopping, authorization replies, manager probing, configuration documentation, and socket-file documentation are updated for one control socket.
Group-based access configuration and root IPC
src/if-options.c, src/if-options.h, src/control.c, src/privsep-root.c
readgroup is parsed and stored, group membership is evaluated locally or through PS_USER_INGROUP, and root IPC validates and returns the membership result.
Privsep control proxy forwarding
src/privsep-control.c, src/privsep-control.h
Privsep dispatch creates per-message control fds, receives framed payloads, broadcasts command/listen data, and sends messages through ps_ctl_sendmsg.
Controller lifecycle and portable peer credentials
src/privsep.c, compat/getpeereid.*, configure
Controller-specific cleanup preserves root resources, while build detection and compatibility implementations provide getpeereid where needed.
Release and kqueue updates
src/defs.h, src/eloop.c, src/script.c
The version, kqueue command type, and send_interface1 return type are updated.

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
  participant Peers as control_fds

  Client->>Control: send control command
  Control->>Control: recvmsg and extract uid/gid
  Control->>Root: ps_root_user_ingroup(uid, gid, grpid)
  Root-->>Control: group permission result
  Control->>Proxy: ps_ctl_sendmsg(msghdr)
  Proxy->>Peers: dispatch and broadcast command or listen data
  Peers-->>Client: queued response
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title matches a central change: removing the unprivileged control socket.
Description check ✅ Passed The description aligns with the changeset, covering getpeereid, readgroup, errno replies, and multiple control clients.
✨ Finishing Touches 💡 1
📝 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 Outdated
Comment thread src/privsep-control.c Outdated
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.

@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

🤖 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 `@compat/getpeereid.c`:
- Around line 32-35: Update compat/getpeereid.c to include the project or system
header that declares getpeereid, alongside the existing conditional ucred.h
include, so the function definition has a visible matching prototype and
eliminates the -Wmissing-prototypes warning.

In `@configure`:
- Around line 943-949: Update the _getpeereid configure probe to include
<unistd.h> alongside the existing headers before calling getpeereid, so macOS
and BSD systems compile the detection test successfully and select native
support.
🪄 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: 28030e40-8769-4008-a769-556062710614

📥 Commits

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

📒 Files selected for processing (4)
  • compat/getpeereid.c
  • compat/getpeereid.h
  • configure
  • src/control.c
💤 Files with no reviewable changes (1)
  • src/control.c

Comment thread compat/getpeereid.c
Comment thread configure

@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: 1

🤖 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/eloop.c`:
- Around line 255-256: Preserve the caller’s original nsignals value before the
normalization at lines 243-245, then use that preserved zero-state to choose
EVFILT_SIGNAL’s EV_DELETE versus EV_ADD command in the EV_SET call. Keep the
existing normalized eloop->nsignals value for subsequent signal processing.
🪄 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: 1c9fa012-e070-46f9-a827-a4f069f14078

📥 Commits

Reviewing files that changed from the base of the PR and between e578711 and 5f6020c.

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

Comment thread src/eloop.c Outdated
@rsmarples

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 15, 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.

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

🧹 Nitpick comments (1)
src/control.c (1)

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

Consider undefining the macro or using a function-like macro to prevent namespace pollution.

Defining a generic macro like GID mid-file without an #undef allows it to leak, which can cause unexpected macro expansions in code added later. Using a function-like macro also clarifies the intent since it visually resembles a cast or function call rather than a prefix operator.

♻️ Proposed refactor
 `#ifdef` __APPLE__
 	/* why is getgrouplist API is different ..... */
 	int *groups = NULL, *gp;
-#define GID (int)
+#define CAST_GID(x) ((int)(x))
 `#else`
 	gid_t *groups = NULL, *gp;
-#define GID
+#define CAST_GID(x) (x)
 `#endif`
 	struct passwd *pw;
 	int ngroups = 10, err = -1;
 
 	pw = getpwuid(uid);
 	if (pw == NULL)
 		return 0; /* unknown user is not privileged */
 
 	groups = reallocarray(groups, (size_t)ngroups, sizeof(*groups));
 	if (groups == NULL)
 		return -1;
 
-	if (getgrouplist(pw->pw_name, GID gid, groups, &ngroups) == -1) {
+	if (getgrouplist(pw->pw_name, CAST_GID(gid), groups, &ngroups) == -1) {
 		gp = reallocarray(groups, (size_t)ngroups, sizeof(*groups));
 		if (gp == NULL)
 			goto out;
 		groups = gp;
-		if (getgrouplist(pw->pw_name, GID gid, groups, &ngroups) == -1)
+		if (getgrouplist(pw->pw_name, CAST_GID(gid), groups, &ngroups) == -1)
 			goto out;
 	}
 
 	for (gp = groups; ngroups != 0; ngroups--, gp++) {
-		if (*gp == GID ctx->control_group) {
+		if (*gp == CAST_GID(ctx->control_group)) {
 			err = 1;
 			goto out;
 		}
 	}
 	err = 0;
 
 out:
+
+#undef CAST_GID
 	free(groups);
 	return err;
🤖 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 651 - 689, Replace the object-like GID macro in
the getgrouplist logic with a function-like cast macro, or undefine it
immediately after its final use, so it cannot affect later code. Preserve the
platform-specific cast behavior for the getgrouplist and group comparison
expressions.
🤖 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.

Nitpick comments:
In `@src/control.c`:
- Around line 651-689: Replace the object-like GID macro in the getgrouplist
logic with a function-like cast macro, or undefine it immediately after its
final use, so it cannot affect later code. Preserve the platform-specific cast
behavior for the getgrouplist and group comparison expressions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d10855b1-e3d6-45ea-8e2f-0722494cfa49

📥 Commits

Reviewing files that changed from the base of the PR and between 5f6020c and aa61e1b.

📒 Files selected for processing (2)
  • src/control.c
  • src/eloop.c

data to everyone on the system.

Always send an error back when controlling dhcpcd.

@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

Caution

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

⚠️ Outside diff range comments (1)
src/privsep-control.c (1)

175-185: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Return after the fatal receive error.

After recvmsg returns -1, execution reaches Line 184 and converts it to SIZE_MAX, causing control_queuef to consume an invalid length from buf.

Proposed fix
 	if (msglen == -1) {
 		logerr("%s: recvmsg", __func__);
 		eloop_exit(ctx->eloop, EXIT_FAILURE);
+		return;
 	}
🤖 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 175 - 185, Return immediately after
handling the fatal recvmsg error in the msglen == -1 branch, before iterating
over control_fds or calling control_queuef. Preserve the existing logging and
eloop_exit behavior.
🧹 Nitpick comments (1)
src/privsep-control.c (1)

111-126: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Add credential-boundary regression coverage.

The PR objective calls out missing testing. Cover control-group access, readgroup-only access, rejected privileged commands, and concurrent control clients sharing the proxy.

🤖 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 111 - 126, Add regression tests for the
control handling path around control_new and control_recvmsg covering
control-group access, readgroup-only access, rejection of privileged commands,
and concurrent control clients sharing the proxy. Verify both permitted and
denied operations and preserve existing cleanup behavior for each client
scenario.
🤖 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/dhcpcd.c`:
- Around line 1669-1682: Add BSD integration tests covering the authorization
matrix in the doauth path of dhcpcd: validate FD_CONTROL for normal requests,
FD_READ for DHCPCD_DUMPLEASE, and neither flag for both cases. Include
--getinterfaces framing checks and assert EPERM responses for unauthorized
peers, using the existing BSD integration-test helpers and peer-credential/group
setup.
- Around line 1893-1907: Update dhcpcd_readerror to accumulate exactly
sizeof(error) bytes from ctx->control_fd before decoding the error value,
handling partial reads and continuing until the complete frame is received.
Preserve the existing -1 failure behavior and EINVAL handling for an incomplete
or invalid stream response, then set errno and return the decoded error only
after the full frame is assembled.
- Around line 1602-1606: Initialize opts before the --getinterfaces branch in
the argument-processing flow, ensuring it has a defined zero value before the
opts |= DHCPCD_DUMPLEASE operation and subsequent doauth jump. Preserve the
existing option initialization semantics for all other argument paths.

In `@src/privsep-control.c`:
- Around line 84-97: Update ps_ctl_handleargs to use length-bounded command
comparisons based on len instead of strcmp, since data may not be
NUL-terminated. Preserve the existing matches and command-specific handling for
--version, --getconfigfile, --isprivileged, and --listen, including their
expected newline distinctions.

---

Outside diff comments:
In `@src/privsep-control.c`:
- Around line 175-185: Return immediately after handling the fatal recvmsg error
in the msglen == -1 branch, before iterating over control_fds or calling
control_queuef. Preserve the existing logging and eloop_exit behavior.

---

Nitpick comments:
In `@src/privsep-control.c`:
- Around line 111-126: Add regression tests for the control handling path around
control_new and control_recvmsg covering control-group access, readgroup-only
access, rejection of privileged commands, and concurrent control clients sharing
the proxy. Verify both permitted and denied operations and preserve existing
cleanup behavior for each client scenario.
🪄 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 Plus

Run ID: fe92d6a4-70e1-4043-ba77-4f0f1cd03236

📥 Commits

Reviewing files that changed from the base of the PR and between aa61e1b and 3b36cd9.

📒 Files selected for processing (13)
  • src/control.c
  • src/control.h
  • src/dhcpcd.c
  • src/dhcpcd.conf.5.in
  • src/dhcpcd.h
  • src/if-options.c
  • src/if-options.h
  • src/privsep-control.c
  • src/privsep-control.h
  • src/privsep-root.c
  • src/privsep-root.h
  • src/privsep.h
  • src/script.c
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/privsep-root.h
  • src/privsep-control.h
  • src/control.c

Comment thread src/dhcpcd.c
Comment on lines 1602 to +1606
} else if (strcmp(*argv, "--getinterfaces") == 0) {
oifind = argc = 0;
goto dumplease;
opts |= DHCPCD_DUMPLEASE;
do_getifs = true;
goto doauth;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/sh
rg -n -C3 'unsigned long long opts|opts \|= DHCPCD_DUMPLEASE|opts = 0' src/dhcpcd.c

Repository: NetworkConfiguration/dhcpcd

Length of output: 1821


🏁 Script executed:

#!/bin/sh
sed -n '1576,1640p' src/dhcpcd.c

Repository: NetworkConfiguration/dhcpcd

Length of output: 1950


🏁 Script executed:

#!/bin/sh
printf '%s\n' '--- doauth usage in handler ---'
sed -n '1660,1780p' src/dhcpcd.c

printf '\n--- DHCPCD_DUMPLEASE definition and uses ---'
rg -n 'DHCPCD_DUMPLEASE|do_getifs|opts \|=|opts =' src/dhcpcd.c src/privsep-control.c 2>/dev/null | sed -n '1,120p'

Repository: NetworkConfiguration/dhcpcd

Length of output: 4630


Initialize opts before handling --getinterfaces.

opts is initialized at opts = 0 after the long-argument parsing, but this control-socket branch compound-assigns DHCPCD_DUMPLEASE before that and then jumps to doauth; this reads indeterminate storage.

Proposed fix
-		opts |= DHCPCD_DUMPLEASE;
+		opts = DHCPCD_DUMPLEASE;
📝 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
} else if (strcmp(*argv, "--getinterfaces") == 0) {
oifind = argc = 0;
goto dumplease;
opts |= DHCPCD_DUMPLEASE;
do_getifs = true;
goto doauth;
} else if (strcmp(*argv, "--getinterfaces") == 0) {
oifind = argc = 0;
opts = DHCPCD_DUMPLEASE;
do_getifs = true;
goto doauth;
🧰 Tools
🪛 Cppcheck (2.21.0)

[error] 1604-1604: Uninitialized variable

(legacyUninitvar)

🤖 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 1602 - 1606, Initialize opts before the
--getinterfaces branch in the argument-processing flow, ensuring it has a
defined zero value before the opts |= DHCPCD_DUMPLEASE operation and subsequent
doauth jump. Preserve the existing option initialization semantics for all other
argument paths.

Source: Linters/SAST tools

Comment thread src/dhcpcd.c
Comment on lines +1669 to +1682
/* If we are not dumping the lease we must be able to control
* dhcpcd */
doauth:
if (opts & DHCPCD_DUMPLEASE) {
if (fd->flags & FD_READ)
error = 0;
else
error = EPERM;
} else {
if (fd->flags & FD_CONTROL)
error = 0;
else
error = EPERM;
}

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 & Privacy | 🟠 Major | 🏗️ Heavy lift

Add BSD integration coverage for the new authorization boundary.

Cover the FD_CONTROL / FD_READ / neither matrix, including --getinterfaces framing and EPERM replies. This PR moves access control to peer credentials and group membership, so regressions here become privilege-boundary failures.

🤖 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 1669 - 1682, Add BSD integration tests covering
the authorization matrix in the doauth path of dhcpcd: validate FD_CONTROL for
normal requests, FD_READ for DHCPCD_DUMPLEASE, and neither flag for both cases.
Include --getinterfaces framing checks and assert EPERM responses for
unauthorized peers, using the existing BSD integration-test helpers and
peer-credential/group setup.

Comment thread src/dhcpcd.c
Comment on lines +1893 to +1907
static int
dhcpcd_readerror(struct dhcpcd_ctx *ctx)
{
int error = 0;
ssize_t len;

len = read(ctx->control_fd, &error, sizeof(error));
if (len == -1)
return -1;
if (len != sizeof(error)) {
errno = EINVAL;
return -1;
}
errno = error;
return error;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Accumulate the complete error frame before decoding it.

SOCK_STREAM does not preserve write boundaries; read may return fewer than sizeof(error) bytes. A valid fragmented response is currently rejected with EINVAL.

Proposed fix
 static int
 dhcpcd_readerror(struct dhcpcd_ctx *ctx)
 {
 	int error = 0;
-	ssize_t len;
+	size_t off = 0;
 
-	len = read(ctx->control_fd, &error, sizeof(error));
-	if (len == -1)
-		return -1;
-	if (len != sizeof(error)) {
-		errno = EINVAL;
-		return -1;
+	while (off != sizeof(error)) {
+		ssize_t len = read(ctx->control_fd, (char *)&error + off,
+		    sizeof(error) - off);
+
+		if (len == -1) {
+			if (errno == EINTR)
+				continue;
+			return -1;
+		}
+		if (len == 0) {
+			errno = EINVAL;
+			return -1;
+		}
+		off += (size_t)len;
 	}
 	errno = error;
 	return error;
 }
📝 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
static int
dhcpcd_readerror(struct dhcpcd_ctx *ctx)
{
int error = 0;
ssize_t len;
len = read(ctx->control_fd, &error, sizeof(error));
if (len == -1)
return -1;
if (len != sizeof(error)) {
errno = EINVAL;
return -1;
}
errno = error;
return error;
static int
dhcpcd_readerror(struct dhcpcd_ctx *ctx)
{
int error = 0;
size_t off = 0;
while (off != sizeof(error)) {
ssize_t len = read(ctx->control_fd, (char *)&error + off,
sizeof(error) - off);
if (len == -1) {
if (errno == EINTR)
continue;
return -1;
}
if (len == 0) {
errno = EINVAL;
return -1;
}
off += (size_t)len;
}
errno = error;
return error;
}
🤖 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 1893 - 1907, Update dhcpcd_readerror to accumulate
exactly sizeof(error) bytes from ctx->control_fd before decoding the error
value, handling partial reads and continuing until the complete frame is
received. Preserve the existing -1 failure behavior and EINVAL handling for an
incomplete or invalid stream response, then set errno and return the decoded
error only after the full frame is assembled.

Comment thread src/privsep-control.c
Comment on lines +84 to +97
ps_ctl_handleargs(struct fd_list *fd, const char *data, __unused size_t len)
{
/* Make any change here in dhcpcd.c as well. */
if (strncmp(data, "--version", MIN(strlen("--version"), len)) == 0) {
return control_queue(fd, UNCONST(VERSION), strlen(VERSION) + 1);
} else if (strncmp(data, "--getconfigfile",
MIN(strlen("--getconfigfile"), len)) == 0) {
return control_queue(fd, UNCONST(fd->ctx->cffile),
/* Make any change here in dhcpcd.c as well.
* --version is NOT terminated with \n. */
if (strcmp(data, "--version") == 0) {
return control_queue(fd, VERSION, strlen(VERSION) + 1);
} else if (strcmp(data, "--getconfigfile\n") == 0) {
return control_queue(fd, fd->ctx->cffile,
strlen(fd->ctx->cffile) + 1);
} else if (strncmp(data, "--listen", MIN(strlen("--listen"), len)) ==
0) {
fd->flags |= FD_LISTEN;
return 0;
}
} else if (strcmp(data, "--isprivileged\n") == 0) {
const char *ret = fd->flags & FD_CONTROL ? "true" : "false";
return control_queue(fd, ret, strlen(ret) + 1);
} else if (strcmp(data, "--listen\n") == 0)
return control_handle_listen(fd);

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 & Privacy | 🟠 Major | ⚡ Quick win

Use length-bounded command matching.

data is raw recvmsg input and need not be NUL-terminated; each strcmp can read past len.

Proposed fix
-ps_ctl_handleargs(struct fd_list *fd, const char *data, __unused size_t len)
+ps_ctl_handleargs(struct fd_list *fd, const char *data, size_t len)
 {
-	if (strcmp(data, "--version") == 0) {
+	if (len == sizeof("--version") - 1 &&
+	    memcmp(data, "--version", sizeof("--version") - 1) == 0) {
 		return control_queue(fd, VERSION, strlen(VERSION) + 1);
-	} else if (strcmp(data, "--getconfigfile\n") == 0) {
+	} else if (len == sizeof("--getconfigfile\n") - 1 &&
+	    memcmp(data, "--getconfigfile\n",
+	    sizeof("--getconfigfile\n") - 1) == 0) {
 		return control_queue(fd, fd->ctx->cffile,
 		    strlen(fd->ctx->cffile) + 1);
-	} else if (strcmp(data, "--isprivileged\n") == 0) {
+	} else if (len == sizeof("--isprivileged\n") - 1 &&
+	    memcmp(data, "--isprivileged\n",
+	    sizeof("--isprivileged\n") - 1) == 0) {
 		const char *ret = fd->flags & FD_CONTROL ? "true" : "false";
 		return control_queue(fd, ret, strlen(ret) + 1);
-	} else if (strcmp(data, "--listen\n") == 0)
+	} else if (len == sizeof("--listen\n") - 1 &&
+	    memcmp(data, "--listen\n", sizeof("--listen\n") - 1) == 0)
 		return control_handle_listen(fd);
🤖 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 84 - 97, Update ps_ctl_handleargs to use
length-bounded command comparisons based on len instead of strcmp, since data
may not be NUL-terminated. Preserve the existing matches and command-specific
handling for --version, --getconfigfile, --isprivileged, and --listen, including
their expected newline distinctions.

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