Skip to content

pidfile: Import new pidfile from NetBSD#617

Open
rsmarples wants to merge 6 commits into
masterfrom
pidfile_unlock
Open

pidfile: Import new pidfile from NetBSD#617
rsmarples wants to merge 6 commits into
masterfrom
pidfile_unlock

Conversation

@rsmarples
Copy link
Copy Markdown
Member

Adapt code, chroot and sandboxing is more readily supported.

ctx->pidfile is now malloced rather than a fixed size.

rsmarples added 2 commits May 30, 2026 17:15
While here, remove the hard coded limit on pidfile size.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Refactors pidfile handling to use a heap-allocated path and an FD-tracked lock with conditional unlinking; adds pidfile accessors and unlock API; updates configure to enable compat pidfile; adjusts dhcpcd main, privsep flows, seccomp rules; and adds POSIX includes where needed.

Changes

Pidfile infrastructure and integration

Layer / File(s) Summary
Pidfile API header and public interface
compat/pidfile.h
Adds pidfile_unlock(), pidfile_fd(), pidfile_path(), and pidfile_unremoveable() declarations; updates includes/copyright.
Pidfile implementation with dynamic state and FD tracking
compat/pidfile.c
Introduces global dynamic state (pf_path, pf_fd, pf_removeable); adds pidfile_readfd, accessors, pidfile_unremoveable, and pidfile_unlock; reworks pidfile_read()/pidfile_lock() to reuse tracked FD or open fallback, persist locks via open+write, and changes pidfile_clean() to validate ownership, truncate, and conditionally unlink.
Build system feature detection and configuration
configure
Updates pidfile probe to call xpidfile_unlock() and, if locking is unavailable, defines PIDFILE_LOCAL, includes the compat header, and adds compat/pidfile.c to build sources.
Main application context and pidfile initialization
src/dhcpcd.h, src/dhcpcd.c
Changes struct dhcpcd_ctx.pidfile to char *; main() allocates ctx.pidfile via asprintf() with error handling; in privsep manager startup attempts cap_rights_limit(pidfile_fd()); frees ctx.pidfile on shutdown; removes _exit() fast-exit to allow normal return.
Privilege separation pidfile state and seccomp rules
src/privsep.c, src/privsep-linux.c
In ps_startprocess() unlocks pidfile earlier and removes a prior pidfile_clean() call; ps_managersandbox() marks pidfile unremoveable; seccomp allow-list adds ftruncate and lseek when available.
POSIX include additions for I/O declarations
src/bpf-bsd.c, src/common.c, src/privsep-control.c, src/bpf-linux.c
Adds #include <unistd.h> to ensure POSIX I/O symbols are declared.

Sequence Diagrams

sequenceDiagram
  participant app as Application
  participant pf as pidfile module
  participant fs as Filesystem
  app->>pf: pidfile_lock(path)
  pf->>pf: allocate pf_path
  pf->>fs: open with O_RDWR|O_CREAT|O_NONBLOCK
  pf->>fs: write current PID via dprintf
  pf->>pf: store pf_fd, register atexit
  app->>pf: pidfile_path()
  pf-->>app: return pf_path
  app->>pf: pidfile_unlock()
  pf->>pf: validate pf_fd ownership
  pf->>fs: truncate and conditionally unlink
  pf->>pf: close fd, free pf_path
Loading
flowchart TD
  main["main()"]
  asprintf_call["asprintf(ctx.pidfile)"]
  cap_limit["cap_rights_limit(pidfile_fd())"]
  shutdown["Shutdown"]
  free_call["free(ctx.pidfile)"]
  main-->asprintf_call
  asprintf_call-->cap_limit
  shutdown-->free_call
Loading

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: importing a new pidfile implementation from NetBSD into the compat layer. It clearly summarizes the primary objective of the changeset.
Description check ✅ Passed The description relates to the changeset by mentioning key changes: adapting code for chroot/sandboxing support and converting ctx->pidfile from fixed-size to malloc-allocated. While brief, it conveys meaningful information about the main intent.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pidfile_unlock

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: 4

Caution

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

⚠️ Outside diff range comments (1)
configure (1)

1042-1072: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix configure pidfile feature probe to test the full pidfile API used by the build

  • The current probe only compiles/references pidfile_unlock(); later code also needs pidfile_lock(), pidfile_read(), pidfile_fd() (under PRIVSEP_RIGHTS), and pidfile_unremoveable().
  • On systems where <util.h>/libutil provides only a partial pidfile API, configure may set HAVE_UTIL_H and skip compat/pidfile.c, leading to missing symbols or wrong declarations.
Suggested fix
 	cat <<EOF >_pidfile.c
 `#include` <stdlib.h>
 `#include` <util.h>
 int main(void) {
-	return (int)pidfile_unlock();
+	(void)pidfile_unlock();
+	(void)pidfile_lock(NULL);
+	(void)pidfile_read(NULL);
+	(void)pidfile_fd();
+	pidfile_unremoveable();
+	return 0;
 }
 EOF
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@configure` around lines 1042 - 1072, The pidfile feature probe currently only
tests pidfile_unlock(), which can let partial libutil implementations slip
through; update the probe in configure to compile/link a small test program that
references the full pidfile API used by the build (pidfile_lock, pidfile_unlock,
pidfile_read, pidfile_fd, pidfile_unremoveable) so the check only succeeds if
all those symbols are available (and keep using -lutil when needed); ensure the
probe sets HAVE_UTIL_H and skips adding compat/pidfile.c only when the combined
test program actually links and runs successfully, otherwise fall back to
defining PIDFILE_LOCAL and adding compat/pidfile.c to CONFIG_MK.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@compat/pidfile.c`:
- Around line 111-123: The function pidfile_unlock currently sets error = EBADF
when pf_fd == -1 which returns a positive errno instead of the documented -1;
change the branch so that when pf_fd == -1 it sets error = -1 (preserving the
documented contract), keep the existing behavior for the close() path (capture
close(pf_fd) return into error, then set pf_fd = -1), and ensure pf_path is
still freed and cleared; update only pidfile_unlock and references to
pf_fd/pf_path as needed.
- Around line 193-214: pidfile_read() currently opens a temporary fd when
reading a pidfile that is not the tracked pf_path and never closes it, leaking
descriptors; after calling pidfile_readfd(fd) in pidfile_read(), if fd is not
the tracked pf_fd (i.e., pf_fd != -1 and strcmp(...) check failed) close the fd
(and only then) before jumping to out, making sure you don't close pf_fd or -1;
reference variables/functions: pidfile_read(), pidfile_readfd(), fd, pf_fd,
pf_path, dpath.
- Around line 262-263: The fallback branch that reads descriptor flags into opts
with fcntl(fd, F_GETFD) should set the close-on-exec flag using F_SETFD
(descriptor flags), not F_SETFL (file-status flags); change the failing call to
fcntl(fd, F_SETFD, opts | FD_CLOEXEC) so the FD_CLOEXEC bit is applied to fd
(the same opts from F_GETFD) and keep the existing -1 error checks in
pidfile_lock()/the surrounding block.

In `@compat/pidfile.h`:
- Around line 34-44: The header uses pid_t in declarations (e.g., pidfile_lock,
pidfile_read) but doesn't guarantee the typedef is available; add a
type-providing include such as `#include` <sys/types.h> (or another portable
header that defines pid_t) at the top of compat/pidfile.h before the
pid_t-containing declarations so pid_t is always defined even when config.h is
generated by configure.

---

Outside diff comments:
In `@configure`:
- Around line 1042-1072: The pidfile feature probe currently only tests
pidfile_unlock(), which can let partial libutil implementations slip through;
update the probe in configure to compile/link a small test program that
references the full pidfile API used by the build (pidfile_lock, pidfile_unlock,
pidfile_read, pidfile_fd, pidfile_unremoveable) so the check only succeeds if
all those symbols are available (and keep using -lutil when needed); ensure the
probe sets HAVE_UTIL_H and skips adding compat/pidfile.c only when the combined
test program actually links and runs successfully, otherwise fall back to
defining PIDFILE_LOCAL and adding compat/pidfile.c to CONFIG_MK.
🪄 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: 02aff9f8-6fb6-4088-aa8a-9e236ca9a3a2

📥 Commits

Reviewing files that changed from the base of the PR and between 15ff549 and 3513a92.

📒 Files selected for processing (10)
  • compat/pidfile.c
  • compat/pidfile.h
  • configure
  • src/bpf-bsd.c
  • src/common.c
  • src/dhcpcd.c
  • src/dhcpcd.h
  • src/privsep-control.c
  • src/privsep-linux.c
  • src/privsep.c

Comment thread compat/pidfile.c
Comment thread compat/pidfile.c
Comment thread compat/pidfile.c Outdated
Comment thread compat/pidfile.h
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.

Caution

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

⚠️ Outside diff range comments (3)
compat/pidfile.c (3)

302-307: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing NULL check on strdup() return.

If strdup() fails, pf_path becomes NULL while pf_fd remains valid. Later, pidfile_clean() would call unlink(pf_path) with NULL, which is undefined behavior.

🛡️ Suggested fix
 		if (path == dpath) {
 			pf_path = dpath;
 			dpath = NULL;
-		} else
+		} else if ((pf_path = strdup(path)) == NULL) {
+			int error = errno;
+			(void)close(pf_fd);
+			pf_fd = -1;
+			errno = error;
+			goto out;
+		}
-			pf_path = strdup(path);
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@compat/pidfile.c` around lines 302 - 307, The code uses pf_path =
strdup(path) without checking for NULL; if strdup fails pf_path will be NULL
while pf_fd stays valid and pidfile_clean() may call unlink(pf_path) on NULL.
Update the logic around strdup in the block handling (path vs dpath) to detect a
NULL return from strdup(path), handle the error by cleaning up the open
descriptor (pf_fd) and any owned dpath (set pf_fd to an invalid value or close
it and free dpath), ensure pf_path is left in a safe state (non-NULL or handled
so pidfile_clean() won't call unlink on NULL), and propagate/return an error as
appropriate; reference the symbols pf_path, pf_fd, pidfile_clean(), strdup(),
dpath, and path to locate the change.

79-81: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix non-portable NetBSD/OpenBSD-only strtoi() usage in compat/pidfile.c

strtoi() is not a standard C/POSIX function; it’s a NetBSD/OpenBSD extension and may be undeclared on other platforms, breaking the build. Replace it with strtol() plus explicit range/trailing-character checks to preserve the existing 1..INT_MAX and “trailing newline is OK” behavior.

🐛 Suggested fix using strtol
-	pid = (pid_t)strtoi(buf, &eptr, 10, 1, INT_MAX, &error);
-	if (error != 0 && !(error == ENOTSUP && *eptr == '\n'))
+	errno = 0;
+	long val = strtol(buf, &eptr, 10);
+	if (errno != 0 || eptr == buf || (val < 1 || val > INT_MAX))
 		return -1;
+	if (*eptr != '\n' && *eptr != '\0')
+		return -1;
+	pid = (pid_t)val;

If error becomes unused after this change, remove it to avoid -Werror=unused-variable failures.

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

In `@compat/pidfile.c` around lines 79 - 81, The code uses the nonportable
NetBSD/OpenBSD strtoi() in compat/pidfile.c (the pid = (pid_t)strtoi(buf, &eptr,
10, 1, INT_MAX, &error); block); replace that call with standard strtol(): call
strtol(buf, &eptr, 10), check errno for ERANGE, ensure the returned long is
within 1..INT_MAX and cast to pid_t only if in range, and validate trailing
characters so a newline is allowed (treat *eptr == '\n' or *eptr == '\0' as OK);
remove the now-unused error variable if it becomes unused.

182-184: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix build: provide a glibc-safe getprogname() for compat/pidfile.c

compat/pidfile.c calls getprogname() (lines 182-184), but on the build path where configure sets -DPIDFILE_LOCAL and compiles compat/pidfile.c, it includes only "pidfile.h" (not <util.h>), and there’s no linkable getprogname() available for that translation unit on glibc (the only other getprogname() found is static in src/logerr.c, plus an inline stub in compat/setproctitle.h that isn’t included here).

  • Action: add a real getprogname() shim (and header declaration) used by compat/pidfile.c, or replace the call with a glibc-safe alternative (e.g., program_invocation_short_name / /proc/self/exe basename).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@compat/pidfile.c` around lines 182 - 184, compat/pidfile.c calls
getprogname() but that symbol isn’t available for that translation unit under
glibc; either add a small shim function and header declaring getprogname() that
compat/pidfile.c includes, or replace the call with a glibc-safe alternative:
use the external program_invocation_short_name (from <errno.h>/<string.h>
availability) or derive a basename from /proc/self/exe; implement the shim as a
non-static getprogname() that returns a const char* and ensure its prototype is
visible to compat/pidfile.c (or update pidfile.c to use
program_invocation_short_name or a /proc/self/exe basename retrieval instead of
getprogname()).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@compat/pidfile.c`:
- Around line 302-307: The code uses pf_path = strdup(path) without checking for
NULL; if strdup fails pf_path will be NULL while pf_fd stays valid and
pidfile_clean() may call unlink(pf_path) on NULL. Update the logic around strdup
in the block handling (path vs dpath) to detect a NULL return from strdup(path),
handle the error by cleaning up the open descriptor (pf_fd) and any owned dpath
(set pf_fd to an invalid value or close it and free dpath), ensure pf_path is
left in a safe state (non-NULL or handled so pidfile_clean() won't call unlink
on NULL), and propagate/return an error as appropriate; reference the symbols
pf_path, pf_fd, pidfile_clean(), strdup(), dpath, and path to locate the change.
- Around line 79-81: The code uses the nonportable NetBSD/OpenBSD strtoi() in
compat/pidfile.c (the pid = (pid_t)strtoi(buf, &eptr, 10, 1, INT_MAX, &error);
block); replace that call with standard strtol(): call strtol(buf, &eptr, 10),
check errno for ERANGE, ensure the returned long is within 1..INT_MAX and cast
to pid_t only if in range, and validate trailing characters so a newline is
allowed (treat *eptr == '\n' or *eptr == '\0' as OK); remove the now-unused
error variable if it becomes unused.
- Around line 182-184: compat/pidfile.c calls getprogname() but that symbol
isn’t available for that translation unit under glibc; either add a small shim
function and header declaring getprogname() that compat/pidfile.c includes, or
replace the call with a glibc-safe alternative: use the external
program_invocation_short_name (from <errno.h>/<string.h> availability) or derive
a basename from /proc/self/exe; implement the shim as a non-static getprogname()
that returns a const char* and ensure its prototype is visible to
compat/pidfile.c (or update pidfile.c to use program_invocation_short_name or a
/proc/self/exe basename retrieval instead of getprogname()).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3fb0e453-9b71-4e39-bcce-445d1d3b5c2b

📥 Commits

Reviewing files that changed from the base of the PR and between 3513a92 and 0eed17f.

📒 Files selected for processing (3)
  • compat/pidfile.c
  • compat/pidfile.h
  • configure
🚧 Files skipped from review as they are similar to previous changes (2)
  • compat/pidfile.h
  • configure

@rsmarples
Copy link
Copy Markdown
Member Author

@coderabbitai full review combat/pidfile.c

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 31, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cat compat/pidfile.c
echo "===HEADER==="
cat compat/pidfile.h
echo "===DHCPCD_C==="
cat src/dhcpcd.c
echo "===PRIVSEP_C==="
cat src/privsep.c

Length of output: 103851



@rsmarples Here is a full review of compat/pidfile.c:


Overall assessment

The implementation is a clean, well-structured port of the NetBSD pidfile from libc. The logic for locking, cleaning, and sandboxing support is sound. I found one portability issue that will cause compilation failures on Linux (the primary target for the compat layer) plus a few minor items.


1. 🔴 strtoi() is not available on Linux/glibc (portability blocker)

pidfile_readfd() uses strtoi():

pid = (pid_t)strtoi(buf, &eptr, 10, 1, INT_MAX, &error);
if (error != 0 && !(error == ENOTSUP && *eptr == '\n'))
    return -1;

strtoi() is a NetBSD libc extension (and was added to POSIX.1-2024) but is not present in glibc. Since PIDFILE_LOCAL is defined precisely when the system does not provide pidfile_unlock (e.g., Linux, older FreeBSD), this compat file will fail to compile on those platforms.

Suggested fix — fall back to strtol with manual range checking when strtoi is unavailable:

static pid_t
pidfile_readfd(int fd)
{
    char buf[16], *eptr;
    ssize_t n;
    long val;

    if (lseek(fd, 0, SEEK_SET) == -1)
        return -1;

    n = read(fd, buf, sizeof(buf) - 1);
    if (n <= 0)
        return -1;

    buf[n] = '\0';
    errno = 0;
    val = strtol(buf, &eptr, 10);
    if (errno != 0 || eptr == buf || (*eptr != '\0' && *eptr != '\n'))
        return -1;
    if (val <= 0 || val > INT_MAX) {
        errno = ERANGE;
        return -1;
    }
    return (pid_t)val;
}

Or alternatively, guard the strtoi path with a configure check and compile in the strtol fallback when strtoi is absent.


2. 🟡 Inaccurate comment on pidfile_clean() return value

The comment says:

/* Returns 0 on truncation success, otherwise -1. */

But the function returns 0 only when both truncation succeeds and (if pf_removeable) unlink succeeds. If unlink() fails the function returns -1 even though the file was truncated. The comment should be updated to reflect this:

/* Returns 0 on success (truncated, and unlinked when removeable),
 * otherwise -1. */

3. 🟡 getprogname() availability on PIDFILE_LOCAL targets

pidfile_varrun_path() calls getprogname(). This function has been available in glibc since ~2.16 (via <stdlib.h> on some distributions), but is not guaranteed on every target where PIDFILE_LOCAL would be defined. A configure check or a #ifdef-guarded fallback using /proc/self/comm or __progname would be safer for portability:

`#ifndef` HAVE_GETPROGNAME
extern char *__progname;
static inline const char *getprogname(void) { return __progname; }
`#endif`

4. 🟢 Logic and design observations (no issues)

Area Assessment
pf_fd != -1pf_path != NULL invariant ✅ Maintained in all paths of pidfile_lock()
pidfile_clean() calls pidfile_unlock() after using pf_path in unlink() ✅ No use-after-free
Double pidfile_clean() via atexit is safe ✅ Second call sees pf_fd == -1, returns -1/EBADF harmlessly
pidfile_lock() re-entry after fork (rewrite PID) ✅ Correctly skips open() block, rewrites via ftruncate/lseek/dprintf
pf_removeable = false skips unlink in pidfile_clean() ✅ Short-circuit && correctly bypasses unlink
buf[16] in pidfile_readfd() ✅ Sufficient for INT_MAX PID (10 digits + \n + \0 = 12 bytes)
TOCTOU between open() and flock() on non-O_EXLOCK systems ✅ Known inherent limitation of the flock approach; accepted in practice
File mode 0644 ✅ Standard for pidfiles; allows unprivileged readers
registered_atexit static local in pidfile_lock() ✅ Safe for single-threaded dhcpcd eloop model

Summary

Severity Issue
🔴 Blocker strtoi() unavailable in glibc — compilation failure when PIDFILE_LOCAL is set on Linux
🟡 Minor pidfile_clean() comment understates the return condition
🟡 Minor getprogname() portability on non-BSD PIDFILE_LOCAL targets

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