Files
Devin Teske 3fe5961a0b Add sysconf(8) and libbsdconf(3)
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 <[email protected]>
Reviewed by:	fuz, kfv
Differential Revision:	https://reviews.freebsd.org/D58066
2026-09-16 14:28:14 -07:00

265 lines
7.1 KiB
C

/*
* Copyright (c) 2013-2026 Devin Teske <[email protected]>
* Copyright (c) 2021-2026 Faraz Vahedi <[email protected]>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
/*
* Apply or check assignments against conf files (bsdconf_put path).
*/
#include <ctype.h>
#include "sysconf_priv.h"
/*
* Process `-c' from the results of a completed scan_pass() without touching
* any file: a write request differs when the authoritative value does not
* match, a make(1) `+=value' request differs when no identical `+=value'
* line is already present, and a removal request differs when the
* directive is still present. Returns EXIT_SUCCESS when no changes would
* be required.
*/
int
do_checks(void)
{
int differs;
int rv = EXIT_SUCCESS;
unsigned int n;
for (n = 0; n < nreqs; n++) {
if (reqs[n].newvalue == NULL && !reqs[n].remove)
continue;
if (reqs[n].remove)
differs = reqs[n].found;
else if (reqs[n].op == BSDCONF_OP_APPEND)
differs = !reqs[n].append_present;
else
differs = !reqs[n].found ||
strcmp(reqs[n].value, reqs[n].newvalue) != 0;
if (differs) {
if (!quiet)
warnx("%s: value differs", reqs[n].name);
rv = EXIT_FAILURE;
}
}
return (rv);
}
/*
* Process write and remove requests using the srcidx/infile maps recorded
* by scan_pass(). A directive is rewritten in the file that authoritatively
* defines it (the last file in sourcing order that lists it) so the change
* survives a reboot; a directive defined nowhere is appended to the
* format's default file. Removals are applied to every file listing the
* directive, lest an earlier definition be unmasked. Each modified file is
* replaced in one atomic bsdconf_put() transaction. Returns EXIT_SUCCESS
* or EXIT_FAILURE.
*/
int
do_writes(void)
{
int fd;
int rv = EXIT_SUCCESS;
size_t f;
unsigned int n;
unsigned int nopts;
struct bsdconf_option *opt;
struct bsdconf_option *options;
struct request **owner;
if ((options = calloc(nreqs + 1, sizeof(*options))) == NULL ||
(owner = calloc(nreqs, sizeof(*owner))) == NULL)
err(EXIT_FAILURE, NULL);
for (f = 0; f < nconf_files; f++) {
/* Collect the requests this file is responsible for */
nopts = 0;
for (n = 0; n < nreqs; n++) {
size_t target;
if (reqs[n].remove) {
if (!reqs[n].infile[f])
continue;
} else if (reqs[n].newvalue != NULL) {
/*
* A directive whose only definition is
* the read-only defaults file is written
* like one defined nowhere: appended to
* the format's default write target.
*/
if (reqs[n].srcidx < 0 ||
reqs[n].srcidx == defaults_idx)
target = write_idx;
else
target = (size_t)reqs[n].srcidx;
if (target != f)
continue;
/*
* An identical make(1) `+=value' line is
* already present: nothing to write, but
* still report the effective value unless
* -q (see report loop for the SET case).
*/
if (reqs[n].op == BSDCONF_OP_APPEND &&
reqs[n].append_present) {
if (!quiet)
print_write_echo(conf_files[f],
0, reqs[n].name,
BSDCONF_OP_APPEND,
reqs[n].value,
reqs[n].value, 0, 0, 0);
continue;
}
/*
* -s only ensures presence: if the
* WITH_/WITHOUT_ knob is already defined
* (any value), leave the file alone.
* An explicit name= (including empty)
* always writes the requested value.
*/
if (set_knobs && make_knob_p(reqs[n].name) &&
reqs[n].found) {
if (!quiet)
print_write_echo(conf_files[f],
0, reqs[n].name,
reqs[n].op,
reqs[n].value,
reqs[n].value, 0, 0,
0);
continue;
}
} else
continue;
opt = &options[nopts];
memset(opt, 0, sizeof(*opt));
opt->type = BSDCONF_TYPE_STR;
opt->directive = reqs[n].name;
opt->op = reqs[n].op;
if (reqs[n].remove) {
opt->action = BSDCONF_ACTION_REMOVE;
opt->value.str = NULL;
} else {
opt->action = BSDCONF_ACTION_SET_VALUE;
opt->value.str =
(char *)(uintptr_t)reqs[n].newvalue;
}
owner[nopts++] = &reqs[n];
}
if (nopts == 0)
continue;
memset(&options[nopts], 0, sizeof(options[nopts]));
/*
* Create the target if it does not yet exist. O_EXCL makes
* the test-and-create atomic: an existing file (or one that
* appears concurrently) is left untouched.
*/
if ((fd = open(conf_files[f], O_WRONLY | O_CREAT | O_EXCL,
0666)) >= 0)
close(fd);
else if (errno != EEXIST)
err(EXIT_FAILURE, "%s", conf_files[f]);
if (bsdconf_put(options, conf_files[f],
bsdconf_format_processing(format),
bsdconf_format_put(format)) != 0)
err(EXIT_FAILURE, "%s", conf_files[f]);
/* Report the results */
for (n = 0; n < nopts; n++) {
opt = &options[n];
if (opt->action == BSDCONF_ACTION_REMOVE) {
if ((opt->result &
BSDCONF_DIRECTIVE_REMOVED) != 0) {
owner[n]->found = 1;
/*
* Removals are quiet unless -v/-V
* (or -q which is already silent).
*/
if (!quiet &&
(verbose || verbose_trail))
print_write_echo(
conf_files[f],
opt->line,
opt->directive,
opt->op, "", "", 0, 0,
1);
}
continue;
}
if (!quiet) {
const char *old = owner[n]->value;
char *neweff = NULL;
const char *shown;
/*
* Gate the echo on whether bsdconf_put()
* actually edited the file
* (BSDCONF_VALUE_CHANGED). With -V, shape the
* echo like a read trail
* (`file:line: name=old -> name=new'). For
* make(1) `+=', show the effective value
* before and after the append rather than
* the fragment alone.
*/
if (opt->op == BSDCONF_OP_APPEND) {
neweff = make_apply(BSDCONF_OP_APPEND,
old, opt->value.str);
if (neweff == NULL)
err(EXIT_FAILURE, NULL);
shown = neweff;
} else
shown = opt->value.str;
/*
* -V shows statement shape at file:line; a
* newly added statement is `(added)' rather
* than `op -> op+value'. -v keeps effective
* old -> new for `+='.
*/
if (verbose_trail &&
(opt->result &
BSDCONF_DIRECTIVE_ADDED) != 0)
print_write_echo(conf_files[f],
opt->line, opt->directive, opt->op,
"", opt->value.str,
(opt->result &
BSDCONF_VALUE_CHANGED) != 0, 1, 0);
else if (verbose_trail)
print_write_echo(conf_files[f],
opt->line, opt->directive, opt->op,
old == NULL ? "" : old, shown,
(opt->result &
BSDCONF_VALUE_CHANGED) != 0, 0, 0);
else
print_write_echo(conf_files[f],
opt->line, opt->directive, opt->op,
old, shown,
(opt->result &
BSDCONF_VALUE_CHANGED) != 0, 0, 0);
if (neweff != NULL) {
free(owner[n]->value);
owner[n]->value = neweff;
}
}
}
}
/* A removal that matched no file at all is an unknown directive */
for (n = 0; n < nreqs; n++) {
if (!reqs[n].remove || reqs[n].found)
continue;
if (ignore_unknown)
continue;
if (!quiet)
warnx("unknown directive '%s'", reqs[n].name);
rv = EXIT_FAILURE;
}
free(options);
free(owner);
return (rv);
}