Skip to content

dhcpsd: -w waits for dynmically created interfaces#4

Open
rsmarples wants to merge 16 commits intomasterfrom
wait
Open

dhcpsd: -w waits for dynmically created interfaces#4
rsmarples wants to merge 16 commits intomasterfrom
wait

Conversation

@rsmarples
Copy link
Copy Markdown
Member

The interface you may want to configure might not
exist when dhcpsd first starts.

The -w option will cause dhcpcd to only configure
an interface when it recieves a message on it.
We assume if we have received a message it's
ready as we cannot guess which address to use.
So ensure the server address is added before the
interface is marked as up!

If the interface is marked as down then the
interface is deconfigured.

This allows dhcpsd to be used in-place of the
macOS DHCP server as provided by the UTM
framework.

The interface you may want to configure might not
exist when dhcpsd first starts.

The -w option will cause dhcpcd to only configure
an interface when it recieves a message on it.
We assume if we have received a message it's
ready as we cannot guess which address to use.
So ensure the server address is added before the
interface is marked as up!

If the interface is marked as down then the
interface is deconfigured.

This allows dhcpsd to be used in-place of the
macOS DHCP server as provided by the UTM
framework.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 16, 2026

Walkthrough

Adds a privilege-separated helper for BPF and interface ops, moves interface discovery to an unprivileged helper, introduces platform-specific link monitors (route/netlink), refactors interface lifecycle/configuration, updates DHCP send paths to use the priv helper, and improves configure/Makefile detection and sanitiser settings.

Changes

Cohort / File(s) Summary
Privilege Separation Layer
src/priv.h, src/priv.c
New privileged helper service and client API: priv_init, priv_openbpf, priv_sendbpf, priv_freeif; dispatch handlers for P_OPENBPF/P_SENDBPF/P_FREEIF and integration with srv loop.
Unprivileged Helper & RPC
src/unpriv.h, src/unpriv.c
Expanded unprivileged protocol with U_GETIFADDRS, U_LEARNIF, U_FREEIF; added unpriv_getifaddrs, unpriv_learnif, unpriv_freeif and corresponding server-side handlers that serialize/deserialize ifaddrs and manage interfaces.
Link Monitoring (platform-specific)
src/route.c, src/netlink.c
New link monitoring modules exposing link_open/link_free: PF_ROUTE implementation (route.c) and NETLINK_ROUTE implementation (netlink.c); both remove/free interfaces on down/departure events.
Core Daemon & Context
src/dhcpsd.h, src/dhcpsd.c, src/service.c
Refactored init/control flow to initialize priv helper early, added ctx_priv, ctx_link, argc/argv storage, dhcpsd_configure_pools(), DHCPSD_WAITIF flag, and adjusted signal/exit handling and cleanup.
Interface Discovery & Helpers
src/if.h, src/if.c, src/common.h, src/common.c
Refactored if_learnifaces() to use unpriv_getifaddrs(), added helpers if_update, if_update_output, if_update_mtu, if_sockaddr_active, if_link_match_index, and public sa_is_link() plus alignment macros and sa_len return type change.
DHCP / BPF Path Changes
src/dhcp.h, src/dhcp.c, src/bpf.c
Removed dhcp_openbpf, switched DHCP send paths to priv_sendbpf, made cookie comparator external/const-correct, changed output functions to return ssize_t, enabled non-Linux BPF buffer handling and added null-check in bpf_read.
Plugins & Lua
src/plugins/auto.c, src/plugins/lua.c
auto.c now uses unpriv_getifaddrs() and handles errors; lua.c moved CIDR/logging to post-processing, adjusted stack/pop and validation checks.
Build, Configure & Makefile
configure, src/Makefile
Configure now detects link-interface mechanism (route.h vs rtnetlink.h) and writes LINK_SRC, enables -fsanitize=address,undefined when requested, and always prints DHCPSD_USER in config output; Makefile adds priv.c and includes ${LINK_SRC} in SRCS.
Seccomp & Minor
src/seccomp.c, src/if_ether.c
Seccomp VALGRIND allowlist adjusted to include __NR_unlinkat; minor formatting change in if_ether_output.
Documentation
src/dhcpsd.8.in
Updated man date and added -w option documenting wait-for-interface behavior.

Sequence Diagram(s)

sequenceDiagram
    participant Main as dhcpsd (main)
    participant Unpriv as Unpriv Helper
    participant Priv as Priv Helper
    participant Kernel as Kernel/BPF

    Main->>Unpriv: unpriv_getifaddrs(match_if_index)
    Unpriv->>Kernel: getifaddrs()
    Kernel-->>Unpriv: ifaddrs list
    Unpriv-->>Main: packed ifaddrs reply

    Main->>Priv: priv_openbpf(if_index/name)
    Priv->>Priv: dispatch P_OPENBPF (find/create ifp)
    Priv->>Kernel: open BPF (bpf_open / ioctl)
    Kernel-->>Priv: bpf fd
    Priv-->>Main: reply (fd/result)

    Main->>Priv: priv_sendbpf(if_index, iovec)
    Priv->>Priv: dispatch P_SENDBPF (locate ifp)
    Priv->>Kernel: write/send via bpf fd
    Kernel-->>Priv: nbytes/result
    Priv-->>Main: reply (nbytes)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: adding a -w option that waits for dynamically created interfaces before configuring them.
Description check ✅ Passed The description explains the -w option's purpose, behavior with dynamic interfaces, handling of interface state changes, and the intended use case with the macOS UTM framework.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 wait

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (5)
src/unpriv.c (1)

88-172: Document that returned ifaddrs must be freed with free(), not freeifaddrs().

The unpriv_getifaddrs() function returns an ifaddrs structure allocated with malloc() (line 113), not by the system's getifaddrs(). Callers must use free() to release the memory, not freeifaddrs(). This differs from the standard API convention and could lead to memory corruption if misused.

Consider adding a comment documenting this or providing a dedicated free function.

📝 Suggested documentation
+/*
+ * Retrieve interface addresses via the unprivileged helper.
+ * Unlike the system getifaddrs(), the returned structure must be
+ * freed with free(), not freeifaddrs().
+ */
 int
 unpriv_getifaddrs(struct srv_ctx *ctx, struct ifaddrs **ifahead,
     const unsigned int *match_if_index)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/unpriv.c` around lines 88 - 172, The returned ifaddrs list from
unpriv_getifaddrs() is allocated with malloc() (buf) and must be released with
free(), not freeifaddrs(); update the function to document this difference by
adding a clear comment at the top of unpriv_getifaddrs() (or add a helper like
unpriv_freeifaddrs() that simply calls free()) so callers know to use free() (or
the new unpriv_freeifaddrs()) instead of the system freeifaddrs().
src/dhcp.c (1)

41-42: Unusual include path for header in same directory.

The include "src/priv.h" uses a path relative to the project root, but since this file is already in src/, the conventional include would be "priv.h". This inconsistency may cause issues if include paths change or when building from different directories.

♻️ Suggested fix
-#include "src/priv.h"
+#include "priv.h"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dhcp.c` around lines 41 - 42, The include in dhcp.c currently uses a
project-root relative path ("src/priv.h") which is inconsistent for a header in
the same directory; change the include to the local header form by replacing
"src/priv.h" with "priv.h" in src/dhcp.c (look for the include line at the top
of the file) so the compiler uses the local header path and avoids build issues
when include paths change.
src/Makefile (1)

10-16: Inconsistent indentation in OBJS assignments.

Lines 10, 13, and 16 use spaces for indentation while the rest of the Makefile uses tabs. While GNU Make is generally tolerant of this in variable assignments, it's best to maintain consistency.

Note: The pipeline failure on if.o compilation indicates an issue in src/if.c rather than this Makefile. The build system changes here appear correct.

♻️ Suggested fix for consistent indentation
 SRCS+=		${LINK_SRC}
-OBJS+=      ${SRCS:.c=.o}
+OBJS+=		${SRCS:.c=.o}

 PVENDOR_SRCS=	${VENDOR_SRCS:vendor/%=${TOP}/vendor/%}
-OBJS+=			${PVENDOR_SRCS:.c=.o}
+OBJS+=		${PVENDOR_SRCS:.c=.o}

 PCOMPAT_SRCS=	${COMPAT_SRCS:compat/%=${TOP}/compat/%}
-OBJS+=			${PCOMPAT_SRCS:.c=.o}
+OBJS+=		${PCOMPAT_SRCS:.c=.o}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Makefile` around lines 10 - 16, The OBJS-related variable assignment
lines use space indentation while the rest of the Makefile uses tabs; update the
assignments for OBJS, PVENDOR_SRCS, and PCOMPAT_SRCS so their leading
indentation is a tab character (not spaces) to match the file's
style—specifically adjust the lines that reference SRCS/.c=.o,
VENDOR_SRCS:vendor/%=${TOP}/vendor/%, and COMPAT_SRCS:compat/%=${TOP}/compat/%
so they are indented with tabs.
configure (1)

594-615: Route detection test may not fail as expected when RTM_IFINFO is undefined.

The test program compiles successfully even when RTM_IFINFO is not defined because the #ifdef RTM_IFINFO block simply returns 0, and the else case (implicit) falls through to end of main() without a return statement, which in C11 is undefined behavior for non-void functions but often compiles without error.

Consider making the test fail explicitly when the required macro is missing:

♻️ Proposed fix for reliable detection
 	cat << EOF >_route.c
 `#include` <net/route.h>
 int main(void) {
-#ifdef RTM_IFINFO
-	return 0;
-#endif
+#ifndef RTM_IFINFO
+#error RTM_IFINFO not defined
+#endif
+	return 0;
 }
 EOF
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@configure` around lines 594 - 615, The test _route.c currently returns 0
regardless of RTM_IFINFO being defined because the `#ifdef` block doesn't enforce
a failure when the macro is missing; update the test source generation so that
main explicitly returns success (0) only when RTM_IFINFO is defined and returns
non‑zero (or triggers a preprocessor `#error`) when RTM_IFINFO is not defined,
then keep the existing compilation check that sets LINK_SRC="route.c" on success
and reports "no" on failure.
src/route.c (1)

87-89: Missing return after logging unexpected event.

When an unexpected event type is received, the function logs an error but continues processing. Consider returning early to avoid processing invalid events.

♻️ Proposed fix
 	if (e != ELE_READ) {
 		logerrx("%s: unexpeced event %d", __func__, e);
+		return;
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/route.c` around lines 87 - 89, The code logs an unexpected event when e
!= ELE_READ but then keeps executing; modify the branch containing logerrx("%s:
unexpeced event %d", __func__, e); to return immediately after logging to avoid
processing invalid events—i.e., after the logerrx call, add a return from the
current function (or return an appropriate error code if the function has a
non-void signature) so that further code does not run on unexpected events.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/bpf.c`:
- Around line 400-402: The allocation check for bpf->bpf_buffer must test the
open access mode correctly instead of using flags & (O_RDONLY | O_RDWR)
(O_RDONLY == 0), so modify the allocation logic (in the bpf open/init path that
sets bpf->bpf_buffer and bpf_size) to inspect (flags & O_ACCMODE) and allocate
when the mode is O_RDONLY or O_RDWR (or equivalent read-capable modes); update
the condition that gates buffer allocation to use (flags & O_ACCMODE) ==
O_RDONLY || (flags & O_ACCMODE) == O_RDWR (or test for read-capable flag),
ensure bpf_size > 0 when allocating, and keep bpf_read() usage consistent so
read(bpf->bpf_fd, bpf->bpf_buffer, bpf_size) never happens with a NULL buffer or
zero size.

In `@src/if.c`:
- Around line 186-201: The function if_link_match_index has three issues:
replace the incorrect use of ifa->ifa_addr with the provided sa when handling
AF_PACKET, cast sa to struct sockaddr_ll (use a variable sll) and compare
sll->sll_ifindex (not sll_index) to if_index, keep the AF_LINK branch using sdl
and sdl->sdl_index, and add a default return path (e.g., set errno to ENOSYS or
EINVAL and return -1) for builds where neither AF_LINK nor AF_PACKET is defined
so the function always returns a value.
- Around line 161-184: The function if_sockaddr_active incorrectly references an
undefined variable ifa; replace those references with the function parameters:
use strlcpy(iflr.iflr_name, if_name, sizeof(iflr.iflr_name)), memcpy(&iflr.addr,
sa, MIN(sa->sa_len, sizeof(iflr.addr))) (keeping the existing cast to const
struct sockaddr_dl *sdl for calculating prefix length), set iflr.prefixlen =
(unsigned int)sdl->sdl_alen * NBBY, and then perform the ioctl against
ctx->ctx_pf_link_fd and test IFLR_ACTIVE as written; remove the undefined ifa
usage so the function only uses ctx, if_name, and sa.
- Around line 348-357: The log call uses the incorrect symbol log_infox causing
a compile error; update the call in the block that checks ifp->if_flags &
IF_ACTIVE to use the correct logging function name loginfox, keeping the same
format string and arguments (ifp->if_name, ifp->if_index); verify surrounding
error handling that calls dhcpsd_configure_pools and if_free remains unchanged.

In `@src/plugins/auto.c`:
- Around line 122-123: The buffer returned by unpriv_getifaddrs() is
heap-allocated and must be freed with free(), so replace any calls to
freeifaddrs(ifaddrs) with free(ifaddrs) and ensure the error path also uses
free(ifaddrs) for the ifaddrs variable; keep all frees consistent (use free for
ifaddrs when it came from unpriv_getifaddrs), referencing the ifaddrs variable
and the unpriv_getifaddrs / freeifaddrs symbols to locate the changes.

In `@src/priv.c`:
- Around line 171-176: The code currently dereferences ifp->if_bpf without
ensuring ifp->if_bpf is non-NULL; update the block after the ifp NULL check to
validate ifp->if_bpf before calling ifp->if_output: if ifp->if_bpf is NULL, set
errno (e.g., ENODEV or ENXIO) and goto err, otherwise call ifp->if_output(ifp,
ifp->if_bpf->bpf_fd, iov, ARRAYCOUNT(iov)); this prevents a null-pointer
dereference when BPF was not opened for the interface.
- Around line 112-133: After calloc(1, sizeof(*ifp)) in this allocation branch,
add a NULL check for the returned pointer (ifp) and handle allocation failure
instead of immediately dereferencing it; on failure set errno = ENOMEM (or
appropriate error), jump to the existing err label, and avoid using ifp further.
Update the block around the allocation (the code that sets ifp->if_ctx,
ifp->if_index, calls strlcpy, memcpy, if_update_output, and TAILQ_INSERT_TAIL)
so those operations only run when ifp is non-NULL.

In `@src/route.c`:
- Around line 138-141: The code returns -1 when xsocket(PF_ROUTE, SOCK_RAW |
SOCK_CXNB, AF_UNSPEC) fails but does not free the previously allocated lctx,
causing a memory leak; update the error path in the block where link_fd is set
(the xsocket call) to free the allocated lctx (and any associated resources
initialized earlier) before returning -1 (e.g., call free(lctx) and null out the
pointer or call the existing cleanup helper if one exists) so that lctx is
properly released on socket creation failure.
- Around line 165-172: The function fails to assign the newly allocated link
context to ctx->ctx_link and leaks resources on error; set ctx->ctx_link = lctx
after successful allocation and before registering the event, and if
eloop_event_add(ctx->ctx_eloop, lctx->link_fd, ELE_READ, route_dispatch, lctx)
returns -1, clean up the allocated context and socket (call link_free(lctx) or
otherwise close lctx->link_fd and free lctx) before returning -1 so there is no
leak and link_free() later sees a valid ctx->ctx_link.
- Around line 142-161: The socket option calls are using the wrong context
variable (ctx->link_fd) before ctx->ctx_link is assigned; update all setsockopt
invocations (the SO_RERROR block and the RO_MSGFILTER / ROUTE_MSGFILTER blocks
where setsockopt is called) to use lctx->link_fd instead of ctx->link_fd so they
operate on the local link context; ensure you replace every occurrence in those
blocks (SO_RERROR, RO_MSGFILTER, ROUTE_MSGFILTER) to reference lctx->link_fd
consistently.

In `@src/service.c`:
- Around line 70-75: When recvmsg() returns 0 (EOF) in srv_recvl, synthesize a
hangup so the existing hangup suppression logic works: set the connection's
event mask to include ELE_HANGUP (e.g., sctx->srv_events |= ELE_HANGUP) or
otherwise mark the event as a hangup before jumping to the hangup handling code,
and ensure srv_recvl returns the same path as a real hangup; this ensures the
hangup label/srv handling will call eloop_exit(EXIT_SUCCESS) without later
treating the EOF-as-read case as a failure. Reference srv_recvl, ELE_HANGUP, the
hangup handling block that calls eloop_exit, and sctx->srv_events/sctx->srv_fd
when making the change.

In `@src/unpriv.c`:
- Around line 516-520: The TAILQ_INSERT_TAIL call unconditionally adds ifp to
ctx->ctx_ifaces even when an existing interface (same if_index) was found and
updated, risking duplicate/corrupted list entries; change the logic so
TAILQ_INSERT_TAIL(ctx->ctx_ifaces, ifp, if_next) is executed only when the
interface was newly allocated (i.e., when the code set free_ifp to true /
allocation branch occurred) and skip insertion when the code matched and updated
an existing interface (the branch that locates by if_index and reuses ifp).

---

Nitpick comments:
In `@configure`:
- Around line 594-615: The test _route.c currently returns 0 regardless of
RTM_IFINFO being defined because the `#ifdef` block doesn't enforce a failure when
the macro is missing; update the test source generation so that main explicitly
returns success (0) only when RTM_IFINFO is defined and returns non‑zero (or
triggers a preprocessor `#error`) when RTM_IFINFO is not defined, then keep the
existing compilation check that sets LINK_SRC="route.c" on success and reports
"no" on failure.

In `@src/dhcp.c`:
- Around line 41-42: The include in dhcp.c currently uses a project-root
relative path ("src/priv.h") which is inconsistent for a header in the same
directory; change the include to the local header form by replacing "src/priv.h"
with "priv.h" in src/dhcp.c (look for the include line at the top of the file)
so the compiler uses the local header path and avoids build issues when include
paths change.

In `@src/Makefile`:
- Around line 10-16: The OBJS-related variable assignment lines use space
indentation while the rest of the Makefile uses tabs; update the assignments for
OBJS, PVENDOR_SRCS, and PCOMPAT_SRCS so their leading indentation is a tab
character (not spaces) to match the file's style—specifically adjust the lines
that reference SRCS/.c=.o, VENDOR_SRCS:vendor/%=${TOP}/vendor/%, and
COMPAT_SRCS:compat/%=${TOP}/compat/% so they are indented with tabs.

In `@src/route.c`:
- Around line 87-89: The code logs an unexpected event when e != ELE_READ but
then keeps executing; modify the branch containing logerrx("%s: unexpeced event
%d", __func__, e); to return immediately after logging to avoid processing
invalid events—i.e., after the logerrx call, add a return from the current
function (or return an appropriate error code if the function has a non-void
signature) so that further code does not run on unexpected events.

In `@src/unpriv.c`:
- Around line 88-172: The returned ifaddrs list from unpriv_getifaddrs() is
allocated with malloc() (buf) and must be released with free(), not
freeifaddrs(); update the function to document this difference by adding a clear
comment at the top of unpriv_getifaddrs() (or add a helper like
unpriv_freeifaddrs() that simply calls free()) so callers know to use free() (or
the new unpriv_freeifaddrs()) instead of the system freeifaddrs().
🪄 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: b11aea62-7993-437f-be3c-f9bdf92f2b81

📥 Commits

Reviewing files that changed from the base of the PR and between 66d1245 and 067d14a.

📒 Files selected for processing (20)
  • configure
  • src/Makefile
  • src/bpf.c
  • src/common.c
  • src/common.h
  • src/dhcp.c
  • src/dhcp.h
  • src/dhcpsd.8.in
  • src/dhcpsd.c
  • src/dhcpsd.h
  • src/if.c
  • src/if.h
  • src/if_ether.c
  • src/plugins/auto.c
  • src/priv.c
  • src/priv.h
  • src/route.c
  • src/service.c
  • src/unpriv.c
  • src/unpriv.h

Comment thread src/bpf.c
Comment on lines +400 to 402
#if 1
int ibuf_len = 0;
#endif
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e

printf 'bpf_open call sites and nearby flags:\n\n'
rg -nP -C3 '\bbpf_open\s*\(' src

printf '\nCurrent allocation guard in src/bpf.c:\n\n'
sed -n '505,518p' src/bpf.c

Repository: NetworkConfiguration/dhcpsd

Length of output: 1385


🏁 Script executed:

#!/bin/bash

# 1. Check POSIX flag values in standard headers
echo "=== POSIX O_* flag definitions ===" 
grep -rn "define O_RDONLY\|define O_WRONLY\|define O_RDWR\|define O_ACCMODE" /usr/include --include="*.h" 2>/dev/null | head -20

echo ""
echo "=== All bpf_open call sites ===" 
rg -n '\bbpf_open\s*\(' src

echo ""
echo "=== Context: src/bpf.c lines 395-410 (around line 400) ===" 
sed -n '395,410p' src/bpf.c

echo ""
echo "=== Context: src/bpf.c lines 505-520 (allocation guard) ===" 
sed -n '505,520p' src/bpf.c

echo ""
echo "=== Check for bpf_read definition and usage ===" 
grep -n "^bpf_read" src/bpf.c | head -5
rg -n 'bpf_read\s*\(' src --max-count=10

Repository: NetworkConfiguration/dhcpsd

Length of output: 1372


🏁 Script executed:

#!/bin/bash

echo "=== All invocations of bpf_read() ===" 
rg -n '\bbpf_read\s*\(' src

echo ""
echo "=== bpf_read function body (lines 534-560) ===" 
sed -n '534,560p' src/bpf.c

echo ""
echo "=== Check lines 530-584 context mentioned in review ===" 
sed -n '530,545p' src/bpf.c

echo ""
echo "=== Verify POSIX flag semantics via C headers ===" 
cat > /tmp/check_flags.c << 'EOF'
`#include` <fcntl.h>
`#include` <stdio.h>
int main() {
    printf("O_RDONLY=%d\nO_WRONLY=%d\nO_RDWR=%d\nO_ACCMODE=%d\n",
           O_RDONLY, O_WRONLY, O_RDWR, O_ACCMODE);
    printf("\nO_RDONLY & (O_RDONLY | O_RDWR) = %d\n", O_RDONLY & (O_RDONLY | O_RDWR));
    printf("O_RDWR & (O_RDONLY | O_RDWR) = %d\n", O_RDWR & (O_RDONLY | O_RDWR));
    printf("(O_RDONLY & O_ACCMODE) != O_WRONLY: %d\n", (O_RDONLY & O_ACCMODE) != O_WRONLY);
    printf("(O_RDWR & O_ACCMODE) != O_WRONLY: %d\n", (O_RDWR & O_ACCMODE) != O_WRONLY);
    printf("(O_WRONLY & O_ACCMODE) != O_WRONLY: %d\n", (O_WRONLY & O_ACCMODE) != O_WRONLY);
    return 0;
}
EOF
gcc /tmp/check_flags.c -o /tmp/check_flags && /tmp/check_flags

Repository: NetworkConfiguration/dhcpsd

Length of output: 1549


🏁 Script executed:

#!/bin/bash

echo "=== Check if bpf_read is exported or used elsewhere ===" 
rg -n 'bpf_read' src --type c --type h

echo ""
echo "=== Verify the flags semantics with known values ===" 
python3 << 'EOF'
# POSIX standard values
O_RDONLY = 0
O_WRONLY = 1
O_RDWR = 2
O_ACCMODE = 3

print("POSIX flag values:")
print(f"  O_RDONLY = {O_RDONLY}")
print(f"  O_WRONLY = {O_WRONLY}")
print(f"  O_RDWR = {O_RDWR}")
print(f"  O_ACCMODE = {O_ACCMODE}")

print("\nCurrent guard: if (flags & (O_RDONLY | O_WRONLY))")
print(f"  With O_RDONLY: {O_RDONLY} & ({O_RDONLY} | {O_RDWR}) = {O_RDONLY & (O_RDONLY | O_RDWR)} (False - FAILS TO ALLOCATE)")
print(f"  With O_WRONLY: {O_WRONLY} & ({O_RDONLY} | {O_RDWR}) = {O_WRONLY & (O_RDONLY | O_RDWR)} (False - expected)")
print(f"  With O_RDWR:   {O_RDWR} & ({O_RDONLY} | {O_RDWR}) = {O_RDWR & (O_RDONLY | O_RDWR)} (True - correct)")

print("\nProposed fix: if ((flags & O_ACCMODE) != O_WRONLY)")
print(f"  With O_RDONLY: ({O_RDONLY} & {O_ACCMODE}) != {O_WRONLY} = {(O_RDONLY & O_ACCMODE) != O_WRONLY} (True - correct)")
print(f"  With O_WRONLY: ({O_WRONLY} & {O_ACCMODE}) != {O_WRONLY} = {(O_WRONLY & O_ACCMODE) != O_WRONLY} (False - correct)")
print(f"  With O_RDWR:   ({O_RDWR} & {O_ACCMODE}) != {O_WRONLY} = {(O_RDWR & O_ACCMODE) != O_WRONLY} (True - correct)")
EOF

echo ""
echo "=== Check bpf_read at line 543 for buffer access ===" 
sed -n '540,545p' src/bpf.c

Repository: NetworkConfiguration/dhcpsd

Length of output: 1023


Allocate the BPF read buffer by access mode, not O_RDONLY | O_RDWR.

Re-enabling bpf_read() makes this path depend on bpf->bpf_buffer, but the allocation check uses flags & (O_RDONLY | O_RDWR). Since O_RDONLY is 0, this condition evaluates to flags & 2, which fails for read-only opens and leaves bpf_buffer as NULL and bpf_size as 0. A read-only open to bpf_read() will then call read(bpf->bpf_fd, NULL, 0) at line 543, causing undefined behavior.

Proposed fix
-	if (flags & (O_RDONLY | O_RDWR)) {
+	if ((flags & O_ACCMODE) != O_WRONLY) {
 		/* Get the required BPF buffer length from the kernel. */
 		if (ioctl(bpf->bpf_fd, BIOCGBLEN, &ibuf_len) == -1)
 			goto eexit;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bpf.c` around lines 400 - 402, The allocation check for bpf->bpf_buffer
must test the open access mode correctly instead of using flags & (O_RDONLY |
O_RDWR) (O_RDONLY == 0), so modify the allocation logic (in the bpf open/init
path that sets bpf->bpf_buffer and bpf_size) to inspect (flags & O_ACCMODE) and
allocate when the mode is O_RDONLY or O_RDWR (or equivalent read-capable modes);
update the condition that gates buffer allocation to use (flags & O_ACCMODE) ==
O_RDONLY || (flags & O_ACCMODE) == O_RDWR (or test for read-capable flag),
ensure bpf_size > 0 when allocating, and keep bpf_read() usage consistent so
read(bpf->bpf_fd, bpf->bpf_buffer, bpf_size) never happens with a NULL buffer or
zero size.

Comment thread src/if.c
Comment thread src/if.c
Comment thread src/if.c
Comment thread src/plugins/auto.c
Comment thread src/route.c
Comment on lines +138 to +141
lctx->link_fd = xsocket(PF_ROUTE, SOCK_RAW | SOCK_CXNB, AF_UNSPEC);
if (lctx->link_fd == -1)
return -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.

⚠️ Potential issue | 🟠 Major

Memory leak when socket creation fails.

If xsocket() fails at line 138, the function returns -1 without freeing the lctx that was allocated at line 117. This was correctly flagged by static analysis.

🐛 Proposed fix
 	lctx->link_fd = xsocket(PF_ROUTE, SOCK_RAW | SOCK_CXNB, AF_UNSPEC);
-	if (lctx->link_fd == -1)
+	if (lctx->link_fd == -1) {
+		free(lctx);
 		return -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
lctx->link_fd = xsocket(PF_ROUTE, SOCK_RAW | SOCK_CXNB, AF_UNSPEC);
if (lctx->link_fd == -1)
return -1;
lctx->link_fd = xsocket(PF_ROUTE, SOCK_RAW | SOCK_CXNB, AF_UNSPEC);
if (lctx->link_fd == -1) {
free(lctx);
return -1;
}
🧰 Tools
🪛 Cppcheck (2.20.0)

[error] 140-140: Memory leak

(memleak)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/route.c` around lines 138 - 141, The code returns -1 when
xsocket(PF_ROUTE, SOCK_RAW | SOCK_CXNB, AF_UNSPEC) fails but does not free the
previously allocated lctx, causing a memory leak; update the error path in the
block where link_fd is set (the xsocket call) to free the allocated lctx (and
any associated resources initialized earlier) before returning -1 (e.g., call
free(lctx) and null out the pointer or call the existing cleanup helper if one
exists) so that lctx is properly released on socket creation failure.

Comment thread src/route.c
Comment thread src/route.c
Comment on lines +165 to +172
if (eloop_event_add(ctx->ctx_eloop, lctx->link_fd, ELE_READ,
route_dispatch, lctx) == -1) {
logerr("%s: eloop_event_add", __func__);
return -1;
}

return 0;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Missing assignment of lctx to ctx->ctx_link and memory leak on failure.

Two issues:

  1. The function never assigns ctx->ctx_link = lctx, so link_free() will receive NULL and the allocated context is leaked.
  2. If eloop_event_add() fails, the function returns -1 without cleaning up lctx or closing the socket.
🐛 Proposed fix
 	if (eloop_event_add(ctx->ctx_eloop, lctx->link_fd, ELE_READ,
 		route_dispatch, lctx) == -1) {
 		logerr("%s: eloop_event_add", __func__);
+		close(lctx->link_fd);
+		free(lctx);
 		return -1;
 	}

+	ctx->ctx_link = lctx;
 	return 0;
 }
📝 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 (eloop_event_add(ctx->ctx_eloop, lctx->link_fd, ELE_READ,
route_dispatch, lctx) == -1) {
logerr("%s: eloop_event_add", __func__);
return -1;
}
return 0;
}
if (eloop_event_add(ctx->ctx_eloop, lctx->link_fd, ELE_READ,
route_dispatch, lctx) == -1) {
logerr("%s: eloop_event_add", __func__);
close(lctx->link_fd);
free(lctx);
return -1;
}
ctx->ctx_link = lctx;
return 0;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/route.c` around lines 165 - 172, The function fails to assign the newly
allocated link context to ctx->ctx_link and leaks resources on error; set
ctx->ctx_link = lctx after successful allocation and before registering the
event, and if eloop_event_add(ctx->ctx_eloop, lctx->link_fd, ELE_READ,
route_dispatch, lctx) returns -1, clean up the allocated context and socket
(call link_free(lctx) or otherwise close lctx->link_fd and free lctx) before
returning -1 so there is no leak and link_free() later sees a valid
ctx->ctx_link.

Comment thread src/service.c Outdated
Comment on lines 70 to 75
if (sctx->srv_ctx->ctx_options & DHCPSD_MAIN)
logdebugx("%s(%d): fd %d hungup", __func__, getpid(),
sctx->srv_fd);
close(sctx->srv_fd);
sctx->srv_fd = -1;
eloop_exit(sctx->srv_ctx->ctx_eloop, EXIT_SUCCESS);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clean EOF is still reported as failure.

recvmsg(...)=0 also lands in hangup:, but srv_recvl() only suppresses failure when the original event mask had ELE_HANGUP. If the peer close arrives as ELE_READ + EOF, this path still exits with EXIT_FAILURE after already requesting EXIT_SUCCESS.

Proposed fix
 static void
 srv_recvl(void *arg, unsigned short e)
 {
 	struct srv_ctx *sctx = arg;
 
-	if (srv_recv(sctx, e) == -1 && !(e & ELE_HANGUP))
+	if (srv_recv(sctx, e) == -1 && sctx->srv_fd != -1)
 		eloop_exit(sctx->srv_ctx->ctx_eloop, EXIT_FAILURE);
 }

Also applies to: 140-141

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/service.c` around lines 70 - 75, When recvmsg() returns 0 (EOF) in
srv_recvl, synthesize a hangup so the existing hangup suppression logic works:
set the connection's event mask to include ELE_HANGUP (e.g., sctx->srv_events |=
ELE_HANGUP) or otherwise mark the event as a hangup before jumping to the hangup
handling code, and ensure srv_recvl returns the same path as a real hangup; this
ensures the hangup label/srv handling will call eloop_exit(EXIT_SUCCESS) without
later treating the EOF-as-read case as a failure. Reference srv_recvl,
ELE_HANGUP, the hangup handling block that calls eloop_exit, and
sctx->srv_events/sctx->srv_fd when making the change.

Comment thread src/unpriv.c Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (2)
src/plugins/lua.c (2)

186-207: ⚠️ Potential issue | 🟠 Major

Lua stack imbalance: popping 4 values but 5 were pushed.

After lua_getfield for lease_time at line 186, there are 5 values on the stack (address, netmask, from, to, lease_time). However, line 207 only pops 4, leaving lease_time on the stack each iteration. This stack leak will eventually corrupt the Lua stack or cause memory issues.

🐛 Proposed fix
 	skip:
-		lua_pop(L, 4);
+		lua_pop(L, 5);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/lua.c` around lines 186 - 207, The Lua stack is left with the
lease_time value because lua_getfield(L, -5, "lease_time") pushes an extra value
but the code calls lua_pop(L, 4); change the pop to remove all five values
(e.g., replace lua_pop(L, 4); with lua_pop(L, 5);) or explicitly pop the
lease_time after using lua_tonumber to ensure the stack is balanced; update
usages around lua_getfield, lua_tonumber, and the skip label so pool parsing no
longer leaks lease_time onto the Lua stack.

316-324: ⚠️ Potential issue | 🟠 Major

Redundant and always-failing length check logic.

The nested condition is logically flawed:

  1. Outer if at line 318: len < offsetof(struct bootp, vend) + sizeof(uint32_t)
  2. Inner if at line 319: Same check || cookie comparison

When the outer condition is true (we enter the block), the first part of the inner || is guaranteed true, so the inner condition always evaluates to true, making the dhcp_cookiecmp check unreachable. This means any packet with a truncated vendor area will fail regardless of cookie validity.

🐛 Proposed fix - check cookie only when length is sufficient for it
 	/* Allow for a truncated vendor area if it can at least
 	 * contain a DHCP cookie. */
-	if (len < offsetof(struct bootp, vend) + sizeof(uint32_t)) {
-		if (len < offsetof(struct bootp, vend) + sizeof(uint32_t) ||
-		    dhcp_cookiecmp(l->l_req) != 0) {
-			errno = EINVAL;
-			goto out;
-		}
+	if (len < offsetof(struct bootp, vend) + sizeof(uint32_t) ||
+	    dhcp_cookiecmp(l->l_req) != 0) {
+		errno = EINVAL;
+		goto out;
 	}

Or if the intent was to allow packets without vendor area but require valid cookie when present:

 	/* Allow for a truncated vendor area if it can at least
 	 * contain a DHCP cookie. */
-	if (len < offsetof(struct bootp, vend) + sizeof(uint32_t)) {
-		if (len < offsetof(struct bootp, vend) + sizeof(uint32_t) ||
-		    dhcp_cookiecmp(l->l_req) != 0) {
-			errno = EINVAL;
-			goto out;
-		}
-	}
+	if (len >= offsetof(struct bootp, vend) + sizeof(uint32_t) &&
+	    dhcp_cookiecmp(l->l_req) != 0) {
+		errno = EINVAL;
+		goto out;
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/lua.c` around lines 316 - 324, The nested length check is
redundant and makes dhcp_cookiecmp(l->l_req) unreachable; update the logic so
the cookie comparison only runs when the buffer is large enough to contain the
DHCP cookie. Specifically, remove the inner duplicated length test and replace
the block around len and offsetof(struct bootp, vend) + sizeof(uint32_t) with:
first validate that len is not too small if you intend to require at least a
cookie, or otherwise accept truncated vendor area, and then call
dhcp_cookiecmp(l->l_req) only when len >= offsetof(struct bootp, vend) +
sizeof(uint32_t); on cookie failure set errno = EINVAL and goto out (preserving
errno/EINVAL and goto out behavior).
♻️ Duplicate comments (2)
src/bpf.c (1)

400-402: ⚠️ Potential issue | 🟠 Major

Use access-mode masking for read-buffer allocation.

Line 509 still uses flags & (O_RDONLY | O_RDWR). Because O_RDONLY == 0, read-only opens won’t allocate bpf->bpf_buffer. Since this path is compiled again (Line 400), switch to O_ACCMODE and guard against non-positive BIOCGBLEN.

Proposed fix
-	if (flags & (O_RDONLY | O_RDWR)) {
+	if ((flags & O_ACCMODE) != O_WRONLY) {
 		/* Get the required BPF buffer length from the kernel. */
 		if (ioctl(bpf->bpf_fd, BIOCGBLEN, &ibuf_len) == -1)
 			goto eexit;
+		if (ibuf_len <= 0) {
+			errno = EINVAL;
+			goto eexit;
+		}
 
 		bpf->bpf_size = (size_t)ibuf_len;
 		bpf->bpf_buffer = malloc(bpf->bpf_size);
#!/bin/bash
set -euo pipefail

echo "=== bpf_open call sites ==="
rg -nP '\bbpf_open\s*\(' src

echo
echo "=== Allocation guard in src/bpf.c ==="
sed -n '505,518p' src/bpf.c

echo
echo "=== Access-mode semantics check ==="
python3 - <<'PY'
O_RDONLY=0
O_WRONLY=1
O_RDWR=2
O_ACCMODE=3
for name, flags in [("O_RDONLY", O_RDONLY), ("O_WRONLY", O_WRONLY), ("O_RDWR", O_RDWR)]:
    old = bool(flags & (O_RDONLY | O_RDWR))
    new = ((flags & O_ACCMODE) != O_WRONLY)
    print(f"{name}: old_guard={old}, new_guard={new}")
PY

Also applies to: 509-517

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bpf.c` around lines 400 - 402, Change the read-allocation guards to use
access-mode masking and validate buffer size: replace checks like "flags &
(O_RDONLY | O_RDWR)" with "((flags & O_ACCMODE) != O_WRONLY)" when deciding to
allocate bpf->bpf_buffer in the bpf open path, call BIOCGBLEN and ensure the
returned ibuf_len (or the variable used for length) is positive before
allocating or assigning bpf->bpf_buffer, and apply the same fix to the other
occurrence around the allocation block (the region handling BIOCGBLEN/ibuf_len).
Ensure you reference BIOCGBLEN, ibuf_len, bpf->bpf_buffer and the bpf open
allocation logic when making the changes.
src/plugins/auto.c (1)

122-122: ⚠️ Potential issue | 🔴 Critical

Use free() instead of freeifaddrs() for buffer from unpriv_getifaddrs().

The buffer returned by unpriv_getifaddrs() is allocated via malloc() (see src/unpriv.c line 113), not by the system's getifaddrs(). Calling freeifaddrs() on this buffer causes undefined behavior because freeifaddrs() expects the specific allocation structure from the system call.

,

🐛 Proposed fix
 		if (pool == NULL) {
 			logerr("%s: realloc", __func__);
-			freeifaddrs(ifaddrs);
+			free(ifaddrs);
 			return -1;
 		}
-	freeifaddrs(ifaddrs);
+	free(ifaddrs);
 	return npools;

Also applies to: 156-156

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugins/auto.c` at line 122, The code calls freeifaddrs(ifaddrs) for
buffers returned by unpriv_getifaddrs(); change those calls to free(ifaddrs)
because unpriv_getifaddrs() allocates with malloc() and freeifaddrs() is only
valid for system getifaddrs() allocations—locate the usages (e.g., the
freeifaddrs call handling the ifaddrs variable in src/plugins/auto.c) and
replace them with free to avoid undefined behavior.
🧹 Nitpick comments (1)
src/if.c (1)

40-42: Remove src/ prefix from include paths for consistency.

Lines 40-42 use #include "src/common.h" etc., while other includes in the same file (lines 62-68) and all other files in the codebase use plain filenames like #include "common.h". This inconsistency should be fixed.

♻️ Proposed fix
 `#include` "queue.h"
-#include "src/common.h"
-#include "src/priv.h"
-#include "src/unpriv.h"
+#include "common.h"
+#include "priv.h"
+#include "unpriv.h"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/if.c` around lines 40 - 42, The three include directives currently
written as `#include` "src/common.h", `#include` "src/priv.h", and `#include`
"src/unpriv.h" are inconsistent with the rest of the codebase; remove the "src/"
prefix and change them to `#include` "common.h", `#include` "priv.h", and `#include`
"unpriv.h" so the include style matches other headers in this file and the
project.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/if.c`:
- Line 241: The buffer returned by unpriv_getifaddrs() is malloc'd by our code,
so replace the incorrect freeifaddrs(ifaddrs) call with free(ifaddrs) to avoid
undefined behavior; locate the call site in src/if.c (where unpriv_getifaddrs()
is used) and change the deallocation function from freeifaddrs to free, ensuring
any related comments reflect that the buffer is heap-allocated by
unpriv_getifaddrs().
- Around line 138-141: The AF_PACKET branch currently tests the destination
length (ifp->if_hwlen) instead of the source length, causing zero-byte copies
when ifp->if_hwlen is 0; update the condition to check the source sll->sll_halen
(e.g., if (sll->sll_halen <= sizeof(ifp->if_hwaddr))) and then set ifp->if_hwlen
= sll->sll_halen followed by memcpy(ifp->if_hwaddr, sll->sll_addr,
ifp->if_hwlen) so the copy uses the actual source length; adjust the condition
in the block referencing ifp, sll, if_hwlen, sll_halen, and memcpy accordingly.

In `@src/unpriv.c`:
- Around line 122-124: Add a portable fallback for the ALIGN macro in
src/unpriv.c by inserting a conditional definition after the existing includes:
if ALIGN is not already defined, define it to round its argument up to the
machine word boundary using uintptr_t (i.e., add a `#ifndef` ALIGN / `#define`
ALIGN(...) / `#endif` block that computes ((uintptr_t)(x) + (sizeof(long)-1)) &
~(sizeof(long)-1)). Also ensure <stdint.h> is included if uintptr_t is not
available; this fixes uses like ALIGN(sizeof(*ifa)), ALIGN(IFNAMSIZ) and
ALIGN(sizeof(salen) * IFA_NADDRS) without changing the call sites.

---

Outside diff comments:
In `@src/plugins/lua.c`:
- Around line 186-207: The Lua stack is left with the lease_time value because
lua_getfield(L, -5, "lease_time") pushes an extra value but the code calls
lua_pop(L, 4); change the pop to remove all five values (e.g., replace
lua_pop(L, 4); with lua_pop(L, 5);) or explicitly pop the lease_time after using
lua_tonumber to ensure the stack is balanced; update usages around lua_getfield,
lua_tonumber, and the skip label so pool parsing no longer leaks lease_time onto
the Lua stack.
- Around line 316-324: The nested length check is redundant and makes
dhcp_cookiecmp(l->l_req) unreachable; update the logic so the cookie comparison
only runs when the buffer is large enough to contain the DHCP cookie.
Specifically, remove the inner duplicated length test and replace the block
around len and offsetof(struct bootp, vend) + sizeof(uint32_t) with: first
validate that len is not too small if you intend to require at least a cookie,
or otherwise accept truncated vendor area, and then call
dhcp_cookiecmp(l->l_req) only when len >= offsetof(struct bootp, vend) +
sizeof(uint32_t); on cookie failure set errno = EINVAL and goto out (preserving
errno/EINVAL and goto out behavior).

---

Duplicate comments:
In `@src/bpf.c`:
- Around line 400-402: Change the read-allocation guards to use access-mode
masking and validate buffer size: replace checks like "flags & (O_RDONLY |
O_RDWR)" with "((flags & O_ACCMODE) != O_WRONLY)" when deciding to allocate
bpf->bpf_buffer in the bpf open path, call BIOCGBLEN and ensure the returned
ibuf_len (or the variable used for length) is positive before allocating or
assigning bpf->bpf_buffer, and apply the same fix to the other occurrence around
the allocation block (the region handling BIOCGBLEN/ibuf_len). Ensure you
reference BIOCGBLEN, ibuf_len, bpf->bpf_buffer and the bpf open allocation logic
when making the changes.

In `@src/plugins/auto.c`:
- Line 122: The code calls freeifaddrs(ifaddrs) for buffers returned by
unpriv_getifaddrs(); change those calls to free(ifaddrs) because
unpriv_getifaddrs() allocates with malloc() and freeifaddrs() is only valid for
system getifaddrs() allocations—locate the usages (e.g., the freeifaddrs call
handling the ifaddrs variable in src/plugins/auto.c) and replace them with free
to avoid undefined behavior.

---

Nitpick comments:
In `@src/if.c`:
- Around line 40-42: The three include directives currently written as `#include`
"src/common.h", `#include` "src/priv.h", and `#include` "src/unpriv.h" are
inconsistent with the rest of the codebase; remove the "src/" prefix and change
them to `#include` "common.h", `#include` "priv.h", and `#include` "unpriv.h" so the
include style matches other headers in this file and the project.
🪄 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: dec9a201-c27c-4164-b54a-81371d14c334

📥 Commits

Reviewing files that changed from the base of the PR and between 067d14a and ef68a9b.

📒 Files selected for processing (7)
  • src/bpf.c
  • src/if.c
  • src/plugins/auto.c
  • src/plugins/lua.c
  • src/route.c
  • src/service.c
  • src/unpriv.c
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/service.c
  • src/route.c

Comment thread src/if.c Outdated
Comment thread src/if.c Outdated
Comment thread src/unpriv.c
Comment on lines +122 to +124
if (len < ALIGN(sizeof(*ifa)) + ALIGN(IFNAMSIZ) +
ALIGN(sizeof(salen) * IFA_NADDRS))
goto err;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if ALIGN is defined in sys/param.h or elsewhere
rg -n '#define\s+ALIGN' --type c --type h

Repository: NetworkConfiguration/dhcpsd

Length of output: 53


🏁 Script executed:

#!/bin/bash
# Check the includes at the top of src/unpriv.c
head -40 src/unpriv.c | cat -n

echo "---"
# Check lines around 122 where ALIGN is supposedly used
sed -n '120,130p' src/unpriv.c | cat -n

echo "---"
# Check other usage locations
sed -n '363,370p' src/unpriv.c | cat -n
sed -n '414,422p' src/unpriv.c | cat -n

Repository: NetworkConfiguration/dhcpsd

Length of output: 2996


🏁 Script executed:

#!/bin/bash
# Search for ALIGN usage across the entire file
rg -n 'ALIGN\(' src/unpriv.c

Repository: NetworkConfiguration/dhcpsd

Length of output: 1006


🏁 Script executed:

#!/bin/bash
# Check if sys/param.h is a system header that typically defines ALIGN
# Also check for any other definitions or conditionals
rg -n 'sys/param\.h|define.*ALIGN|ifndef.*ALIGN' src/unpriv.c

Repository: NetworkConfiguration/dhcpsd

Length of output: 96


🏁 Script executed:

#!/bin/bash
# Look for build configuration files
fd -t f -E '.git' '(Makefile|CMakeLists\.txt|configure|\.mk|meson\.build|build\.yml|CI\.yml)' | head -20

Repository: NetworkConfiguration/dhcpsd

Length of output: 193


🏁 Script executed:

#!/bin/bash
# Check for any platform-specific documentation or README
fd -t f -name 'README*' -o -name 'PLATFORM*' -o -name 'BUILD*' | xargs cat 2>/dev/null | head -100

Repository: NetworkConfiguration/dhcpsd

Length of output: 301


🏁 Script executed:

#!/bin/bash
# Look for GitHub Actions or CI configuration
fd -t f '.github/workflows' -o '.gitlab-ci.yml' -o '.travis.yml' | head -20

Repository: NetworkConfiguration/dhcpsd

Length of output: 218


🏁 Script executed:

#!/bin/bash
# Check the Makefile
head -50 Makefile | cat -n

Repository: NetworkConfiguration/dhcpsd

Length of output: 1623


🏁 Script executed:

#!/bin/bash
# Check configure script for platform handling
grep -n 'ALIGN\|param\.h\|platform\|BSD\|Linux' configure | head -30

Repository: NetworkConfiguration/dhcpsd

Length of output: 338


🏁 Script executed:

#!/bin/bash
# Check Makefile.inc for platform configuration
cat Makefile.inc | cat -n

Repository: NetworkConfiguration/dhcpsd

Length of output: 733


🏁 Script executed:

#!/bin/bash
# Look for CI/workflow files without using fd syntax
find . -path './.git' -prune -o -type f -name '*.yml' -o -name '*.yaml' | grep -E '(github|gitlab|travis|workflow)' 2>/dev/null

Repository: NetworkConfiguration/dhcpsd

Length of output: 100


🏁 Script executed:

#!/bin/bash
# Check the GitHub Actions workflow
cat .github/workflows/build.yml | head -100

Repository: NetworkConfiguration/dhcpsd

Length of output: 2433


🏁 Script executed:

#!/bin/bash
# Look for any ALIGN-related issues or test reports
rg -n 'ALIGN|undeclared' .github/ src/ --type yaml --type txt 2>/dev/null | head -20

Repository: NetworkConfiguration/dhcpsd

Length of output: 53


🏁 Script executed:

#!/bin/bash
# Check if there's any documentation about target platforms
grep -r 'target\|platform\|BSD\|Linux' README* CONTRIBUTING* 2>/dev/null | head -20

Repository: NetworkConfiguration/dhcpsd

Length of output: 392


🏁 Script executed:

#!/bin/bash
# Check if there are any platform-specific headers or conditionals for ALIGN
rg -n 'ifdef|ifndef|if defined' src/unpriv.c | head -20

Repository: NetworkConfiguration/dhcpsd

Length of output: 167


🏁 Script executed:

#!/bin/bash
# Check recent git history to see if this code was recently added or modified
git log --oneline -20 -- src/unpriv.c 2>/dev/null | head -20

Repository: NetworkConfiguration/dhcpsd

Length of output: 127


🏁 Script executed:

#!/bin/bash
# Search for any conditional includes or ALIGN handling elsewhere in codebase
rg -n 'ALIGN' --type c --type h | head -30

Repository: NetworkConfiguration/dhcpsd

Length of output: 1352


🌐 Web query:

ALIGN macro sys/param.h Linux availability

💡 Result:

The ALIGN macro is not available in sys/param.h on Linux systems. sys/param.h in glibc (used by Linux) is a compatibility header primarily defining BSD-style constants and limits like NBBY, NGROUPS, MAXSYMLINKS, howmany, roundup, powerof2, MIN, and MAX, but does not define ALIGN. ALIGN(p) is defined in some BSD-derived systems (e.g., OpenBSD, FreeBSD) as _ALIGN(p), typically for pointer alignment: #define ALIGN(p) (((u_long)(p) + ALIGNBYTES) & ~ALIGNBYTES), where ALIGNBYTES is architecture-specific (e.g., 7 for 64-bit). Android's Bionic libc defines ALIGN(p) in sys/param.h as (((uintptr_t)(p) + ALIGNBYTES) & ~ALIGNBYTES), but this is Android-specific, not standard Linux/glibc. In the Linux kernel, ALIGN(x, a) is defined in include/linux/kernel.h as ALIGN_KERNEL((x), (a)), which rounds up x to the nearest multiple of a (assuming a is a power of 2). A similar kernel-only round_up(x, y) exists. These are not in userspace sys/param.h. Linux userspace code seeking alignment macros should use standard C11 alignas/aligned_alloc or architecture-specific intrinsics, not sys/param.h ALIGN, as it is not portable to Linux/glibc.

Citations:


🏁 Script executed:

#!/bin/bash
# Get more detailed history
git log --all --oneline -- src/unpriv.c | head -10

Repository: NetworkConfiguration/dhcpsd

Length of output: 324


🏁 Script executed:

#!/bin/bash
# Check file creation date and status
git log --diff-filter=A --find-object -- src/unpriv.c

Repository: NetworkConfiguration/dhcpsd

Length of output: 117


🏁 Script executed:

#!/bin/bash
# Check if file exists in current HEAD
test -f src/unpriv.c && echo "File exists" || echo "File does not exist"

Repository: NetworkConfiguration/dhcpsd

Length of output: 82


🏁 Script executed:

#!/bin/bash
# Check when src/unpriv.c was added to the repository
git log --all --follow --oneline src/unpriv.c | tail -5

Repository: NetworkConfiguration/dhcpsd

Length of output: 324


🏁 Script executed:

#!/bin/bash
# Check the actual commit that added this file
git log --all --diff-filter=A --oneline -- 'src/unpriv.c'

Repository: NetworkConfiguration/dhcpsd

Length of output: 252


🏁 Script executed:

#!/bin/bash
# Let's check if there are recent failed builds or if the file was just added
git show HEAD:src/unpriv.c | head -30

Repository: NetworkConfiguration/dhcpsd

Length of output: 1570


🏁 Script executed:

#!/bin/bash
# Check if sys/param.h on this system actually has ALIGN
# We can look at what it includes
cat -n /usr/include/sys/param.h 2>/dev/null | head -50 || echo "Not available in sandbox"

Repository: NetworkConfiguration/dhcpsd

Length of output: 53


🏁 Script executed:

#!/bin/bash
# Check if ALIGN usage is protected by `#ifdef` BSD
sed -n '35,50p' src/unpriv.c | cat -n
sed -n '370,385p' src/unpriv.c | cat -n
sed -n '435,450p' src/unpriv.c | cat -n

Repository: NetworkConfiguration/dhcpsd

Length of output: 1518


🏁 Script executed:

#!/antml/bash
# Let's check the exact structure of the functions using ALIGN
# to see if they're BSD-guarded
rg -B5 'ALIGN\(sizeof\(\*ifa\)\)' src/unpriv.c | head -30

Repository: NetworkConfiguration/dhcpsd

Length of output: 726


🏁 Script executed:

#!/bin/bash
# Find the function containing lines 122-124 and check if it's guarded
sed -n '100,135p' src/unpriv.c | cat -n

Repository: NetworkConfiguration/dhcpsd

Length of output: 1369


🏁 Script executed:

#!/bin/bash
# Check what calls this function and if it's guarded
rg -B10 'unpriv_dispatch_copyout_ifaddrs|ifaddrs' src/unpriv.c | head -40

Repository: NetworkConfiguration/dhcpsd

Length of output: 862


🏁 Script executed:

#!/antml/bash
# Check if getifaddrs or the entire ifaddrs code path is BSD-guarded
grep -n 'getifaddrs\|freeifaddrs\|#ifdef\|#endif' src/unpriv.c | head -30

Repository: NetworkConfiguration/dhcpsd

Length of output: 584


🏁 Script executed:

#!/bin/bash
# Let me check the exact function signature and if it's platform-guarded
sed -n '89,120p' src/unpriv.c | cat -n

Repository: NetworkConfiguration/dhcpsd

Length of output: 1017


🏁 Script executed:

#!/bin/bash
# Check if getifaddrs is used unconditionally
rg -n 'getifaddrs|freeifaddrs' src/unpriv.c

Repository: NetworkConfiguration/dhcpsd

Length of output: 434


🏁 Script executed:

#!/bin/bash
# Check if getifaddrs is available on Linux and how it's used
rg -n 'ifdef.*getifaddrs|ifndef.*getifaddrs|getifaddrs' src/unpriv.c

Repository: NetworkConfiguration/dhcpsd

Length of output: 352


🏁 Script executed:

#!/bin/bash
# Check if there are any preprocessor guards around the entire getifaddrs functionality
sed -n '320,340p' src/unpriv.c | cat -n
sed -n '338,400p' src/unpriv.c | cat -n

Repository: NetworkConfiguration/dhcpsd

Length of output: 2728


🏁 Script executed:

#!/antml/bash
# Let's verify if the code is actually compiled by checking recent builds
# Check if there are any disabled tests or ifdef guards we're missing
grep -n 'ifdef\|ifndef\|if defined' src/unpriv.c | sort -u

Repository: NetworkConfiguration/dhcpsd

Length of output: 167


Add fallback definition for ALIGN macro.

The ALIGN macro is not defined in sys/param.h on Linux systems. While it exists on BSD platforms, the code uses it unconditionally without platform guards, causing compilation to fail on Linux (including the active Ubuntu CI builds).

Add fallback definition after includes
 `#include` "unpriv.h"

+/* Fallback if ALIGN is not provided by sys/param.h */
+#ifndef ALIGN
+#define ALIGN(x) (((x) + (sizeof(long) - 1)) & ~(sizeof(long) - 1))
+#endif
+
 `#define` U_GETIFADDRS  1

Affects: lines 122-124, 365-367, 416-421

🧰 Tools
🪛 GitHub Actions: Build

[error] 122-122: clang error: call to undeclared function 'ALIGN' (ISO C99 and later do not support implicit function declarations) [-Wimplicit-function-declaration]

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/unpriv.c` around lines 122 - 124, Add a portable fallback for the ALIGN
macro in src/unpriv.c by inserting a conditional definition after the existing
includes: if ALIGN is not already defined, define it to round its argument up to
the machine word boundary using uintptr_t (i.e., add a `#ifndef` ALIGN / `#define`
ALIGN(...) / `#endif` block that computes ((uintptr_t)(x) + (sizeof(long)-1)) &
~(sizeof(long)-1)). Also ensure <stdint.h> is included if uintptr_t is not
available; this fixes uses like ALIGN(sizeof(*ifa)), ALIGN(IFNAMSIZ) and
ALIGN(sizeof(salen) * IFA_NADDRS) without changing the call sites.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

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

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

1608-1616: ⚠️ Potential issue | 🟡 Minor

Consider propagating dhcp_output failures.

dhcp_output now returns ssize_t (−1 on realloc/send failure), but the caller still discards it. In particular, if dhcp_output fails before committing, the code then calls dhcp_commit_lease for non-NAK paths as if the reply were sent. The lease gets persisted and the client sees silence. Consider checking the result and skipping commit on −1 so state stays consistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dhcp.c` around lines 1608 - 1616, dhcp_output now returns ssize_t and
failures must be propagated: in the switch branch handling
DHCP_OFFER/DHCP_ACK/DHCP_NAK call dhcp_output(ifp, lease, type, msg, bootp, len)
and check its return value (e.g., ssize_t rv); if rv == -1 do not call
dhcp_commit_lease(ctx, lease, bootp, len) and handle the error path (log and/or
return) so you don't persist a lease when the reply failed to send; ensure this
check is applied for both OFFER and ACK paths while NAK still skips commit.
🧹 Nitpick comments (5)
src/dhcp.c (2)

41-41: Include path style inconsistency.

Other headers in this file (and "priv.h" in src/dhcpsd.c line 56) are included without the src/ prefix. Prefer #include "priv.h" for consistency, relying on the include path configuration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dhcp.c` at line 41, The include in src/dhcp.c currently uses a prefixed
path ("src/priv.h") which is inconsistent with the other includes and with how
priv.h is included in dhcpsd.c; update the include directive in dhcp.c to use
the unprefixed form (`#include` "priv.h") so it matches the project's include
style and relies on the configured include path; ensure no other occurrences in
dhcp.c use the src/ prefix for local headers.

1670-1673: Dead #if 1 wrapper.

The #if 1 ... #endif`` is equivalent to unconditional inclusion. Either drop the guard or restore the previous behavior-selecting macro.

♻️ Proposed cleanup
-#if 1
 		logwarnx("dhcp: interface not found 0x%x %s", bootp->xid,
 		    inet_ntoa(from.sin_addr));
-#endif
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dhcp.c` around lines 1670 - 1673, Remove the dead preprocessing guard
around the log line: delete the surrounding "#if 1" and "#endif" so the call to
logwarnx("dhcp: interface not found 0x%x %s", bootp->xid,
inet_ntoa(from.sin_addr)); is unconditional (or if you intend conditional
logging, replace "#if 1" with the appropriate feature/debug macro instead). This
touches the logwarnx call referencing bootp->xid and inet_ntoa(from.sin_addr) in
dhcp.c.
src/route.c (1)

76-78: logwarnx is noisy for a normal state transition.

An interface being brought down is a routine operational event, not a warning-worthy condition. The netlink backend uses loginfox for the equivalent path (src/netlink.c:154). Suggest aligning severity for consistency.

♻️ Proposed tweak
-	logwarnx("%s: interface is down", ifp->if_name);
+	loginfox("%s: interface is down", ifp->if_name);

Apply the same change to the IFAN_DEPARTURE log at line 103 if desired.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/route.c` around lines 76 - 78, Replace the noisy warning-level log when
an interface goes down by changing the call site that currently uses
logwarnx("%s: interface is down", ifp->if_name) to use loginfox so the event is
logged at info level; update the same pattern for the IFAN_DEPARTURE message
(the other logwarnx instance near the IFAN_DEPARTURE handling) to maintain
consistent severity, keeping the surrounding operations
TAILQ_REMOVE(ctx->ctx_ifaces, ifp, if_next) and if_free(ifp) unchanged.
src/netlink.c (1)

71-84: Mixing size_t and ssize_t obscures the error path.

Assigning recvmsg()'s ssize_t return into a size_t and then casting back to ssize_t for the -1 comparison is error-prone. Using ssize_t throughout is clearer and avoids UB concerns about lossy conversions.

♻️ Proposed cleanup
-	size_t len;
+	ssize_t nread;
+	size_t len;
 ...
 recv_again:
-	len = (size_t)recvmsg(fd, &msg, flags);
-	if (len == 0 || (ssize_t)len == -1)
-		return (int)len;
+	nread = recvmsg(fd, &msg, flags);
+	if (nread <= 0)
+		return (int)nread;
+	len = (size_t)nread;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/netlink.c` around lines 71 - 84, The recvmsg return value is stored in a
size_t (len) which is then cast back to ssize_t—change len to type ssize_t in
the recv_again block so recvmsg's signed return is handled natively; update the
checks to use len == 0 and len == -1 (or len <= 0 if you prefer a single check),
keep the same return semantics (cast the ssize_t return to int where the code
returns), and leave the subsequent checks for msg.msg_namelen and nladdr.nl_pid
unchanged; specifically modify the local variable declaration for len and the
conditional that tests recvmsg(fd, &msg, flags) so no unsigned-to-signed casts
are performed.
src/dhcpsd.c (1)

365-381: LGTM — -w flag and getopt error handling.

Switching f_flag to bool and adding the ? exit path for getopt errors are appropriate. Consider emitting a brief usage line before goto exit so an unknown option gives the user actionable feedback rather than a silent exit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dhcpsd.c` around lines 365 - 381, The getopt '?' handler currently jumps
to exit silently; update the case '?' in the getopt switch (OPTS handling) to
print a short usage message to stderr before the existing goto exit so unknown
options give actionable feedback—either call the program's existing
usage/print_usage function (if available) or emit a one-line usage via
fprintf(stderr, ...) that lists the supported flags (e.g. -d -f -p -w) and then
goto exit; keep the change localized to the getopt switch (case '?') near OPTS
and f_flag handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/dhcp.c`:
- Around line 865-874: The calculation of len from ifp->if_mtu can underflow or
produce a buffer smaller than struct bootp causing a subsequent memset overflow;
before using ifp->if_mtu, validate and clamp it: compute min_payload =
sizeof(struct bootp) and required_mtu = min_payload + sizeof(struct udphdr) +
sizeof(struct ip), then if ifp->if_mtu is 0 or less than required_mtu treat it
as required_mtu (or skip realloc and keep existing ctx->dhcp_bootplen if
larger), set len = ifp->if_mtu - sizeof(struct udphdr) - sizeof(struct ip) only
after clamping, and ensure the resulting len >= sizeof(struct bootp) before
calling realloc on ctx->dhcp_bootp and updating ctx->dhcp_bootplen to prevent
heap overflow from the subsequent memset of the bootp structure.

In `@src/dhcpsd.c`:
- Around line 526-539: The variable npools is left uninitialized when
DHCPSD_WAITIF is set because the earlier initialization is skipped; initialize
npools to 0 at its declaration (the size_t npools variable) so it has a defined
value before the TAILQ_FOREACH loop and subsequent check against ctx.ctx_options
& DHCPSD_WAITIF and the npools == 0 test; update the declaration near where
size_t npools is defined (so usages in TAILQ_FOREACH, dhcpsd_configure_pools
handling, and the later conditional remain safe).

In `@src/netlink.c`:
- Line 3: Fix the typo in the file header comment by changing "dhcpsd -
netlink(7) supprt" to "dhcpsd - netlink(7) support" in src/netlink.c (and make
the identical correction in the header comment in src/route.c); locate the
top-of-file comment that contains the exact string "dhcpsd - netlink(7) supprt"
and replace "supprt" with "support".
- Around line 120-121: The re-entry condition in netlink_get is inverted: since
netlink_handle always passes fd = lctx->link_fd, the test (lctx->link_fd != fd)
is always false so multipart NLM_F_MULTI messages aren't drained; fix by
changing the condition to (lctx != NULL && lctx->link_fd == fd) so recv_again is
taken and the sequence is drained in one call, or if single-shot behavior is
intentional, add a clear comment next to the condition explaining why
lctx->link_fd != fd is used to intentionally defer multipart handling to the
next eloop iteration; update the check inside netlink_get where the goto
recv_again is decided to reference lctx and fd accordingly.
- Around line 216-218: The setsockopt call for NETLINK_BROADCAST_ERROR on
lctx->link_fd currently only logs a generic message via logerr("%s:
NETLINK_BROADCAST_ERROR", __func__) and continues; update this to handle
failures explicitly: if setsockopt(...) == -1, capture errno and include it in
the log (use strerror(errno)) and treat unsupported option (e.g.,
ENOPROTOOPT/ENOTSUP) as a non-fatal condition with a clear comment that older
kernels may not support NETLINK_BROADCAST_ERROR, but for any other errno
(unexpected failures) either return an error from the surrounding initialization
function or abort initialization so callers don't silently run with dropped link
events; reference the exact call sites/identifiers: setsockopt, lctx->link_fd,
SOL_NETLINK, NETLINK_BROADCAST_ERROR, logerr, and __func__ when making the
change.
- Line 33: Remove the kernel-internal include by deleting the line that pulls in
<asm/types.h>; instead rely on the userspace-exposed headers already present
(e.g., <linux/netlink.h>, <linux/rtnetlink.h>) which define netlink types like
struct nlmsghdr and struct ifinfomsg, so remove the `#include` <asm/types.h> from
src/netlink.c to avoid using kernel-only headers.

In `@src/route.c`:
- Line 83: The function signature for route_dispatch_ifannounce(struct link_ctx
*lctx, const struct rt_msghdr *rtm) has trailing whitespace after the closing
parenthesis; remove the extra space so the declaration ends immediately after
the closing paren. Update the signature in src/route.c (function
route_dispatch_ifannounce) to eliminate the trailing space and ensure consistent
formatting.
- Around line 116-123: The check for the event uses logerrx("%s: unexpeced event
%d", __func__, e) but does not return, so execution continues into
read(lctx->link_fd, &rtm, sizeof(rtm)); update the branch handling ELE_READ in
src/route.c to either return immediately after logging (add a return; after
logerrx) or replace the log with an assertion-style fatal log to stop execution;
also fix the typo "unexpeced" → "unexpected" in the log message.

---

Outside diff comments:
In `@src/dhcp.c`:
- Around line 1608-1616: dhcp_output now returns ssize_t and failures must be
propagated: in the switch branch handling DHCP_OFFER/DHCP_ACK/DHCP_NAK call
dhcp_output(ifp, lease, type, msg, bootp, len) and check its return value (e.g.,
ssize_t rv); if rv == -1 do not call dhcp_commit_lease(ctx, lease, bootp, len)
and handle the error path (log and/or return) so you don't persist a lease when
the reply failed to send; ensure this check is applied for both OFFER and ACK
paths while NAK still skips commit.

---

Nitpick comments:
In `@src/dhcp.c`:
- Line 41: The include in src/dhcp.c currently uses a prefixed path
("src/priv.h") which is inconsistent with the other includes and with how priv.h
is included in dhcpsd.c; update the include directive in dhcp.c to use the
unprefixed form (`#include` "priv.h") so it matches the project's include style
and relies on the configured include path; ensure no other occurrences in dhcp.c
use the src/ prefix for local headers.
- Around line 1670-1673: Remove the dead preprocessing guard around the log
line: delete the surrounding "#if 1" and "#endif" so the call to logwarnx("dhcp:
interface not found 0x%x %s", bootp->xid, inet_ntoa(from.sin_addr)); is
unconditional (or if you intend conditional logging, replace "#if 1" with the
appropriate feature/debug macro instead). This touches the logwarnx call
referencing bootp->xid and inet_ntoa(from.sin_addr) in dhcp.c.

In `@src/dhcpsd.c`:
- Around line 365-381: The getopt '?' handler currently jumps to exit silently;
update the case '?' in the getopt switch (OPTS handling) to print a short usage
message to stderr before the existing goto exit so unknown options give
actionable feedback—either call the program's existing usage/print_usage
function (if available) or emit a one-line usage via fprintf(stderr, ...) that
lists the supported flags (e.g. -d -f -p -w) and then goto exit; keep the change
localized to the getopt switch (case '?') near OPTS and f_flag handling.

In `@src/netlink.c`:
- Around line 71-84: The recvmsg return value is stored in a size_t (len) which
is then cast back to ssize_t—change len to type ssize_t in the recv_again block
so recvmsg's signed return is handled natively; update the checks to use len ==
0 and len == -1 (or len <= 0 if you prefer a single check), keep the same return
semantics (cast the ssize_t return to int where the code returns), and leave the
subsequent checks for msg.msg_namelen and nladdr.nl_pid unchanged; specifically
modify the local variable declaration for len and the conditional that tests
recvmsg(fd, &msg, flags) so no unsigned-to-signed casts are performed.

In `@src/route.c`:
- Around line 76-78: Replace the noisy warning-level log when an interface goes
down by changing the call site that currently uses logwarnx("%s: interface is
down", ifp->if_name) to use loginfox so the event is logged at info level;
update the same pattern for the IFAN_DEPARTURE message (the other logwarnx
instance near the IFAN_DEPARTURE handling) to maintain consistent severity,
keeping the surrounding operations TAILQ_REMOVE(ctx->ctx_ifaces, ifp, if_next)
and if_free(ifp) unchanged.
🪄 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: 507413c2-998c-4f59-9adf-1a1f2e75ea8d

📥 Commits

Reviewing files that changed from the base of the PR and between ef68a9b and a900878.

📒 Files selected for processing (12)
  • configure
  • src/common.c
  • src/common.h
  • src/dhcp.c
  • src/dhcpsd.c
  • src/if.c
  • src/netlink.c
  • src/plugins/auto.c
  • src/plugins/lua.c
  • src/route.c
  • src/seccomp.c
  • src/unpriv.c
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/plugins/auto.c
  • configure
  • src/plugins/lua.c
  • src/unpriv.c
  • src/if.c

Comment thread src/dhcp.c
Comment on lines +865 to +874
len = (size_t)ifp->if_mtu - sizeof(struct udphdr) - sizeof(struct ip);
if (len > ctx->dhcp_bootplen) {
void *n = realloc(ctx->dhcp_bootp, len);
if (n == NULL) {
logerr("%s: realloc", __func__);
return -1;
}
ctx->dhcp_bootp = n;
ctx->dhcp_bootplen = len;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate MTU to prevent short-buffer overflow.

len = (size_t)ifp->if_mtu - sizeof(struct udphdr) - sizeof(struct ip) is unchecked. For ifp->if_mtu values that are 0 (uninitialized, plausible under -w before the interface is fully configured) or <= 28, the subtraction wraps and produces a huge size_t, causing realloc to fail and the function to error out — recoverable. However, for MTUs in the narrow range of ~29..~327, the realloc succeeds with a buffer smaller than sizeof(struct bootp), and the very next memset(bootp, 0, sizeof(*bootp)) at line 877 overflows the heap. Clamp to a sensible floor.

🐛 Proposed fix
-	len = (size_t)ifp->if_mtu - sizeof(struct udphdr) - sizeof(struct ip);
+	if (ifp->if_mtu <= 0 ||
+	    (size_t)ifp->if_mtu <= sizeof(struct udphdr) + sizeof(struct ip)) {
+		logerrx("%s: interface mtu too small: %d",
+		    ifp->if_name, ifp->if_mtu);
+		return -1;
+	}
+	len = (size_t)ifp->if_mtu - sizeof(struct udphdr) - sizeof(struct ip);
+	if (len < sizeof(struct bootp))
+		len = sizeof(struct bootp);
 	if (len > ctx->dhcp_bootplen) {
📝 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
len = (size_t)ifp->if_mtu - sizeof(struct udphdr) - sizeof(struct ip);
if (len > ctx->dhcp_bootplen) {
void *n = realloc(ctx->dhcp_bootp, len);
if (n == NULL) {
logerr("%s: realloc", __func__);
return -1;
}
ctx->dhcp_bootp = n;
ctx->dhcp_bootplen = len;
}
if (ifp->if_mtu <= 0 ||
(size_t)ifp->if_mtu <= sizeof(struct udphdr) + sizeof(struct ip)) {
logerrx("%s: interface mtu too small: %d",
ifp->if_name, ifp->if_mtu);
return -1;
}
len = (size_t)ifp->if_mtu - sizeof(struct udphdr) - sizeof(struct ip);
if (len < sizeof(struct bootp))
len = sizeof(struct bootp);
if (len > ctx->dhcp_bootplen) {
void *n = realloc(ctx->dhcp_bootp, len);
if (n == NULL) {
logerr("%s: realloc", __func__);
return -1;
}
ctx->dhcp_bootp = n;
ctx->dhcp_bootplen = len;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dhcp.c` around lines 865 - 874, The calculation of len from ifp->if_mtu
can underflow or produce a buffer smaller than struct bootp causing a subsequent
memset overflow; before using ifp->if_mtu, validate and clamp it: compute
min_payload = sizeof(struct bootp) and required_mtu = min_payload +
sizeof(struct udphdr) + sizeof(struct ip), then if ifp->if_mtu is 0 or less than
required_mtu treat it as required_mtu (or skip realloc and keep existing
ctx->dhcp_bootplen if larger), set len = ifp->if_mtu - sizeof(struct udphdr) -
sizeof(struct ip) only after clamping, and ensure the resulting len >=
sizeof(struct bootp) before calling realloc on ctx->dhcp_bootp and updating
ctx->dhcp_bootplen to prevent heap overflow from the subsequent memset of the
bootp structure.

Comment thread src/dhcpsd.c
Comment on lines 526 to 539
TAILQ_FOREACH(ifp, ctx.ctx_ifaces, if_next) {
if (!(ifp->if_flags & IF_ACTIVE))
continue;
PLUGIN_FOREACH(&ctx, p)
{
if (p->p_configure_pools == NULL)
continue;
ssize_t n = p->p_configure_pools(p, ifp);
if (n == -1 && argc != 0)
goto exit;
if (n == -1 || n == 0)
continue;
/* XXX When we grow DHCPv6 only open BPF if we configure
* a DHCPv4 pool. */
if (dhcp_openbpf(ifp) == -1)
goto exit;
/* First plugin with config wins */
npools += (size_t)n;
break;
}
ssize_t n = dhcpsd_configure_pools(ifp);
if (n == -1 && argc != 0)
goto exit;
if (n == -1 || n == 0)
continue;
npools += (size_t)n;
}
if (npools == 0) {
if (!(ctx.ctx_options & DHCPSD_WAITIF) && npools == 0) {
logerrx("no pools, nothing to serve");
goto exit;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

npools is uninitialized when DHCPSD_WAITIF is set.

When -w is in effect, the block at lines 490–514 is skipped, so npools = 0 (assigned at line 492) never executes. Execution reaches the TAILQ_FOREACH at line 526 and eventually the npools == 0 check at line 536, but npools at that point is whatever size_t npools; was declared as at line 351 — uninitialized. Under -w, ctx_ifaces is empty so the TAILQ_FOREACH body doesn't run and npools is never written. The subsequent check short-circuits on !(ctx.ctx_options & DHCPSD_WAITIF), so no undefined-behavior read happens today, but this is fragile: any future edit that reads npools outside the short-circuit (or reorders the condition) will read garbage.

Please initialize npools = 0; at declaration.

🐛 Proposed fix
-	size_t npools;
+	size_t npools = 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dhcpsd.c` around lines 526 - 539, The variable npools is left
uninitialized when DHCPSD_WAITIF is set because the earlier initialization is
skipped; initialize npools to 0 at its declaration (the size_t npools variable)
so it has a defined value before the TAILQ_FOREACH loop and subsequent check
against ctx.ctx_options & DHCPSD_WAITIF and the npools == 0 test; update the
declaration near where size_t npools is defined (so usages in TAILQ_FOREACH,
dhcpsd_configure_pools handling, and the later conditional remain safe).

Comment thread src/netlink.c Outdated
@@ -0,0 +1,240 @@
/* SPDX-License-Identifier: BSD-2-Clause */
/*
* dhcpsd - netlink(7) supprt
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo in header: "supprt" → "support". Same typo exists in src/route.c header.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/netlink.c` at line 3, Fix the typo in the file header comment by changing
"dhcpsd - netlink(7) supprt" to "dhcpsd - netlink(7) support" in src/netlink.c
(and make the identical correction in the header comment in src/route.c); locate
the top-of-file comment that contains the exact string "dhcpsd - netlink(7)
supprt" and replace "supprt" with "support".

Comment thread src/netlink.c Outdated
Comment thread src/netlink.c Outdated
Comment thread src/netlink.c
Comment on lines +216 to +218
if (setsockopt(lctx->link_fd, SOL_NETLINK, NETLINK_BROADCAST_ERROR, &on,
sizeof(on)) == -1)
logerr("%s: NETLINK_BROADCAST_ERROR", __func__);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

NETLINK_BROADCAST_ERROR failure swallowed.

Setting this sockopt is important for detecting kernel-side broadcast delivery failures (e.g., ENOBUFS). The code logs and continues on failure. If that's intentional (older kernels may not support it), a comment would help; otherwise consider failing or at least documenting that dropped link events can result in stale interface state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/netlink.c` around lines 216 - 218, The setsockopt call for
NETLINK_BROADCAST_ERROR on lctx->link_fd currently only logs a generic message
via logerr("%s: NETLINK_BROADCAST_ERROR", __func__) and continues; update this
to handle failures explicitly: if setsockopt(...) == -1, capture errno and
include it in the log (use strerror(errno)) and treat unsupported option (e.g.,
ENOPROTOOPT/ENOTSUP) as a non-fatal condition with a clear comment that older
kernels may not support NETLINK_BROADCAST_ERROR, but for any other errno
(unexpected failures) either return an error from the surrounding initialization
function or abort initialization so callers don't silently run with dropped link
events; reference the exact call sites/identifiers: setsockopt, lctx->link_fd,
SOL_NETLINK, NETLINK_BROADCAST_ERROR, logerr, and __func__ when making the
change.

Comment thread src/route.c Outdated
Comment thread src/route.c
Comment on lines +116 to +123
if (e != ELE_READ) {
logerrx("%s: unexpeced event %d", __func__, e);
}
len = read(lctx->link_fd, &rtm, sizeof(rtm));
if (len == -1) {
logerr("%s: read", __func__);
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.

⚠️ Potential issue | 🟡 Minor

Missing early return after unexpected event.

logerrx logs but does not exit, so execution falls through to read(lctx->link_fd, ...) even though the event indicated something other than ELE_READ. In most configurations this is harmless (read will EAGAIN or behave normally), but the intent is clearly to bail. Either add a return; or make the check an assertion-style log without the fallthrough.

🐛 Proposed fix
 	if (e != ELE_READs) {
-		logerrx("%s: unexpeced event %d", __func__, e);
+		logerrx("%s: unexpected event %d", __func__, e);
+		return;
 	}

Also fixes the "unexpeced" → "unexpected" typo.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/route.c` around lines 116 - 123, The check for the event uses
logerrx("%s: unexpeced event %d", __func__, e) but does not return, so execution
continues into read(lctx->link_fd, &rtm, sizeof(rtm)); update the branch
handling ELE_READ in src/route.c to either return immediately after logging (add
a return; after logerrx) or replace the log with an assertion-style fatal log to
stop execution; also fix the typo "unexpeced" → "unexpected" in the log message.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

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

1604-1615: ⚠️ Potential issue | 🟠 Major

dhcp_output return value is discarded; lease is committed even if the reply was never sent.

dhcp_output now returns ssize_t and can fail on realloc, dhcp_addoptions, sendto, or priv_sendbpf. The caller ignores it, so on BPF/UDP send failure we still fall through to dhcp_commit_lease, persisting a lease the client never acknowledged (and possibly never received). At minimum skip the commit when the response could not be sent.

🐛 Proposed fix
 	case DHCP_NAK:
-		dhcp_output(ifp, lease, type, msg, bootp, len);
-		/* Commit the lease after sending it as it might
-		 * be a slow operation depending on what the plugins do. */
-		if (type != DHCP_NAK)
+		if (dhcp_output(ifp, lease, type, msg, bootp, len) == -1)
+			break;
+		/* Commit the lease after sending it as it might
+		 * be a slow operation depending on what the plugins do. */
+		if (type != DHCP_NAK)
 			dhcp_commit_lease(ctx, lease, bootp, len);
 		break;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dhcp.c` around lines 1604 - 1615, The code currently ignores
dhcp_output's ssize_t return value and always calls dhcp_commit_lease, which can
persist a lease even if dhcp_output failed
(realloc/dhcp_addoptions/sendto/priv_sendbpf); change the switch handling so you
capture the result of dhcp_output(…) into a ssize_t (e.g. rc) and only call
dhcp_commit_lease(ctx, lease, bootp, len) when rc >= 0 (or otherwise indicates
success), skipping the commit and handling/logging the error when dhcp_output
fails; adjust the switch case around
DHCP_BOOTREQUEST/DHCP_OFFER/DHCP_ACK/DHCP_NAK accordingly to use the return
value.
🧹 Nitpick comments (4)
src/unpriv.c (1)

190-214: Serializing the entire struct interface across IPC leaks pointers and couples layouts.

unpriv_dispatch_learnif sends buf = ifp; len = sizeof(*ifp) and the client validates rdata_len != sizeof(*ifp). However, struct interface (see src/if.h:53-70) contains a TAILQ_ENTRY (internal linked-list pointers), several pointer fields (if_ctx, if_pools, if_bpf), and a function pointer (if_output). Transmitting these across the privilege boundary leaks privileged-side addresses (weakening ASLR as a mitigation) and tightly couples the wire format to struct layout — any future field added/reordered silently breaks the protocol despite the sizeof check passing on both ends.

src/priv.c:110-140 already demonstrates the correct pattern: serialize only the meaningful fields (if_hwtype, if_hwlen, if_hwaddr, if_mtu, if_name). Consider mirroring that — e.g., define a small struct unpriv_interface wire type with just name/hwtype/hwlen/hwaddr/mtu, populate it on the server, and parse it on the client. It also makes the sizeof equality check a precise contract rather than an accidental one.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/unpriv.c` around lines 190 - 214, The code currently transmits a full
struct interface blob in unpriv_dispatch_learnif/unpriv.c which leaks internal
pointers and couples the wire format to in-memory layout; replace that with a
dedicated wire-format struct (e.g., struct unpriv_interface) containing only the
serialized fields (if_name, if_hwtype, if_hwlen, fixed-size if_hwaddr array,
if_mtu), have the privileged side populate and send that struct (see
priv.c:110-140 for the pattern), update the client-side validation to check
rdata_len == sizeof(struct unpriv_interface) and memcpy/strlcpy only those
fields into struct interface (copy hwaddr using the provided hwlen, validate
bounds), and stop sending the full struct interface across the IPC boundary to
avoid leaking pointers/function pointers and layout coupling.
src/netlink.c (2)

217-223: Comment references "Routing socket"; this is a netlink socket.

Looks like a copy-paste from src/route.c. Minor, but worth tightening to avoid confusion:

📝 Proposed fix
-	/* Routing socket can overflow if the kernel sends too many messages.
-	 * We need to reliably track state and if we can't we need to know. */
+	/* The netlink socket can overflow if the kernel sends too many
+	 * messages.  We need to reliably track state, and if we can't we
+	 * need to know. */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/netlink.c` around lines 217 - 223, The comment incorrectly calls this a
"Routing socket"—change it to "Netlink socket" to avoid confusion; update the
comment above the NETLINK_BROADCAST_ERROR setsockopt block to reference "Netlink
socket can overflow" (keeping the rest of the logic intact), and ensure the
surrounding identifiers (lctx->link_fd, NETLINK_BROADCAST_ERROR, logerr,
__func__) remain unchanged.

114-117: cb errors are silently overwritten by later messages in the same batch.

r = cb(lctx, nlm); stores only the last callback's return. If one message fails (e.g. EBADMSG returned by netlink_link) and a subsequent message in the same recvmsg batch succeeds, the error is lost. Consider breaking out on -1, or at minimum preserving a sticky failure:

♻️ Proposed fix
-		if (cb == NULL)
-			continue;
-		r = cb(lctx, nlm);
+		if (cb == NULL)
+			continue;
+		if (cb(lctx, nlm) == -1)
+			r = -1;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/netlink.c` around lines 114 - 117, The callback result from cb(lctx, nlm)
is currently overwritten by later callbacks so earlier errors (e.g. from
netlink_link) are lost; change the loop that invokes cb to preserve the first
negative error or break immediately on error: after calling r = cb(lctx, nlm),
if r < 0 either break out of the loop or store it into a sticky variable (e.g.
err = err ?: r) and avoid overwriting err with subsequent non-negative returns,
then return that preserved error after the recvmsg/batch processing completes.
src/dhcpsd.c (1)

305-334: Redundant argc/ctx_argc check between helper and caller.

dhcpsd_configure_pools already returns -1 only when ctx->ctx_argc != 0 (line 320), yet the caller at lines 529–531 re-tests n == -1 && argc != 0. Either drop the inner guard and let the caller decide, or drop the caller guard — splitting the policy across both sites makes future maintenance error-prone. Prefer keeping the policy in one place (typically the caller, so the helper just propagates -1).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dhcpsd.c` around lines 305 - 334, dhcpsd_configure_pools currently tests
ctx->ctx_argc before returning -1 which duplicates the caller's policy; change
the helper to simply propagate errors by replacing the guarded check (if (n ==
-1 && ctx->ctx_argc != 0) return -1;) with an unconditional error propagation
(if (n == -1) return -1;) in dhcpsd_configure_pools so the caller retains
responsibility for inspecting argc/ctx_argc and policy stays in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/dhcpsd.c`:
- Around line 490-513: When DHCPSD_WAITIF is set the validation block is skipped
causing unknown interface names to be accepted; modify the logic around
ctx.ctx_options & DHCPSD_WAITIF so that requested argv names are recorded and
validated later: capture the requested names into a pending list when parsing
argv (use a new pending structure or mark entries in argv), call
if_learnifaces(&ctx) as now, but also validate pending names against
ctx.ctx_ifaces when interfaces are learned (e.g. on initial learn in
if_learnifaces and inside the netlink_link / RTM_NEWLINK handler), and log or
error for names that never appear (use logerrx or a debug log listing pending
names) rather than silently ignoring them. Ensure references: DHCPSD_WAITIF
check, if_learnifaces, ctx.ctx_ifaces, netlink_link/RTM_NEWLINK handlers are
updated to consult the pending list.

In `@src/unpriv.c`:
- Around line 511-515: The truncation check after calling strlcpy is off-by-one:
change the condition that validates nlen so it treats values >=
sizeof(ifp->if_name) as truncation. Specifically, in the block using nlen =
strlcpy(ifp->if_name, ifa->ifa_name, sizeof(ifp->if_name)) replace the current
`nlen > sizeof(ifp->if_name)` test with `nlen >= sizeof(ifp->if_name)` and keep
the same errno = EINVAL; goto err; handling to match the correct check used
elsewhere (see the similar check at the earlier reference).

---

Outside diff comments:
In `@src/dhcp.c`:
- Around line 1604-1615: The code currently ignores dhcp_output's ssize_t return
value and always calls dhcp_commit_lease, which can persist a lease even if
dhcp_output failed (realloc/dhcp_addoptions/sendto/priv_sendbpf); change the
switch handling so you capture the result of dhcp_output(…) into a ssize_t (e.g.
rc) and only call dhcp_commit_lease(ctx, lease, bootp, len) when rc >= 0 (or
otherwise indicates success), skipping the commit and handling/logging the error
when dhcp_output fails; adjust the switch case around
DHCP_BOOTREQUEST/DHCP_OFFER/DHCP_ACK/DHCP_NAK accordingly to use the return
value.

---

Nitpick comments:
In `@src/dhcpsd.c`:
- Around line 305-334: dhcpsd_configure_pools currently tests ctx->ctx_argc
before returning -1 which duplicates the caller's policy; change the helper to
simply propagate errors by replacing the guarded check (if (n == -1 &&
ctx->ctx_argc != 0) return -1;) with an unconditional error propagation (if (n
== -1) return -1;) in dhcpsd_configure_pools so the caller retains
responsibility for inspecting argc/ctx_argc and policy stays in one place.

In `@src/netlink.c`:
- Around line 217-223: The comment incorrectly calls this a "Routing
socket"—change it to "Netlink socket" to avoid confusion; update the comment
above the NETLINK_BROADCAST_ERROR setsockopt block to reference "Netlink socket
can overflow" (keeping the rest of the logic intact), and ensure the surrounding
identifiers (lctx->link_fd, NETLINK_BROADCAST_ERROR, logerr, __func__) remain
unchanged.
- Around line 114-117: The callback result from cb(lctx, nlm) is currently
overwritten by later callbacks so earlier errors (e.g. from netlink_link) are
lost; change the loop that invokes cb to preserve the first negative error or
break immediately on error: after calling r = cb(lctx, nlm), if r < 0 either
break out of the loop or store it into a sticky variable (e.g. err = err ?: r)
and avoid overwriting err with subsequent non-negative returns, then return that
preserved error after the recvmsg/batch processing completes.

In `@src/unpriv.c`:
- Around line 190-214: The code currently transmits a full struct interface blob
in unpriv_dispatch_learnif/unpriv.c which leaks internal pointers and couples
the wire format to in-memory layout; replace that with a dedicated wire-format
struct (e.g., struct unpriv_interface) containing only the serialized fields
(if_name, if_hwtype, if_hwlen, fixed-size if_hwaddr array, if_mtu), have the
privileged side populate and send that struct (see priv.c:110-140 for the
pattern), update the client-side validation to check rdata_len == sizeof(struct
unpriv_interface) and memcpy/strlcpy only those fields into struct interface
(copy hwaddr using the provided hwlen, validate bounds), and stop sending the
full struct interface across the IPC boundary to avoid leaking pointers/function
pointers and layout coupling.
🪄 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: 0c71ebaa-45d0-429e-9f72-0ef01b1c62aa

📥 Commits

Reviewing files that changed from the base of the PR and between a900878 and 54d9a82.

📒 Files selected for processing (5)
  • src/dhcp.c
  • src/dhcpsd.c
  • src/netlink.c
  • src/route.c
  • src/unpriv.c
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/route.c

Comment thread src/dhcpsd.c
Comment on lines +490 to 513
if (!(ctx.ctx_options & DHCPSD_WAITIF)) {
if_learnifaces(&ctx);
TAILQ_FOREACH(ifp, ctx.ctx_ifaces, if_next) {
if (argc == 0)
ifp->if_flags |= IF_ACTIVE;
else {
for (ch = 0; ch < argc; ch++) {
if (strcmp(argv[ch], ifp->if_name) ==
0) {
ifp->if_flags |= IF_ACTIVE;
argv[ch][0] = '\0';
break;
}
}
}
}
}

for (ch = 0; ch < argc; ch++) {
if (argv[ch][0] != '\0') {
logerrx("%s: no such interface", argv[ch]);
goto exit;
for (ch = 0; ch < argc; ch++) {
if (argv[ch][0] != '\0') {
logerrx("%s: no such interface", argv[ch]);
goto exit;
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Under -w, interface-name arguments are never validated.

When DHCPSD_WAITIF is set, the entire block that matches argv[ch] against ctx_ifaces and reports "no such interface" is skipped. A user typo like dhcpsd -w eth0 bda0 (intending bdg0) will be silently accepted, and the daemon will wait forever for an interface that can never appear. Consider stashing the requested names and validating (or at least logging) them as interfaces arrive via netlink_link / RTM_NEWLINK, or printing a debug line listing the pending names so misconfigurations are diagnosable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dhcpsd.c` around lines 490 - 513, When DHCPSD_WAITIF is set the
validation block is skipped causing unknown interface names to be accepted;
modify the logic around ctx.ctx_options & DHCPSD_WAITIF so that requested argv
names are recorded and validated later: capture the requested names into a
pending list when parsing argv (use a new pending structure or mark entries in
argv), call if_learnifaces(&ctx) as now, but also validate pending names against
ctx.ctx_ifaces when interfaces are learned (e.g. on initial learn in
if_learnifaces and inside the netlink_link / RTM_NEWLINK handler), and log or
error for names that never appear (use logerrx or a debug log listing pending
names) rather than silently ignoring them. Ensure references: DHCPSD_WAITIF
check, if_learnifaces, ctx.ctx_ifaces, netlink_link/RTM_NEWLINK handlers are
updated to consult the pending list.

Comment thread src/netlink.c
Comment on lines +114 to +123
if (cb == NULL)
continue;
r = cb(lctx, nlm);
}

if (again || !terminated)
goto recv_again;

return r;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the netlink.c file
find . -name "netlink.c" -type f

Repository: NetworkConfiguration/dhcpsd

Length of output: 86


🏁 Script executed:

# Check the size of the netlink.c file
wc -l src/netlink.c 2>/dev/null || echo "File not found at standard path"

Repository: NetworkConfiguration/dhcpsd

Length of output: 88


🏁 Script executed:

# Read the netlink_get function and surrounding code, focusing on the recv_again logic
sed -n '70,230p' src/netlink.c

Repository: NetworkConfiguration/dhcpsd

Length of output: 3606


🏁 Script executed:

# Check if there are other places where MSG_DONTWAIT is used or comments about broadcast behavior
rg -n "MSG_DONTWAIT|NLM_F_MULTI|RTM_NEWLINK|RTM_DELLINK" src/netlink.c

Repository: NetworkConfiguration/dhcpsd

Length of output: 334


🏁 Script executed:

# Look for any tests or documentation about netlink broadcast handling
find . -type f \( -name "*.md" -o -name "*.txt" -o -name "*test*" -o -name "*example*" \) | head -20

Repository: NetworkConfiguration/dhcpsd

Length of output: 120


🏁 Script executed:

# Check git history or comments around the netlink_get function for context on the design intent
head -80 src/netlink.c

Repository: NetworkConfiguration/dhcpsd

Length of output: 2475


Broadcast link events trigger spurious error logs due to unconditional recv_again loop with MSG_DONTWAIT.

For broadcast messages (RTM_NEWLINK, RTM_DELLINK), which are single, non-multipart messages:

  • again = (nlm->nlmsg_flags & NLM_F_MULTI)0 (assignment, not OR, overwrites any prior value each iteration)
  • terminated remains false (only set on NLMSG_DONE or NLMSG_ERROR, neither sent for broadcasts)
  • Condition if (again || !terminated) evaluates to true → unconditional goto recv_again
  • Next recvmsg(fd, &msg, MSG_DONTWAIT) finds no waiting data → returns -1 with errno=EAGAIN
  • netlink_get returns -1
  • netlink_handle logs "netlink_get: Resource temporarily unavailable" for every legitimate event

The messages are processed correctly, but the spurious error logs mislead operators into thinking failures occurred. Additionally, each link event incurs an extra system call guaranteed to fail.

Fix:

  • Distinguish between "no more data in buffer" (success after MSG_DONTWAIT) and actual errors
  • Use bitwise OR (|=) for the again flag to preserve multipart state across iterations
Proposed fix
 recv_again:
 	len = recvmsg(fd, &msg, flags);
-	if (len == 0 || len == -1)
+	if (len == 0)
 		return (int)len;
+	if (len == -1) {
+		if (errno == EAGAIN || errno == EWOULDBLOCK)
+			return 0;
+		return -1;
+	}
@@
-		again = (nlm->nlmsg_flags & NLM_F_MULTI);
+		again |= (nlm->nlmsg_flags & NLM_F_MULTI);

Comment thread src/unpriv.c
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
src/netlink.c (2)

217-223: ⚠️ Potential issue | 🟡 Minor

NETLINK_BROADCAST_ERROR setsockopt failure is still silently swallowed.

The preceding comment explicitly notes the reliability concern ("if we can't we need to know"), yet an error here only produces a log line and lets initialization proceed. Either fail link_open on unexpected errnos, or treat only ENOPROTOOPT/EOPNOTSUPP as acceptable (older kernels) and log errno explicitly so operators can distinguish "unsupported" from real misconfigurations.

Proposed fix
 `#ifdef` NETLINK_BROADCAST_ERROR
 	if (setsockopt(lctx->link_fd, SOL_NETLINK, NETLINK_BROADCAST_ERROR, &on,
-		sizeof(on)) == -1)
-		logerr("%s: NETLINK_BROADCAST_ERROR", __func__);
+		sizeof(on)) == -1) {
+		if (errno != ENOPROTOOPT && errno != EOPNOTSUPP) {
+			logerr("%s: NETLINK_BROADCAST_ERROR", __func__);
+			return -1;
+		}
+		/* Older kernels may not support this option. */
+	}
 `#endif`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/netlink.c` around lines 217 - 223, link_open currently ignores setsockopt
failures for NETLINK_BROADCAST_ERROR by only logging a generic message; instead,
treat ENOPROTOOPT and EOPNOTSUPP as ok (older kernels) but for any other errno
log the errno (use strerror(errno) or %d) and fail link_open (return an
error/cleanup) so initialization does not silently proceed; update the
setsockopt handling around NETLINK_BROADCAST_ERROR (referencing lctx->link_fd,
setsockopt, NETLINK_BROADCAST_ERROR, and link_open) to check errno and act
accordingly.

70-120: ⚠️ Potential issue | 🟠 Major

Spurious netlink_get: EAGAIN error log on every broadcast link event (still unresolved).

For single, non-multipart broadcasts (RTM_NEWLINK/RTM_DELLINK) delivered through MSG_DONTWAIT:

  • Line 89 uses assignment, so again ends up 0 after the loop (no NLM_F_MULTI flag on broadcast notifications).
  • terminated stays false (neither NLMSG_DONE nor NLMSG_ERROR is sent for broadcasts).
  • Line 119: again || !terminated is true → unconditional goto recv_again.
  • Line 71: next recvmsg(…, MSG_DONTWAIT) returns -1/EAGAIN.
  • Line 72-73 returns -1 without distinguishing EAGAIN, so netlink_handle logs logerr("%s: netlink_get", __func__) for every legitimate event, plus incurs an extra syscall guaranteed to fail.
Proposed fix
 recv_again:
 	len = recvmsg(fd, &msg, flags);
-	if (len == 0 || len == -1)
+	if (len == 0)
 		return (int)len;
+	if (len == -1) {
+		if (errno == EAGAIN || errno == EWOULDBLOCK)
+			return 0;
+		return -1;
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/netlink.c` around lines 70 - 120, netlink_get currently treats any lack
of NLMSG_DONE/NLMSG_ERROR as "more to read" and loops back, causing a subsequent
recvmsg with MSG_DONTWAIT to return -1/EAGAIN which is propagated as error;
change behavior so a non-multipart message that leaves terminated == false does
not force another recv: in netlink_get (label recv_again) after the
message-processing loop, only loop again when again (NLM_F_MULTI) is set (keep
the NLMSG_ERROR/NLMSG_DONE handling), otherwise return 0 to indicate success;
additionally, when recvmsg returns -1, check errno and return 0 instead of -1
for EAGAIN (but preserve other errno cases), so callers like netlink_handle
won't log spurious EAGAIN errors.
🧹 Nitpick comments (1)
src/netlink.c (1)

150-158: Dead else branch in state logging.

netlink_link() is only reached for RTM_DELLINK/RTM_NEWLINK (see netlink_dispatch), and line 140 already returns for any RTM_NEWLINK with IFF_UP set. Therefore the else at line 154-155 ("interface going away for no reason") is unreachable — it would require RTM_NEWLINK with IFF_UP set, which was filtered out above. Consider collapsing to a straightforward if/else to make the intent clearer and avoid the misleading log message.

Proposed fix
-	if (nlm->nlmsg_type == RTM_DELLINK)
-		loginfox("%s: interface has departed", ifp->if_name);
-	else if (!(ifi->ifi_flags & IFF_UP))
-		loginfox("%s: interface is down", ifp->if_name);
-	else
-		logerrx("%s: interface going away for no reason", ifp->if_name);
+	loginfox("%s: interface %s", ifp->if_name,
+	    nlm->nlmsg_type == RTM_DELLINK ? "has departed" : "is down");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/netlink.c` around lines 150 - 158, In netlink_link(), remove the
unreachable third branch and collapse the logging to a two-way decision: in the
netlink_link function, check nlm->nlmsg_type == RTM_DELLINK and call
loginfox("%s: interface has departed", ifp->if_name); otherwise (handle the
non-RTM_DELLINK case) call loginfox("%s: interface is down", ifp->if_name) based
on the existing IFF_UP logic — delete the dead logerrx(...) branch. Keep the
surrounding TAILQ_REMOVE(ctx->ctx_ifaces, ifp, if_next); if_free(ifp); and
return 0; unchanged; references: netlink_link, netlink_dispatch, RTM_DELLINK,
RTM_NEWLINK, IFF_UP, loginfox, logerrx.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/netlink.c`:
- Around line 217-223: link_open currently ignores setsockopt failures for
NETLINK_BROADCAST_ERROR by only logging a generic message; instead, treat
ENOPROTOOPT and EOPNOTSUPP as ok (older kernels) but for any other errno log the
errno (use strerror(errno) or %d) and fail link_open (return an error/cleanup)
so initialization does not silently proceed; update the setsockopt handling
around NETLINK_BROADCAST_ERROR (referencing lctx->link_fd, setsockopt,
NETLINK_BROADCAST_ERROR, and link_open) to check errno and act accordingly.
- Around line 70-120: netlink_get currently treats any lack of
NLMSG_DONE/NLMSG_ERROR as "more to read" and loops back, causing a subsequent
recvmsg with MSG_DONTWAIT to return -1/EAGAIN which is propagated as error;
change behavior so a non-multipart message that leaves terminated == false does
not force another recv: in netlink_get (label recv_again) after the
message-processing loop, only loop again when again (NLM_F_MULTI) is set (keep
the NLMSG_ERROR/NLMSG_DONE handling), otherwise return 0 to indicate success;
additionally, when recvmsg returns -1, check errno and return 0 instead of -1
for EAGAIN (but preserve other errno cases), so callers like netlink_handle
won't log spurious EAGAIN errors.

---

Nitpick comments:
In `@src/netlink.c`:
- Around line 150-158: In netlink_link(), remove the unreachable third branch
and collapse the logging to a two-way decision: in the netlink_link function,
check nlm->nlmsg_type == RTM_DELLINK and call loginfox("%s: interface has
departed", ifp->if_name); otherwise (handle the non-RTM_DELLINK case) call
loginfox("%s: interface is down", ifp->if_name) based on the existing IFF_UP
logic — delete the dead logerrx(...) branch. Keep the surrounding
TAILQ_REMOVE(ctx->ctx_ifaces, ifp, if_next); if_free(ifp); and return 0;
unchanged; references: netlink_link, netlink_dispatch, RTM_DELLINK, RTM_NEWLINK,
IFF_UP, loginfox, logerrx.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 593b9f7d-e7dd-4c07-a6e6-1e27c39dd556

📥 Commits

Reviewing files that changed from the base of the PR and between 54d9a82 and 192daa3.

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

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

586-593: ⚠️ Potential issue | 🟠 Major

servname is assigned from u_gai_hostname (typo).

When a non-empty u_gai_servname is supplied, the helper passes the hostname string to getaddrinfo() as the service name. This looks like a pre-existing bug that is now encapsulated in the extracted static dispatcher — worth fixing while the area is being refactored.

🐛 Proposed fix
 	if (u_gai->u_gai_servname[0] != '\0')
-		servname = u_gai->u_gai_hostname;
+		servname = u_gai->u_gai_servname;
 	else
 		servname = NULL;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/unpriv.c` around lines 586 - 593, The code mistakenly assigns servname
from u_gai->u_gai_hostname; update the servname assignment so that when
u_gai->u_gai_servname[0] != '\0' you set servname = u_gai->u_gai_servname
(otherwise servname = NULL), leaving the hostname logic unchanged (hostname =
u_gai->u_gai_hostname when non-empty); also scan the surrounding code in
src/unpriv.c (the getaddrinfo helper) for any other hostname/servname mix-ups to
ensure getaddrinfo() receives the correct service string.
♻️ Duplicate comments (2)
src/netlink.c (2)

217-223: ⚠️ Potential issue | 🟡 Minor

NETLINK_BROADCAST_ERROR setsockopt failure is silently swallowed.

Re-iterating the earlier observation: this option is what lets the daemon notice dropped broadcasts (e.g., ENOBUFS) and re-sync interface state. If the intent is to tolerate older kernels that lack the option, handle that case explicitly (e.g., errno == ENOPROTOOPT) and treat other failures as setup errors; otherwise stale interface state can accumulate silently.

Suggested shape
 `#ifdef` NETLINK_BROADCAST_ERROR
 	if (setsockopt(lctx->link_fd, SOL_NETLINK, NETLINK_BROADCAST_ERROR, &on,
-		sizeof(on)) == -1)
-		logerr("%s: NETLINK_BROADCAST_ERROR", __func__);
+		sizeof(on)) == -1 && errno != ENOPROTOOPT) {
+		logerr("%s: NETLINK_BROADCAST_ERROR", __func__);
+		return -1;
+	}
 `#endif`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/netlink.c` around lines 217 - 223, The NETLINK_BROADCAST_ERROR setsockopt
failure is currently only logged and can be silently ignored; change the
setsockopt error handling around the call on lctx->link_fd (the
NETLINK_BROADCAST_ERROR branch) to inspect errno: if errno == ENOPROTOOPT treat
it as an acceptable older-kernel case and continue, otherwise treat it as a real
setup failure—call logerr with a clear message including __func__ and errno (or
strerror(errno)) and propagate/fail initialization (e.g., return error or exit)
instead of swallowing the error. Ensure you update the error path where
setsockopt(...) == -1 to implement this conditional handling.

70-123: ⚠️ Potential issue | 🟠 Major

Broadcast link events still trigger spurious EAGAIN error logs.

Re-checking the current implementation, the issue flagged previously still manifests:

  • For single (non-multipart) broadcast events like RTM_NEWLINK/RTM_DELLINK, after parsing the buffer again = (nlm->nlmsg_flags & NLM_F_MULTI) is 0 and terminated remains false (neither NLMSG_DONE nor NLMSG_ERROR is emitted for broadcasts).
  • The condition at line 119 if (again || !terminated) is therefore true, so we always re-enter recv_again.
  • The follow-up recvmsg(fd, &msg, MSG_DONTWAIT) finds no pending data and returns -1 with errno=EAGAIN, which is returned verbatim by netlink_get and causes netlink_handle to log "netlink_get: Resource temporarily unavailable" for every legitimate link event.

Additionally, line 89 uses assignment (again = ...) rather than OR (again |= ...), so within a single buffer a later non-multipart header can clear the multipart state set by an earlier one.

Suggested fix
 recv_again:
 	len = recvmsg(fd, &msg, flags);
-	if (len == 0 || len == -1)
-		return (int)len;
+	if (len == 0)
+		return 0;
+	if (len == -1) {
+		if (errno == EAGAIN || errno == EWOULDBLOCK)
+			return 0;
+		return -1;
+	}
@@
-		again = (nlm->nlmsg_flags & NLM_F_MULTI);
+		again |= (nlm->nlmsg_flags & NLM_F_MULTI);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/netlink.c` around lines 70 - 123, The loop incorrectly resets multipart
state and treats single non-multipart broadcast messages as unterminated,
causing a spurious retry and an EAGAIN propagated to netlink_get/netlink_handle;
fix by changing the multipart assignment to accumulate flags (use again |=
(nlm->nlmsg_flags & NLM_F_MULTI) instead of again = ...) and ensure that if no
multipart flag is set for any message in the received buffer you mark the batch
as terminated (set terminated = true when again == 0 after processing the NLMSG
loop) so the code does not goto recv_again for single-part broadcasts.
🧹 Nitpick comments (4)
configure (1)

313-326: Sanitizer probe tests only address but enables address,undefined.

The feature test on line 320 compiles with -fsanitize=address, but on success the final CFLAGS/LDFLAGS get -fsanitize=address,undefined. A compiler/toolchain that supports ASan but not UBSan will pass the probe and then fail the actual build. Either test both sanitizers together, or separate them so UBSan is only enabled when its own probe succeeds.

Suggested diff
-		if $CC -fsanitize=address _test.c -o _test 2>&3; then
+		if $CC -fsanitize=address,undefined _test.c -o _test 2>&3; then
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@configure` around lines 313 - 326, The probe currently controlled by
SANITIZEADDRESS compiles with "-fsanitize=address" but then the config writes
"-fsanitize=address,undefined" into CONFIG_MK; update the probe so the toolchain
is actually tested for what will be enabled: either (A) change the compile test
that uses CC and _test.c to try "-fsanitize=address,undefined" so both
ASan+UBSan are verified before writing CPPFLAGS/CFLAGS/LDFLAGS, or (B) split the
logic into two probes (e.g., SANITIZEADDRESS and a new SANITIZEUNDEFINED) that
compile separately and only append "-fsanitize=undefined" into CFLAGS/LDFLAGS
when the UBSan probe succeeds; make sure to reference and update the same
CONFIG_MK write paths that currently emit CPPFLAGS/CFLAGS/LDFLAGS so the flags
recorded match the successful probe results.
src/unpriv.c (3)

101-103: Nit: pass NULL instead of 0 for the plugin pointer.

The other srv_run call sites in this file (lines 185, 223, 261) pass NULL; 0 here is inconsistent and relies on implicit conversion.

-	err = srv_run(ctx, 0, U_GETIFADDRS, match_if_index,
+	err = srv_run(ctx, NULL, U_GETIFADDRS, match_if_index,
 	    match_if_index != NULL ? sizeof(*match_if_index) : 0, &result,
 	    &rdata, &rdata_len);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/unpriv.c` around lines 101 - 103, The srv_run call in unpriv.c uses an
integer literal 0 for the plugin pointer which is inconsistent with other call
sites; change the third argument from 0 to NULL in the err = srv_run(ctx, 0,
U_GETIFADDRS, match_if_index, ...) invocation so it matches the other calls (see
srv_run, U_GETIFADDRS, match_if_index, result, rdata, rdata_len) and avoids
relying on implicit conversion.

351-365: Size-calculation pass doesn't apply the if_sockaddr_active filter.

The first pass (lines 351-384) accounts for every entry returned by getifaddrs, while the second pass (line 412-414) skips entries failing if_sockaddr_active. Net effect: calloc may over-allocate. Not a correctness bug — len is recomputed from bp at line 453 before srv_send — but worth aligning the two passes to avoid wasted allocation and keep future maintainers from assuming both loops are in lock-step.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/unpriv.c` around lines 351 - 365, In the first pass over the ifaddrs list
(the loop iterating ifa from ifaddrs that uses match_index and calls
if_link_match_index), add the same if_sockaddr_active(ifa->ifa_addr) check used
in the second pass so entries with inactive sockaddr are skipped when computing
len/allocation; specifically, after the match_index switch (or before
counting/size accumulation) call if_sockaddr_active(ifa->ifa_addr) and continue
if it returns false so the size-calculation loop mirrors the second pass (bp/len
logic) and avoids over-allocation.

521-528: Full struct interface (including in-process pointers) sent across IPC.

buf = ifp; len = sizeof(*ifp); ships the entire record — including TAILQ_ENTRY, if_ctx, if_pools, if_bpf, and if_output — to the main process. The current client (unpriv_learnif, lines 195-211) only reads name/hwtype/hwlen/hwaddr/mtu, so it works today, but it is fragile: any future client reader of the other fields would dereference pointers from the helper's address space, and uninitialised struct padding is exposed on the wire. Consider serialising only the intended fields (e.g., into a small dedicated reply struct) — same pattern as struct unpriv_addrinfo for getaddrinfo.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/unpriv.c` around lines 521 - 528, The code currently sends the entire
struct interface (buf = ifp; len = sizeof(*ifp)) across IPC which leaks
in-process pointers and padding; instead define a small dedicated reply struct
(like struct unpriv_addrinfo used for getaddrinfo) containing only the fields
the client reads (name, hwtype, hwlen, hwaddr, mtu), populate that struct from
ifp in unpriv_learnif path, and change buf and len to point to and size of that
reply struct; ensure no TAILQ_ENTRY or pointer fields (if_ctx, if_pools, if_bpf,
if_output) are copied or transmitted and update any receiver code expecting the
compact reply accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@configure`:
- Around line 614-632: The configure netlink feature probe is including the
kernel-internal header <asm/types.h>, which causes false negatives; update the
test stub (_netlink.c used when LINK_SRC is unset and compiled with XCC) to
remove the line that includes <asm/types.h> so it only includes the same headers
used by the real source (e.g., <linux/netlink.h>, <linux/rtnetlink.h>,
<sys/socket.h>), ensuring the probe matches src/netlink.c and will succeed on
systems where <asm/types.h> is not available.

In `@src/netlink.c`:
- Around line 140-155: The final else branch with logerrx is unreachable because
the early return at the top (if nlm->nlmsg_type != RTM_DELLINK && ifi->ifi_flags
& IFF_UP) guarantees only two states reach the later switch: RTM_DELLINK or
RTM_NEWLINK with IFF_UP cleared; remove the dead else (logerrx) or, if you
intended defensive handling, move the IFF_UP check out of the early-return and
instead branch on nlm->nlmsg_type inside the block (use nlm->nlmsg_type,
RTM_DELLINK, RTM_NEWLINK and ifi->ifi_flags / IFF_UP to explicitly handle the
three cases) so the code in netlink.c that uses loginfox/logerrx and the iface
lookup (ctx->ctx_ifaces / ifp) only executes reachable branches.

In `@src/unpriv.c`:
- Around line 101-111: The client currently ignores the server-side return code
and treats an empty reply as success; update the post-srv_run handling in the
block using srv_run, result, rdata_len, rdata and ifahead so that you check both
err and result (i.e., if err == -1 || result == -1) and return -1 on failure
(and ensure any allocated rdata is handled appropriately), mirroring the
behavior in unpriv_learnif and acknowledging failures signaled by
unpriv_dispatch_getifaddrs.

---

Outside diff comments:
In `@src/unpriv.c`:
- Around line 586-593: The code mistakenly assigns servname from
u_gai->u_gai_hostname; update the servname assignment so that when
u_gai->u_gai_servname[0] != '\0' you set servname = u_gai->u_gai_servname
(otherwise servname = NULL), leaving the hostname logic unchanged (hostname =
u_gai->u_gai_hostname when non-empty); also scan the surrounding code in
src/unpriv.c (the getaddrinfo helper) for any other hostname/servname mix-ups to
ensure getaddrinfo() receives the correct service string.

---

Duplicate comments:
In `@src/netlink.c`:
- Around line 217-223: The NETLINK_BROADCAST_ERROR setsockopt failure is
currently only logged and can be silently ignored; change the setsockopt error
handling around the call on lctx->link_fd (the NETLINK_BROADCAST_ERROR branch)
to inspect errno: if errno == ENOPROTOOPT treat it as an acceptable older-kernel
case and continue, otherwise treat it as a real setup failure—call logerr with a
clear message including __func__ and errno (or strerror(errno)) and
propagate/fail initialization (e.g., return error or exit) instead of swallowing
the error. Ensure you update the error path where setsockopt(...) == -1 to
implement this conditional handling.
- Around line 70-123: The loop incorrectly resets multipart state and treats
single non-multipart broadcast messages as unterminated, causing a spurious
retry and an EAGAIN propagated to netlink_get/netlink_handle; fix by changing
the multipart assignment to accumulate flags (use again |= (nlm->nlmsg_flags &
NLM_F_MULTI) instead of again = ...) and ensure that if no multipart flag is set
for any message in the received buffer you mark the batch as terminated (set
terminated = true when again == 0 after processing the NLMSG loop) so the code
does not goto recv_again for single-part broadcasts.

---

Nitpick comments:
In `@configure`:
- Around line 313-326: The probe currently controlled by SANITIZEADDRESS
compiles with "-fsanitize=address" but then the config writes
"-fsanitize=address,undefined" into CONFIG_MK; update the probe so the toolchain
is actually tested for what will be enabled: either (A) change the compile test
that uses CC and _test.c to try "-fsanitize=address,undefined" so both
ASan+UBSan are verified before writing CPPFLAGS/CFLAGS/LDFLAGS, or (B) split the
logic into two probes (e.g., SANITIZEADDRESS and a new SANITIZEUNDEFINED) that
compile separately and only append "-fsanitize=undefined" into CFLAGS/LDFLAGS
when the UBSan probe succeeds; make sure to reference and update the same
CONFIG_MK write paths that currently emit CPPFLAGS/CFLAGS/LDFLAGS so the flags
recorded match the successful probe results.

In `@src/unpriv.c`:
- Around line 101-103: The srv_run call in unpriv.c uses an integer literal 0
for the plugin pointer which is inconsistent with other call sites; change the
third argument from 0 to NULL in the err = srv_run(ctx, 0, U_GETIFADDRS,
match_if_index, ...) invocation so it matches the other calls (see srv_run,
U_GETIFADDRS, match_if_index, result, rdata, rdata_len) and avoids relying on
implicit conversion.
- Around line 351-365: In the first pass over the ifaddrs list (the loop
iterating ifa from ifaddrs that uses match_index and calls if_link_match_index),
add the same if_sockaddr_active(ifa->ifa_addr) check used in the second pass so
entries with inactive sockaddr are skipped when computing len/allocation;
specifically, after the match_index switch (or before counting/size
accumulation) call if_sockaddr_active(ifa->ifa_addr) and continue if it returns
false so the size-calculation loop mirrors the second pass (bp/len logic) and
avoids over-allocation.
- Around line 521-528: The code currently sends the entire struct interface (buf
= ifp; len = sizeof(*ifp)) across IPC which leaks in-process pointers and
padding; instead define a small dedicated reply struct (like struct
unpriv_addrinfo used for getaddrinfo) containing only the fields the client
reads (name, hwtype, hwlen, hwaddr, mtu), populate that struct from ifp in
unpriv_learnif path, and change buf and len to point to and size of that reply
struct; ensure no TAILQ_ENTRY or pointer fields (if_ctx, if_pools, if_bpf,
if_output) are copied or transmitted and update any receiver code expecting the
compact reply accordingly.
🪄 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: 50fba8d2-ee58-4d7f-a9ba-e4a8cc0a35d0

📥 Commits

Reviewing files that changed from the base of the PR and between 54d9a82 and e0780a2.

📒 Files selected for processing (5)
  • configure
  • src/dhcpsd.c
  • src/if.c
  • src/netlink.c
  • src/unpriv.c
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/dhcpsd.c
  • src/if.c

Comment thread configure
Comment on lines +614 to +632
if [ -z "$LINK_SRC" ]; then
printf "Testing for netlink ... "
cat << EOF >_netlink.c
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <sys/socket.h>
int main(void) {
return 0;
}
EOF
if $XCC _netlink.c -o _netlink 2>&3; then
LINK_SRC="netlink.c"
echo "yes"
else
echo "no"
fi
rm -rf _netlink.* _netlink
fi
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine src/netlink.c
find . -name "netlink.c" -type f

Repository: NetworkConfiguration/dhcpsd

Length of output: 86


🏁 Script executed:

# Also check the configure script at the specified lines
sed -n '614,632p' configure

Repository: NetworkConfiguration/dhcpsd

Length of output: 426


🏁 Script executed:

# Let's search for asm/types.h usage in the codebase
rg "asm/types\.h" --type-list | head -20

Repository: NetworkConfiguration/dhcpsd

Length of output: 460


🏁 Script executed:

# Check for asm/types.h in the entire repository
rg "asm/types" -A 2 -B 2

Repository: NetworkConfiguration/dhcpsd

Length of output: 258


🏁 Script executed:

# Examine src/netlink.c to see its current includes
head -30 ./src/netlink.c

Repository: NetworkConfiguration/dhcpsd

Length of output: 1539


🏁 Script executed:

# Also check if asm/types.h appears anywhere in src/netlink.c
rg "asm/types" ./src/netlink.c

Repository: NetworkConfiguration/dhcpsd

Length of output: 53


🏁 Script executed:

# Get the first 50 lines to see all includes in src/netlink.c
sed -n '1,50p' ./src/netlink.c

Repository: NetworkConfiguration/dhcpsd

Length of output: 1887


Feature probe includes kernel-internal <asm/types.h> — inconsistent with src/netlink.c.

The source file src/netlink.c does not include <asm/types.h>, relying on <linux/types.h> via <linux/netlink.h> and <linux/rtnetlink.h>. The feature probe in configure (lines 617) still includes <asm/types.h>. On systems where <asm/types.h> is not exposed to userspace (musl, sanitized header setups), the probe will incorrectly fail even though the actual netlink.c would compile fine.

Remove <asm/types.h> from the probe to match the source file.

Suggested diff
 	cat << EOF >_netlink.c
-#include <asm/types.h>
 `#include` <linux/netlink.h>
 `#include` <linux/rtnetlink.h>
 `#include` <sys/socket.h>
 int main(void) {
 	return 0;
 }
 EOF
📝 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 [ -z "$LINK_SRC" ]; then
printf "Testing for netlink ... "
cat << EOF >_netlink.c
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <sys/socket.h>
int main(void) {
return 0;
}
EOF
if $XCC _netlink.c -o _netlink 2>&3; then
LINK_SRC="netlink.c"
echo "yes"
else
echo "no"
fi
rm -rf _netlink.* _netlink
fi
if [ -z "$LINK_SRC" ]; then
printf "Testing for netlink ... "
cat << EOF >_netlink.c
`#include` <linux/netlink.h>
`#include` <linux/rtnetlink.h>
`#include` <sys/socket.h>
int main(void) {
return 0;
}
EOF
if $XCC _netlink.c -o _netlink 2>&3; then
LINK_SRC="netlink.c"
echo "yes"
else
echo "no"
fi
rm -rf _netlink.* _netlink
fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@configure` around lines 614 - 632, The configure netlink feature probe is
including the kernel-internal header <asm/types.h>, which causes false
negatives; update the test stub (_netlink.c used when LINK_SRC is unset and
compiled with XCC) to remove the line that includes <asm/types.h> so it only
includes the same headers used by the real source (e.g., <linux/netlink.h>,
<linux/rtnetlink.h>, <sys/socket.h>), ensuring the probe matches src/netlink.c
and will succeed on systems where <asm/types.h> is not available.

Comment thread src/netlink.c
Comment on lines +140 to +155
if (nlm->nlmsg_type != RTM_DELLINK && ifi->ifi_flags & IFF_UP)
return 0;

TAILQ_FOREACH(ifp, ctx->ctx_ifaces, if_next) {
if (ifp->if_index == (unsigned int)ifi->ifi_index)
break;
}
if (ifp == NULL)
return 0;

if (nlm->nlmsg_type == RTM_DELLINK)
loginfox("%s: interface has departed", ifp->if_name);
else if (!(ifi->ifi_flags & IFF_UP))
loginfox("%s: interface is down", ifp->if_name);
else
logerrx("%s: interface going away for no reason", ifp->if_name);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unreachable else branch — the "going away for no reason" logerrx is dead code.

Line 140 already short-circuits with return 0 whenever nlm->nlmsg_type != RTM_DELLINK && (ifi->ifi_flags & IFF_UP). By the time execution reaches line 150, we are guaranteed to be in one of two states:

  1. nlmsg_type == RTM_DELLINK → first branch.
  2. nlmsg_type == RTM_NEWLINK with IFF_UP cleared → second branch.

The else at line 154 (RTM_NEWLINK with IFF_UP set) is unreachable, so the logerrx never fires and the message is misleading to anyone reading the code. Drop the dead branch (or, if it is defensive, restructure so the IFF_UP filter is only applied inside this block rather than as an early-return guard).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/netlink.c` around lines 140 - 155, The final else branch with logerrx is
unreachable because the early return at the top (if nlm->nlmsg_type !=
RTM_DELLINK && ifi->ifi_flags & IFF_UP) guarantees only two states reach the
later switch: RTM_DELLINK or RTM_NEWLINK with IFF_UP cleared; remove the dead
else (logerrx) or, if you intended defensive handling, move the IFF_UP check out
of the early-return and instead branch on nlm->nlmsg_type inside the block (use
nlm->nlmsg_type, RTM_DELLINK, RTM_NEWLINK and ifi->ifi_flags / IFF_UP to
explicitly handle the three cases) so the code in netlink.c that uses
loginfox/logerrx and the iface lookup (ctx->ctx_ifaces / ifp) only executes
reachable branches.

Comment thread src/unpriv.c
Comment on lines +101 to +111
err = srv_run(ctx, 0, U_GETIFADDRS, match_if_index,
match_if_index != NULL ? sizeof(*match_if_index) : 0, &result,
&rdata, &rdata_len);
if (err == -1)
return -1;

/* Should be impossible - lo0 will always exist. */
if (rdata_len == 0) {
*ifahead = NULL;
return 0;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Server-side errors are silently swallowed.

srv_run captures the server's result code into result, but the client never inspects it. On the server, unpriv_dispatch_getifaddrs reports failures (e.g., getifaddrs() returning -1 at line 339) by replying with err = -1, buf = NULL, len = 0. Here at line 108 the client sees rdata_len == 0, sets *ifahead = NULL, and returns 0 — reporting success for a failed call. Compare with unpriv_learnif (line 187) which correctly checks both err == -1 || result == -1.

🐛 Proposed fix
 	err = srv_run(ctx, 0, U_GETIFADDRS, match_if_index,
 	    match_if_index != NULL ? sizeof(*match_if_index) : 0, &result,
 	    &rdata, &rdata_len);
-	if (err == -1)
+	if (err == -1 || result == -1)
 		return -1;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/unpriv.c` around lines 101 - 111, The client currently ignores the
server-side return code and treats an empty reply as success; update the
post-srv_run handling in the block using srv_run, result, rdata_len, rdata and
ifahead so that you check both err and result (i.e., if err == -1 || result ==
-1) and return -1 on failure (and ensure any allocated rdata is handled
appropriately), mirroring the behavior in unpriv_learnif and acknowledging
failures signaled by unpriv_dispatch_getifaddrs.

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