Page MenuHomeFreeBSD

Add sysconf(8) and libbsdconf(3)
Needs ReviewPublic

Authored by dteske on Mon, Jul 6, 11:42 PM.
Tags
None
Referenced Files
F164174522: D58066.diff
Wed, Jul 29, 8:54 AM
F164165206: D58066.id181411.diff
Wed, Jul 29, 7:17 AM
Unknown Object (File)
Tue, Jul 28, 2:21 PM
Unknown Object (File)
Tue, Jul 28, 2:21 PM
Unknown Object (File)
Tue, Jul 28, 2:01 PM
Unknown Object (File)
Tue, Jul 28, 11:18 AM
Unknown Object (File)
Tue, Jul 28, 6:25 AM
Unknown Object (File)
Tue, Jul 28, 4:15 AM

Details

Reviewers
kfv_kfv.io
adrian
des
bdrewery
emaste
Group Reviewers
manpages
Summary

Complete the native configuration trinity: sysctl(8) modifies kernel
state, sysrc(8) manages rc.conf(5), and sysconf(8) reads and modifies
the remaining configuration files of the base system -- loader.conf(5),
sysctl.conf(5), and the make.conf(5) family -- atop a new library,
libbsdconf(3).

The library resurrects the figpar parsing engine, re-engineered into a
unified reader/writer. The parser keeps figpar's ethos intact: raw
directive/value pairs are handed to caller callbacks that own all
semantics, so drastically different formats (space-separated
httpd-style statements, cumulative directives) are read with the stock
engine and bespoke callbacks alone. Statements may span physical
lines via backslash-newline continuation (essential to make.conf and
its siblings), and input that cannot seek -- a pipe or socket, stdin
included -- is detected up front and spooled to an unlinked temporary
through the exported bsdconf_spool(), so streams parse as readily as
files. Three bugs inherited from figpar are fixed in the port:
unquoted inline comment markers leaked into values, the terminating
semicolon was retained in the value under BREAK_ON_SEMICOLON, and
reported line numbers drifted on multi-line directives.

Writes never touch a file in place. Updates stream to a mkstemp(3)
temporary in the target directory, are flushed with fsync(2), and are
atomically rename(2)d over the target with mode and ownership
propagated, so a crash or power loss mid-write cannot truncate or
corrupt a configuration file. The transaction is hardened against
interference as well as interruption: only a regular file is accepted
as a write target (opened O_NONBLOCK so a fifo fails fast instead of
blocking), mode and ownership are taken by fstat(2) from the
descriptor actually read and applied with fchmod(2)/fchown(2), the
temporary never transits a world-writable directory, and the optional
`.bak' backup refuses to follow a planted symbolic link (O_NOFOLLOW).
Symbolic links in the target path are resolved first, so writing
through a link rewrites the file it points at and preserves the link.
The properties (and the deliberate non-goals) are documented in a
SECURITY CONSIDERATIONS section of bsdconf(3).

Above the engine sits a format descriptor layer, one format per source
file plus a central registry: a descriptor contributes a target
keyword, an ordered list of backing files, and tokenizing/quoting
bitmasks -- never private parsing code. Applications bolt on formats
at run time by deriving from the nearest built-in and registering the
result. Built-in formats honor the full syntax of their consumers:
loader.conf(5) values are always written quoted with strict equals, as
the boot loader's reader demands; sysctl.conf(5) values are quoted
only when required to round-trip; and the make(1) family (make.conf,
src.conf, src-env.conf) is written unquoted with assignment modifiers
(+=, ?=, :=, !=) and the empty value (the WITH_/WITHOUT_ knob idiom)
preserved.

Formats backed by multiple files (loader.conf + loader.conf.d/*.conf +
loader.conf.local; sysctl.conf + sysctl.conf.local +
sysctl.kld.d/<module>.conf) are consulted automatically in the order
the system sources them at boot. Reads report the authoritative
(last) value, writes land in the authoritative file so the change
survives a reboot, new directives are appended to the default file,
and removals strike every file listing the directive lest deleting the
authoritative definition merely unmask an earlier one.

The sysconf(8) frontend maps assignment syntax directly onto
sysctl(8): a name alone reads, name=value writes, and options are
accepted on either side of the required target keyword. The format is
always stated explicitly by the target and never guessed from a
filename; -f overrides which file is operated on, never how, so a
loader.conf-formatted file can be staged anywhere and any format can
be deliberately applied to any file (a generic target serves files
matching no known format). A file of `-' means standard input,
permitting reads and checks from a pipeline; fifos and /dev/stdin are
spooled before the sandbox closes, and write requests against
standard input are rejected up front. -a dumps the union across a target's
files, -l/-L list backing files and module drop-in candidates (-E
filters to those on disk), -F names the file holding each directive's
effective value, -k includes a module's sysctl.kld.d drop-in, -c
checks without modifying, -x removes, and -j/-R operate on a jail or
alternate root. Writes to the sysctl target of the running system are
validated against the kernel first: unknown OIDs, read-only OIDs, and
loader-only tunables (CTLFLAG_TUN) are reported and skipped -- the
latter with a pointer to the loader target -- while remaining valid
assignments still apply. Read-only invocations run under a
capsicum(4) sandbox.

The library is strict POSIX C and builds with -std=c99
-D_POSIX_C_SOURCE=200809L with no FreeBSD headers; capsicum, jail,
kld, and sysctl validation layers in the frontend are conditional, and
bsdconf.h supplies a __BEGIN_DECLS fallback for libcs lacking
<sys/cdefs.h>. The bsdconf(3) manual defines the format descriptor at
first use, walks through adding formats (callbacks first, descriptors
second), and closes with a compiled-and-run EXAMPLES section covering
callback parsing, cumulative-directive accumulation, and run-time
format registration. The parser, writer, every format, and every
frontend flag were exercised in a fake-root harness under
AddressSanitizer/UndefinedBehaviorSanitizer, including quoting and
line-continuation round-trips against the real consumers' rules and a
symlink-attack test of the backup path; both manual pages pass
mandoc -Tlint. Not yet exercised: a full buildworld against the new
Makefiles.

libfigpar remains in the tree untouched; its removal is left to a
future commit once downstream consumers are migrated.

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

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 Skipped
Unit
Tests Skipped
Build Status
Buildable 75268
Build 72151: arc lint + arc unit

Event Timeline

dteske added a reviewer: adrian.
dteske added reviewers: des, manpages.
dteske added a reviewer: bdrewery.
dteske edited the test plan for this revision. (Show Details)

Playing around (and yes, my hostname is literally "BSDCan2026" because this is running on my Framework Laptop 12 that I purchased just-prior-to and took to BSDCan 2026 conference):

Test loader -ae as a litmus (can it show all the configuration options on my system?):

dteske@BSDCan2026 ~ $ sysconf loader -ae
kernel="kernel_latest"
kernels-"kernel_latest kernel_test kernel"
aesni_load="YES"
geom_eli_load="YES"
kern.geom.label.disk_ident.enable="0"
kern.geom. label.gptid.enable="0"
zfs_load-"YES"
compat.linuxkpi.80211.hw_crypto="1"
compat.linuxkpi.iwlwifi_11n_disable="0"
compat.linuxkpi.iwlwifi_disable_11n="0"
compat.linuxkpi.iwlwifi_disable_11ac="0"

Test loader -av; adding -v to print where directives are defined:

dteske@BSDCan2026 ~ $ sysconf loader -av
/boot/loader.conf: kernel: kernel_latest
/boot/loader.conf: kernels: kernel_latest kernel_test kernel
/boot/loader.conf: aesni_load: YES
/boot/loader.conf: geom_eli_load: YES
/boot/Loader.conf: kern.geom.label.disk_ident. enable: 0
/boot/loader.conf: kern.geom.label.gptid.enable: 0
/boot/loader.conf: zfs_load: YES
/boot/loader.conf.d/iwlwifi-ax210.conf: compat.linuxkpi.80211.hw_crypto: 1
/boot/loader.conf.d/iwlwifi-ax210.conf: compat.linuxkpi.iwlwifi_11n_disable: 0
/boot/loader.conf.d/iwlwifi-ax210.conf: compat.linuxkpi.iwlwifi_disable_11n: 0
/boot/loader.conf.d/iwlwifi-ax210.conf: compat.linuxkpi.iwlwifi_disable_11ac: 0

Test loader <key> to fetch the value of <key>:

dteske@BSDCan2026 ~ $ sysconf loader compat.linuxkpi.80211.hw_crypto
compat.linuxkpi.80211.hw_crypto: 1

Test sysctl -a to show all the sysctl.conf(5) settings on my laptop:

dteske@BSDCan2026 ~ $ sysconf sysctl -a
vfs.zfs.vdev.min_auto_ashift: 12
hw.acpi .power_button_state: fw_suspend
hw.acpi.lid_switch_state: suspend_to_idle

Test sysctl -av; adding -v to see where directives are configured:

deske@BSDCan2026 ~ $ sysconf sysctl -av
/etc/sysctl.conf: vfs.zfs.vdev.min_auto_ashift: 12
/etc/sysctl.conf: hw.acpi.power_button_state: fw_suspend
/etc/sysctl.conf: hw.acpi.lid_switch_state: suspend_to_idle

Test sysctl -ave; just because; adding -e to get it in <key>=<value> format:

dteske@BSDCan2026 ~ $ sysconf sysctl -ave
/etc/sysctl.conf: vfs.zfs.vdev.min_auto_ashift=12
/etc/sysctl.conf: hw.acpi.power_button_state=fw_suspend
/etc/sysctl.conf: hw.acpi.lid_switch_state-suspend_to_idle

Test trying to get a directive that we know is unconfigured/unknown (should produce an error):

dteske@BSDCan2026 ~ $ sysconf sysctl foobar
sysconf: unknown directive 'foobar"

Test first getting a value, then setting it, then setting it back (while also testing without root privileges first to make sure we get permission denied):

dteske@BSDCan2026 ~ $ sysconf loader geom_eli_load=NO
sysconf: /boot/Loader.conf: Permission denied
deske@BSDCan2026 ~ $ sudo sysconf loader geom_eli_load=NO
[sudo] Password:
dteske@BSDCan2026 ~ S sysconf loader geom_eli_load
geom_eli_load: NO
dteske@BSDCan2026 ~ $ sudo sysconf loader geom_eli_load-YES
dteske@BSDCan2026 ~ $ sysconf loader geom_eli_load
geom_eli_load: YES

I think I might update the code to print the before/after value. I think it should, when setting a value, like sysrc(8), show (for example) <old> -> <new>

Echo 'name: old -> new' on writes after the fashion of sysctl(8)/sysrc(8), silenced by -q

Honor loader_conf_files discovery from /boot/defaults/loader.conf (kevans); add sysrc-style -d/-D/-A defaults querying, LOADER_DEFAULTS, and a FILES section

Let -d compose with the dump flags as in sysrc(8): -ad/-Ad/-aDd dump descriptions of every directive in scope; sysctl -ad pairs configured directives with kernel OID descriptions

Allow -A with -D as in sysrc(8): -D's scoping wins, -A keeps its dump-implying role, so -ADd describes all defaults and only defaults

Add rc pass-through to sysrc(8) (all arguments verbatim, unvalidated), sysrc-style name+=word/name-=word list editing for non-make targets, and --version

Defaults-aware reads: the defaults file is always scanned first (like the boot loader), so 'sysconf loader acpi_video_load' reports the effective boot value; the defaults slot is read-only and never appears in -l/-L, write targeting, or removals

Touch-ups: --version prints '1.0 Jul-6,2026' (sysrc format), -h/--help advertise name[[+|-]=value], SYNOPSIS renders name[[+|-]=value] via sysrc.8's mdoc idiom

@kfv_kfv.io are you okay with using the abridged license form shown in style.9 and the freebsd license guide?

Adopt the abridged license form per style(9) and the FreeBSD license guide across all new files (requested by ziaee): copyright lines first, then SPDX-License-Identifier: BSD-2-Clause, dropping the full 2-clause text

Adversarial parser fuzzing (ASan/UBSan) found and fixed two figpar-inherited memory-safety bugs: NULL directive buffer on an empty-name line, and a bsdconf_strexpand() heap over-read/over-write when a value shrinks as backslash escapes collapse (plus a one-byte over-read on a trailing backslash)

lib/libbsdconf/Makefile
16

Listed in the page's .Nm block (bsdconf.3:27).

lib/libbsdconf/bsdconf_string.c
81–82

We're not using it anywhere to distinguish success from failure.

208–210

UB per C23 §7.4.1:

The header <ctype.h> declares several functions useful for classifying and mapping characters.
In all cases the argument is an int, the value of which shall be representable as an unsigned char
or shall equal the value of the macro EOF. If the argument has any other value, the behavior is
undefined.

216

I'm pretty sure this is the same class as the two bugs fixed in Diff 181466.

When the byte after x is not a hex digit, the "unknown escape" branch consumes it unconditionally. If that byte is the NUL terminator, the chr++ at line 247 steps past it and the while (*chr != '\0') loop keeps reading out of bounds until it happens to find a zero byte, so it is not a bounded 1-byte over-read: adjacent heap contents can be copied into the parsed value.

So we can perhaps guard the consumption on *(chr + 1) != '\0' or emit x verbatim for trailing \x with no digits. Your call.

222–229

Similar to the above-mentioned case, UB per C23 §7.4.1.

238

Similar to above-mentioned occurrences, UB per C23 §7.4.1.

273

Same story, UB per C23 §7.4.1.

The return cast on the other hand is just to say the narrowing is deliberate, so it'd keep the line quiet under conversion-class diagnostics; which we don't have enabled at the time, but I still think it'd be a good practice.

I haven't reviewed the rest yet, but I'll go through them shortly and submit my reviews as well. For bsdconf.c and sysconf.c, at least, I've noticed a few things I'd like to share my notes with you. I'll get back to the rest soon again.

In the meantime, .Lb libbsdconf results in unknown library name, so please take care of it contrib/mandoc/lib.in.

This revision now requires changes to proceed.Mon, Jul 27, 7:08 AM
lib/libbsdconf/bsdconf_string.c
207

This is incredibly wasteful. The ctype(3) functions are locale-aware and therefore slow, and the work they are doing here is needlessly duplicated by the later strtoul(3) call. It would be more efficient (and far more readable) to use direct comparisons and convert as you go. It would also save you from having to copy the digits before converting. Same for the octal conversion below.

lib/libbsdconf/bsdconf_string.c
56

Why only allocated memory? Surely a local or global non-const array will work fine? Also, we usually use the term “buffer”, not “block of memory”.

64

Do you think that maybe in 2026 you could do better than crash? Like take a size argument?

132

This function actually unexpands rather than expands. Otherwise it would not be able to operate in-place.

136–137

Passing a constant to this function invokes undefined behavior. Whether or not it segfaults is beside the point. Either way, the compiler won't let you do it, unless you force its hand.

139–143

“may safely share the buffer” is academic since you don't offer the caller a choice. It would be germane if the function took two arguments and the caller could choose to pass the same buffer to both, cf. strunvis(3).

147

That's not how C-style octal escapes work, DEL (ASCII 127) is encoded as \177, not \0177.

161–166

buf, src, dst` instead of source, chr, and pos would be easier to follow.

Please keep in mind that style(9) mandates sorting variables first in order of decreasing size, then alphabetically.

261

Why only allocated memory? Surely a local or global non-const array will work fine?

Same as above: undefined behaviour per C23 §7.4.1.

lib/libbsdconf/bsdconf.c
212–213
236
273
305
313
463
475
483
dteske marked 24 inline comments as done.

kfv + des: ctype UB casts; strunexpand(dst, src) with convert-as-you-go
hex/octal; size-aware replaceall; mandoc .Lb + format_register MLINK.

lib/libbsdconf/bsdconf.c
208

We are not checking read(2) for failure, so bsdconf_fparse() could spin forever when it fails. read() returns −1 on error, which != 0 holds true for, so every loop treats a failure as "more data available". For example, assume a descriptor open on a directory (lseek(2) succeeds, so the spool path is not taken; read(2) then fails with EISDIR forever).

Worse, the directive-length loop below increments n on every failed read and the buffer is grown to n + 1, so it is an unbounded realloc() growth loop as well as a hang. Besides, since n is uint32_t, at 4 GiB it wraps and read(fd, directive, n) is then called with a length far exceeding the allocation. bsdconf_spool() and bsdconf_readfile() both handle r < 0 correctly with if (r < 0) { if (errno == EINTR) continue; goto fail; }.

kfv: honor read(2) failures in bsdconf_fparse (EINTR retry / fail via read1/readn); no hang or unbounded realloc on EISDIR

lib/libbsdconf/bsdconf.c
540–541

It's nothing critical, but I recommend adding the t > value guard here for parity with the upcoming occurrences for #/; trim. Safe today as the value scan always leaves t on real content first, but I think it could be a latent one-byte underflow if that invariant ever broke.

lib/libbsdconf/bsdconf_put.c
192

After counting a backslash run, p already points at the next character and this continue statement does another p++, so a byte after an even-length run is never examined. Unescaped # / " after \ pairs can pass validation and be written verbatim; round-trips then lose or truncate data (e.g. a\\#b becomes a\). Please re-evaluate that character when (nbs & 1) == 0 (you could possibly drop the continue or adjust p).

Besides, while here, please clarify and document whether BSDCONF_PUT_UNQUOTED values are literal bytes or pre-escaped text, since the reader runs strunexpand() but the writer does not escape.

543–545

bsdconf_put() always runs the mkstemp/write/fsync/rename path even when every option is CHECK or the value is unchanged. That violates the documented "without modifying the file" contract, bumps mtime, severs hard links, and requires write access to the directory (fails for unprivileged callers on an equal-value check). Please skip the commit when nothing would change; especially when all actions are CHECK. Same root cause seems to make idempotent writes (a=1 when already 1) non-idempotent; sysconf -c is fine because it never calls put, but library consumers are not.

usr.sbin/sysconf/sysconf.c
157

The pre-scan skips the argument after a cluster whose last character is one of the option letters taking an argument. Attached forms like sysconf -f/boot/loader.conf loader rc are mishandled, as here for example, it would treat rc as the target and exec sysrc with the wrong argv.

To my understanding, any -f/-j/-k/-R value ending in A a c d D E e F f i j k l L n N q R v x is affected. Since this exists only to detect the rc pass-through, perhaps we could use real getopt parsing or at least treat attached arguments as self-contained so we don't consume the following argv element.

623–624

The behaviour here may confuse users even though it is correct for each format.

For make/src/src-env, a trailing + on the name selects the make += operator and we replace the whole assignment. So, for instance, an existing CFLAGS+=-g -pipe plus sysconf make CFLAGS+=-O2 becomes CFLAGS+=-O2, dropping -g -pipe.

For every other target, trailing +/- is a list edit and merge_list_requests() appends or strikes words in the current value, so IGNORE_LIST+=c, for example, merges into what is already there.

From maintainer perspective this matches each consumer’s syntax and looks just fine; from user perspective, though, the same CLI spelling has different effects on different targets, easily misused, especially with -q that'd hide the old -> new echo that would otherwise warn them.

I don't know whether we should internally turn it into something well established to avoid such cases and prevent foreseen misuse, but it concerns me a little, to be honest.

1464–1466

Somewhat related to the other comment atop bsdconf_put(), but I recommend consulting opt->result & BSDCONF_VALUE_CHANGED, regardless.

This is mostly a matter of personal preference, so it may be entirely subjective. I just think it would improve the user experience, but I'm happy with whatever you decide.

1775–1776

You've excluded check here, but do_checks() touches no file, so it is read-only like pure reads, right? So it's possibly worth dropping it from the condition here so checks get the same Capsicum path.

kfv: skip no-op put commits; fix verbatim_ok and find_target -f attach

  • bsdconf_put: defer mkstemp/rename until a real edit (CHECK and equal-value SET leave mtime/hardlinks alone; unprivileged equal-value SET no longer needs dir write)
  • bsdconf_verbatim_ok: re-examine the byte after an even-length backslash run (a\\#b was accepted then truncated on re-parse); document BSDCONF_PUT_UNQUOTED as literal file text
  • find_target: attached -fPATH/-Rdir are self-contained (sysconf -f/boot/loader.conf loader rc no longer misidentifies rc)
  • empty-value trim: t > value guard; -c joins the Capsicum path; echo only when VALUE_CHANGED
  • make += vs list-edit left as-is (matches each consumer; already documented in sysconf.8)