Page MenuHomeFreeBSD

Add sysconf(8) and libbsdconf(3)
ClosedPublic

Authored by dteske on Jul 6 2026, 11:42 PM.
Tags
None
Referenced Files
F172326187: D58066.diff
Thu, Sep 17, 6:20 PM
F172315991: D58066.id185794.diff
Thu, Sep 17, 4:42 PM
F172310892: D58066.id183013.diff
Thu, Sep 17, 3:56 PM
F172258945: D58066.id181390.diff
Thu, Sep 17, 7:43 AM
F172240870: D58066.id183255.diff
Thu, Sep 17, 4:18 AM
F172240867: D58066.id183255.diff
Thu, Sep 17, 4:18 AM
F172240578: D58066.id183255.diff
Thu, Sep 17, 4:13 AM
Unknown Object (File)
Wed, Sep 16, 12:27 PM

Details

Summary

Complete the native configuration trinity: sysctl(8) for live kernel
state, sysrc(8) for rc.conf(5), and sysconf(8) for the remaining base
configuration -- loader.conf(5), sysctl.conf(5), and the make.conf(5)
family -- atop libbsdconf(3).

libbsdconf resurrects figpar as a unified reader/writer. Callbacks own
semantics; statements may span multiple lines via backslash continuation;
non-seekable input is spooled; writes are atomic (mkstemp, fsync, rename)
with mode/owner preservation. Format descriptors name each target, its
files, and quoting rules without private parsers. Multi-file targets
follow boot sourcing order; loader chases loader_conf_files as the boot
loader does.

sysconf(8) is the operator-facing tool: name / name=value on a required
target, sysrc-style list edits, make append and list-strike where they
belong, jail/altroot, and a capsicum sandbox for read-only use.

Sysctl writes validate against the running kernel first -- unknown and
read-only OIDs, CTLFLAG_TUN (pointing at the loader target), and CTLTYPE
range checks -- so a typo or overflow does not land in sysctl.conf.

Make and src treat WITH_/WITHOUT_ as presence knobs (as bsd.mkopt.mk /
src.conf(5) do) and warn on the WITH_*=no form that does not disable the
option, so a bad assignment is caught before an /usr/src build surfaces
it.

The rc target passes through to sysrc(8).

Defaults querying (-d/-D/-A) mirrors sysrc for dumps and descriptions on
targets that have a defaults file; named reads already see defaults, and
-A only widens dump scope.

Manuals are split pkg(8)-style (bsdconf/put/format; sysconf plus
per-target pages). ATF coverage exercises the frontend.

Co-authored-by: Faraz Vahedi <kfv@FreeBSD.org>

Test Plan

Standalone builds of the full library + sysconf(8) with
cc -Wall -Wextra -Wshadow -Werror, plus AddressSanitizer/
UndefinedBehaviorSanitizer builds; all functional tests below were run
under the sanitized binary (leak-free).

Every library translation unit also compiles under strict POSIX
(-std=c99 -D_POSIX_C_SOURCE=200809L, FreeBSD undefined) to keep the
Linux portability promise honest.

Functional testing in a fake root (-R; no root privilege required)
with fixtures for the loader, sysctl, make, src, and src-env targets:

  • Every flag exercised: -a -c -e -E -F -f -i -j/-R exclusivity, -k, -l, -L, -n, -N, -q, -v, -x, --help, and `-f -' (stdin).
  • Option placement on both sides of the target keyword.
  • Quoting round-trips against each consumer's actual rules: loader.conf always-quoted with strict equals; sysctl.conf quoted only when required; make.conf family verbatim with += ?= := != operators and empty values (WITHOUT_* idiom).
  • Multi-file semantics: authoritative (last-file) reads, writes to the authoritative file, appends to the default file, removals from every file listing the directive (no unmasking).
  • Backslash-newline continuation: read, and collapse-on-rewrite, without disturbing neighboring statements.
  • Streams: /dev/stdin and fifo reads (spooled, including inside the capsicum sandbox); writes to a fifo or stdin cleanly rejected.
  • Security: .bak symlink-attack refused (O_NOFOLLOW), create-if-missing is O_EXCL-atomic, temp file mode/ownership propagation verified.
  • Live-system sysctl validation: read-only OIDs and loader-only tunables (CTLFLAG_TUN) rejected with a pointer to the loader target while remaining valid assignments still apply.

Both manual pages pass mandoc -Tlint; the three EXAMPLES in bsdconf.3
were extracted, compiled with -Werror under the sanitizers, and run.

Not yet done: a full make buildworld against the new Makefiles
(pending; the library and utility build standalone with the same
sources and flags).

Diff Detail

Repository
rG FreeBSD src repository
Lint
Lint Not Applicable
Unit
Tests Not Applicable

Event Timeline

There are a very large number of changes, so older changes are hidden. Show Older Changes
lib/libbsdconf/bsdconf.c
48

This is kind of a weird design that makes things very racy and non-reentrant (multiple threads or multiple program parts fighting over the same sentinel they have written to).

I don't get why you don't just return NULL on failure to find an option. Returning a pointer a sentinel instead of a null pointer is not generally considered to be good design, permitting writes to the sentinel doubly so. Just hides errors when callers failed to account for the sentinel, whereas failing to account for a null pointer return generates an easily debuggable loud null pointer dereference.

189

This whole logic could probably be simplified a lot if you just copied the file into an in-memory buffer first (e.g. using open_memstream) and then processed the file as an array of characters instead of doing a gazillion of IO calls. Config files are small enough that they fit into memory, particularly on modern machines.

241

And then make all of these bool.

313

You could use open_memstream to have a self-resizing buffer for the directive instead of manually reallocating.

691

Why do we need to call realpath here? Why not just open path directly?

lib/libbsdconf/bsdconf_format.c
182

Less magic numbers.

202

This logic is incorrect if path ends in a trailing slash.

It might be easier to copy path into an on-stack buffer and then call basename on it instead of trying to reinvent it from scratch.

299

Easier to read, avoids magic numbers.

313–321

No need to reinvent the wheel.

lib/libbsdconf/bsdconf_internal.h
62

These could be bools.

lib/libbsdconf/bsdconf_put.c
60–70

These look like they should all be bools.

474

This may malfunction if the original file is read-only and in any case is afflicted by umask. Also restoring the setuid/setgid bits seems dangerous. I'd create the file with mode 0600 and the fchmod to sb.st_mode & (0777) for the backup.

lib/libbsdconf/bsdconf_stmt.c
63

This goes into an endless loop if the end of a medium (e.g. a tape) is reached, in which case write will keep returning 0.

You should at least error out in this case.

82

This function fails to account for the file growing while it is being read. It may be less error-prone to avoid the TOCTTOU race by reading the file into a variable-length buffer (e.g. using open_memstream) until EOF is encountered.

157

This can make the file an executable setuid binary which seems like an easy footgun causing vulnerabilities. Consider masking to 0666 to avoid this sort of issue.

336

Might as well use memcmp here.

lib/libbsdconf/bsdconf_string.c
25–49

This seems awfully inefficient, running in O(nm) time. How about this instead?

70

This should be designed to either always be in-place or always be out-of-place but not randomly one or the other.
The code can be made more efficient by using strstr to look for candidates for replacement as suggested for bsdconf_strcount.

usr.sbin/sysconf/sysconf.c
34

No love for getprogname()?

52

These could all be bools.

usr.sbin/sysconf/sysconf_edit_make.c
207

Are you sure that shouldn't be 0666 as usual?

usr.sbin/sysconf/sysconf_priv.h
40

make downstream consumers happy with a simple date format.

130

bool?

usr.sbin/sysconf/sysconf_query_sysctl.c
36

A candidate for sysctlnametomib?

252

sysctlnametomib?

Handle fuz inline comments

This revision is now accepted and ready to land.Wed, Aug 19, 11:58 AM
lib/libbsdconf/bsdconf.h
94

The format is tied to not-only the file format but the file collection.

BSDCONF_FORMAT_SRC for example is tied to bsdconf_format_src_def which details not-only what you described but also .sources set to bsdconf_src_sources, shown below:

static const struct bsdconf_source bsdconf_src_sources[] = {
        { BSDCONF_SOURCE_FILE, "/etc/src-env.conf" },
        { BSDCONF_SOURCE_FILE, "/etc/make.conf" },
        { BSDCONF_SOURCE_FILE, "/etc/src.conf" },
        { BSDCONF_SOURCE_FILE, NULL },
};

Wherein the format of each file is more nuanced than just "may quote", but rather it is described as:

.processing     = BSDCONF_BREAK_ON_EQUALS | BSDCONF_CASE_SENSITIVE |
                  BSDCONF_OPERATOR_EQUALS,
.put            = BSDCONF_PUT_UNQUOTED | BSDCONF_PUT_ALLOW_EMPTY,

See the 38 line file bsdconf_format_src.c which details what BSD_FORMAT_SRC consists of.

lib/libbsdconf/bsdconf.c
189

Simplified, maybe, but at what cost? The present IOPS-driven design keeps memory requirements low: we never copy the whole descriptor into RAM. bsdconf_fparse takes any fd, stdin included; unbounded input should not grow the process heap without a cap.

Use bool for bsdconf_fparse() processing options (fuz)

This revision now requires review to proceed.Fri, Sep 4, 2:22 AM

kfv@kfv.io -> kfv@FreeBSD.org (kfv)

lib/libbsdconf/bsdconf.c
313

True, a self-resizing buffer would spare the manual realloc. However, here the scan already gives n, so the realloc is one exact buffer, kept and reused (dsize) for later statements. open_memstream would hide the size, pull a FILE * into an fd / lseek scanner, and be worse at that reuse. Value is the same pattern.

lib/libbsdconf/bsdconf.c
691

You’re right — for a read, open(path) already follows the whole chain. I put realpath here when this was figpar (2001) and I was targeting the SourceForge farm. POSIX.1-2001 was new; Cygwin’s symlink emulation was in flux; MinGW had no POSIX links, so I wrote a realpath that understood .lnk and opened what it returned; Linux would ELOOP a nest BSD would accept. Canonicalize, then open, was the hedge.

The library still compiles under POSIX.1-2008 / musl, but that open follows. I’ll drop it here.

Remove realpath(3) call in bsdconf_parse() (fuz)

lib/libbsdconf/bsdconf_format.c
313–321

True -- this is asprintf(3). I keep the two-pass snprintf so the library still compiles under _POSIX_C_SOURCE=200809L. asprintf only entered POSIX in Issue 8 (2024). Using it here would move the floor from _POSIX_C_SOURCE=200809L (POSIX.1-2008) to _POSIX_C_SOURCE=202405L (POSIX.1-2024). On GNU libc, asprintf is still behind _GNU_SOURCE -- glibc's "turn on GNU extensions" switch, which is not a standard and is not how FreeBSD headers work. People lift this out of the tree; that 2008 line is the promise, and I don't want to push it forward for one path join.

lib/libbsdconf/bsdconf_stmt.c
82

True -- if the file grows between fstat and the read, the new tail is not carried into the rewrite. Anything appended after the read is lost under either design, since the rename replaces the file; reading to EOF only narrows that window, and never closes it against a writer that keeps appending. Shrink is already handled. Config files are not append logs, so I would rather keep the st_size cap than read unbounded.

dteske marked an inline comment as done.

Refactor bsdconf_fparse() (fuz)

No further objections from my side.

lib/libbsdconf/bsdconf.c
189

It'll be simpler and faster, as we avoid expensive IO calls per character. The memory requirement will rise by a few 100 kB, which is insignificant. Unbounded input already causes unbounded storage requirement in storing the parsed configuration file, so it's not a big deal.

429

The != 0 part is not required as assigning to a boolean turns zero into false and any nonzero value to true.

usr.sbin/sysconf/sysconf.c
247

This is a bit silly. Either we have the getprogname() API (BSD only), in which case pgm is not needed, or we don't have it, in which case you can't use it.

This revision is now accepted and ready to land.Tue, Sep 15, 11:05 AM

I may be a little late to this game, but do we really want to add a command-line tool that shares a name with a completely unrelated POSIX library function?

In D58066#1370013, @des wrote:

I may be a little late to this game, but do we really want to add a command-line tool that shares a name with a completely unrelated POSIX library function?

the CLI access to POSIX sysconf(3) is getconf(1) and POSIX sysconf(3) is (and always will be) read-only; and one would think that if a sysconf(1) has failed to materialize for 38 years, that we have nothing to fear. The POSIX sysconf(3) call is older than FreeBSD itself, and in all that time, has left the name unclaimed.

Now, I'm with you, that claiming it takes some chutzpah, but the value-add in using "sys" and "conf" together is that we have sysrc and sysctl -- what we're trying to do here is keep the "sys" prefix to make a "triad." It just so happens that "conf" is the most appropriate suffix.

In D58066#1370013, @des wrote:

I may be a little late to this game, but do we really want to add a command-line tool that shares a name with a completely unrelated POSIX library function?

Also worth adding:

write(1) vs POSIX write -- the two are completely unrelated. The former sends a message to another user's terminal session while the latter writes data from a buffer to a specific file descriptor.

time(1) vs POSIX time -- again, unrelated. The former measures how long a given command takes and the latter returns the current time_t

crypt(1) vs POSIX crypt -- former is for stream/file encryption, latter is for password hashing (encryption and hashing are two different things)

On Solaris (and ilk), connect(1) vs POSIX connect -- former is a proxy tool, latter is TCP connect

On Linux, connect(8) vs POSIX connect -- former is PPP, latter is TCP

It is often worried about by developers that naming a utility something generic will conflict with or be too similar to standard library mappings or standards (e.g., <string.h> functions vs strings(3)). However, standard system binaries often step right over library conventions.

It's precisely why we have built a culture around appending the man-section to words that are ambiguous. The ambiguity even exists when there is no conflict, such as link(1) vs link(2) (adding fuel to the fire, ... vs link(5))

dteske added inline comments.
lib/libbsdconf/bsdconf.c
189

Unbounded input to disk can be constrained in a number of ways such as partition size, dataset size, quota, etc. whereas unbounded input to memory is harder to constrain. Overflowing /tmp is probably better than overflowing memory. We're not talking about slurping in a 100 kB file to memory instead of disk. We're talking about what happens if someone runs yes | sysconf generic -af - (where - is equivalent to /dev/stdin). tmpfile(3) unlinks at creation. With yes, we spool into that file first; on crash or ENOSPC there is no name to find and delete, and the space comes back on close. That is a better failure than open_memstream(), because the OOM killer can pick someone else, and there is no unlink-on-close equivalent for the heap.

429

Good catch. I'll fix that in a follow-up.

usr.sbin/sysconf/sysconf.c
247

pgm is 3 letters. getprogname() is 13. It makes adhering to 80c easier.

dteske added inline comments.
lib/libbsdconf/bsdconf.c
429

Unbounded input to disk can be constrained in a number of ways such as partition size, dataset size, quota, etc. whereas unbounded input to memory is harder to constrain. Overflowing /tmp is probably better than overflowing memory. We're not talking about slurping in a 100 kB file to memory instead of disk. We're talking about what happens if someone runs yes | sysconf generic -af - (where - is equivalent to /dev/stdin). tmpfile(3) unlinks at creation. With yes, we spool into that file first; on crash or ENOSPC there is no name to find and delete, and the space comes back on close. That is a better failure than open_memstream(), because the OOM killer can pick someone else, and there is no unlink-on-close equivalent for the heap.

At this point you are just rationalising your existing design choice without reflection. Arguably an OOM condition is easier as it's ephemeral (goes away after the process dies), whereas ENOSPC means that everything else on the system will now have a problem writing to disk. If you are afraid of degenerate file sizes, enact a configurable limit on configuration file size and abort if it is exceeded. But you didn't, so it's clear that this isn't really an important case to guard against for you. The logic to manipulate a buffer in memory is clearly easier than thousands of individual reads, but it is clearly more difficult than keeping the logic as is. Be not afraid to step back and examine your choices critically.

In D58066#1370525, @fuz wrote:

Unbounded input to disk can be constrained in a number of ways such as partition size, dataset size, quota, etc. whereas unbounded input to memory is harder to constrain. Overflowing /tmp is probably better than overflowing memory. We're not talking about slurping in a 100 kB file to memory instead of disk. We're talking about what happens if someone runs yes | sysconf generic -af - (where - is equivalent to /dev/stdin). tmpfile(3) unlinks at creation. With yes, we spool into that file first; on crash or ENOSPC there is no name to find and delete, and the space comes back on close. That is a better failure than open_memstream(), because the OOM killer can pick someone else, and there is no unlink-on-close equivalent for the heap.

At this point you are just rationalising your existing design choice without reflection.

Discussed offline. We discussed how there was actually much reflection in the rationality, among other things.

Arguably an OOM condition is easier as it's ephemeral (goes away after the process dies),

Not without a blast radius.

whereas ENOSPC means that everything else on the system will now have a problem writing to disk.

An HPC cluster used at Universities and companies alike segregates the data store for the SGE/Slurm jobs precisely so that something writing to /tmp (a separate filesystem) doesn't have the same blast radius as something taking a modicum of unscheduled memory. Scheduling software will often allocate all physical memory in a box for jobs safe for about 2GiB reserved for the OS to operate itself.

If you are afraid of degenerate file sizes, enact a configurable limit on configuration file size and abort if it is exceeded.

I've committed in discussion to doing a followup that implements this but we still have to discuss what exactly that reasonable limit would be for envisioned worst-case (best-case?) scenarios.

But you didn't, so it's clear that this isn't really an important case to guard against for you.

You are right, admittedly, that bounding the unconstrained input was predicated on embracing the logic that setting limits could preclude some unknown presumed-valid use-case. The better approach (in a follow-up) is to perhaps set a reasonable limit and as a compromise, allow that limit to be raised through some identifiable mechanism documented in a manual (for example, how bpftrace on Linux bounds the sizes of strings extracted from kernel memory but allows setting BPFTRACE_MAX_STRLEN environment variable to override that sensible limit as an opt-in pain threshold).

The logic to manipulate a buffer in memory is clearly easier than thousands of individual reads, but it is clearly more difficult than keeping the logic as is. Be not afraid to step back and examine your choices critically.

We've agreed to slate this for a 2.0 venture, avoiding last-minute restructuring before lifting off the runway.

Just finished make buildkernel buildworld update-packages and verified the contents of pkgbase:

dteske@FreeBSD src $ for pkg in /usr/obj/usr/src/repo/FreeBSD:16:amd64/16.snap20260916173837/*.pkg; do echo $pkg | grep -q FreeBSD-src- && continue; match=$( tar tf $pkg | grep -e 'bsdconf\>' -e sysconf ) || continue; echo "==> $pkg"; echo "$match"; done
==> /usr/obj/usr/src/repo/FreeBSD:16:amd64/16.snap20260916173837/FreeBSD-clibs-dev-16.snap20260916173837.pkg
/usr/share/man/man3/sysconf.3.gz
==> /usr/obj/usr/src/repo/FreeBSD:16:amd64/16.snap20260916173837/FreeBSD-tests-16.snap20260916173837.pkg
/usr/tests/usr.sbin/sysconf/Kyuafile
/usr/tests/usr.sbin/sysconf/sysconf_test
/usr/lib/debug/usr/tests/usr.sbin/sysconf/
/usr/tests/usr.sbin/sysconf/
==> /usr/obj/usr/src/repo/FreeBSD:16:amd64/16.snap20260916173837/FreeBSD-utilities-16.snap20260916173837.pkg
/usr/lib/libbsdconf.so.1
/usr/sbin/sysconf
/usr/share/man/man8/sysconf-generic.8.gz
/usr/share/man/man8/sysconf-loader.8.gz
/usr/share/man/man8/sysconf-make.8.gz
/usr/share/man/man8/sysconf-rc.8.gz
/usr/share/man/man8/sysconf-src.8.gz
/usr/share/man/man8/sysconf-sysctl.8.gz
/usr/share/man/man8/sysconf-targets.8.gz
/usr/share/man/man8/sysconf.8.gz
==> /usr/obj/usr/src/repo/FreeBSD:16:amd64/16.snap20260916173837/FreeBSD-utilities-dbg-16.snap20260916173837.pkg
/usr/lib/debug/usr/lib/libbsdconf.so.1.debug
/usr/lib/debug/usr/sbin/sysconf.debug
==> /usr/obj/usr/src/repo/FreeBSD:16:amd64/16.snap20260916173837/FreeBSD-utilities-dbg-lib32-16.snap20260916173837.pkg
/usr/lib/debug/usr/lib32/libbsdconf.so.1.debug
==> /usr/obj/usr/src/repo/FreeBSD:16:amd64/16.snap20260916173837/FreeBSD-utilities-dev-16.snap20260916173837.pkg
/usr/include/bsdconf.h
/usr/lib/libbsdconf.a
/usr/lib/libbsdconf.so
/usr/share/man/man3/bsdconf.3.gz
==> /usr/obj/usr/src/repo/FreeBSD:16:amd64/16.snap20260916173837/FreeBSD-utilities-dev-lib32-16.snap20260916173837.pkg
/usr/lib32/libbsdconf.a
/usr/lib32/libbsdconf.so
==> /usr/obj/usr/src/repo/FreeBSD:16:amd64/16.snap20260916173837/FreeBSD-utilities-lib32-16.snap20260916173837.pkg
/usr/lib32/libbsdconf.so.1
This revision was automatically updated to reflect the committed changes.

Please write something in src/RELNOTES. You can use Relnotes: yes commit message trailer in the future to automate this a bit. In general, please please do this anytime you add a new thing. Otherwise writing relnotes is an act of heroism.

Please write something in src/RELNOTES. You can use Relnotes: yes commit message trailer in the future to automate this a bit. In general, please please do this anytime you add a new thing. Otherwise writing relnotes is an act of heroism.

Derp! Thanks, I intended to do that on this one and forgot. I'll examine src/RELNOTES and perform the act of heroism so nobody else has to

EDIT: Created D59747

Thanks!

usr.sbin/sysconf/sysconf-generic.8
7

Where did you get the idea to put this slash here? Interestingly, it doesn't seem to do anything.

usr.sbin/sysconf/sysconf-generic.8
7

Oh, it's just a visual line-break that doesn't render. I'll raise a review to remove them

usr.sbin/sysconf/sysconf-generic.8
7

Created D59746