Skip to content

Introduce age verification helper functions to get the bracket#310

Merged
laffer1 merged 4 commits intomasterfrom
feature/libutil-age
Mar 15, 2026
Merged

Introduce age verification helper functions to get the bracket#310
laffer1 merged 4 commits intomasterfrom
feature/libutil-age

Conversation

@laffer1
Copy link
Copy Markdown
Member

@laffer1 laffer1 commented Mar 15, 2026

Adds age verification helper functions for apps to call

Summary by Sourcery

Add age verification helper APIs and documentation to libutil for retrieving and updating user age information via a dedicated daemon.

New Features:

  • Introduce agev_get_age_bracket(), agev_set_age(), and agev_set_dob() helper functions in libutil for managing user age data through a UNIX domain socket to the age daemon.

Build:

  • Register the new agev.c source file and agev(3) manual page, including MLINKS for function-specific manpage aliases, in the libutil build configuration.

Documentation:

  • Add agev(3) manual page describing the new age verification helper functions and their usage, return values, and errors.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Mar 15, 2026

Reviewer's Guide

Introduces new libutil age verification helper APIs that communicate with an external age daemon over a Unix domain socket, wires them into the build and installs corresponding manual pages and links.

Sequence diagram for agev_get_age_bracket Unix socket interaction

sequenceDiagram
    participant App
    participant libutil_agev as libutil_agev
    participant aged_daemon as aged_daemon

    App->>libutil_agev: agev_get_age_bracket(username)
    libutil_agev->>libutil_agev: getpwnam(username)
    alt user_not_found
        libutil_agev-->>App: NULL (errno = EINVAL)
    else user_found
        libutil_agev->>aged_daemon: socket(AF_UNIX, SOCK_STREAM)
        alt socket_failure
            libutil_agev-->>App: NULL
        else socket_ok
            libutil_agev->>aged_daemon: connect(SOCKET_PATH)
            alt connect_failure
                libutil_agev->>aged_daemon: close(fd)
                libutil_agev-->>App: NULL
            else connect_ok
                libutil_agev->>aged_daemon: write("GET <uid>")
                libutil_agev->>aged_daemon: read(response)
                alt response_contains_dash
                    libutil_agev->>libutil_agev: parse "min-max" to two ints
                    libutil_agev-->>App: int[2] ages
                else invalid_response
                    libutil_agev-->>App: NULL
                end
                libutil_agev->>aged_daemon: close(fd)
            end
        end
    end
Loading

Sequence diagram for privileged age updates via agev_set_age and agev_set_dob

sequenceDiagram
    participant App
    participant libutil_agev as libutil_agev
    participant aged_daemon as aged_daemon

    rect rgb(235, 245, 255)
    App->>libutil_agev: agev_set_age(username, age)
    libutil_agev->>libutil_agev: geteuid()
    alt not_root
        libutil_agev-->>App: -1 (errno = EPERM)
    else root
        libutil_agev->>libutil_agev: getpwnam(username)
        alt user_not_found
            libutil_agev-->>App: -1 (errno = EINVAL)
        else user_found
            libutil_agev->>aged_daemon: socket(AF_UNIX, SOCK_STREAM)
            alt socket_or_connect_failure
                libutil_agev-->>App: -1
            else connect_ok
                libutil_agev->>aged_daemon: write("SET <uid> age <age>")
                libutil_agev->>aged_daemon: close(fd)
                libutil_agev-->>App: 0
            end
        end
    end
    end

    rect rgb(235, 255, 235)
    App->>libutil_agev: agev_set_dob(username, dob)
    libutil_agev->>libutil_agev: geteuid()
    alt not_root
        libutil_agev-->>App: -1 (errno = EPERM)
    else root
        libutil_agev->>libutil_agev: getpwnam(username)
        alt user_not_found
            libutil_agev-->>App: -1 (errno = EINVAL)
        else user_found
            libutil_agev->>aged_daemon: socket(AF_UNIX, SOCK_STREAM)
            alt socket_or_connect_failure
                libutil_agev-->>App: -1
            else connect_ok
                libutil_agev->>aged_daemon: write("SET <uid> dob <dob>")
                libutil_agev->>aged_daemon: close(fd)
                libutil_agev-->>App: 0
            end
        end
    end
    end
Loading

Class diagram for agev age verification helper API

classDiagram
    class libutil_agev {
        +int* agev_get_age_bracket(const char* username)
        +int agev_set_age(const char* username, int age)
        +int agev_set_dob(const char* username, const char* dob)
    }

    class aged_daemon {
        +handle_get(uid_t uid)
        +handle_set_age(uid_t uid, int age)
        +handle_set_dob(uid_t uid, const char* dob)
    }

    libutil_agev ..> aged_daemon : uses Unix_socket
Loading

File-Level Changes

Change Details Files
Add libutil age verification client helpers that talk to /var/run/aged/aged.sock to get and set per-user age information.
  • Implement agev_get_age_bracket to resolve a username, query an age daemon over a Unix domain socket, parse a "min-max" response, and return a dynamically allocated 2-int array.
  • Implement agev_set_age to require root, look up the target user, and send a formatted "SET age " command to the age daemon over a Unix domain socket.
  • Implement agev_set_dob to require root, look up the target user, and send a formatted "SET dob " command to the age daemon over a Unix domain socket.
lib/libutil/agev.c
Expose the new age verification APIs via libutil headers and documentation, and integrate them into the build and manual page system.
  • Declare agev_get_age_bracket, agev_set_age, and agev_set_dob in the public libutil.h header.
  • Add agev.c to the libutil build and install the new agev.3 manual page.
  • Add MLINKS so get_age_bracket.3, set_age.3, and set_dob.3 link to agev.3, and document usage, return values, and errors of the new functions.
lib/libutil/libutil.h
lib/libutil/Makefile
lib/libutil/agev.3

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 5 issues, and left some high level feedback:

  • The socket I/O in agev.c does not check the return values of write(2) and assumes a single read(2) will yield a complete response; consider handling short writes/reads and propagating EIO or similar on protocol/transport errors instead of silently returning NULL/-1.
  • agev_get_age_bracket() returns a heap-allocated int * without any bounds/validity checking on the parsed values or overflow handling; consider validating the parsed ages and using a small struct or explicit out-parameters to make ownership and size clearer.
  • The HISTORY section of agev.3 references get_age_bracket, set_age, and set_dob, which do not match the exported function names agev_get_age_bracket, agev_set_age, and agev_set_dob; align these names to avoid confusion.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The socket I/O in agev.c does not check the return values of write(2) and assumes a single read(2) will yield a complete response; consider handling short writes/reads and propagating EIO or similar on protocol/transport errors instead of silently returning NULL/-1.
- agev_get_age_bracket() returns a heap-allocated int * without any bounds/validity checking on the parsed values or overflow handling; consider validating the parsed ages and using a small struct or explicit out-parameters to make ownership and size clearer.
- The HISTORY section of agev.3 references get_age_bracket, set_age, and set_dob, which do not match the exported function names agev_get_age_bracket, agev_set_age, and agev_set_dob; align these names to avoid confusion.

## Individual Comments

### Comment 1
<location path="lib/libutil/agev.c" line_range="69-73" />
<code_context>
+		return NULL;
+	}
+
+	snprintf(buf, sizeof(buf), "GET %d", pw->pw_uid);
+	write(fd, buf, strlen(buf));
+
+	memset(buf, 0, sizeof(buf));
+	ssize_t n = read(fd, buf, sizeof(buf) - 1);
+
+	if (n > 0) {
</code_context>
<issue_to_address>
**issue (bug_risk):** Handle partial/failed writes and reads when talking to the daemon.

`write()` and `read()` can both fail or transfer fewer bytes than requested (e.g., due to `EINTR`). Here the `write()` result is ignored, and `read()` is only checked for `> 0`, so requests/responses may be silently truncated or lost. Please loop until the full request is written (or an error occurs), check and handle `write()`’s return value, and treat short/negative reads as errors (propagating `errno` appropriately).
</issue_to_address>

### Comment 2
<location path="lib/libutil/agev.c" line_range="41-50" />
<code_context>
+#define SOCKET_PATH "/var/run/aged/aged.sock"
+
+int *
+agev_get_age_bracket(const char *username)
+{
+	int fd;
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Guard against NULL or empty usernames before calling getpwnam.

`username` is passed directly to `getpwnam()`, which has undefined behavior for NULL and may misbehave for an empty string. Add an explicit check for NULL/empty and return NULL with `errno = EINVAL` before calling `getpwnam()` to match the documented EINVAL semantics.

```suggestion
int *
agev_get_age_bracket(const char *username)
{
	int fd;
	struct sockaddr_un addr;
	char buf[256] = {0};
	int *ages = NULL;
	struct passwd *pw;

	if (username == NULL || username[0] == '\0') {
		errno = EINVAL;
		return NULL;
	}

	pw = getpwnam(username);
```
</issue_to_address>

### Comment 3
<location path="lib/libutil/agev.c" line_range="131-140" />
<code_context>
+}
+
+int
+agev_set_dob(const char *username, const char *dob)
+{
+	int fd;
+	struct sockaddr_un addr;
+	char buf[256] = {0};
+	struct passwd *pw;
+
+	if (geteuid() != 0) {
+		errno = EPERM;
+		return -1;
+	}
+
+	pw = getpwnam(username);
+	if (!pw) {
+		errno = EINVAL;
+		return -1;
+	}
+
+	if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
+		return -1;
+	}
+
+	memset(&addr, 0, sizeof(addr));
+	addr.sun_family = AF_UNIX;
+	strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);
+
+	if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
+		close(fd);
+		return -1;
+	}
+
+	snprintf(buf, sizeof(buf), "SET %d dob %s", pw->pw_uid, dob);
+	write(fd, buf, strlen(buf));
+	
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider validating dob format and guarding against NULL/oversized strings.

The function accepts any `dob` string (including NULL) and writes it directly into the protocol message. Please validate `dob` as non-NULL, enforce a reasonable max length and strict `YYYY-MM-DD` formatting (e.g., 10 chars, digits + hyphens), and return `EINVAL` on failure to avoid malformed requests and unexpected characters in the protocol.

Suggested implementation:

```c
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <ctype.h>

```

```c
	if (dob == NULL) {
		errno = EINVAL;
		return -1;
	}

	size_t dob_len = strlen(dob);
	if (dob_len != 10 ||
	    !isdigit((unsigned char)dob[0]) ||
	    !isdigit((unsigned char)dob[1]) ||
	    !isdigit((unsigned char)dob[2]) ||
	    !isdigit((unsigned char)dob[3]) ||
	    dob[4] != '-' ||
	    !isdigit((unsigned char)dob[5]) ||
	    !isdigit((unsigned char)dob[6]) ||
	    dob[7] != '-' ||
	    !isdigit((unsigned char)dob[8]) ||
	    !isdigit((unsigned char)dob[9])) {
		errno = EINVAL;
		return -1;
	}

	if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
		return -1;
	}

	memset(&addr, 0, sizeof(addr));

```
</issue_to_address>

### Comment 4
<location path="lib/libutil/agev.3" line_range="78-83" />
<code_context>
+.El
+.Sh SEE ALSO
+.Xr agectl 8
+.Sh HISTORY
+The
+.Fn get_age_bracket ,
+.Fn set_age ,
+and
+.Fn set_dob
+functions first appeared in MidnightBSD 4.0.
</code_context>
<issue_to_address>
**issue:** HISTORY section uses function names that don't match the public API.

This section references .Fn get_age_bracket, .Fn set_age, and .Fn set_dob, but the exported API (and the NAME section) use agev_get_age_bracket, agev_set_age, and agev_set_dob. Please update HISTORY to use the exported names to avoid confusing users.
</issue_to_address>

### Comment 5
<location path="lib/libutil/agev.c" line_range="56" />
<code_context>
+		return NULL;
+	}
+
+	if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
+		return NULL;
+	}
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting shared helpers for socket setup and common root/user lookup logic to eliminate repeated boilerplate in these functions while keeping behavior the same.

You can reduce the duplication without changing behavior by extracting two small internal helpers: one for socket setup and one for the “root + getpwnam” pattern used by the setters.

### 1. Factor out socket setup/connect

The socket code is repeated three times with identical behavior. A tiny static helper keeps the public functions focused on their command logic:

```c
static int
agev_open_socket(void)
{
	int fd;
	struct sockaddr_un addr;

	fd = socket(AF_UNIX, SOCK_STREAM, 0);
	if (fd == -1)
		return -1;

	memset(&addr, 0, sizeof(addr));
	addr.sun_family = AF_UNIX;
	strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);

	if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
		close(fd);
		return -1;
	}
	return fd;
}
```

Usage in the three functions:

```c
int fd = agev_open_socket();
if (fd == -1)
	return NULL;   /* or -1, matching existing behavior */

/* existing snprintf/write/read/close logic stays unchanged */
```

This keeps all existing error handling semantics (just centralizes the implementation).

### 2. Factor out the “root + getpwnam” pattern

`agev_set_age` and `agev_set_dob` do identical root and user lookup checks:

```c
static int
agev_get_uid_for_update(const char *username, uid_t *uid_out)
{
	struct passwd *pw;

	if (geteuid() != 0) {
		errno = EPERM;
		return -1;
	}
	pw = getpwnam(username);
	if (!pw) {
		errno = EINVAL;
		return -1;
	}
	*uid_out = pw->pw_uid;
	return 0;
}
```

Then each setter becomes shorter and more focused:

```c
int
agev_set_age(const char *username, int age)
{
	int fd;
	char buf[256] = {0};
	uid_t uid;

	if (agev_get_uid_for_update(username, &uid) == -1)
		return -1;

	fd = agev_open_socket();
	if (fd == -1)
		return -1;

	snprintf(buf, sizeof(buf), "SET %d age %d", uid, age);
	write(fd, buf, strlen(buf));
	close(fd);
	return 0;
}
```

```c
int
agev_set_dob(const char *username, const char *dob)
{
	int fd;
	char buf[256] = {0};
	uid_t uid;

	if (agev_get_uid_for_update(username, &uid) == -1)
		return -1;

	fd = agev_open_socket();
	if (fd == -1)
		return -1;

	snprintf(buf, sizeof(buf), "SET %d dob %s", uid, dob);
	write(fd, buf, strlen(buf));
	close(fd);
	return 0;
}
```

`agev_get_age_bracket` can reuse `agev_open_socket()` and keep its current non-root `getpwnam()` behavior unchanged. This refactor removes the copy‑pasted plumbing while preserving the protocol and error semantics.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread lib/libutil/agev.c
Comment on lines +69 to +73
snprintf(buf, sizeof(buf), "GET %d", pw->pw_uid);
write(fd, buf, strlen(buf));

memset(buf, 0, sizeof(buf));
ssize_t n = read(fd, buf, sizeof(buf) - 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.

issue (bug_risk): Handle partial/failed writes and reads when talking to the daemon.

write() and read() can both fail or transfer fewer bytes than requested (e.g., due to EINTR). Here the write() result is ignored, and read() is only checked for > 0, so requests/responses may be silently truncated or lost. Please loop until the full request is written (or an error occurs), check and handle write()’s return value, and treat short/negative reads as errors (propagating errno appropriately).

Comment thread lib/libutil/agev.c
Comment thread lib/libutil/agev.c
Comment thread lib/libutil/agev.3
Comment thread lib/libutil/agev.c
return NULL;
}

if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -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.

issue (complexity): Consider extracting shared helpers for socket setup and common root/user lookup logic to eliminate repeated boilerplate in these functions while keeping behavior the same.

You can reduce the duplication without changing behavior by extracting two small internal helpers: one for socket setup and one for the “root + getpwnam” pattern used by the setters.

1. Factor out socket setup/connect

The socket code is repeated three times with identical behavior. A tiny static helper keeps the public functions focused on their command logic:

static int
agev_open_socket(void)
{
	int fd;
	struct sockaddr_un addr;

	fd = socket(AF_UNIX, SOCK_STREAM, 0);
	if (fd == -1)
		return -1;

	memset(&addr, 0, sizeof(addr));
	addr.sun_family = AF_UNIX;
	strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);

	if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
		close(fd);
		return -1;
	}
	return fd;
}

Usage in the three functions:

int fd = agev_open_socket();
if (fd == -1)
	return NULL;   /* or -1, matching existing behavior */

/* existing snprintf/write/read/close logic stays unchanged */

This keeps all existing error handling semantics (just centralizes the implementation).

2. Factor out the “root + getpwnam” pattern

agev_set_age and agev_set_dob do identical root and user lookup checks:

static int
agev_get_uid_for_update(const char *username, uid_t *uid_out)
{
	struct passwd *pw;

	if (geteuid() != 0) {
		errno = EPERM;
		return -1;
	}
	pw = getpwnam(username);
	if (!pw) {
		errno = EINVAL;
		return -1;
	}
	*uid_out = pw->pw_uid;
	return 0;
}

Then each setter becomes shorter and more focused:

int
agev_set_age(const char *username, int age)
{
	int fd;
	char buf[256] = {0};
	uid_t uid;

	if (agev_get_uid_for_update(username, &uid) == -1)
		return -1;

	fd = agev_open_socket();
	if (fd == -1)
		return -1;

	snprintf(buf, sizeof(buf), "SET %d age %d", uid, age);
	write(fd, buf, strlen(buf));
	close(fd);
	return 0;
}
int
agev_set_dob(const char *username, const char *dob)
{
	int fd;
	char buf[256] = {0};
	uid_t uid;

	if (agev_get_uid_for_update(username, &uid) == -1)
		return -1;

	fd = agev_open_socket();
	if (fd == -1)
		return -1;

	snprintf(buf, sizeof(buf), "SET %d dob %s", uid, dob);
	write(fd, buf, strlen(buf));
	close(fd);
	return 0;
}

agev_get_age_bracket can reuse agev_open_socket() and keep its current non-root getpwnam() behavior unchanged. This refactor removes the copy‑pasted plumbing while preserving the protocol and error semantics.

@laffer1 laffer1 merged commit d0beef5 into master Mar 15, 2026
4 of 13 checks passed
@laffer1 laffer1 deleted the feature/libutil-age branch March 15, 2026 22:06
laffer1 added a commit that referenced this pull request Mar 18, 2026
* Introduce age verification helper functions to get the bracket
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