Page MenuHomeFreeBSD

D58066.id183354.diff
No OneTemporary

D58066.id183354.diff

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/contrib/mandoc/lib.in b/contrib/mandoc/lib.in
--- a/contrib/mandoc/lib.in
+++ b/contrib/mandoc/lib.in
@@ -31,6 +31,7 @@
LINE("libarm32", "ARM32 Architecture Library (libarm32, \\-larm32)")
LINE("libbe", "Boot Environment Library (libbe, \\-lbe)")
LINE("libbluetooth", "Bluetooth Library (libbluetooth, \\-lbluetooth)")
+LINE("libbsdconf", "Configuration File Library (libbsdconf, \\-lbsdconf)")
LINE("libbsdxml", "eXpat XML parser library (libbsdxml, \\-lbsdxml)")
LINE("libbsm", "Basic Security Module Library (libbsm, \\-lbsm)")
LINE("libc", "Standard C\\~Library (libc, \\-lc)")
diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist
--- a/etc/mtree/BSD.tests.dist
+++ b/etc/mtree/BSD.tests.dist
@@ -1315,6 +1315,8 @@
..
sa
..
+ sysconf
+ ..
syslogd
..
sysrc
diff --git a/lib/Makefile b/lib/Makefile
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -36,6 +36,7 @@
libarchive \
libbegemot \
libblocksruntime \
+ libbsdconf \
libbsddialog \
libbsdstat \
libbsm \
diff --git a/lib/libbsdconf/Makefile b/lib/libbsdconf/Makefile
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/Makefile
@@ -0,0 +1,30 @@
+PACKAGE= utilities
+LIB= bsdconf
+SHLIB_MAJOR= 1
+INCS= bsdconf.h
+MAN= bsdconf.3 bsdconf_format.3 bsdconf_put.3
+MLINKS= bsdconf.3 bsdconf_fparse.3 \
+ bsdconf.3 bsdconf_get_option.3 \
+ bsdconf.3 bsdconf_parse.3 \
+ bsdconf.3 bsdconf_spool.3 \
+ bsdconf.3 bsdconf_unquote.3 \
+ bsdconf_format.3 bsdconf_format_derive.3 \
+ bsdconf_format.3 bsdconf_format_files.3 \
+ bsdconf_format.3 bsdconf_format_files_free.3 \
+ bsdconf_format.3 bsdconf_format_find.3 \
+ bsdconf_format.3 bsdconf_format_guess.3 \
+ bsdconf_format.3 bsdconf_format_lookup.3 \
+ bsdconf_format.3 bsdconf_format_path.3 \
+ bsdconf_format.3 bsdconf_format_processing.3 \
+ bsdconf_format.3 bsdconf_format_put.3 \
+ bsdconf_format.3 bsdconf_format_register.3 \
+ bsdconf_put.3 bsdconf_set_option.3
+
+CFLAGS+= -I${.CURDIR}
+
+SRCS= bsdconf.c bsdconf_format.c bsdconf_format_generic.c \
+ bsdconf_format_loader.c bsdconf_format_make.c \
+ bsdconf_format_src.c bsdconf_format_sysctl.c \
+ bsdconf_put.c bsdconf_stmt.c bsdconf_string.c
+
+.include <bsd.lib.mk>
diff --git a/lib/libbsdconf/Makefile.depend b/lib/libbsdconf/Makefile.depend
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/Makefile.depend
@@ -0,0 +1,15 @@
+# Autogenerated - do NOT edit!
+
+DIRDEPS = \
+ include \
+ include/xlocale \
+ lib/${CSU_DIR} \
+ lib/libc \
+ lib/libcompiler_rt \
+
+
+.include <dirdeps.mk>
+
+.if ${DEP_RELDIR} == ${_DEP_RELDIR}
+# local dependencies - needed for -jN in clean tree
+.endif
diff --git a/lib/libbsdconf/bsdconf.h b/lib/libbsdconf/bsdconf.h
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf.h
@@ -0,0 +1,269 @@
+/*
+ * Copyright (c) 2002-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#ifndef _BSDCONF_H_
+#define _BSDCONF_H_
+
+#ifdef __FreeBSD__
+#include <sys/cdefs.h>
+#endif
+
+#include <stddef.h>
+#include <stdint.h>
+
+/*
+ * Supplant functionality missing on non-FreeBSD systems (musl libc, for
+ * example, provides no <sys/cdefs.h>). On glibc, <stdint.h> pulls in the
+ * definitions via <features.h>.
+ */
+#ifndef __BEGIN_DECLS
+#ifdef __cplusplus
+#define __BEGIN_DECLS extern "C" {
+#define __END_DECLS }
+#else
+#define __BEGIN_DECLS
+#define __END_DECLS
+#endif
+#endif
+
+/*
+ * Union for storing various types of data in a single common container.
+ *
+ * NB: When writing a value with bsdconf_put(), the caller supplies the value
+ * as a NUL-terminated string via the `str' member regardless of `type'; the
+ * type only governs how the value is rendered (see bsdconf_put() below).
+ */
+union bsdconf_value {
+ void *data; /* Opaque pointer (DATA1..DATA3) */
+ char *str; /* Pointer to NUL-terminated string */
+ char **strarray; /* Pointer to an array of strings */
+ int32_t num; /* Signed 32-bit integer value */
+ uint32_t u_num; /* Unsigned 32-bit integer value */
+ int64_t num64; /* Signed 64-bit integer value */
+ uint64_t u_num64; /* Unsigned 64-bit integer value */
+ uint32_t boolean:1; /* Boolean integer value (0 or 1) */
+};
+
+/*
+ * Option types (based on above value union)
+ */
+enum bsdconf_type {
+ BSDCONF_TYPE_NONE = 0x0000, /* directives with no value */
+ BSDCONF_TYPE_BOOL = 0x0001, /* boolean */
+ BSDCONF_TYPE_INT = 0x0002, /* signed 32-bit integer */
+ BSDCONF_TYPE_UINT = 0x0004, /* unsigned 32-bit integer */
+ BSDCONF_TYPE_STR = 0x0008, /* string pointer */
+ BSDCONF_TYPE_STRARRAY = 0x0010, /* string array pointer */
+ BSDCONF_TYPE_DATA1 = 0x0020, /* void data type-1 (open) */
+ BSDCONF_TYPE_DATA2 = 0x0040, /* void data type-2 (open) */
+ BSDCONF_TYPE_DATA3 = 0x0080, /* void data type-3 (open) */
+ BSDCONF_TYPE_INT64 = 0x0100, /* signed 64-bit integer */
+ BSDCONF_TYPE_UINT64 = 0x0200, /* unsigned 64-bit integer */
+ BSDCONF_TYPE_RESERVED = 0x0400, /* reserved */
+};
+
+/*
+ * Assignment operators. The default for a given file format is
+ * BSDCONF_OP_ASSIGN; the remainder require BSDCONF_OPERATOR_EQUALS (make(1)
+ * style configuration files such as make.conf(5)).
+ */
+enum bsdconf_op {
+ BSDCONF_OP_DEFAULT = 0, /* format's natural operator */
+ BSDCONF_OP_ASSIGN, /* `=' assign */
+ BSDCONF_OP_APPEND, /* `+=' append (make) */
+ BSDCONF_OP_COND, /* `?=' assign if undefined (make) */
+ BSDCONF_OP_EXPAND, /* `:=' assign with expansion (make) */
+ BSDCONF_OP_SHELL, /* `!=' assign shell output (make) */
+};
+
+/*
+ * Known configuration file formats (see bsdconf_format(3)). Formats numbered
+ * BSDCONF_FORMAT_USER and above are assigned by bsdconf_format_register().
+ */
+enum bsdconf_format {
+ BSDCONF_FORMAT_GENERIC = 0, /* quote values only when required */
+ BSDCONF_FORMAT_LOADER, /* loader.conf(5); always quoted */
+ BSDCONF_FORMAT_SYSCTL, /* sysctl.conf(5); quote when needed */
+ BSDCONF_FORMAT_MAKE, /* make.conf(5); `+=' et al. */
+ BSDCONF_FORMAT_SRC, /* src build triad; make(1) syntax */
+ BSDCONF_FORMAT_USER = 32, /* first registered format */
+};
+
+/*
+ * A single entry in a format's ordered list of configuration sources. Most
+ * formats are backed by more than one file (read in a deterministic order
+ * with directives in later files overriding earlier ones) and some pull
+ * additional files from drop-in directories.
+ */
+enum bsdconf_source_type {
+ BSDCONF_SOURCE_FILE = 0, /* a single file */
+ BSDCONF_SOURCE_DIR, /* each `*.conf' in a directory */
+ BSDCONF_SOURCE_MODDIR, /* `<module>.conf' in a directory */
+};
+struct bsdconf_source {
+ enum bsdconf_source_type type; /* how to interpret path */
+ const char *path; /* file or directory path */
+};
+
+/*
+ * The format descriptor: a read-only table of properties characterizing a
+ * configuration file format (no relation to file descriptors). The built-in
+ * formats (above) are described by format descriptors of this same shape; a
+ * new format is "bolted on" by registering a format descriptor of its own --
+ * typically derived from the built-in it most resembles (see
+ * bsdconf_format_derive() below) with only the differing members adjusted.
+ *
+ * A format whose backing files are themselves configuration data -- the
+ * boot loader reads only /boot/defaults/loader.conf and discovers every
+ * other file from the loader_conf_files, loader_conf_dirs, and
+ * local_loader_conf_files directives it finds along the way -- describes
+ * that machinery with the `defaults' member quintet below, and
+ * bsdconf_format_files() performs the same discovery the consumer does.
+ * The static `sources' list remains as the fallback for systems whose
+ * defaults file is missing.
+ */
+struct bsdconf_format_def {
+ const char *keyword; /* target keyword (or NULL) */
+ const char *path; /* default write path (or NULL) */
+ const struct bsdconf_source
+ *sources; /* ordered sources; NULL-path
+ terminated (or NULL if `path'
+ is the only source) */
+ const char *defaults; /* defaults file (or NULL) */
+ const char *defaults_env; /* environment variable overriding
+ the defaults file (or NULL) */
+ const char *files_directive; /* directive listing conf files */
+ const char *dirs_directive; /* directive listing drop-in dirs */
+ const char *local_directive; /* directive listing local files */
+ uint16_t processing; /* processing_options bitmask */
+ uint16_t put; /* put_options bitmask */
+};
+
+/*
+ * Options to bsdconf_parse() and bsdconf_put() for processing_options bitmask
+ */
+#define BSDCONF_BREAK_ON_EQUALS 0x0001 /* stop reading directive at `=' */
+#define BSDCONF_BREAK_ON_SEMICOLON 0x0002 /* `;' starts a new line */
+#define BSDCONF_CASE_SENSITIVE 0x0004 /* directives are case sensitive */
+#define BSDCONF_REQUIRE_EQUALS 0x0008 /* assignment directives only */
+#define BSDCONF_STRICT_EQUALS 0x0010 /* `=' must be part of directive */
+#define BSDCONF_OPERATOR_EQUALS 0x0020 /* recognize `+=' `?=' `:=' `!=' */
+
+/*
+ * Options to bsdconf_put() for put_options bitmask
+ */
+#define BSDCONF_PUT_NO_DUPLICATES 0x0001 /* error if directive found twice */
+#define BSDCONF_PUT_ALLOW_EMPTY 0x0002 /* allow SET_VALUE of empty value */
+#define BSDCONF_PUT_BACKUP 0x0004 /* back up config file (`.bak') */
+#define BSDCONF_PUT_UNQUOTED 0x0008 /* value.str as literal file text */
+#define BSDCONF_PUT_QUOTE_ALWAYS 0x0010 /* always quote values on output */
+
+/*
+ * Per-directive actions for bsdconf_put()
+ */
+#define BSDCONF_ACTION_SET_VALUE 0x0000 /* set/replace value (default) */
+#define BSDCONF_ACTION_CHECK 0x0001 /* compare against current value */
+#define BSDCONF_ACTION_REMOVE 0x0002 /* remove directive from config */
+
+/*
+ * Per-directive result codes set by bsdconf_put()
+ */
+#define BSDCONF_DIRECTIVE_FOUND 0x0001 /* vs not found (see added) */
+#define BSDCONF_VALUE_CHANGED 0x0002 /* vs no change required */
+#define BSDCONF_DIRECTIVE_ADDED 0x0004 /* vs already existed */
+#define BSDCONF_DIRECTIVE_REMOVED 0x0008 /* vs not found */
+
+/*
+ * Anatomy of a config file option; used for both reading and writing.
+ *
+ * When parsing with bsdconf_parse(), `directive' is an fnmatch(3) pattern and
+ * `parse' is invoked for each matching statement (with `op' set to the
+ * statement's assignment operator beforehand).
+ *
+ * When writing with bsdconf_put(), `directive' is an exact token (a pattern
+ * is not a writable target), `action' selects the operation, and `result'
+ * and `line' report what was done. A non-zero `match_line' restricts the
+ * put to the statement on that physical line (0 matches any, as before);
+ * make(1) `+=' SET_VALUE still appends a new line when `match_line' is 0,
+ * but rewrites the matched statement when `match_line' selects it.
+ */
+struct bsdconf_option {
+ enum bsdconf_type type; /* Option value type */
+ const char *directive; /* config file keyword */
+ union bsdconf_value value; /* NB: set by parse action;
+ * value to write for put */
+ enum bsdconf_op op; /* assignment operator */
+ uint8_t action; /* bsdconf_put() action */
+ uint16_t result; /* NB: set by bsdconf_put() */
+ uint32_t line; /* NB: set by bsdconf_put() */
+ uint32_t match_line; /* put: 0=any, else only line */
+
+ /*
+ * Function pointer; action to be taken when the directive is found
+ * by bsdconf_parse(). Non-zero return aborts the parse (and is
+ * propagated to the bsdconf_parse() caller).
+ */
+ int (*parse)(struct bsdconf_option *option, uint32_t line,
+ char *directive, char *value);
+};
+extern struct bsdconf_option bsdconf_dummy_option;
+
+/*
+ * Function prototypes
+ *
+ * All functions returning int return zero on success (except
+ * bsdconf_spool(), which returns a new file descriptor); otherwise -1 (or
+ * the non-zero result of a parse call-back) and errno should be consulted.
+ */
+__BEGIN_DECLS
+int bsdconf_parse(struct bsdconf_option _options[],
+ const char *_path,
+ int (*_unknown)(struct bsdconf_option *_option,
+ uint32_t _line, char *_directive, char *_value),
+ uint16_t _processing_options);
+int bsdconf_fparse(struct bsdconf_option _options[],
+ int _fd,
+ int (*_unknown)(struct bsdconf_option *_option,
+ uint32_t _line, char *_directive, char *_value),
+ uint16_t _processing_options);
+int bsdconf_spool(int _fd);
+struct bsdconf_option *bsdconf_get_option(struct bsdconf_option _options[],
+ const char *_directive);
+char *bsdconf_unquote(char *_value);
+int bsdconf_set_option(struct bsdconf_option _options[],
+ const char *_directive,
+ union bsdconf_value *_value);
+int bsdconf_put(struct bsdconf_option _options[],
+ const char *_path, uint16_t _processing_options,
+ uint16_t _put_options);
+
+/*
+ * Format abstraction layer (see bsdconf_format(3))
+ */
+int bsdconf_format_derive(enum bsdconf_format _base,
+ struct bsdconf_format_def *_def);
+int bsdconf_format_files(enum bsdconf_format _format,
+ const char *_rootdir, const char *_module,
+ const char *_defaults, char ***_filesp,
+ size_t *_nfilesp, size_t *_write_idxp);
+void bsdconf_format_files_free(char **_files,
+ size_t _nfiles);
+int bsdconf_format_find(const char *_keyword,
+ enum bsdconf_format *_format);
+enum bsdconf_format bsdconf_format_guess(const char *_path);
+const struct bsdconf_format_def
+ *bsdconf_format_lookup(enum bsdconf_format _format);
+const char *bsdconf_format_path(enum bsdconf_format _format);
+uint16_t bsdconf_format_processing(
+ enum bsdconf_format _format);
+uint16_t bsdconf_format_put(enum bsdconf_format _format);
+int bsdconf_format_register(
+ const struct bsdconf_format_def *_def,
+ enum bsdconf_format *_format);
+__END_DECLS
+
+#endif /* !_BSDCONF_H_ */
diff --git a/lib/libbsdconf/bsdconf.3 b/lib/libbsdconf/bsdconf.3
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf.3
@@ -0,0 +1,429 @@
+.\" Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+.\" Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.Dd August 2, 2026
+.Dt BSDCONF 3
+.Os
+.Sh NAME
+.Nm bsdconf ,
+.Nm bsdconf_parse ,
+.Nm bsdconf_fparse ,
+.Nm bsdconf_get_option ,
+.Nm bsdconf_spool ,
+.Nm bsdconf_unquote
+.Nd configuration file reading library
+.Sh LIBRARY
+.Lb libbsdconf
+.Sh SYNOPSIS
+.In bsdconf.h
+.Ft int
+.Fo bsdconf_parse
+.Fa "struct bsdconf_option options[]"
+.Fa "const char *path"
+.Fa "int \*[lp]*unknown\*[rp]\*[lp]struct bsdconf_option *option"
+.Fa "uint32_t line"
+.Fa "char *directive"
+.Fa "char *value\*[rp]"
+.Fa "uint16_t processing_options"
+.Fc
+.Ft int
+.Fo bsdconf_fparse
+.Fa "struct bsdconf_option options[]"
+.Fa "int fd"
+.Fa "int \*[lp]*unknown\*[rp]\*[lp]struct bsdconf_option *option"
+.Fa "uint32_t line"
+.Fa "char *directive"
+.Fa "char *value\*[rp]"
+.Fa "uint16_t processing_options"
+.Fc
+.Ft "struct bsdconf_option *"
+.Fo bsdconf_get_option
+.Fa "struct bsdconf_option options[]"
+.Fa "const char *directive"
+.Fc
+.Ft int
+.Fo bsdconf_spool
+.Fa "int fd"
+.Fc
+.Ft "char *"
+.Fo bsdconf_unquote
+.Fa "char *value"
+.Fc
+.Sh DESCRIPTION
+The
+.Nm
+library provides a light-weight,
+portable framework for reading and writing configuration
+files.
+It is the successor to the retired
+.Nm figpar
+library,
+extending the original read-only token parser into a unified,
+resilient reader/writer engine.
+.Pp
+Due to the fact that configuration files may have basic syntax differences,
+the library does not attempt to impose any structure on the data but instead
+provides raw data to a set of callback functions.
+These callback functions can in-turn initiate abort through their return
+value,
+allowing custom syntax validation during parsing.
+.Pp
+Syntax differences between well-known file formats are described by format
+descriptors:
+a format descriptor is a small read-only table of properties
+.Pq Vt struct bsdconf_format_def ; see Xr bsdconf_format 3
+naming a format's target keyword,
+backing files,
+and tokenizing/quoting rules,
+which parameterize a single shared engine.
+.Pp
+Despite the name, it bears no relation to a file descriptor;
+it is closer in spirit to a driver's method table:
+static data describing behavior,
+consulted rather than executed.
+Writing is documented in
+.Xr bsdconf_put 3 ;
+built-in formats and discovery in
+.Xr bsdconf_format 3 .
+.Pp
+Configuration directives,
+types,
+and callback functions are provided through data structures defined in
+.In bsdconf.h :
+.Bd -literal -offset indent
+struct bsdconf_option {
+ enum bsdconf_type type; /* value type */
+ const char *directive; /* keyword */
+ union bsdconf_value value; /* value */
+ enum bsdconf_op op; /* assignment operator */
+ uint8_t action; /* bsdconf_put() action */
+ uint16_t result; /* set by bsdconf_put() */
+ uint32_t line; /* set by bsdconf_put() */
+ uint32_t match_line;
+ /* put: 0=any, else line */
+
+ /* Pointer to function used when directive is found */
+ int (*parse)(struct bsdconf_option *option, uint32_t line,
+ char *directive, char *value);
+};
+
+enum bsdconf_type {
+ BSDCONF_TYPE_NONE = 0x0000, /* directives with no value */
+ BSDCONF_TYPE_BOOL = 0x0001, /* boolean */
+ BSDCONF_TYPE_INT = 0x0002, /* signed 32-bit integer */
+ BSDCONF_TYPE_UINT = 0x0004, /* unsigned 32-bit integer */
+ BSDCONF_TYPE_STR = 0x0008, /* string pointer */
+ BSDCONF_TYPE_STRARRAY = 0x0010, /* string array pointer */
+ BSDCONF_TYPE_DATA1 = 0x0020, /* void data type-1 (open) */
+ BSDCONF_TYPE_DATA2 = 0x0040, /* void data type-2 (open) */
+ BSDCONF_TYPE_DATA3 = 0x0080, /* void data type-3 (open) */
+ BSDCONF_TYPE_INT64 = 0x0100, /* signed 64-bit integer */
+ BSDCONF_TYPE_UINT64 = 0x0200, /* unsigned 64-bit integer */
+ BSDCONF_TYPE_RESERVED = 0x0400, /* reserved */
+};
+
+union bsdconf_value {
+ void *data; /* Opaque pointer (DATA1..DATA3) */
+ char *str; /* Pointer to NUL-terminated string */
+ char **strarray; /* Pointer to an array of strings */
+ int32_t num; /* Signed 32-bit integer value */
+ uint32_t u_num; /* Unsigned 32-bit integer value */
+ int64_t num64; /* Signed 64-bit integer value */
+ uint64_t u_num64; /* Unsigned 64-bit integer value */
+ uint32_t boolean:1; /* Boolean integer value (0 or 1) */
+};
+.Ed
+.Pp
+The
+.Fa processing_options
+argument to
+.Fn bsdconf_parse ,
+.Fn bsdconf_fparse ,
+and
+.Xr bsdconf_put 3
+is a mask of bit fields which indicate various processing options.
+The possible flags are:
+.Bl -tag -width BSDCONF_BREAK_ON_SEMICOLON
+.It Dv BSDCONF_BREAK_ON_EQUALS
+An equals sign
+.Pq Ql =
+is normally considered part of the directive.
+This flag enables terminating the directive at the equals sign.
+Also makes equals sign optional and transient.
+.It Dv BSDCONF_BREAK_ON_SEMICOLON
+A semicolon
+.Pq Ql \&;
+is normally considered part of the value.
+This flag enables terminating the value at the semicolon.
+Also allows multiple statements on a single line separated by semicolon.
+.It Dv BSDCONF_CASE_SENSITIVE
+Normally directives are matched case insensitively using
+.Xr fnmatch 3 .
+This flag enables directive matching to be case sensitive.
+.It Dv BSDCONF_REQUIRE_EQUALS
+If a directive is not followed by an equals,
+processing is aborted.
+.It Dv BSDCONF_STRICT_EQUALS
+Equals must be part of the directive
+.Pq no whitespace before or after
+to be considered a delimiter between directive and value.
+Required by file formats whose readers reject whitespace around the equals
+sign,
+such as the
+.Fx
+boot loader's processing of
+.Xr loader.conf 5 .
+.It Dv BSDCONF_OPERATOR_EQUALS
+Recognize
+.Xr make 1
+style assignment modifiers
+.Po
+.Ql += ,
+.Ql ?= ,
+.Ql := ,
+and
+.Ql !=
+.Pc
+and split them off the tail of the directive.
+The parsed operator is reported through the
+.Va op
+member of the matched option
+.Pq one of Dv BSDCONF_OP_ASSIGN , BSDCONF_OP_APPEND , BSDCONF_OP_COND , BSDCONF_OP_EXPAND , No or Dv BSDCONF_OP_SHELL .
+.El
+.Pp
+The
+.Fa options
+struct array pointer can be NULL and every directive will run the
+.Fn unknown
+function argument.
+.Pp
+The directive for each bsdconf_option item in the
+.Fn bsdconf_parse
+options argument is matched against each parsed directive using
+.Xr fnmatch 3
+until a match is found.
+If a match is found,
+the
+.Fn parse
+function for that bsdconf_option directive is run with the line number,
+directive,
+and value.
+Otherwise if no match,
+the
+.Fn unknown
+function is run
+.Pq with the same arguments .
+When
+.Dv BSDCONF_OPERATOR_EQUALS
+is set,
+.Fn unknown
+receives a non-NULL
+.Fa option
+whose
+.Va op
+member holds the statement's assignment operator
+.Pq there is no matched options-array slot to hang it on ;
+otherwise
+.Fa option
+may be
+.Dv NULL .
+.Pp
+If either
+.Fn parse
+or
+.Fn unknown
+return non-zero,
+.Fn bsdconf_parse
+aborts reading the file and returns the error value to its caller.
+.Pp
+A value normally ends at the first unescaped newline,
+but a statement may span multiple lines:
+a backslash immediately preceding the newline continues the value on the
+next line,
+in the manner of
+.Xr make 1
+.Pq essential to Pa make.conf and its siblings .
+The backslash-newline pairs are removed from the value delivered to the
+callbacks
+.Pq surrounding whitespace is preserved verbatim ,
+and reported line numbers are those of each statement's first line.
+.Xr bsdconf_put 3
+recognizes the same continuations when locating a value;
+rewriting a continued value replaces all of its lines with the single new
+value.
+.Pp
+.Fn bsdconf_fparse
+is identical to
+.Fn bsdconf_parse
+except that it operates on an already-open file descriptor
+.Fa fd ,
+which remains open on return
+.Pq the caller retains ownership .
+This allows the caller to constrain the process
+.Pq for example with Xr capsicum 4
+before parsing begins.
+The scanner requires a seekable descriptor;
+input that cannot seek
+.Pq a pipe or socket, standard input included
+is detected up front and transparently spooled through
+.Fn bsdconf_spool ,
+at the cost of one transient copy of the data.
+.Pp
+.Fn bsdconf_spool
+copies the remaining contents of
+.Fa fd
+to an unlinked temporary file
+.Pq created with Xr tmpfile 3
+and returns a seekable descriptor referencing it,
+which the caller must
+.Xr close 2
+.Pq the backing storage is reclaimed then .
+It is exported for callers that must adapt non-seekable input themselves
+before revoking their own ability to create files,
+as
+.Xr sysconf 8
+does before entering its
+.Xr capsicum 4
+sandbox.
+.Pp
+.Fn bsdconf_get_option
+traverses the options-array and returns the option that matches via
+.Xr strcmp 3 ,
+or if no match a pointer to a static dummy struct is returned
+.Pq whose values are all zero or NULL .
+.Pp
+.Fn bsdconf_unquote
+strips one layer of surrounding double-quotes from
+.Fa value
+in place and returns it.
+Parsed values retain their quotes so that data round-trips losslessly;
+this helper is for display and comparison purposes.
+.Sh RETURN VALUES
+.Fn bsdconf_parse
+and
+.Fn bsdconf_fparse
+return zero on success;
+otherwise -1
+.Pq or the non-zero result of a callback
+is returned and the global variable
+.Va errno
+is set to indicate the error.
+.Fn bsdconf_spool
+returns a new seekable file descriptor on success;
+otherwise -1 with
+.Va errno
+set to indicate the error.
+.Fn bsdconf_get_option
+returns a pointer to the matching option,
+or to a static dummy struct when none matches.
+.Sh EXAMPLES
+Read two known directives from a
+.Ql name=value
+file,
+routing every statement through a callback:
+.Bd -literal -offset indent
+#include <err.h>
+#include <stdio.h>
+#include <bsdconf.h>
+
+static int
+show(struct bsdconf_option *option, uint32_t line,
+ char *directive, char *value)
+{
+ printf("%u: %s is %s\en", line, directive,
+ bsdconf_unquote(value));
+ return (0);
+}
+
+static struct bsdconf_option options[] = {
+ { .directive = "hostname", .parse = show },
+ { .directive = "timeout", .parse = show },
+ { .directive = NULL }
+};
+
+int
+main(void)
+{
+ if (bsdconf_parse(options, "/usr/local/etc/myapp.conf",
+ NULL, BSDCONF_BREAK_ON_EQUALS) != 0)
+ err(1, "myapp.conf");
+ return (0);
+}
+.Ed
+.Pp
+Callbacks own the semantics,
+so a directive that legitimately repeats is accumulated rather than
+overwritten;
+no format descriptor is involved.
+The file here is Apache-style
+.Pq space-separated, no equals sign ,
+so
+.Dv BSDCONF_BREAK_ON_EQUALS
+is simply omitted:
+.Bd -literal -offset indent
+static char *servers[16];
+static size_t nservers;
+
+static int
+addserver(struct bsdconf_option *option, uint32_t line,
+ char *directive, char *value)
+{
+ if (nservers >= 16 ||
+ (servers[nservers] = strdup(value)) == NULL)
+ return (-1); /* abort the parse */
+ nservers++;
+ return (0);
+}
+
+static struct bsdconf_option cumulative[] = {
+ { .directive = "server", .parse = addserver },
+ { .directive = NULL }
+};
+
+ ...
+ if (bsdconf_parse(cumulative, path, NULL, 0) != 0)
+ err(1, "%s", path);
+.Ed
+.Sh SEE ALSO
+.Xr bsdconf_format 3 ,
+.Xr bsdconf_put 3 ,
+.Xr loader.conf 5 ,
+.Xr sysctl.conf 5 ,
+.Xr sysconf 8
+.Sh HISTORY
+The
+.Nm
+library first appeared in
+.Fx 16.0 .
+It supersedes the
+.Nm figpar
+library which first appeared in
+.Fx 10.2
+and was retired to the ports tree.
+.Sh AUTHORS
+.An Devin Teske Aq Mt dteske@FreeBSD.org
+.An Faraz Vahedi Aq Mt kfv@kfv.io
+.Sh BUGS
+This is the first implementation of the library,
+and the interface may be subject to refinement.
+.Pp
+Write-path limitations for cumulative directives are discussed in the
+.Sx LIMITATIONS
+section of
+.Xr bsdconf_put 3 .
+.Sh SECURITY CONSIDERATIONS
+Parsing allocates buffers sized by the longest directive and value
+encountered rather than by untrusted length fields,
+and
+.Fn bsdconf_fparse
+accepts an already-open descriptor precisely so that a caller may
+sandbox itself
+.Pq for example with Xr capsicum 4
+before touching untrusted input,
+as
+.Xr sysconf 8
+does for its read-only operations.
+Write-path hardening is documented in
+.Xr bsdconf_put 3 .
diff --git a/lib/libbsdconf/bsdconf.c b/lib/libbsdconf/bsdconf.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf.c
@@ -0,0 +1,701 @@
+/*
+ * Copyright (c) 2002-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <fnmatch.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "bsdconf.h"
+#include "bsdconf_internal.h"
+
+struct bsdconf_option bsdconf_dummy_option;
+
+/*
+ * Search for a config option (struct bsdconf_option) in the array of config
+ * options, returning the struct whose directive matches the given parameter.
+ * If no match is found, a pointer to the static dummy option (above) is
+ * returned.
+ *
+ * This is to eliminate dependency on the index position of an item in the
+ * array, since the index position is more apt to be changed as code grows.
+ */
+struct bsdconf_option *
+bsdconf_get_option(struct bsdconf_option options[], const char *directive)
+{
+ uint32_t n;
+
+ /* Check arguments */
+ if (options == NULL || directive == NULL)
+ return (&bsdconf_dummy_option);
+
+ /* Loop through the array, return the first match */
+ for (n = 0; options[n].directive != NULL; n++)
+ if (strcmp(options[n].directive, directive) == 0)
+ return (&(options[n]));
+
+ /* Re-initialize the dummy option in case it was written to */
+ memset(&bsdconf_dummy_option, 0, sizeof(bsdconf_dummy_option));
+
+ return (&bsdconf_dummy_option);
+}
+
+/*
+ * Strip one layer of surrounding double-quotes from `value' in place (the
+ * parser preserves quotes so that values round-trip losslessly; see
+ * bsdconf_fparse() below). Returns `value' for convenience. If the value is
+ * not a quoted string, it is returned unmodified.
+ */
+char *
+bsdconf_unquote(char *value)
+{
+ size_t len;
+
+ if (value == NULL || (len = strlen(value)) < 2)
+ return (value);
+ if (value[0] != '"' || value[len - 1] != '"')
+ return (value);
+
+ memmove(value, value + 1, len - 2);
+ value[len - 2] = '\0';
+
+ return (value);
+}
+
+/*
+ * Copy the remaining contents of the open file descriptor `fd' to an
+ * unlinked temporary file and return a seekable descriptor referencing it
+ * (which the caller must close(2); the backing storage is reclaimed then).
+ * This adapts input that cannot seek -- a pipe or socket, standard input
+ * included -- for the scanner in bsdconf_fparse() below, which seeks
+ * liberally. Returns the new descriptor on success; otherwise returns -1
+ * and errno should be consulted.
+ */
+int
+bsdconf_spool(int fd)
+{
+ FILE *tmp;
+ int error;
+ int newfd;
+ int tfd;
+ ssize_t r;
+ char buf[8192];
+
+ if ((tmp = tmpfile()) == NULL)
+ return (-1);
+ tfd = fileno(tmp);
+
+ for (;;) {
+ r = read(fd, buf, sizeof(buf));
+ if (r < 0) {
+ if (errno == EINTR)
+ continue;
+ goto fail;
+ }
+ if (r == 0)
+ break;
+ if (bsdconf_writeall(tfd, buf, (size_t)r) != 0)
+ goto fail;
+ }
+ if (lseek(tfd, 0, SEEK_SET) == -1)
+ goto fail;
+
+ /* Detach the descriptor from the stream before closing it */
+ if ((newfd = dup(tfd)) == -1)
+ goto fail;
+ fclose(tmp);
+ return (newfd);
+
+fail:
+ error = errno; /* preserve errno across fclose(3) */
+ fclose(tmp);
+ errno = error;
+ return (-1);
+}
+
+/*
+ * Read one byte into `*p', restarting on EINTR. Returns 1 on success, 0 on
+ * EOF, or -1 on error (with errno set). Callers must treat a negative return
+ * as failure: a loop conditioned only on `r != 0' spins forever on error
+ * because read(2) returns -1, and a length counter in such a loop can grow
+ * without bound (see the directive scan in bsdconf_fparse() below).
+ */
+static ssize_t
+bsdconf_read1(int fd, char *p)
+{
+ ssize_t r;
+
+ do {
+ r = read(fd, p, 1);
+ } while (r < 0 && errno == EINTR);
+ return (r);
+}
+
+/*
+ * Read exactly `n' bytes into `buf', restarting on EINTR. Returns 0 on
+ * success, or -1 on error / premature EOF (with errno set; EIO for a short
+ * read after the caller measured a length on a seekable descriptor).
+ */
+static int
+bsdconf_readn(int fd, void *buf, size_t n)
+{
+ char *p = buf;
+ size_t off = 0;
+ ssize_t r;
+
+ while (off < n) {
+ r = read(fd, p + off, n - off);
+ if (r < 0) {
+ if (errno == EINTR)
+ continue;
+ return (-1);
+ }
+ if (r == 0) {
+ errno = EIO;
+ return (-1);
+ }
+ off += (size_t)r;
+ }
+ return (0);
+}
+
+/*
+ * Parse the configuration data on the open file descriptor `fd' and execute
+ * the `parse' call-back functions for any directives defined by the array of
+ * config options (first argument).
+ *
+ * For unknown directives that are encountered, you can optionally pass a
+ * call-back function for the third argument to be called for unknowns.
+ *
+ * The scanner requires a seekable descriptor; input that cannot seek (a
+ * pipe or socket, standard input included) is detected up front and spooled
+ * through bsdconf_spool() above, parsed from the temporary, and costs one
+ * transient copy of the data. The descriptor is left positioned at
+ * end-of-file (non-seekable input is left drained) and remains open (the
+ * caller retains ownership).
+ *
+ * Returns zero on success; otherwise returns -1 (or the non-zero result of a
+ * call-back) and errno should be consulted.
+ */
+int
+bsdconf_fparse(struct bsdconf_option options[], int fd,
+ int (*unknown)(struct bsdconf_option *option, uint32_t line,
+ char *directive, char *value), uint16_t processing_options)
+{
+ uint8_t bequals;
+ uint8_t bsemicolon;
+ uint8_t case_sensitive;
+ uint8_t comment = 0;
+ uint8_t ecomment;
+ uint8_t end;
+ uint8_t found;
+ uint8_t have_equals = 0;
+ uint8_t operator_equals;
+ uint8_t quote;
+ uint8_t require_equals;
+ uint8_t strict_equals;
+ char p[2];
+ char *directive = NULL;
+ char *t;
+ char *value = NULL;
+ enum bsdconf_op op;
+ int error;
+ int rv = 0;
+ int spoolfd = -1;
+ ssize_t r = 1;
+ uint32_t dline;
+ uint32_t dsize = 0;
+ uint32_t line = 1;
+ uint32_t n;
+ uint32_t vsize = 0;
+ uint32_t x;
+ off_t charpos;
+ off_t curpos;
+
+ /* Sanity check: if no options and no unknown function, return */
+ if (options == NULL && unknown == NULL) {
+ errno = EINVAL;
+ return (-1);
+ }
+
+ /* Spool input that cannot seek (see bsdconf_spool() above) */
+ if (lseek(fd, 0, SEEK_CUR) == -1) {
+ if (errno != ESPIPE)
+ return (-1);
+ if ((spoolfd = bsdconf_spool(fd)) == -1)
+ return (-1);
+ fd = spoolfd;
+ }
+
+ /* Processing options */
+ bequals = (processing_options & BSDCONF_BREAK_ON_EQUALS) == 0 ? 0 : 1;
+ bsemicolon =
+ (processing_options & BSDCONF_BREAK_ON_SEMICOLON) == 0 ? 0 : 1;
+ case_sensitive =
+ (processing_options & BSDCONF_CASE_SENSITIVE) == 0 ? 0 : 1;
+ operator_equals =
+ (processing_options & BSDCONF_OPERATOR_EQUALS) == 0 ? 0 : 1;
+ require_equals =
+ (processing_options & BSDCONF_REQUIRE_EQUALS) == 0 ? 0 : 1;
+ strict_equals =
+ (processing_options & BSDCONF_STRICT_EQUALS) == 0 ? 0 : 1;
+
+ /* Read the file until EOF */
+ while (r > 0) {
+ r = bsdconf_read1(fd, p);
+ if (r < 0)
+ goto fail;
+
+ /* Skip to the beginning of a directive */
+ while (r > 0 && (isspace((unsigned char)*p) || *p == '#' ||
+ comment || (bsemicolon && *p == ';'))) {
+ if (*p == '#')
+ comment = 1;
+ else if (*p == '\n') {
+ comment = 0;
+ line++;
+ }
+ r = bsdconf_read1(fd, p);
+ if (r < 0)
+ goto fail;
+ }
+ /* Test for EOF; if EOF then no directive was found */
+ if (r == 0)
+ goto cleanup;
+
+ /* Record the line number the directive appears on */
+ dline = line;
+
+ /* Get the current offset */
+ if ((curpos = lseek(fd, 0, SEEK_CUR)) == -1)
+ goto fail;
+ curpos--;
+
+ /* Find the length of the directive */
+ for (n = 0; r > 0; n++) {
+ if (isspace((unsigned char)*p))
+ break;
+ if (bequals && *p == '=') {
+ have_equals = 1;
+ break;
+ }
+ if (bsemicolon && *p == ';')
+ break;
+ r = bsdconf_read1(fd, p);
+ if (r < 0)
+ goto fail;
+ }
+
+ /* Test for EOF, if EOF then no directive was found */
+ if (n == 0 && r == 0)
+ goto cleanup;
+
+ /* Go back to the beginning of the directive */
+ if (lseek(fd, curpos, SEEK_SET) == -1)
+ goto fail;
+
+ /*
+ * Allocate and read the directive into memory. The buffer
+ * must be grown on the first pass (directive == NULL) even
+ * when the name is empty (a line beginning with `='), lest
+ * the string terminator below store through a NULL pointer.
+ */
+ if (directive == NULL || n > dsize) {
+ if ((t = realloc(directive, n + 1)) == NULL)
+ goto fail;
+ directive = t;
+ dsize = n;
+ }
+ if (bsdconf_readn(fd, directive, n) != 0)
+ goto fail;
+
+ /* Advance beyond the equals sign if appropriate/desired */
+ if (bequals && *p == '=') {
+ if (lseek(fd, 1, SEEK_CUR) != -1) {
+ r = bsdconf_read1(fd, p);
+ if (r < 0)
+ goto fail;
+ }
+ if (strict_equals && isspace((unsigned char)*p))
+ *p = '\n';
+ }
+
+ /* Terminate the string */
+ directive[n] = '\0';
+
+ /*
+ * Split a make(1)-style operator (`+=' `?=' `:=' `!=') off
+ * the tail of the directive if requested. The operator
+ * character rode along with the directive because only the
+ * `=' terminates the directive scan (above).
+ */
+ op = have_equals ? BSDCONF_OP_ASSIGN : BSDCONF_OP_DEFAULT;
+ if (operator_equals && have_equals && n > 1) {
+ switch (directive[n - 1]) {
+ case '+': op = BSDCONF_OP_APPEND; break;
+ case '?': op = BSDCONF_OP_COND; break;
+ case ':': op = BSDCONF_OP_EXPAND; break;
+ case '!': op = BSDCONF_OP_SHELL; break;
+ }
+ if (op != BSDCONF_OP_ASSIGN)
+ directive[--n] = '\0';
+ }
+
+ /* Convert directive to lower case before comparison */
+ if (!case_sensitive)
+ bsdconf_strtolower(directive);
+
+ /* Move to what may be the start of the value */
+ if (!(bsemicolon && *p == ';') &&
+ !(strict_equals && *p == '=')) {
+ while (r > 0 && isspace((unsigned char)*p) &&
+ *p != '\n') {
+ r = bsdconf_read1(fd, p);
+ if (r < 0)
+ goto fail;
+ }
+ }
+
+ /* An equals sign may have stopped us, should we eat it? */
+ if (r > 0 && bequals && *p == '=' && !strict_equals) {
+ have_equals = 1;
+ r = bsdconf_read1(fd, p);
+ if (r < 0)
+ goto fail;
+ while (r > 0 && isspace((unsigned char)*p) &&
+ *p != '\n') {
+ r = bsdconf_read1(fd, p);
+ if (r < 0)
+ goto fail;
+ }
+ }
+
+ /* If no value, allocate a dummy value and jump to action */
+ if (r == 0 || *p == '\n' || *p == '#' ||
+ (bsemicolon && *p == ';')) {
+ /* Count the consumed terminator if a newline */
+ if (r > 0 && *p == '\n')
+ line++;
+ /* Flag a trailing comment so it is skipped */
+ if (r > 0 && *p == '#')
+ comment = 1;
+ /* Initialize the value if not already done */
+ if (value == NULL && (value = malloc(1)) == NULL)
+ goto fail;
+ value[0] = '\0';
+ goto call_function;
+ }
+
+ /* Get the current offset */
+ if ((curpos = lseek(fd, 0, SEEK_CUR)) == -1)
+ goto fail;
+ curpos--;
+
+ /* Find the end of the value */
+ quote = 0;
+ ecomment = 0;
+ end = 0;
+ while (r > 0 && end == 0) {
+ /* Advance to the next character if we know we can */
+ if (*p != '\"' && *p != '#' && *p != '\n' &&
+ (!bsemicolon || *p != ';')) {
+ r = bsdconf_read1(fd, p);
+ if (r < 0)
+ goto fail;
+ continue;
+ }
+
+ /*
+ * If we get this far, we've hit an end-key
+ */
+
+ /* Get the current offset */
+ if ((charpos = lseek(fd, 0, SEEK_CUR)) == -1)
+ goto fail;
+ charpos--;
+
+ /*
+ * Go back so we can read the character before the key
+ * to check if the character is escaped (which means
+ * we should continue).
+ */
+ if (lseek(fd, -2, SEEK_CUR) == -1)
+ goto fail;
+ r = bsdconf_read1(fd, p);
+ if (r < 0)
+ goto fail;
+
+ /*
+ * Count how many backslashes there are (an odd number
+ * means the key is escaped, even means otherwise).
+ */
+ for (n = 1; r > 0 && *p == '\\'; n++) {
+ /* Move back another offset to read */
+ if (lseek(fd, -2, SEEK_CUR) == -1)
+ goto fail;
+ r = bsdconf_read1(fd, p);
+ if (r < 0)
+ goto fail;
+ }
+
+ /* Move offset back to the key and read it */
+ if (lseek(fd, charpos, SEEK_SET) == -1)
+ goto fail;
+ r = bsdconf_read1(fd, p);
+ if (r < 0)
+ goto fail;
+
+ /*
+ * If an even number of backslashes was counted
+ * meaning key is not escaped, we should evaluate what
+ * to do.
+ */
+ if ((n & 1) == 1) {
+ switch (*p) {
+ case '\"':
+ /*
+ * Flag current sequence of characters
+ * to follow as being quoted (hashes
+ * are not considered comments).
+ */
+ quote = !quote;
+ break;
+ case '#':
+ /*
+ * If we aren't in a quoted series, we
+ * just hit an inline comment and have
+ * found the end of the value. Flag
+ * the remainder of the line as a
+ * comment so it is not mistaken for
+ * a new directive.
+ */
+ if (!quote) {
+ ecomment = comment = 1;
+ end = 1;
+ }
+ break;
+ case '\n':
+ /*
+ * Newline characters must always be
+ * escaped, whether inside a quoted
+ * series or not, otherwise they
+ * terminate the value.
+ */
+ line++;
+ end = 1;
+ /* FALLTHROUGH */
+ case ';':
+ if (!quote && bsemicolon)
+ end = 1;
+ break;
+ }
+ } else if (*p == '\n')
+ /* Escaped newline character. increment */
+ line++;
+
+ /* Advance to the next character */
+ r = bsdconf_read1(fd, p);
+ if (r < 0)
+ goto fail;
+ }
+
+ /* Get the current offset */
+ if ((charpos = lseek(fd, 0, SEEK_CUR)) == -1)
+ goto fail;
+
+ /* Get the length of the value */
+ n = (uint32_t)(charpos - curpos);
+ if (r > 0) /* more to read, but don't read ending key */
+ n--;
+
+ /* Move offset back to the beginning of the value */
+ if (lseek(fd, curpos, SEEK_SET) == -1)
+ goto fail;
+
+ /* Allocate and read the value into memory */
+ if (n > vsize) {
+ if ((t = realloc(value, n + 1)) == NULL)
+ goto fail;
+ value = t;
+ vsize = n;
+ }
+ if (bsdconf_readn(fd, value, n) != 0)
+ goto fail;
+
+ /* Terminate the string */
+ value[n] = '\0';
+
+ /* Cut trailing whitespace off by termination */
+ t = value + n;
+ while (t > value && isspace((unsigned char)*--t))
+ *t = '\0';
+
+ /*
+ * Cut off a trailing inline-comment or statement-terminator
+ * key likewise. The `#' or `;' that terminated the value
+ * rides along with it (historic figpar behavior) and must
+ * not be mistaken for data. An escaped `;' is data and is
+ * never a terminator (see the end-key scan above).
+ */
+ if (ecomment && t > value && *t == '#') {
+ *t = '\0';
+ while (t > value && isspace((unsigned char)*--t))
+ *t = '\0';
+ } else if (bsemicolon && t > value && *t == ';') {
+ for (x = 0; t - x > value &&
+ *(t - x - 1) == '\\'; x++)
+ ;
+ if ((x & 1) == 0) {
+ *t = '\0';
+ while (t > value &&
+ isspace((unsigned char)*--t))
+ *t = '\0';
+ }
+ }
+
+ /* Escape the escaped quotes (see bsdconf_string.c) */
+ x = bsdconf_strcount(value, "\\\"");
+ if (x != 0 && (n + x) > vsize) {
+ if ((t = realloc(value, n + x + 1)) == NULL)
+ goto fail;
+ value = t;
+ vsize = n + x;
+ }
+ if (bsdconf_replaceall(value, vsize + 1, "\\\"", "\\\\\"") < 0)
+ goto fail;
+
+ /* Remove all escaped newline characters */
+ if (bsdconf_replaceall(value, vsize + 1, "\\\n", "") < 0)
+ goto fail;
+
+ /* Resolve escape sequences */
+ bsdconf_strunexpand(value, value);
+
+call_function:
+ /* Abort if we're seeking only assignments */
+ if (require_equals && !have_equals) {
+ errno = EINVAL;
+ goto fail;
+ }
+
+ found = have_equals = 0; /* reset */
+
+ /*
+ * Report the statement's assignment operator through a
+ * stack-local option when invoking the unknown call-back
+ * (there is no matched options[] slot to hang it on).
+ */
+ if (options == NULL && unknown != NULL) {
+ struct bsdconf_option unk;
+
+ memset(&unk, 0, sizeof(unk));
+ unk.op = op;
+ error = unknown(&unk, dline, directive, value);
+ if (error != 0) {
+ rv = error;
+ goto cleanup;
+ }
+ continue;
+ }
+
+ /* Loop through the array looking for a match */
+ for (n = 0; options[n].directive != NULL; n++) {
+ error = fnmatch(options[n].directive, directive,
+ FNM_NOESCAPE);
+ if (error == 0) {
+ found = 1;
+ /* Call function for array index item */
+ options[n].op = op;
+ if (options[n].parse != NULL) {
+ error = options[n].parse(
+ &options[n],
+ dline, directive, value);
+ if (error != 0) {
+ rv = error;
+ goto cleanup;
+ }
+ }
+ } else if (error != FNM_NOMATCH) {
+ /* An error has occurred */
+ errno = EINVAL;
+ goto fail;
+ }
+ }
+ if (!found && unknown != NULL) {
+ struct bsdconf_option unk;
+
+ /*
+ * No match was found for the value we read from the
+ * file; call function designated for unknown values.
+ */
+ memset(&unk, 0, sizeof(unk));
+ unk.op = op;
+ error = unknown(&unk, dline, directive, value);
+ if (error != 0) {
+ rv = error;
+ goto cleanup;
+ }
+ }
+ }
+
+ goto cleanup;
+
+fail:
+ rv = -1;
+
+cleanup:
+ x = errno; /* preserve errno across free(3) and close(2) */
+ if (spoolfd != -1)
+ close(spoolfd);
+ free(directive);
+ free(value);
+ errno = x;
+
+ return (rv);
+}
+
+/*
+ * Parse the configuration file at `path' and execute the `parse' call-back
+ * functions for any directives defined by the array of config options (first
+ * argument). This is a convenience wrapper around bsdconf_fparse() above.
+ *
+ * Returns zero on success; otherwise returns -1 (or the non-zero result of a
+ * call-back) and errno should be consulted.
+ */
+int
+bsdconf_parse(struct bsdconf_option options[], const char *path,
+ int (*unknown)(struct bsdconf_option *option, uint32_t line,
+ char *directive, char *value), uint16_t processing_options)
+{
+ int error;
+ int fd;
+ char rpath[PATH_MAX];
+
+ /* Sanity check: if no options and no unknown function, return */
+ if (options == NULL && unknown == NULL) {
+ errno = EINVAL;
+ return (-1);
+ }
+
+ /* Resolve the file path */
+ if (realpath(path, rpath) == NULL)
+ return (-1);
+
+ /* Open the file */
+ if ((fd = open(rpath, O_RDONLY)) < 0)
+ return (-1);
+
+ error = bsdconf_fparse(options, fd, unknown, processing_options);
+
+ close(fd);
+ return (error);
+}
diff --git a/lib/libbsdconf/bsdconf_format.3 b/lib/libbsdconf/bsdconf_format.3
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_format.3
@@ -0,0 +1,560 @@
+.\" Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+.\" Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.Dd August 2, 2026
+.Dt BSDCONF_FORMAT 3
+.Os
+.Sh NAME
+.Nm bsdconf_format_derive ,
+.Nm bsdconf_format_files ,
+.Nm bsdconf_format_files_free ,
+.Nm bsdconf_format_find ,
+.Nm bsdconf_format_guess ,
+.Nm bsdconf_format_lookup ,
+.Nm bsdconf_format_path ,
+.Nm bsdconf_format_processing ,
+.Nm bsdconf_format_put ,
+.Nm bsdconf_format_register
+.Nd configuration format descriptors and discovery
+.Sh LIBRARY
+.Lb libbsdconf
+.Sh SYNOPSIS
+.In bsdconf.h
+.Ft int
+.Fo bsdconf_format_derive
+.Fa "enum bsdconf_format base"
+.Fa "struct bsdconf_format_def *def"
+.Fc
+.Ft int
+.Fo bsdconf_format_files
+.Fa "enum bsdconf_format format"
+.Fa "const char *rootdir"
+.Fa "const char *module"
+.Fa "const char *defaults"
+.Fa "char ***filesp"
+.Fa "size_t *nfilesp"
+.Fa "size_t *write_idxp"
+.Fc
+.Ft void
+.Fo bsdconf_format_files_free
+.Fa "char **files"
+.Fa "size_t nfiles"
+.Fc
+.Ft int
+.Fo bsdconf_format_find
+.Fa "const char *keyword"
+.Fa "enum bsdconf_format *format"
+.Fc
+.Ft "enum bsdconf_format"
+.Fo bsdconf_format_guess
+.Fa "const char *path"
+.Fc
+.Ft "const struct bsdconf_format_def *"
+.Fo bsdconf_format_lookup
+.Fa "enum bsdconf_format format"
+.Fc
+.Ft "const char *"
+.Fo bsdconf_format_path
+.Fa "enum bsdconf_format format"
+.Fc
+.Ft uint16_t
+.Fo bsdconf_format_processing
+.Fa "enum bsdconf_format format"
+.Fc
+.Ft uint16_t
+.Fo bsdconf_format_put
+.Fa "enum bsdconf_format format"
+.Fc
+.Ft int
+.Fo bsdconf_format_register
+.Fa "const struct bsdconf_format_def *def"
+.Fa "enum bsdconf_format *format"
+.Fc
+.Sh DESCRIPTION
+Every supported configuration file format is described by the same small
+format descriptor
+.Pq introduced in Xr bsdconf 3 :
+.Bd -literal -offset indent
+struct bsdconf_format_def {
+ const char *keyword; /* target keyword (or NULL) */
+ const char *path; /* default write path (or NULL) */
+ const struct bsdconf_source
+ *sources; /* ordered sources (or NULL) */
+ const char *defaults; /* defaults file (or NULL) */
+ const char *defaults_env; /* environment override */
+ const char *files_directive;
+ /* directive listing conf files */
+ const char *dirs_directive;
+ /* directive listing drop-in dirs */
+ const char *local_directive;
+ /* directive listing local files */
+ uint16_t processing; /* processing_options bitmask */
+ uint16_t put; /* put_options bitmask */
+};
+
+enum bsdconf_source_type {
+ BSDCONF_SOURCE_FILE = 0, /* a single file */
+ BSDCONF_SOURCE_DIR, /* each `*.conf' in a directory */
+ BSDCONF_SOURCE_MODDIR, /* `<module>.conf' in a directory */
+};
+
+struct bsdconf_source {
+ enum bsdconf_source_type type; /* how to interpret path */
+ const char *path; /* file or directory path */
+};
+.Ed
+.Pp
+There is exactly one parsing and writing engine;
+a format descriptor merely parameterizes it.
+The built-in formats,
+with their target keywords and default write paths,
+are:
+.Bl -column "BSDCONF_FORMAT_GENERIC" "keyword" "/boot/loader.conf"
+.It Sy format Ta Sy keyword Ta Sy "default write path"
+.It Dv BSDCONF_FORMAT_GENERIC Ta generic Ta \&-
+.It Dv BSDCONF_FORMAT_LOADER Ta loader Ta /boot/loader.conf
+.It Dv BSDCONF_FORMAT_SYSCTL Ta sysctl Ta /etc/sysctl.conf
+.It Dv BSDCONF_FORMAT_MAKE Ta make Ta /etc/make.conf
+.It Dv BSDCONF_FORMAT_SRC Ta src Ta /etc/src.conf
+.El
+.Pp
+The default write path
+.Pq the Va path member
+answers only the question of where a
+.Em new
+directive lands:
+the file to which a directive not yet present anywhere is appended.
+Which files are
+.Em consulted
+is a separate and potentially broader question,
+answered by the
+.Va sources
+member.
+When
+.Va sources
+is NULL
+.Pq as for Dv BSDCONF_FORMAT_MAKE
+the default write path is the format's one and only file and the two
+questions collapse into one.
+Otherwise
+.Po
+.Dv BSDCONF_FORMAT_LOADER ,
+.Dv BSDCONF_FORMAT_SYSCTL ,
+and
+.Dv BSDCONF_FORMAT_SRC
+.Pc
+the format is backed by several files and
+.Va sources
+lists them:
+an ordered,
+NULL-path terminated array in which each entry contributes a single file
+.Pq Dv BSDCONF_SOURCE_FILE ,
+every
+.Ql *.conf
+found in a drop-in directory
+.Pq Dv BSDCONF_SOURCE_DIR ,
+or the file
+.Ql <module>.conf
+in a directory keyed by kernel module name
+.Pq Dv BSDCONF_SOURCE_MODDIR .
+The order matches the order in which the system sources the files at boot,
+so a directive in a later file overrides the same directive in an earlier
+one and the last file listing a directive is the authoritative source of
+its value.
+The built-in source lists are:
+.Bl -tag -width BSDCONF_FORMAT_LOADER -offset indent
+.It Dv BSDCONF_FORMAT_LOADER
+discovered from
+.Pa /boot/defaults/loader.conf
+.Pq see below ;
+on a stock system
+.Pa /boot/device.hints ,
+.Pa /boot/loader.conf ,
+each
+.Pa *.conf
+in
+.Pa /boot/loader.conf.d ,
+then
+.Pa /boot/loader.conf.local
+.Pq see Xr loader.conf 5 ;
+.It Dv BSDCONF_FORMAT_SYSCTL
+.Pa /etc/sysctl.conf ,
+.Pa /etc/sysctl.conf.local ,
+then
+.Pa /etc/sysctl.kld.d/<module>.conf
+.Pq see Xr sysctl.conf 5 ;
+.It Dv BSDCONF_FORMAT_MAKE
+.Pa /etc/make.conf
+alone;
+.It Dv BSDCONF_FORMAT_SRC
+.Pa /etc/src-env.conf ,
+.Pa /etc/make.conf ,
+then
+.Pa /etc/src.conf
+.Pq the /usr/src build triad; see Xr src.conf 5 ;
+new writes prefer
+.Pa src.conf .
+.El
+.Pp
+A format whose backing files are themselves configuration data describes
+that machinery with the descriptor's
+.Va defaults
+member quintet rather than a static list alone.
+The boot loader hardcodes only
+.Pa /boot/defaults/loader.conf
+and discovers every other file from the
+.Va loader_conf_files ,
+.Va loader_conf_dirs ,
+and
+.Va local_loader_conf_files
+directives it encounters along the way
+.Pq see Xr loader.conf 5 ,
+and
+.Dv BSDCONF_FORMAT_LOADER
+names that defaults file and those three directives so that
+.Fn bsdconf_format_files
+matches that discovery:
+the defaults file is read,
+.Va loader_conf_files
+is chased
+.Po
+re-read after every file, as the loader does when a file queues
+additional names
+.Pc ,
+and the drop-in directories and local files named by the final
+.Va loader_conf_dirs
+and
+.Va local_loader_conf_files
+values are appended
+.Pq the loader likewise applies those only after the file-list walk .
+The defaults file itself is deliberately excluded from the resolved list,
+mirroring how
+.Xr sysrc 8
+excludes
+.Pa /etc/defaults/rc.conf ;
+the static
+.Va sources
+list serves as the fallback when the defaults file is missing.
+.Pp
+.Fn bsdconf_format_files
+resolves the backing files of
+.Fa format
+into a newly allocated array of paths stored through
+.Fa filesp
+.Pq with the count stored through Fa nfilesp ,
+each prefixed with
+.Fa rootdir
+unless NULL or empty.
+When
+.Fa defaults
+is non-NULL,
+it overrides the descriptor's defaults file for discovery and is used
+verbatim
+.Po
+not prefixed with
+.Fa rootdir ;
+it is ignored for formats without one
+.Pc .
+When
+.Fa write_idxp
+is non-NULL,
+the index of the file recommended for directives found in no file at all
+is stored through it:
+the format's default write path,
+or the last regular
+.Pq non-drop-in, non-local
+configuration file when discovery is in play.
+Sources of type
+.Dv BSDCONF_SOURCE_FILE
+.Pq and files named by a file-list directive
+are always listed,
+whether or not the file exists;
+directory sources contribute only the entries present on disk
+.Pq sorted ;
+module sources contribute
+.Ql <module>.conf
+only when
+.Fa module
+is non-NULL.
+The caller releases the result with
+.Fn bsdconf_format_files_free .
+.Pp
+The formats also differ in quoting on output,
+each honoring the full syntax its consumer accepts:
+.Xr loader.conf 5
+values are always written quoted
+.Pq quoted or unquoted input is accepted when parsing
+and no whitespace is permitted around the equals sign,
+as demanded by the boot loader's reader;
+.Xr sysctl.conf 5
+follows the file parser of
+.Xr sysctl 8 ,
+which trims whitespace around the equals sign and strips one pair of quotes
+around the value,
+so values are written unquoted unless quoting is required
+.Pq embedded whitespace or a comment character ;
+.Pa make.conf
+follows
+.Xr make 1
+syntax where values are never quoted
+.Pq the value runs to the end of the line
+and assignment modifiers such as
+.Ql +=
+are recognized.
+The
+.Dv BSDCONF_FORMAT_SRC
+triad
+.Po
+.Pa src-env.conf ,
+.Pa make.conf ,
+.Pa src.conf
+.Pc
+shares that
+.Pa make.conf
+syntax exactly
+.Pq all three are read by Xr make 1 when building /usr/src ,
+including the empty value,
+which for
+.Pa src.conf
+is the idiom for the value-less
+.Ql WITH_*
+and
+.Ql WITHOUT_*
+build knobs.
+.Pp
+.Fn bsdconf_format_find
+maps a target keyword
+.Pq e.g., Dq loader
+to its format.
+.Fn bsdconf_format_guess
+guesses the format of an arbitrary file from the basename of
+.Fa path ,
+matching
+.Ql <keyword>.conf
+with an optional trailing suffix
+.Pq e.g., Pa /etc/sysctl.conf.local .
+The guess is advisory and offered for consumers that opt into it
+.Pq an interactive picker suggesting a default, for example ;
+where a wrong format silently misformats a file,
+as when writing,
+the caller should require the format to be stated explicitly instead,
+as
+.Xr sysconf 8
+does with its
+.Ar target
+keyword.
+.Fn bsdconf_format_lookup
+returns the format descriptor for a format handle.
+.Fn bsdconf_format_path ,
+.Fn bsdconf_format_processing ,
+and
+.Fn bsdconf_format_put
+return the default file path and the two option bitmasks,
+respectively.
+.Sh ADDING FORMATS
+Before adding a format,
+consider whether one is needed at all:
+the parser hands each statement's raw directive and value to the caller's
+callbacks and imposes no semantics of its own,
+so a file whose directives mean something unusual
+.Po
+cumulative directives that legitimately repeat,
+for example,
+which a
+.Fn parse
+callback accumulates rather than overwrites;
+see
+.Xr bsdconf 3
+.Pc
+is read with the stock engine and bespoke callbacks;
+no format descriptor,
+flag,
+or engine change is required.
+A format descriptor parameterizes only tokenization and writing;
+reach for one when a file needs a
+.Xr sysconf 8
+target keyword,
+a source list,
+or distinct quoting on output.
+Rewriting files built from cumulative directives,
+where a change is additive rather than a replacement,
+is only partly covered by
+.Va match_line
+selection in
+.Xr bsdconf_put 3 ;
+higher-level additive policy remains the caller's concern
+.Pq see that page's Sx LIMITATIONS .
+.Pp
+Beyond callbacks,
+the format descriptor is the entire definition of a format;
+no format has private parsing or writing code.
+Support for a new configuration file format is therefore a data problem,
+approached one of two ways.
+.Pp
+An application whose format is mostly like an existing one bolts it on at
+runtime without duplicating any logic
+.Pq see Sx EXAMPLES :
+.Fn bsdconf_format_derive
+copies the format descriptor of the nearest
+.Fa base
+format into
+.Fa def ,
+the caller adjusts only the members that differ
+.Pq typically the keyword, the paths, and one or two bitmask flags ,
+and
+.Fn bsdconf_format_register
+registers the result and returns a new format handle through
+.Fa format .
+The format descriptor is copied by value but the strings and arrays it
+references
+.Po
+.Va keyword ,
+.Va path ,
+.Va sources ,
+and the
+.Va defaults
+member quintet
+.Pc
+are not;
+they must remain valid for the life of the registration.
+Registered formats participate in keyword and basename resolution exactly
+like built-ins,
+including target resolution in
+.Xr sysconf 8 Ns -style
+consumers.
+Registration is intended to occur during program initialization and is not
+thread-safe.
+.Pp
+Within the library,
+each built-in format is a self-contained translation unit
+.Pa ( bsdconf_format_<keyword>.c )
+that documents the format's syntax rules,
+cites the authority for them
+.Pq the consumer whose reader defines what is legal ,
+and defines its format descriptor,
+including its ordered source list.
+Promoting a format into the library therefore touches no existing parsing
+or writing code:
+a new file of the same shape is dropped in,
+its descriptor is declared alongside its siblings,
+one
+.Dv BSDCONF_FORMAT_*
+constant is appended to
+.Vt enum bsdconf_format ,
+one pointer is appended to the registry table,
+the file is listed in the Makefile,
+and this manual's
+.Sx DESCRIPTION
+section grows one row and one quoting note.
+The keyword becomes a
+.Xr sysconf 8
+target automatically.
+.Pp
+Many formats need no new engine capability at all.
+Formats whose statements are a space-separated directive and value with no
+equals sign
+.Pq the Apache-style directive files the ancestral parser was raised on
+parse today:
+with
+.Dv BSDCONF_BREAK_ON_EQUALS
+omitted,
+the first whitespace-delimited token is the directive and the remainder of
+the line is the raw value
+.Po which a
+.Fn parse
+callback may split further by its own rules
+.Pc ,
+and directive matching is case insensitive unless
+.Dv BSDCONF_CASE_SENSITIVE
+is set.
+Likewise
+.Dv BSDCONF_BREAK_ON_SEMICOLON
+already covers formats that terminate or chain statements with a
+semicolon.
+.Pp
+A candidate format whose syntax the existing option flags cannot express
+.Pq brace-grouped statement blocks being the canonical example
+is handled by teaching the shared engine
+.Pq the scanners in the parsing and writing cores
+one new
+.Va processing
+or
+.Va put
+flag that the new format's descriptor is the first to set.
+The capability lands once,
+composes with every existing flag,
+and becomes available to every other format
+.Pq built-in, derived, or registered
+rather than living in a private parser;
+the new format itself remains nothing more than a format descriptor in its
+own translation unit.
+.Sh RETURN VALUES
+.Fn bsdconf_format_derive
+and
+.Fn bsdconf_format_register
+return zero on success;
+otherwise -1 with
+.Va errno
+set to
+.Er EINVAL
+or
+.Er ENOSPC .
+.Fn bsdconf_format_find
+returns zero on success;
+otherwise -1.
+.Fn bsdconf_format_files
+returns zero on success;
+otherwise -1 with
+.Va errno
+set to indicate the error
+.Pq Er EINVAL for a format with no backing files .
+.Sh EXAMPLES
+Bolt on a new format at runtime by deriving from the built-in it most
+resembles,
+then set a directive in its file with full resilient write semantics:
+.Bd -literal -offset indent
+struct bsdconf_format_def def;
+enum bsdconf_format myfmt;
+struct bsdconf_option set[] = {
+ { .type = BSDCONF_TYPE_STR, .directive = "loglevel",
+ .value = { .str = "debug" },
+ .action = BSDCONF_ACTION_SET_VALUE },
+ { .directive = NULL }
+};
+
+if (bsdconf_format_derive(BSDCONF_FORMAT_SYSCTL, &def) != 0)
+ err(1, "bsdconf_format_derive");
+def.keyword = "myapp";
+def.path = "/usr/local/etc/myapp.conf";
+def.sources = NULL; /* one file; path is the sole source */
+if (bsdconf_format_register(&def, &myfmt) != 0)
+ err(1, "bsdconf_format_register");
+
+if (bsdconf_put(set, bsdconf_format_path(myfmt),
+ bsdconf_format_processing(myfmt),
+ bsdconf_format_put(myfmt)) != 0)
+ err(1, "%s", bsdconf_format_path(myfmt));
+.Ed
+.Pp
+Promoting such a format into the library itself
+.Pq a compiled-in sibling of the built-ins
+is the same data expressed as a translation unit;
+see
+.Sx ADDING FORMATS .
+.Sh SEE ALSO
+.Xr bsdconf 3 ,
+.Xr bsdconf_put 3 ,
+.Xr loader.conf 5 ,
+.Xr src.conf 5 ,
+.Xr sysctl.conf 5 ,
+.Xr sysconf 8
+.Sh HISTORY
+The format descriptor interface first appeared in
+.Fx 16.0
+as part of
+.Xr bsdconf 3 .
+.Sh AUTHORS
+.An Devin Teske Aq Mt dteske@FreeBSD.org
+.An Faraz Vahedi Aq Mt kfv@kfv.io
diff --git a/lib/libbsdconf/bsdconf_format.c b/lib/libbsdconf/bsdconf_format.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_format.c
@@ -0,0 +1,683 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/stat.h>
+
+#include <dirent.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "bsdconf.h"
+#include "bsdconf_formats.h"
+
+/*
+ * The format abstraction layer registry.
+ *
+ * Every configuration file format -- built-in or bolted on at run-time -- is
+ * described by the same small descriptor (struct bsdconf_format_def in
+ * bsdconf.h): a target keyword, a default write path, an ordered list of
+ * configuration sources, and the processing/put bitmasks that drive the
+ * shared parsing and writing cores. There is exactly one engine; a format
+ * merely parameterizes it.
+ *
+ * Each built-in format is a self-contained translation unit
+ * (bsdconf_format_<keyword>.c) documenting and defining its own descriptor;
+ * this file only collects the descriptors into the table indexed by enum
+ * bsdconf_format and resolves keywords, basenames, and source lists against
+ * it (see bsdconf_formats.h for the recipe to add a format).
+ *
+ * A format that is "mostly like" an existing one need not be added at all:
+ * an application calls bsdconf_format_derive() to inherit the nearest
+ * descriptor, adjusts only the members that differ, and registers the
+ * result with bsdconf_format_register(). The handle it gets back taps into
+ * the same core engine everywhere a built-in format works, including
+ * keyword, path, and source-list resolution in sysconf(8).
+ */
+
+static const struct bsdconf_format_def *bsdconf_formats[] = {
+ [BSDCONF_FORMAT_GENERIC] = &bsdconf_format_generic_def,
+ [BSDCONF_FORMAT_LOADER] = &bsdconf_format_loader_def,
+ [BSDCONF_FORMAT_SYSCTL] = &bsdconf_format_sysctl_def,
+ [BSDCONF_FORMAT_MAKE] = &bsdconf_format_make_def,
+ [BSDCONF_FORMAT_SRC] = &bsdconf_format_src_def,
+};
+#define BSDCONF_NFORMATS \
+ (sizeof(bsdconf_formats) / sizeof(*bsdconf_formats))
+
+/*
+ * Registered (bolt-on) formats. Descriptors are copied by value; the
+ * keyword and path strings they reference remain owned by the caller and
+ * must stay valid for the life of the registration.
+ */
+#define BSDCONF_MAXUSERFORMATS 32
+static struct bsdconf_format_def bsdconf_user_formats[BSDCONF_MAXUSERFORMATS];
+static unsigned int bsdconf_nuser_formats = 0;
+
+/*
+ * Return the descriptor for `format' or NULL if the format is neither
+ * built-in nor registered.
+ */
+const struct bsdconf_format_def *
+bsdconf_format_lookup(enum bsdconf_format format)
+{
+ unsigned int n;
+
+ if ((unsigned int)format < BSDCONF_NFORMATS)
+ return (bsdconf_formats[format]);
+
+ n = (unsigned int)format - BSDCONF_FORMAT_USER;
+ if (format >= BSDCONF_FORMAT_USER && n < bsdconf_nuser_formats)
+ return (&bsdconf_user_formats[n]);
+
+ return (NULL);
+}
+
+/*
+ * Copy the descriptor of `base' into `def' so that a caller can adjust only
+ * the members that differ before registering the result as a new format.
+ * On success, returns zero; otherwise returns -1 (unknown base format) and
+ * errno is set to EINVAL.
+ */
+int
+bsdconf_format_derive(enum bsdconf_format base,
+ struct bsdconf_format_def *def)
+{
+ const struct bsdconf_format_def *found;
+
+ /* Check arguments */
+ if (def == NULL || (found = bsdconf_format_lookup(base)) == NULL) {
+ errno = EINVAL;
+ return (-1);
+ }
+
+ *def = *found;
+ return (0);
+}
+
+/*
+ * Register a new configuration file format described by `def', storing its
+ * newly assigned handle through `format'. The descriptor is copied; the
+ * keyword and path strings it references are not (see above). On success,
+ * returns zero; otherwise returns -1 and errno is set (EINVAL for a bad
+ * argument, ENOSPC when the registration table is full).
+ */
+int
+bsdconf_format_register(const struct bsdconf_format_def *def,
+ enum bsdconf_format *format)
+{
+
+ /* Check arguments */
+ if (def == NULL || format == NULL) {
+ errno = EINVAL;
+ return (-1);
+ }
+ if (bsdconf_nuser_formats >= BSDCONF_MAXUSERFORMATS) {
+ errno = ENOSPC;
+ return (-1);
+ }
+
+ bsdconf_user_formats[bsdconf_nuser_formats] = *def;
+ *format = (enum bsdconf_format)
+ (BSDCONF_FORMAT_USER + bsdconf_nuser_formats);
+ bsdconf_nuser_formats++;
+
+ return (0);
+}
+
+/*
+ * Map a target keyword (e.g., "loader") to its format, storing the result
+ * through `format'. Registered formats are searched after the built-ins.
+ * On success, returns zero; otherwise returns -1 (unknown keyword; `format'
+ * is left untouched).
+ */
+int
+bsdconf_format_find(const char *keyword, enum bsdconf_format *format)
+{
+ unsigned int n;
+
+ /* Check arguments */
+ if (keyword == NULL || format == NULL)
+ return (-1);
+
+ for (n = 0; n < BSDCONF_NFORMATS; n++) {
+ if (bsdconf_formats[n]->keyword != NULL &&
+ strcmp(bsdconf_formats[n]->keyword, keyword) == 0) {
+ *format = (enum bsdconf_format)n;
+ return (0);
+ }
+ }
+ for (n = 0; n < bsdconf_nuser_formats; n++) {
+ if (bsdconf_user_formats[n].keyword != NULL &&
+ strcmp(bsdconf_user_formats[n].keyword, keyword) == 0) {
+ *format = (enum bsdconf_format)
+ (BSDCONF_FORMAT_USER + n);
+ return (0);
+ }
+ }
+
+ return (-1);
+}
+
+/*
+ * Test whether the basename of `path' matches `<keyword>.conf' with an
+ * optional trailing suffix (e.g., "sysctl.conf.local" matches "sysctl").
+ */
+static int
+bsdconf_basename_matches(const char *base, const char *keyword)
+{
+ size_t klen;
+
+ if (keyword == NULL)
+ return (0);
+ klen = strlen(keyword);
+ if (strncmp(base, keyword, klen) != 0)
+ return (0);
+ return (strncmp(base + klen, ".conf", 5) == 0);
+}
+
+/*
+ * Guess the format of an arbitrary configuration file from the basename of
+ * its `path' (e.g., "/etc/sysctl.conf.local" is BSDCONF_FORMAT_SYSCTL).
+ * Registered formats are searched after the built-ins. Returns
+ * BSDCONF_FORMAT_GENERIC when nothing matches.
+ */
+enum bsdconf_format
+bsdconf_format_guess(const char *path)
+{
+ unsigned int n;
+ const char *base;
+
+ /* Check arguments */
+ if (path == NULL)
+ return (BSDCONF_FORMAT_GENERIC);
+
+ /* Isolate the basename (do not modify the argument) */
+ if ((base = strrchr(path, '/')) != NULL)
+ base++;
+ else
+ base = path;
+
+ for (n = 0; n < BSDCONF_NFORMATS; n++)
+ if (bsdconf_basename_matches(base,
+ bsdconf_formats[n]->keyword))
+ return ((enum bsdconf_format)n);
+ for (n = 0; n < bsdconf_nuser_formats; n++)
+ if (bsdconf_basename_matches(base,
+ bsdconf_user_formats[n].keyword))
+ return ((enum bsdconf_format)
+ (BSDCONF_FORMAT_USER + n));
+
+ return (BSDCONF_FORMAT_GENERIC);
+}
+
+/*
+ * Return the default file path for `format' (e.g., "/boot/loader.conf") or
+ * NULL if the format has no fixed path (e.g., BSDCONF_FORMAT_GENERIC).
+ */
+const char *
+bsdconf_format_path(enum bsdconf_format format)
+{
+ const struct bsdconf_format_def *def;
+
+ if ((def = bsdconf_format_lookup(format)) == NULL)
+ return (NULL);
+ return (def->path);
+}
+
+/*
+ * Return the processing_options bitmask suited to `format', for passing to
+ * bsdconf_parse(), bsdconf_fparse(), and bsdconf_put(). Unknown formats
+ * fall back to the generic descriptor.
+ */
+uint16_t
+bsdconf_format_processing(enum bsdconf_format format)
+{
+ const struct bsdconf_format_def *def;
+
+ if ((def = bsdconf_format_lookup(format)) == NULL)
+ def = bsdconf_formats[BSDCONF_FORMAT_GENERIC];
+ return (def->processing);
+}
+
+/*
+ * Return the put_options bitmask suited to `format', for passing to
+ * bsdconf_put(). Unknown formats fall back to the generic descriptor.
+ */
+uint16_t
+bsdconf_format_put(enum bsdconf_format format)
+{
+ const struct bsdconf_format_def *def;
+
+ if ((def = bsdconf_format_lookup(format)) == NULL)
+ def = bsdconf_formats[BSDCONF_FORMAT_GENERIC];
+ return (def->put);
+}
+
+/*
+ * Append `path' (already allocated; ownership is taken) to the growing
+ * `files' array. On success, returns zero; otherwise returns -1 (errno set
+ * by realloc(3)) and `path' is freed.
+ */
+static int
+bsdconf_files_add(char ***files, size_t *nfiles, size_t *size, char *path)
+{
+ char **tmp;
+
+ if (path == NULL)
+ return (-1);
+ if (*nfiles >= *size) {
+ *size = (*size == 0) ? 8 : *size << 1;
+ tmp = realloc(*files, *size * sizeof(**files));
+ if (tmp == NULL) {
+ free(path);
+ return (-1);
+ }
+ *files = tmp;
+ }
+ (*files)[(*nfiles)++] = path;
+ return (0);
+}
+
+/*
+ * scandir(3) filter accepting non-hidden `*.conf' entries.
+ */
+static int
+bsdconf_files_filter(const struct dirent *entry)
+{
+ size_t len;
+
+ if (entry->d_name[0] == '.')
+ return (0);
+ len = strlen(entry->d_name);
+ return (len > 5 && strcmp(entry->d_name + len - 5, ".conf") == 0);
+}
+
+/*
+ * Compose the full path `prefix' + `path' [+ `/' + `name' [+ `.conf']] into
+ * newly allocated storage. Returns NULL on allocation failure (errno set).
+ */
+static char *
+bsdconf_files_path(const char *prefix, const char *path, const char *name,
+ const char *suffix)
+{
+ char *full;
+ int len;
+
+ len = snprintf(NULL, 0, "%s%s%s%s%s", prefix, path,
+ name != NULL ? "/" : "", name != NULL ? name : "",
+ suffix != NULL ? suffix : "");
+ if (len < 0 || (full = malloc((size_t)len + 1)) == NULL)
+ return (NULL);
+ snprintf(full, (size_t)len + 1, "%s%s%s%s%s", prefix, path,
+ name != NULL ? "/" : "", name != NULL ? name : "",
+ suffix != NULL ? suffix : "");
+ return (full);
+}
+
+/*
+ * Append each existing `*.conf' entry of directory `path' (prefixed with
+ * `prefix'), sorted, to the growing `files' array. A missing or unreadable
+ * directory is not an error (a drop-in directory is optional by nature).
+ * On success, returns zero; otherwise returns -1 (errno set).
+ */
+static int
+bsdconf_files_add_dir(char ***files, size_t *nfiles, size_t *size,
+ const char *prefix, const char *path)
+{
+ int n, nentries;
+ char *dir;
+ char *full;
+ struct dirent **entries;
+
+ if ((dir = bsdconf_files_path(prefix, path, NULL, NULL)) == NULL)
+ return (-1);
+ nentries = scandir(dir, &entries, bsdconf_files_filter, alphasort);
+ free(dir);
+ if (nentries < 0)
+ return (0);
+ for (n = 0; n < nentries; n++) {
+ full = bsdconf_files_path(prefix, path, entries[n]->d_name,
+ NULL);
+ if (bsdconf_files_add(files, nfiles, size, full) != 0) {
+ while (n < nentries)
+ free(entries[n++]);
+ free(entries);
+ return (-1);
+ }
+ free(entries[n]);
+ }
+ free(entries);
+ return (0);
+}
+
+/*
+ * Parse call-back for the list directives chased by
+ * bsdconf_files_discover() below. The option's `value.data' member points
+ * at the (char *) slot holding the current value of the directive; each
+ * assignment encountered replaces the slot (last assignment wins, exactly
+ * as the consumer of the file behaves).
+ */
+static int
+bsdconf_files_list_cb(struct bsdconf_option *option, uint32_t line,
+ char *directive, char *value)
+{
+ char *copy;
+ char **slot = (char **)option->value.data;
+
+ (void)line;
+ (void)directive;
+
+ if ((copy = strdup(bsdconf_unquote(value))) == NULL)
+ return (-1);
+ free(*slot);
+ *slot = copy;
+ return (0);
+}
+
+/*
+ * Advance to the next whitespace-separated token of `list' at or beyond
+ * `cursor', storing its length through `lenp'. Returns the token or NULL
+ * when the list is exhausted.
+ */
+static const char *
+bsdconf_files_token(const char *cursor, size_t *lenp)
+{
+
+ if (cursor == NULL)
+ return (NULL);
+ cursor += strspn(cursor, " \t");
+ if ((*lenp = strcspn(cursor, " \t")) == 0)
+ return (NULL);
+ return (cursor);
+}
+
+/*
+ * Ceiling on the number of configuration files discovery will chase,
+ * bounding run-away (or maliciously self-referential) file lists.
+ */
+#define BSDCONF_DISCOVER_MAX 64
+
+/*
+ * Resolve the backing files of a format whose descriptor carries a
+ * `defaults' file by performing the same discovery its consumer does: read
+ * the defaults file, then chase the file-list directive (e.g.,
+ * loader_conf_files) -- re-reading it after every file, as the loader does
+ * when a file queues additional names -- and finally append the drop-in
+ * directory entries and local files named by the final values of the other
+ * two list directives (likewise applied only after the file-list walk).
+ * Every file named by the file-list directive is included in
+ * the result whether or not it exists (a listed file is a legitimate
+ * write target before its first directive lands); the defaults file
+ * itself is deliberately excluded. The write candidate stored through
+ * `write_idx' is the last file-list entry, the final word among the
+ * regular configuration files.
+ *
+ * The defaults file is `defaults' verbatim when non-NULL (a caller-level
+ * override; see sysconf(8)'s LOADER_DEFAULTS); otherwise the descriptor
+ * default prefixed with `rootdir'.
+ *
+ * On success, returns zero. Returns 1 when the defaults file is missing
+ * or unreadable, directing the caller to fall back to the static source
+ * list. Otherwise returns -1 (errno set).
+ */
+static int
+bsdconf_files_discover(const struct bsdconf_format_def *def,
+ const char *rootdir, const char *defaults, char ***files,
+ size_t *nfiles, size_t *size, size_t *write_idx)
+{
+ int error, rv = -1;
+ uint16_t processing;
+ size_t len, n, nvisited = 0;
+ char *dpath = NULL;
+ char *full;
+ char *name;
+ char *conf_dirs = NULL;
+ char *conf_files = NULL;
+ char *local_files = NULL;
+ const char *token;
+ char *visited[BSDCONF_DISCOVER_MAX];
+ struct bsdconf_option options[4];
+
+ /* Wire each list directive to the slot holding its value */
+ memset(options, 0, sizeof(options));
+ n = 0;
+ if (def->files_directive != NULL) {
+ options[n].directive = def->files_directive;
+ options[n].value.data = &conf_files;
+ options[n].parse = bsdconf_files_list_cb;
+ n++;
+ }
+ if (def->dirs_directive != NULL) {
+ options[n].directive = def->dirs_directive;
+ options[n].value.data = &conf_dirs;
+ options[n].parse = bsdconf_files_list_cb;
+ n++;
+ }
+ if (def->local_directive != NULL) {
+ options[n].directive = def->local_directive;
+ options[n].value.data = &local_files;
+ options[n].parse = bsdconf_files_list_cb;
+ n++;
+ }
+
+ /* Resolve and read the defaults file (missing means fall back) */
+ if (defaults != NULL)
+ dpath = strdup(defaults);
+ else
+ dpath = bsdconf_files_path(rootdir, def->defaults, NULL,
+ NULL);
+ if (dpath == NULL)
+ goto cleanup;
+ processing = def->processing & ~BSDCONF_REQUIRE_EQUALS;
+ if (bsdconf_parse(options, dpath, NULL, processing) != 0) {
+ rv = 1;
+ goto cleanup;
+ }
+
+ /*
+ * Chase the file-list directive. Each pass rescans the current
+ * value of the list from the start for the first file not yet
+ * visited (any file read may have revised the list), reads it,
+ * and repeats until every listed file has been visited.
+ */
+ for (;;) {
+ token = conf_files;
+ while ((token = bsdconf_files_token(token, &len)) != NULL) {
+ for (n = 0; n < nvisited; n++)
+ if (strncmp(visited[n], token, len) == 0 &&
+ visited[n][len] == '\0')
+ break;
+ if (n == nvisited)
+ break;
+ token += len;
+ }
+ if (token == NULL || nvisited >= BSDCONF_DISCOVER_MAX)
+ break;
+ if ((visited[nvisited] = strndup(token, len)) == NULL)
+ goto cleanup;
+ full = bsdconf_files_path(rootdir, visited[nvisited], NULL,
+ NULL);
+ nvisited++;
+ if (bsdconf_files_add(files, nfiles, size, full) != 0)
+ goto cleanup;
+ /* A listed file that does not (yet) exist is normal */
+ (void)bsdconf_parse(options, full, NULL, processing);
+ }
+ *write_idx = (*nfiles > 0) ? *nfiles - 1 : 0;
+
+ /* Drop-in directories, then local files, from the final lists */
+ token = conf_dirs;
+ while ((token = bsdconf_files_token(token, &len)) != NULL) {
+ if ((name = strndup(token, len)) == NULL)
+ goto cleanup;
+ token += len;
+ error = bsdconf_files_add_dir(files, nfiles, size, rootdir,
+ name);
+ free(name);
+ if (error != 0)
+ goto cleanup;
+ }
+ token = local_files;
+ while ((token = bsdconf_files_token(token, &len)) != NULL) {
+ if ((name = strndup(token, len)) == NULL)
+ goto cleanup;
+ token += len;
+ full = bsdconf_files_path(rootdir, name, NULL, NULL);
+ free(name);
+ if (bsdconf_files_add(files, nfiles, size, full) != 0)
+ goto cleanup;
+ }
+
+ rv = 0;
+
+cleanup:
+ n = errno; /* preserve errno across free(3) */
+ while (nvisited > 0)
+ free(visited[--nvisited]);
+ free(conf_files);
+ free(conf_dirs);
+ free(local_files);
+ free(dpath);
+ errno = n;
+ return (rv);
+}
+
+/*
+ * Resolve the ordered list of configuration files backing `format' into a
+ * newly allocated array of newly allocated paths, stored through `filesp'
+ * with the count stored through `nfilesp'. Files appear in the order the
+ * system sources them at boot; a directive in a later file overrides the
+ * same directive in an earlier one, making the last file listing a
+ * directive the authoritative source of its value. When `write_idxp' is
+ * non-NULL, the index of the file recommended for directives found in no
+ * file at all is stored through it (the format's default write path, or
+ * the last regular configuration file when discovery is in play).
+ *
+ * A format whose descriptor names a defaults file resolves its list by
+ * discovery (see bsdconf_files_discover() above); `defaults' overrides
+ * the descriptor's defaults file when non-NULL and is used verbatim (it
+ * is not prefixed with `rootdir'). For all other formats `defaults' is
+ * ignored and the static source list governs, as it also does when the
+ * defaults file is missing.
+ *
+ * Each static path is prefixed with `rootdir' unless NULL or empty.
+ * Sources of type BSDCONF_SOURCE_FILE are always listed, whether or not
+ * the file exists; sources of type BSDCONF_SOURCE_DIR contribute each
+ * existing `*.conf' entry (sorted); sources of type BSDCONF_SOURCE_MODDIR
+ * contribute `<module>.conf' only when `module' is non-NULL. A format
+ * with no source list resolves to its default path alone.
+ *
+ * On success, returns zero and the caller frees the result with
+ * bsdconf_format_files_free(). Otherwise returns -1 and errno is set
+ * (EINVAL for an unknown format or a format with no backing files).
+ */
+int
+bsdconf_format_files(enum bsdconf_format format, const char *rootdir,
+ const char *module, const char *defaults, char ***filesp,
+ size_t *nfilesp, size_t *write_idxp)
+{
+ int error;
+ size_t nfiles = 0;
+ size_t size = 0;
+ size_t write_idx = 0;
+ char **files = NULL;
+ char *path;
+ const struct bsdconf_format_def *def;
+ const struct bsdconf_source *source;
+
+ /* Check arguments */
+ if (filesp == NULL || nfilesp == NULL ||
+ (def = bsdconf_format_lookup(format)) == NULL) {
+ errno = EINVAL;
+ return (-1);
+ }
+ if (rootdir == NULL)
+ rootdir = "";
+
+ /* Discover the list when the descriptor names a defaults file */
+ if (def->defaults != NULL) {
+ error = bsdconf_files_discover(def, rootdir, defaults,
+ &files, &nfiles, &size, &write_idx);
+ if (error < 0)
+ goto cleanup;
+ if (error == 0 && nfiles > 0)
+ goto done;
+ /* Missing defaults file; fall back to static sources */
+ bsdconf_format_files_free(files, nfiles);
+ files = NULL;
+ nfiles = size = write_idx = 0;
+ }
+
+ /* A format with no source list is backed by its path alone */
+ if (def->sources == NULL) {
+ if (def->path == NULL) {
+ errno = EINVAL;
+ return (-1);
+ }
+ path = bsdconf_files_path(rootdir, def->path, NULL, NULL);
+ if (bsdconf_files_add(&files, &nfiles, &size, path) != 0)
+ goto cleanup;
+ goto done;
+ }
+
+ for (source = def->sources; source->path != NULL; source++) {
+ switch (source->type) {
+ case BSDCONF_SOURCE_FILE:
+ path = bsdconf_files_path(rootdir, source->path,
+ NULL, NULL);
+ if (def->path != NULL &&
+ strcmp(source->path, def->path) == 0)
+ write_idx = nfiles;
+ if (bsdconf_files_add(&files, &nfiles, &size,
+ path) != 0)
+ goto cleanup;
+ break;
+ case BSDCONF_SOURCE_DIR:
+ if (bsdconf_files_add_dir(&files, &nfiles, &size,
+ rootdir, source->path) != 0)
+ goto cleanup;
+ break;
+ case BSDCONF_SOURCE_MODDIR:
+ if (module == NULL)
+ break;
+ path = bsdconf_files_path(rootdir, source->path,
+ module, ".conf");
+ if (bsdconf_files_add(&files, &nfiles, &size,
+ path) != 0)
+ goto cleanup;
+ break;
+ }
+ }
+
+done:
+ *filesp = files;
+ *nfilesp = nfiles;
+ if (write_idxp != NULL)
+ *write_idxp = write_idx;
+ return (0);
+
+cleanup:
+ bsdconf_format_files_free(files, nfiles);
+ return (-1);
+}
+
+/*
+ * Release an array of paths obtained from bsdconf_format_files().
+ */
+void
+bsdconf_format_files_free(char **files, size_t nfiles)
+{
+ size_t n;
+
+ if (files == NULL)
+ return;
+ for (n = 0; n < nfiles; n++)
+ free(files[n]);
+ free(files);
+}
diff --git a/lib/libbsdconf/bsdconf_format_generic.c b/lib/libbsdconf/bsdconf_format_generic.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_format_generic.c
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "bsdconf_formats.h"
+
+/*
+ * BSDCONF_FORMAT_GENERIC: `name[=value]' assignments with values quoted
+ * only when required to round-trip (embedded whitespace or a comment
+ * character). The fallback when nothing more specific is known about a
+ * file, and the default base for derived formats. It has a target keyword
+ * so that a sysconf(8)-style consumer can apply it to an arbitrary file
+ * explicitly, but no default path; the caller must always name the file.
+ *
+ * NB: Formats whose statements are a space-separated directive and value
+ * with no equals sign at all (the httpd.conf style the ancestral figpar
+ * engine was raised on) need no descriptor of their own to parse: with
+ * BSDCONF_BREAK_ON_EQUALS omitted, the first whitespace-delimited token is
+ * the directive and the remainder of the line is the raw value, and
+ * directive matching is case insensitive unless BSDCONF_CASE_SENSITIVE is
+ * set.
+ */
+const struct bsdconf_format_def bsdconf_format_generic_def = {
+ .keyword = "generic",
+ .path = NULL, /* no default path */
+ .sources = NULL, /* no source list */
+ .processing = BSDCONF_BREAK_ON_EQUALS | BSDCONF_CASE_SENSITIVE,
+ .put = 0,
+};
diff --git a/lib/libbsdconf/bsdconf_format_loader.c b/lib/libbsdconf/bsdconf_format_loader.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_format_loader.c
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "bsdconf_formats.h"
+
+/*
+ * BSDCONF_FORMAT_LOADER: loader.conf(5)
+ *
+ * Assignments with no whitespace around the `=' -- the Forth reader in
+ * support.4th rejects anything else, so BSDCONF_STRICT_EQUALS is enforced.
+ * Quoted or unquoted input values are accepted when parsing but values are
+ * always written back quoted (BSDCONF_PUT_QUOTE_ALWAYS), the form every
+ * loader accepts for every value.
+ *
+ * The loader itself hardcodes only /boot/defaults/loader.conf and discovers
+ * every other file from the loader_conf_files, loader_conf_dirs, and
+ * local_loader_conf_files directives encountered along the way -- any file
+ * so discovered may revise those lists in turn. The `defaults' member
+ * quintet below hands that same discovery to bsdconf_format_files(); the
+ * static source list is only the fallback for systems whose defaults file
+ * is missing. The defaults file itself is deliberately excluded from the
+ * resolved list, mirroring how sysrc(8) excludes /etc/defaults/rc.conf.
+ */
+static const struct bsdconf_source bsdconf_loader_sources[] = {
+ { BSDCONF_SOURCE_FILE, "/boot/device.hints" },
+ { BSDCONF_SOURCE_FILE, "/boot/loader.conf" },
+ { BSDCONF_SOURCE_DIR, "/boot/loader.conf.d" },
+ { BSDCONF_SOURCE_FILE, "/boot/loader.conf.local" },
+ { BSDCONF_SOURCE_FILE, NULL },
+};
+
+const struct bsdconf_format_def bsdconf_format_loader_def = {
+ .keyword = "loader",
+ .path = "/boot/loader.conf",
+ .sources = bsdconf_loader_sources,
+ .defaults = "/boot/defaults/loader.conf",
+ .defaults_env = "LOADER_DEFAULTS",
+ .files_directive = "loader_conf_files",
+ .dirs_directive = "loader_conf_dirs",
+ .local_directive = "local_loader_conf_files",
+ .processing = BSDCONF_BREAK_ON_EQUALS |
+ BSDCONF_CASE_SENSITIVE |
+ BSDCONF_REQUIRE_EQUALS |
+ BSDCONF_STRICT_EQUALS,
+ .put = BSDCONF_PUT_QUOTE_ALWAYS |
+ BSDCONF_PUT_ALLOW_EMPTY,
+};
diff --git a/lib/libbsdconf/bsdconf_format_make.c b/lib/libbsdconf/bsdconf_format_make.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_format_make.c
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "bsdconf_formats.h"
+
+/*
+ * BSDCONF_FORMAT_MAKE: make.conf(5)
+ *
+ * make(1) syntax: assignment modifiers (`+=' `?=' `:=' `!=') are
+ * recognized, matched, and rewritten (BSDCONF_OPERATOR_EQUALS), and values
+ * are never quoted (BSDCONF_PUT_UNQUOTED) -- a value runs to the end of
+ * the line, embedded whitespace included, so only a value that could not
+ * round-trip verbatim (an embedded newline or comment marker) is rejected.
+ * A put of `+=' appends a new `name+=value' line rather than rewriting a
+ * prior assignment (see bsdconf_put()); callers that accumulate effective
+ * values honor the same operators on read. sysconf(8) `name-=word' strikes
+ * rewrite or remove a specific prior statement via match_line (make has no
+ * `-=` operator of its own).
+ */
+const struct bsdconf_format_def bsdconf_format_make_def = {
+ .keyword = "make",
+ .path = "/etc/make.conf",
+ .sources = NULL, /* /etc/make.conf alone */
+ .processing = BSDCONF_BREAK_ON_EQUALS | BSDCONF_CASE_SENSITIVE |
+ BSDCONF_OPERATOR_EQUALS,
+ .put = BSDCONF_PUT_UNQUOTED | BSDCONF_PUT_ALLOW_EMPTY,
+};
diff --git a/lib/libbsdconf/bsdconf_format_src.c b/lib/libbsdconf/bsdconf_format_src.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_format_src.c
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "bsdconf_formats.h"
+
+/*
+ * BSDCONF_FORMAT_SRC: /usr/src build audience
+ *
+ * Make(1) syntax, matching make.conf(5) / src.conf(5). The FreeBSD source
+ * tree consults three configuration files in order (see src.sys.env.mk,
+ * sys.mk, and src.sys.mk): src-env.conf, make.conf, then src.conf. New
+ * directives are written to src.conf (def->path); later files override
+ * earlier ones for `=' and accumulate for `+='. Environment overrides
+ * (SRC_ENV_CONF, __MAKE_CONF, SRCCONF) are applied by sysconf(8).
+ *
+ * sys.mk / local.sys.mk and `.if' / control flow are intentionally out of
+ * scope: conf files may contain them, but this format does not evaluate
+ * them (see LIMITATIONS in sysconf(8)).
+ */
+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 },
+};
+
+const struct bsdconf_format_def bsdconf_format_src_def = {
+ .keyword = "src",
+ .path = "/etc/src.conf",
+ .sources = bsdconf_src_sources,
+ .processing = BSDCONF_BREAK_ON_EQUALS | BSDCONF_CASE_SENSITIVE |
+ BSDCONF_OPERATOR_EQUALS,
+ .put = BSDCONF_PUT_UNQUOTED | BSDCONF_PUT_ALLOW_EMPTY,
+};
diff --git a/lib/libbsdconf/bsdconf_format_sysctl.c b/lib/libbsdconf/bsdconf_format_sysctl.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_format_sysctl.c
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "bsdconf_formats.h"
+
+/*
+ * BSDCONF_FORMAT_SYSCTL: sysctl.conf(5)
+ *
+ * Assignments in the format of the sysctl(8) command, whose file parser
+ * trims whitespace around the `=' and strips one pair of quotes around the
+ * value, so values are written unquoted unless quoting is required to
+ * round-trip (embedded whitespace or a comment character).
+ *
+ * Sourced at boot in the order below: the first two by rc.d/sysctl (and
+ * again by rc.d/sysctl_lastload), the module-specific drop-in by
+ * rc.subr(8)'s load_kld when the named kernel module loads.
+ */
+static const struct bsdconf_source bsdconf_sysctl_sources[] = {
+ { BSDCONF_SOURCE_FILE, "/etc/sysctl.conf" },
+ { BSDCONF_SOURCE_FILE, "/etc/sysctl.conf.local" },
+ { BSDCONF_SOURCE_MODDIR, "/etc/sysctl.kld.d" },
+ { BSDCONF_SOURCE_FILE, NULL },
+};
+
+const struct bsdconf_format_def bsdconf_format_sysctl_def = {
+ .keyword = "sysctl",
+ .path = "/etc/sysctl.conf",
+ .sources = bsdconf_sysctl_sources,
+ .processing = BSDCONF_BREAK_ON_EQUALS | BSDCONF_CASE_SENSITIVE |
+ BSDCONF_REQUIRE_EQUALS,
+ .put = BSDCONF_PUT_ALLOW_EMPTY,
+};
diff --git a/lib/libbsdconf/bsdconf_formats.h b/lib/libbsdconf/bsdconf_formats.h
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_formats.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#ifndef _BSDCONF_FORMATS_H_
+#define _BSDCONF_FORMATS_H_
+
+#include "bsdconf.h"
+
+/*
+ * The built-in format descriptors, one translation unit per format.
+ * This header is deliberately not installed.
+ *
+ * Every format is a self-contained bsdconf_format_<keyword>.c defining a
+ * single descriptor; the registry in bsdconf_format.c does nothing but
+ * collect them into the table indexed by enum bsdconf_format. Adding a
+ * format to the library therefore touches no existing parsing or writing
+ * code: drop in the new file, declare its descriptor here, append one
+ * enum constant in bsdconf.h and one pointer to the registry table, and
+ * list the file in the Makefile.
+ *
+ * A candidate format whose syntax the existing processing/put flags cannot
+ * express (for example, brace-grouped statement blocks) is handled by
+ * teaching the shared engine (bsdconf.c and bsdconf_put.c) a new flag that
+ * the new descriptor is first to set, so the capability composes with
+ * every other format rather than living in a private parser.
+ */
+extern const struct bsdconf_format_def bsdconf_format_generic_def;
+extern const struct bsdconf_format_def bsdconf_format_loader_def;
+extern const struct bsdconf_format_def bsdconf_format_make_def;
+extern const struct bsdconf_format_def bsdconf_format_src_def;
+extern const struct bsdconf_format_def bsdconf_format_sysctl_def;
+
+#endif /* !_BSDCONF_FORMATS_H_ */
diff --git a/lib/libbsdconf/bsdconf_internal.h b/lib/libbsdconf/bsdconf_internal.h
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_internal.h
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2001-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#ifndef _BSDCONF_INTERNAL_H_
+#define _BSDCONF_INTERNAL_H_
+
+#include <sys/stat.h>
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "bsdconf.h"
+
+/*
+ * Private interfaces shared between the libbsdconf translation units.
+ * This header is deliberately not installed.
+ */
+
+/*
+ * Not every POSIX system defines ALLPERMS (notably musl libc), and the
+ * sticky bit (S_ISVTX) is an XSI extension unavailable under strict POSIX.
+ */
+#ifndef ALLPERMS
+#ifdef S_ISVTX
+#define ALLPERMS (S_ISUID|S_ISGID|S_ISVTX|S_IRWXU|S_IRWXG|S_IRWXO)
+#else
+#define ALLPERMS (S_ISUID|S_ISGID|S_IRWXU|S_IRWXG|S_IRWXO)
+#endif
+#endif
+
+/* String manipulation routines (bsdconf_string.c) */
+void bsdconf_strunexpand(char *_dst, const char *_src);
+void bsdconf_strtolower(char *_buf);
+int bsdconf_replaceall(char *_buf, size_t _buflen,
+ const char *_find, const char *_replace);
+unsigned int bsdconf_strcount(const char *_source, const char *_find);
+
+/*
+ * Parsed extent of a single statement within the in-memory copy of the file.
+ * All members are byte offsets into the buffer except as noted. A statement
+ * runs from the first byte of its directive to its terminator (newline, an
+ * inline `#' comment, or a semicolon when enabled).
+ */
+struct bsdconf_stmt {
+ size_t line_start; /* start of the physical line (for removal) */
+ size_t dir_start; /* first byte of the directive */
+ size_t dir_end; /* one past the last byte of the directive */
+ size_t op_start; /* first byte of the assignment operator */
+ size_t eq; /* index of the `=' (valid iff have_equals) */
+ size_t val_start; /* first byte of the value (== val_end if none) */
+ size_t val_end; /* one past last byte of value (ws trimmed) */
+ size_t term; /* index of the terminator (`\n', `#', `;', EOF) */
+ size_t line_end; /* one past the terminating newline (for removal) */
+ uint32_t line; /* 1-based line number of the directive */
+ enum bsdconf_op op; /* assignment operator of the statement */
+ int have_value; /* nonzero if a value was present */
+ int have_equals; /* nonzero if an `=' separated directive/value */
+};
+
+/* Statement scan / put helpers (bsdconf_stmt.c) */
+const char *bsdconf_op_token(enum bsdconf_op _op);
+int bsdconf_writeall(int _fd, const void *_data, size_t _len);
+char *bsdconf_readfile(int _fd, size_t _size, size_t *_lenp);
+int bsdconf_emit(int _fd, const void *_data, size_t _len,
+ int *_last);
+int bsdconf_ensure_tmp(int *_tmpfdp, char *_tpath, size_t _tpathsz,
+ const char *_rpath, const struct stat *_sb);
+char *bsdconf_format_value(const struct bsdconf_option *_option,
+ int _unquoted, int _quote_always, int _bsemicolon);
+int bsdconf_value_empty(const struct bsdconf_option *_option);
+int bsdconf_dir_matches(const char *_buf, size_t _start,
+ size_t _end, const char *_directive, int _case_sensitive);
+int bsdconf_scan(const char *_buf, size_t _len, size_t *_ip,
+ uint32_t *_linep, int _bequals, int _bsemicolon,
+ int _strict_equals, int _operator_equals,
+ struct bsdconf_stmt *_st);
+
+#endif /* !_BSDCONF_INTERNAL_H_ */
diff --git a/lib/libbsdconf/bsdconf_put.3 b/lib/libbsdconf/bsdconf_put.3
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_put.3
@@ -0,0 +1,285 @@
+.\" Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+.\" Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.Dd August 2, 2026
+.Dt BSDCONF_PUT 3
+.Os
+.Sh NAME
+.Nm bsdconf_put ,
+.Nm bsdconf_set_option
+.Nd resilient configuration file writing
+.Sh LIBRARY
+.Lb libbsdconf
+.Sh SYNOPSIS
+.In bsdconf.h
+.Ft int
+.Fo bsdconf_put
+.Fa "struct bsdconf_option options[]"
+.Fa "const char *path"
+.Fa "uint16_t processing_options"
+.Fa "uint16_t put_options"
+.Fc
+.Ft int
+.Fo bsdconf_set_option
+.Fa "struct bsdconf_option options[]"
+.Fa "const char *directive"
+.Fa "union bsdconf_value *value"
+.Fc
+.Sh DESCRIPTION
+.Fn bsdconf_put
+rewrites the configuration file at
+.Fa path ,
+applying the per-directive action of each option in the array:
+.Bl -tag -width BSDCONF_ACTION_SET_VALUE
+.It Dv BSDCONF_ACTION_SET_VALUE
+Set the directive to
+.Va value.str ,
+editing it in place if present or appending it to the file if absent.
+.It Dv BSDCONF_ACTION_CHECK
+Report
+.Pq via Va result
+whether the current value differs from
+.Va value.str ,
+without modifying the file.
+.It Dv BSDCONF_ACTION_REMOVE
+Delete the directive from the file.
+.El
+.Pp
+Unlike
+.Fn bsdconf_parse ,
+directives are matched exactly
+.Pq a pattern is not a writable target .
+Comments,
+blank lines,
+statement ordering,
+and the formatting of untouched statements are preserved.
+For each processed option,
+.Va result
+is set to a bitmask of
+.Dv BSDCONF_DIRECTIVE_FOUND ,
+.Dv BSDCONF_VALUE_CHANGED ,
+.Dv BSDCONF_DIRECTIVE_ADDED ,
+and
+.Dv BSDCONF_DIRECTIVE_REMOVED ,
+and
+.Va line
+is set to the line of the first match
+.Pq if found .
+A non-zero
+.Va match_line
+on input restricts the put to the statement on that physical line
+.Pq zero matches any statement, as before ;
+this allows a single
+.Ql name+=value
+among several to be rewritten or removed without appending a new line
+or touching the others
+.Pq make(1) list-strike edits in Xr sysconf 8 .
+.Pp
+The
+.Fa path
+argument to
+.Fn bsdconf_put
+must name a regular file
+.Pq only a regular file can be atomically replaced ;
+anything else is rejected with
+.Er EINVAL .
+When every option is
+.Dv BSDCONF_ACTION_CHECK ,
+or every
+.Dv BSDCONF_ACTION_SET_VALUE
+and
+.Dv BSDCONF_ACTION_REMOVE
+would leave the file unchanged,
+the original is left untouched:
+no temporary is created,
+.Va mtime
+is not bumped,
+and hard links are not severed.
+Otherwise the file is replaced atomically:
+output is streamed to a temporary file created with
+.Xr mkstemp 3
+in the target's own directory,
+flushed to stable storage with
+.Xr fsync 2 ,
+given the mode
+.Pq and, if permitted, the ownership
+of the original,
+and then moved over the original with
+.Xr rename 2 .
+An unexpected system failure or power loss mid-transaction leaves the
+original untouched
+.Pq see also Sx SECURITY CONSIDERATIONS .
+.Pp
+.Fn bsdconf_put
+operates on exactly one file.
+For a format backed by more than one file
+.Pq see Xr bsdconf_format 3 ,
+the caller decides which file to hand it,
+and the deterministic sourcing order makes that decision mechanical:
+because the last file to list a directive dictates its effective value,
+a directive should be rewritten in the last file of the format's ordered
+list that currently lists it
+.Pq found by parsing the files in order with Fn bsdconf_parse ,
+or appended to the format's default file when no file lists it.
+A removal,
+by contrast,
+must be applied to every file listing the directive,
+lest deleting the authoritative definition merely unmask an earlier one.
+This is the policy implemented by
+.Xr sysconf 8 .
+.Pp
+The
+.Fa put_options
+argument is a mask of the following bit fields:
+.Bl -tag -width BSDCONF_PUT_NO_DUPLICATES
+.It Dv BSDCONF_PUT_NO_DUPLICATES
+Fail with
+.Er EEXIST
+if a directive to be written appears more than once in the file.
+.It Dv BSDCONF_PUT_ALLOW_EMPTY
+Permit setting an empty value.
+.It Dv BSDCONF_PUT_BACKUP
+Save a copy of the original file with a
+.Ql .bak
+suffix before replacing it.
+.It Dv BSDCONF_PUT_UNQUOTED
+Emit
+.Va value.str
+as literal file text:
+no quotes are added and no characters are backslash-escaped
+.Pq the caller supplies the bytes that should appear on disk .
+Embedded whitespace is fine
+.Pq the value runs to the end of the line .
+A value that could not round-trip under the parser
+.Pq an embedded newline, unescaped comment marker, or trailing unescaped backslash
+is rejected with
+.Er EINVAL
+rather than silently corrupting the file.
+The reader still runs
+.Fn bsdconf_strunexpand
+on input, so escape sequences present in the file become the logical value
+on read; this flag does not re-encode them on write.
+.It Dv BSDCONF_PUT_QUOTE_ALWAYS
+Enclose every value in double-quotes,
+escaping embedded quotes and backslashes.
+.El
+.Pp
+.Fn bsdconf_set_option
+traverses the options-array and stages
+.Fa value
+into the option whose directive matches
+.Fa directive
+via
+.Xr strcmp 3 .
+.Sh RETURN VALUES
+.Fn bsdconf_put
+returns zero on success;
+otherwise -1 is returned and the global variable
+.Va errno
+is set to indicate the error.
+.Fn bsdconf_set_option
+returns 1 if a matching directive was staged,
+otherwise 0.
+.Sh SEE ALSO
+.Xr bsdconf 3 ,
+.Xr bsdconf_format 3 ,
+.Xr sysconf 8
+.Sh HISTORY
+The
+.Fn bsdconf_put
+and
+.Fn bsdconf_set_option
+functions first appeared in
+.Fx 16.0
+as part of
+.Xr bsdconf 3 .
+.Sh AUTHORS
+.An Devin Teske Aq Mt dteske@FreeBSD.org
+.An Faraz Vahedi Aq Mt kfv@kfv.io
+.Sh LIMITATIONS
+.Fn bsdconf_put
+matches directives by exact name and, by default, treats each name as
+single-valued: the first matching statement is rewritten, or a new line
+is appended when none matches.
+That suits the built-in formats when the last assignment wins.
+.Pp
+Make(1)-style cumulative
+.Ql +=
+chains are supported only in part.
+With
+.Va match_line
+unset, a
+.Ql +=
+.Dv BSDCONF_ACTION_SET_VALUE
+appends a new statement rather than collapsing earlier ones; with
+.Va match_line
+set, one physical line can be rewritten or removed
+.Pq as Xr sysconf 8 does for make/src list strikes .
+Word-level
+.Ql -=
+policy and effective-value accumulation remain the caller's
+responsibility;
+the library does not implement
+.Ql -=
+itself.
+.Pp
+Formats whose directives legitimately repeat without make(1) operators
+.Pq for example Apache-style cumulative keywords; see Xr bsdconf 3
+likewise parse with caller-supplied callbacks, but
+.Fn bsdconf_put
+offers no first-class insert, remove, or reorder of occurrence
+.Em N
+among equals—only exact-name match or
+.Va match_line
+selection.
+.Sh SECURITY CONSIDERATIONS
+The write transaction is designed to be safe against both interruption and
+interference.
+The temporary file is created by
+.Xr mkstemp 3
+.Pq Dv O_EXCL ; never predictable, never followed
+in the target's own directory,
+so the data never crosses a filesystem boundary and never transits a
+world-writable directory such as
+.Pa /tmp .
+The mode and ownership propagated onto it are taken by
+.Xr fstat 2
+from the descriptor actually read,
+not by re-looking up the path,
+and are applied with
+.Xr fchmod 2
+and
+.Xr fchown 2
+on the open descriptor,
+so a concurrently swapped file cannot influence them.
+A backup requested with
+.Dv BSDCONF_PUT_BACKUP
+refuses to follow a symbolic link planted at the
+.Ql .bak
+name
+.Pq Dv O_NOFOLLOW .
+.Pp
+Symbolic links in the
+.Fa path
+argument to
+.Fn bsdconf_put
+are resolved
+.Pq with Xr realpath 3
+before work begins:
+writing through a symbolic link rewrites the file it points at and
+preserves the link itself,
+rather than replacing the link with a regular file.
+The resolution is not re-verified at
+.Xr open 2
+time;
+as with any path-based interface,
+an actor with write access to a directory along the path can redirect it,
+so the containing directories must be trustworthy
+.Pq as those of system configuration files are .
+Because replacement is by
+.Xr rename 2 ,
+a target with multiple hard links is severed from its other names,
+which afterwards continue to reference the old content;
+this is inherent to atomic replacement.
diff --git a/lib/libbsdconf/bsdconf_put.c b/lib/libbsdconf/bsdconf_put.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_put.c
@@ -0,0 +1,531 @@
+/*
+ * Copyright (c) 2015-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "bsdconf.h"
+#include "bsdconf_internal.h"
+
+/*
+ * Rewrite the configuration file at `path', applying the per-directive
+ * actions described by the array of config options (first argument). Each
+ * option's action is one of:
+ *
+ * BSDCONF_ACTION_SET_VALUE set the directive to option->value.str,
+ * editing it in place if present or appending
+ * it to the file if absent;
+ * BSDCONF_ACTION_REMOVE delete the directive from the file;
+ * BSDCONF_ACTION_CHECK report (via option->result) whether the
+ * current value differs from option->value,
+ * without modifying the file.
+ *
+ * Comments, blank lines, statement ordering, and the formatting of untouched
+ * statements are preserved. With BSDCONF_OPERATOR_EQUALS, a non-default
+ * option->op both matches and rewrites the statement's assignment operator
+ * (make.conf(5) `+=' et al.). For each processed option, option->result is
+ * set to a bitmask of BSDCONF_DIRECTIVE_FOUND, BSDCONF_VALUE_CHANGED,
+ * BSDCONF_DIRECTIVE_ADDED, and BSDCONF_DIRECTIVE_REMOVED, and option->line
+ * is set to the line of the first match (if found).
+ *
+ * When every option is BSDCONF_ACTION_CHECK, or every SET_VALUE/REMOVE would
+ * leave the file unchanged, the original is left untouched: no temporary is
+ * created, mtime is not bumped, and hard links are not severed. Otherwise
+ * the file is replaced atomically: output is written to a temporary file in
+ * the same directory, flushed to disk with fsync(2), given the mode (and, if
+ * permitted, the ownership) of the original, and then renamed over it. A
+ * crash mid-transaction leaves the original untouched.
+ *
+ * Returns zero on success; otherwise returns -1 and errno should be
+ * consulted.
+ */
+int
+bsdconf_put(struct bsdconf_option options[], const char *path,
+ uint16_t processing_options, uint16_t put_options)
+{
+ int backup;
+ int bequals;
+ int bsemicolon;
+ int case_sensitive;
+ int emptyok;
+ int nodup;
+ int operator_equals;
+ int quote_always;
+ int require_equals;
+ int strict_equals;
+ int unquoted;
+ int dirfd = -1;
+ int fd = -1;
+ int last_ch = -1; /* last byte emitted, or -1 if none */
+ int opchange;
+ int rv = -1;
+ int saved_errno;
+ int tmpfd = -1;
+ char *buf = NULL;
+ char *slash;
+ char *val = NULL;
+ const char *sep;
+ size_t buflen = 0;
+ size_t i;
+ size_t n;
+ size_t vlen;
+ size_t wc; /* write cursor: next unemitted byte of buf */
+ uint32_t line = 1;
+ struct bsdconf_option *option;
+ struct bsdconf_stmt st;
+ struct stat sb;
+ char rpath[PATH_MAX];
+ char tpath[PATH_MAX];
+
+ /* Sanity check the arguments */
+ if (options == NULL || path == NULL) {
+ errno = EINVAL;
+ return (-1);
+ }
+
+ /* Nothing to unlink until mkstemp(3) succeeds (see cleanup) */
+ tpath[0] = '\0';
+
+ /* Decode processing options */
+ bequals = (processing_options & BSDCONF_BREAK_ON_EQUALS) != 0;
+ bsemicolon = (processing_options & BSDCONF_BREAK_ON_SEMICOLON) != 0;
+ case_sensitive = (processing_options & BSDCONF_CASE_SENSITIVE) != 0;
+ operator_equals = (processing_options & BSDCONF_OPERATOR_EQUALS) != 0;
+ require_equals = (processing_options & BSDCONF_REQUIRE_EQUALS) != 0;
+ strict_equals = (processing_options & BSDCONF_STRICT_EQUALS) != 0;
+
+ /* Decode put options */
+ backup = (put_options & BSDCONF_PUT_BACKUP) != 0;
+ emptyok = (put_options & BSDCONF_PUT_ALLOW_EMPTY) != 0;
+ nodup = (put_options & BSDCONF_PUT_NO_DUPLICATES) != 0;
+ quote_always = (put_options & BSDCONF_PUT_QUOTE_ALWAYS) != 0;
+ unquoted = (put_options & BSDCONF_PUT_UNQUOTED) != 0;
+
+ /* Quoting directives are mutually exclusive */
+ if (unquoted && quote_always) {
+ errno = EINVAL;
+ return (-1);
+ }
+
+ /* Reset per-option results and reject empty values up front */
+ for (n = 0; options[n].directive != NULL; n++) {
+ options[n].result = 0;
+ options[n].line = 0;
+ if (!emptyok && options[n].action == BSDCONF_ACTION_SET_VALUE &&
+ bsdconf_value_empty(&options[n])) {
+ errno = EINVAL;
+ return (-1);
+ }
+ }
+
+ /* Resolve the file path */
+ if (realpath(path, rpath) == NULL)
+ return (-1);
+
+ /*
+ * Open the original for reading. Only a regular file can be
+ * atomically replaced, so anything else is rejected below;
+ * O_NONBLOCK makes that check reachable (it is a no-op for regular
+ * files, but without it opening a fifo blocks awaiting a writer).
+ */
+ if ((fd = open(rpath, O_RDONLY | O_NONBLOCK)) < 0)
+ return (-1);
+ if (fstat(fd, &sb) != 0)
+ goto cleanup;
+ if (!S_ISREG(sb.st_mode)) {
+ errno = EINVAL;
+ goto cleanup;
+ }
+
+ /* Slurp the original into memory */
+ if ((buf = bsdconf_readfile(fd, (size_t)sb.st_size, &buflen)) == NULL)
+ goto cleanup;
+
+ /*
+ * Walk the original statement by statement. The replacement temporary
+ * is created lazily on the first real edit (see bsdconf_ensure_tmp());
+ * CHECK and equal-value paths never open it. Bytes are emitted in
+ * order via the write cursor `wc'; a matched statement diverts
+ * around the region it edits or removes.
+ */
+ wc = 0;
+ i = 0;
+ while (bsdconf_scan(buf, buflen, &i, &line, bequals, bsemicolon,
+ strict_equals, operator_equals, &st)) {
+ /* Statements without an `=' are unwritable targets */
+ if (require_equals && !st.have_equals)
+ continue;
+
+ /* Locate the option (if any) matching this directive */
+ option = NULL;
+ for (n = 0; options[n].directive != NULL; n++) {
+ if (!bsdconf_dir_matches(buf, st.dir_start, st.dir_end,
+ options[n].directive, case_sensitive))
+ continue;
+ /*
+ * A non-zero match_line selects one physical
+ * statement (e.g. make(1) `+=' strike/edit of the
+ * last assignment containing a word).
+ */
+ if (options[n].match_line != 0 &&
+ options[n].match_line != st.line)
+ continue;
+ option = &options[n];
+ break;
+ }
+ if (option == NULL)
+ continue; /* untouched; bytes flushed later */
+
+ /*
+ * make(1)-style `+=': when match_line is unset, never rewrite
+ * an existing statement and do not mark the directive FOUND,
+ * so a fresh `name+=value' line is appended below. Existing
+ * assignments for the same name are left intact so make's
+ * cumulative semantics are preserved. With match_line set,
+ * rewrite (or remove) that specific statement instead. The
+ * caller (sysconf(8)) skips the put entirely when an
+ * identical `+=value' line is already present.
+ */
+ if (operator_equals &&
+ option->action == BSDCONF_ACTION_SET_VALUE &&
+ option->op == BSDCONF_OP_APPEND &&
+ option->match_line == 0)
+ continue;
+
+ /* Enforce the no-duplicates policy */
+ if (nodup && (option->result & BSDCONF_DIRECTIVE_FOUND) != 0) {
+ errno = EEXIST;
+ goto cleanup;
+ }
+ if ((option->result & BSDCONF_DIRECTIVE_FOUND) == 0)
+ option->line = st.line;
+ option->result |= BSDCONF_DIRECTIVE_FOUND;
+
+ if (option->action == BSDCONF_ACTION_REMOVE) {
+ size_t rm_start;
+ size_t rm_end;
+ size_t k;
+ int done = 0;
+ int first_on_line = 1;
+
+ /*
+ * Is this the first statement on its physical line?
+ * With BSDCONF_BREAK_ON_SEMICOLON an earlier
+ * statement may precede it on the same line.
+ */
+ for (k = st.line_start; k < st.dir_start; k++)
+ if (!isspace((unsigned char)buf[k])) {
+ first_on_line = 0;
+ break;
+ }
+
+ /*
+ * If a further statement follows on the same line
+ * (terminator is a semicolon with a directive after
+ * it), drop this statement and the trailing
+ * separator, leaving the neighbour intact.
+ */
+ if (bsemicolon && st.term < buflen &&
+ buf[st.term] == ';') {
+ size_t f = st.term + 1;
+
+ while (f < buflen &&
+ isspace((unsigned char)buf[f]) &&
+ buf[f] != '\n')
+ f++;
+ if (f < buflen && buf[f] != '\n' &&
+ buf[f] != '#') {
+ rm_start = st.dir_start;
+ rm_end = f;
+ done = 1;
+ }
+ }
+
+ if (!done && first_on_line) {
+ /* Alone on the line: drop the whole line */
+ rm_start = st.line_start;
+ rm_end = st.line_end;
+ } else if (!done) {
+ /*
+ * Last on a shared line: drop the preceding
+ * separator (whitespace, `;', whitespace)
+ * along with this statement, keeping the
+ * terminator.
+ */
+ size_t s = st.dir_start;
+
+ while (s > st.line_start &&
+ isspace((unsigned char)buf[s - 1]))
+ s--;
+ if (s > st.line_start && buf[s - 1] == ';')
+ s--;
+ while (s > st.line_start &&
+ isspace((unsigned char)buf[s - 1]))
+ s--;
+ rm_start = s;
+ rm_end = st.term;
+ }
+
+ /* Never rewind before already-emitted bytes */
+ if (rm_start < wc)
+ rm_start = wc;
+ if (bsdconf_ensure_tmp(&tmpfd, tpath, sizeof(tpath),
+ rpath, &sb) != 0)
+ goto cleanup;
+ if (bsdconf_emit(tmpfd, buf + wc, rm_start - wc,
+ &last_ch) != 0)
+ goto cleanup;
+ if (rm_end > wc)
+ wc = rm_end;
+ option->result |= BSDCONF_DIRECTIVE_REMOVED;
+ continue;
+ }
+
+ /* SET_VALUE and CHECK both need the formatted value */
+ free(val);
+ if ((val = bsdconf_format_value(option, unquoted,
+ quote_always, bsemicolon)) == NULL)
+ goto cleanup;
+ vlen = strlen(val);
+
+ /* Does the assignment operator need to be rewritten? */
+ opchange = operator_equals &&
+ option->op != BSDCONF_OP_DEFAULT && st.have_equals &&
+ st.op != option->op;
+
+ /* Compare against the value currently in the file */
+ n = st.val_end - st.val_start;
+ if (!opchange && vlen == n && (n == 0 ||
+ memcmp(val, buf + st.val_start, n) == 0)) {
+ /* Unchanged; nothing to do for either action */
+ continue;
+ }
+
+ if (option->action == BSDCONF_ACTION_CHECK) {
+ option->result |= BSDCONF_VALUE_CHANGED;
+ continue;
+ }
+
+ /* BSDCONF_ACTION_SET_VALUE and an edit is required */
+ if (bsdconf_ensure_tmp(&tmpfd, tpath, sizeof(tpath),
+ rpath, &sb) != 0)
+ goto cleanup;
+ if (opchange) {
+ /* Rewrite the operator token in place */
+ if (bsdconf_emit(tmpfd, buf + wc, st.op_start - wc,
+ &last_ch) != 0)
+ goto cleanup;
+ if (bsdconf_emit(tmpfd, bsdconf_op_token(option->op),
+ strlen(bsdconf_op_token(option->op)),
+ &last_ch) != 0)
+ goto cleanup;
+ wc = st.eq + 1;
+ }
+
+ if (st.have_value) {
+ /* Replace the existing value in place */
+ if (bsdconf_emit(tmpfd, buf + wc, st.val_start - wc,
+ &last_ch) != 0)
+ goto cleanup;
+ if (bsdconf_emit(tmpfd, val, vlen, &last_ch) != 0)
+ goto cleanup;
+ wc = st.val_end;
+ } else if (option->type != BSDCONF_TYPE_NONE) {
+ /* Insert a value onto a value-less directive */
+ if (bsdconf_emit(tmpfd, buf + wc, st.val_start - wc,
+ &last_ch) != 0)
+ goto cleanup;
+ /* Supply a separator unless one already precedes */
+ if (st.val_start == wc ||
+ (buf[st.val_start - 1] != '=' &&
+ !isspace((unsigned char)buf[st.val_start - 1]))) {
+ sep = st.have_equals || require_equals ||
+ bequals ? "=" : " ";
+ if (bsdconf_emit(tmpfd, sep, 1,
+ &last_ch) != 0)
+ goto cleanup;
+ }
+ if (bsdconf_emit(tmpfd, val, vlen, &last_ch) != 0)
+ goto cleanup;
+ wc = st.val_start;
+ }
+ option->result |= BSDCONF_VALUE_CHANGED;
+ }
+
+ /*
+ * Append any SET_VALUE directives that were not found, and finalize
+ * the result flags for CHECK directives that were absent. The tail of
+ * the original is flushed only when a temporary already exists or an
+ * append forces one into being.
+ */
+ for (n = 0; options[n].directive != NULL; n++) {
+ option = &options[n];
+ if ((option->result & BSDCONF_DIRECTIVE_FOUND) != 0)
+ continue;
+ if (option->action == BSDCONF_ACTION_CHECK) {
+ /* Absent means it differs from the desired value */
+ option->result |= BSDCONF_VALUE_CHANGED;
+ continue;
+ }
+ if (option->action != BSDCONF_ACTION_SET_VALUE)
+ continue; /* REMOVE of an absent directive: no-op */
+
+ if (bsdconf_ensure_tmp(&tmpfd, tpath, sizeof(tpath),
+ rpath, &sb) != 0)
+ goto cleanup;
+ /* Flush any unread prefix once before the first append */
+ if (wc < buflen) {
+ if (bsdconf_emit(tmpfd, buf + wc, buflen - wc,
+ &last_ch) != 0)
+ goto cleanup;
+ wc = buflen;
+ }
+
+ /* Ensure the emitted output ends with a newline first */
+ if (last_ch != -1 && last_ch != '\n') {
+ if (bsdconf_emit(tmpfd, "\n", 1, &last_ch) != 0)
+ goto cleanup;
+ }
+
+ if (bsdconf_emit(tmpfd, option->directive,
+ strlen(option->directive), &last_ch) != 0)
+ goto cleanup;
+ if (option->type != BSDCONF_TYPE_NONE) {
+ free(val);
+ if ((val = bsdconf_format_value(option, unquoted,
+ quote_always, bsemicolon)) == NULL)
+ goto cleanup;
+ if (operator_equals &&
+ option->op != BSDCONF_OP_DEFAULT)
+ sep = bsdconf_op_token(option->op);
+ else
+ sep = (bequals || require_equals) ? "=" : " ";
+ if (bsdconf_emit(tmpfd, sep, strlen(sep),
+ &last_ch) != 0)
+ goto cleanup;
+ if (bsdconf_emit(tmpfd, val, strlen(val),
+ &last_ch) != 0)
+ goto cleanup;
+ }
+ if (bsdconf_emit(tmpfd, "\n", 1, &last_ch) != 0)
+ goto cleanup;
+ /*
+ * Line number of the statement appended at EOF: one past
+ * the number of newlines in the original, or the next line
+ * if we have to emit a separating newline first.
+ */
+ {
+ uint32_t al = 1;
+ size_t k;
+
+ for (k = 0; k < buflen; k++)
+ if (buf[k] == '\n')
+ al++;
+ if (buflen > 0 && buf[buflen - 1] != '\n')
+ al++;
+ option->line = al;
+ }
+ option->result |=
+ BSDCONF_DIRECTIVE_ADDED | BSDCONF_VALUE_CHANGED;
+ }
+
+ /*
+ * Nothing changed: every option was CHECK or an equal-value
+ * SET_VALUE/REMOVE-miss. Leave the original file alone.
+ */
+ if (tmpfd < 0) {
+ rv = 0;
+ goto cleanup;
+ }
+
+ /* Flush any remaining unread tail of the original */
+ if (bsdconf_emit(tmpfd, buf + wc, buflen - wc, &last_ch) != 0)
+ goto cleanup;
+
+ /* Optionally back up the original before replacing it */
+ if (backup) {
+ char bpath[PATH_MAX];
+ int bfd;
+
+ if (snprintf(bpath, sizeof(bpath), "%s.bak", rpath) >=
+ (int)sizeof(bpath)) {
+ errno = ENAMETOOLONG;
+ goto cleanup;
+ }
+ /*
+ * O_NOFOLLOW: refuse to follow a planted symbolic link
+ * lest the backup clobber whatever it points at.
+ */
+ if ((bfd = open(bpath, O_WRONLY | O_CREAT | O_TRUNC |
+ O_NOFOLLOW, sb.st_mode & ALLPERMS)) < 0)
+ goto cleanup;
+ if (bsdconf_writeall(bfd, buf, buflen) != 0) {
+ saved_errno = errno;
+ close(bfd);
+ errno = saved_errno;
+ goto cleanup;
+ }
+ if (close(bfd) != 0)
+ goto cleanup;
+ }
+
+ /* Commit: flush to disk, then atomically replace the original */
+ if (fsync(tmpfd) != 0)
+ goto cleanup;
+ if (close(tmpfd) != 0) {
+ tmpfd = -1;
+ goto cleanup;
+ }
+ tmpfd = -1;
+ if (rename(tpath, rpath) != 0)
+ goto cleanup;
+ tpath[0] = '\0'; /* renamed away; nothing to unlink */
+
+ /* Best-effort: persist the directory entry change */
+ if ((slash = strrchr(rpath, '/')) != NULL) {
+ char dpath[PATH_MAX];
+ size_t dlen = (size_t)(slash - rpath);
+
+ if (dlen == 0)
+ dlen = 1; /* the root directory */
+ if (dlen < sizeof(dpath)) {
+ memcpy(dpath, rpath, dlen);
+ dpath[dlen] = '\0';
+ if ((dirfd = open(dpath, O_RDONLY)) >= 0) {
+ (void)fsync(dirfd);
+ close(dirfd);
+ dirfd = -1;
+ }
+ }
+ }
+
+ rv = 0;
+
+cleanup:
+ saved_errno = errno;
+ if (fd >= 0)
+ close(fd);
+ if (tmpfd >= 0)
+ close(tmpfd);
+ if (dirfd >= 0)
+ close(dirfd);
+ if (rv != 0 && tpath[0] != '\0')
+ (void)unlink(tpath); /* discard the incomplete temporary */
+ free(buf);
+ free(val);
+ errno = saved_errno;
+ return (rv);
+}
diff --git a/lib/libbsdconf/bsdconf_stmt.c b/lib/libbsdconf/bsdconf_stmt.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_stmt.c
@@ -0,0 +1,543 @@
+/*
+ * Copyright (c) 2015-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Statement scanning, value rendering, and related helpers used by
+ * bsdconf_put(). Kept separate so bsdconf_put.c stays focused on the
+ * rewrite driver.
+ */
+
+#include <sys/stat.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <unistd.h>
+
+#include "bsdconf.h"
+#include "bsdconf_internal.h"
+
+/*
+ * Map an assignment operator to its config file token. BSDCONF_OP_DEFAULT
+ * maps to the plain `=' token.
+ */
+const char *
+bsdconf_op_token(enum bsdconf_op op)
+{
+ switch (op) {
+ case BSDCONF_OP_APPEND:
+ return ("+=");
+ case BSDCONF_OP_COND:
+ return ("?=");
+ case BSDCONF_OP_EXPAND:
+ return (":=");
+ case BSDCONF_OP_SHELL:
+ return ("!=");
+ case BSDCONF_OP_DEFAULT:
+ case BSDCONF_OP_ASSIGN:
+ default:
+ return ("=");
+ }
+}
+
+/*
+ * Write exactly `len' bytes from `data' to `fd', restarting short and
+ * interrupted writes. Returns zero on success; -1 (with errno set) on error.
+ * Shared with bsdconf_spool() (see bsdconf_internal.h).
+ */
+int
+bsdconf_writeall(int fd, const void *data, size_t len)
+{
+ const char *p = data;
+ ssize_t w;
+
+ while (len > 0) {
+ w = write(fd, p, len);
+ if (w < 0) {
+ if (errno == EINTR)
+ continue;
+ return (-1);
+ }
+ p += w;
+ len -= (size_t)w;
+ }
+ return (0);
+}
+
+/*
+ * Read the entire contents of the open descriptor `fd' (whose size is `size')
+ * into a freshly allocated, NUL-terminated buffer. The actual number of bytes
+ * read is stored through `lenp'. Returns the buffer on success (which the
+ * caller must free) or NULL (with errno set) on error.
+ */
+char *
+bsdconf_readfile(int fd, size_t size, size_t *lenp)
+{
+ char *buf;
+ size_t off = 0;
+ ssize_t r;
+
+ if ((buf = malloc(size + 1)) == NULL)
+ return (NULL);
+
+ while (off < size) {
+ r = read(fd, buf + off, size - off);
+ if (r < 0) {
+ if (errno == EINTR)
+ continue;
+ free(buf);
+ return (NULL);
+ }
+ if (r == 0) /* premature EOF (file shrank); stop */
+ break;
+ off += (size_t)r;
+ }
+
+ buf[off] = '\0';
+ *lenp = off;
+ return (buf);
+}
+
+/*
+ * Write `len' bytes to `fd' and, when any bytes are written, remember the
+ * last one through `last' (as an unsigned char, or left untouched for a
+ * zero-length write). This lets the caller track whether the output so far
+ * ends in a newline without having to re-read the descriptor. Returns zero
+ * on success; -1 (with errno set) on error.
+ */
+int
+bsdconf_emit(int fd, const void *data, size_t len, int *last)
+{
+
+ if (len == 0)
+ return (0);
+ if (bsdconf_writeall(fd, data, len) != 0)
+ return (-1);
+ *last = (unsigned char)((const char *)data)[len - 1];
+ return (0);
+}
+
+/*
+ * Lazily create the replacement temporary in the target's directory. Called
+ * on the first edit so that CHECK-only and equal-value SET_VALUE paths never
+ * open a writable descriptor (and never bump mtime or sever hard links).
+ */
+int
+bsdconf_ensure_tmp(int *tmpfdp, char *tpath, size_t tpathsz,
+ const char *rpath, const struct stat *sb)
+{
+ char *slash;
+
+ if (*tmpfdp >= 0)
+ return (0);
+
+ if ((slash = strrchr(rpath, '/')) != NULL) {
+ if (snprintf(tpath, tpathsz, "%.*s/.bsdconf.XXXXXXXXXX",
+ (int)(slash - rpath), rpath) >= (int)tpathsz) {
+ errno = ENAMETOOLONG;
+ return (-1);
+ }
+ } else if (snprintf(tpath, tpathsz, ".bsdconf.XXXXXXXXXX") >=
+ (int)tpathsz) {
+ errno = ENAMETOOLONG;
+ return (-1);
+ }
+ if ((*tmpfdp = mkstemp(tpath)) == -1) {
+ tpath[0] = '\0';
+ return (-1);
+ }
+ if (fchmod(*tmpfdp, sb->st_mode & ALLPERMS) != 0)
+ return (-1);
+ (void)fchown(*tmpfdp, sb->st_uid, sb->st_gid); /* best effort */
+ return (0);
+}
+
+/*
+ * Determine whether the string value `s' must be double-quoted on output so
+ * that it round-trips through the parser unchanged. Empty strings are quoted
+ * so that they remain distinguishable from a value-less directive.
+ */
+static int
+bsdconf_needs_quote(const char *s, int bsemicolon)
+{
+ const char *p;
+
+ if (*s == '\0')
+ return (1);
+ for (p = s; *p != '\0'; p++) {
+ if (isspace((unsigned char)*p) || *p == '#' || *p == '"' ||
+ *p == '\\')
+ return (1);
+ if (bsemicolon && *p == ';')
+ return (1);
+ }
+ return (0);
+}
+
+/*
+ * Determine whether the string value `s' round-trips through the parser
+ * when written verbatim (no quoting available; make.conf(5) semantics).
+ * Embedded whitespace is fine; what is not is an embedded newline, a `#'
+ * (or, with `bsemicolon', a `;') at an unquoted position -- either would
+ * terminate the value early on re-parse -- or a trailing unescaped
+ * backslash, which would escape the statement's own newline.
+ *
+ * After counting a run of backslashes, `p' already points at the following
+ * byte. An odd-length run escapes that byte (skip it); an even-length run
+ * leaves it unescaped, so it must be re-examined by the loop body below
+ * (do not `continue', which would advance past it a second time).
+ */
+static int
+bsdconf_verbatim_ok(const char *s, int bsemicolon)
+{
+ const char *p;
+ int quote = 0;
+ size_t nbs;
+
+ for (p = s; *p != '\0'; p++) {
+ if (*p == '\\') {
+ for (nbs = 0; *p == '\\'; p++)
+ nbs++;
+ if (*p == '\0')
+ return ((nbs & 1) == 0);
+ if ((nbs & 1) != 0) {
+ /* Odd run: the following byte is escaped */
+ if (*p == '\n')
+ return (0);
+ continue;
+ }
+ /* Even run: *p is unescaped; fall through */
+ }
+ if (*p == '\n')
+ return (0);
+ if (*p == '"')
+ quote = !quote;
+ else if (!quote &&
+ (*p == '#' || (bsemicolon && *p == ';')))
+ return (0);
+ }
+ return (1);
+}
+
+/*
+ * Produce the textual value to be written for `option', following the value
+ * model documented in bsdconf.h: the value is always taken from value.str and
+ * the type governs only whether quoting/escaping is applied.
+ *
+ * With `quote_always' (loader.conf(5) semantics) every value is enclosed in
+ * double-quotes with embedded quotes and backslashes escaped. With `unquoted'
+ * (make.conf(5) semantics / BSDCONF_PUT_UNQUOTED) `value.str' is emitted as
+ * literal file text -- no quotes are added and no characters are
+ * backslash-escaped -- so the caller supplies the bytes that should appear
+ * on disk. Embedded whitespace is fine (the value runs to the end of the
+ * line); a value that could not round-trip under the parser (embedded
+ * newline, unescaped comment marker, or trailing unescaped backslash) is
+ * rejected (errno = EINVAL) rather than silently corrupting the file. Note
+ * the asymmetry with the reader, which still runs bsdconf_strunexpand() on
+ * input: escape sequences present in the file become the logical value on
+ * read, but this path does not re-encode them. Otherwise (sysctl.conf(5)
+ * and generic semantics), values are quoted only when required.
+ *
+ * Returns a newly allocated NUL-terminated string (which the caller must
+ * free) or NULL (with errno set) on failure.
+ */
+char *
+bsdconf_format_value(const struct bsdconf_option *option, int unquoted,
+ int quote_always, int bsemicolon)
+{
+ const char *s;
+ char *out;
+ char *q;
+ size_t extra;
+ size_t i;
+ size_t slen;
+
+ if (option->type == BSDCONF_TYPE_NONE)
+ return (strdup(""));
+
+ s = option->value.str != NULL ? option->value.str : "";
+
+ if (unquoted) {
+ if (!bsdconf_verbatim_ok(s, bsemicolon)) {
+ errno = EINVAL;
+ return (NULL);
+ }
+ return (strdup(s));
+ }
+
+ if (!quote_always) {
+ if (option->type == BSDCONF_TYPE_INT ||
+ option->type == BSDCONF_TYPE_UINT ||
+ option->type == BSDCONF_TYPE_INT64 ||
+ option->type == BSDCONF_TYPE_UINT64 ||
+ option->type == BSDCONF_TYPE_BOOL)
+ return (strdup(s));
+ if (!bsdconf_needs_quote(s, bsemicolon))
+ return (strdup(s));
+ }
+
+ /* Count the characters that require a backslash escape */
+ slen = strlen(s);
+ extra = 0;
+ for (i = 0; i < slen; i++)
+ if (s[i] == '"' || s[i] == '\\')
+ extra++;
+
+ /* Enclosing quotes plus escapes plus terminator */
+ if ((out = malloc(slen + extra + 3)) == NULL)
+ return (NULL);
+ q = out;
+ *q++ = '"';
+ for (i = 0; i < slen; i++) {
+ if (s[i] == '"' || s[i] == '\\')
+ *q++ = '\\';
+ *q++ = s[i];
+ }
+ *q++ = '"';
+ *q = '\0';
+ return (out);
+}
+
+/*
+ * Test whether a SET_VALUE/CHECK value is considered "empty" for the
+ * purposes of BSDCONF_PUT_ALLOW_EMPTY.
+ */
+int
+bsdconf_value_empty(const struct bsdconf_option *option)
+{
+
+ return (option->type != BSDCONF_TYPE_NONE &&
+ (option->value.str == NULL || option->value.str[0] == '\0'));
+}
+
+/*
+ * Match the directive spanning buf[start, end) against the directive of a
+ * bsdconf_option. Comparison is exact (whole-token); unlike bsdconf_parse(),
+ * bsdconf_put() does not glob-match, since a pattern is not a writable
+ * target.
+ */
+int
+bsdconf_dir_matches(const char *buf, size_t start, size_t end,
+ const char *directive, int case_sensitive)
+{
+ size_t len = end - start;
+
+ if (strlen(directive) != len)
+ return (0);
+ if (case_sensitive)
+ return (strncmp(buf + start, directive, len) == 0);
+ return (strncasecmp(buf + start, directive, len) == 0);
+}
+
+/*
+ * Tokenize the statement beginning at buf[*ip], advancing *ip past it and
+ * filling in `st'. The scan mirrors bsdconf_fparse() exactly (comment,
+ * quote, operator, and backslash-escape handling included) so that a file
+ * written by bsdconf_put() re-parses identically. Returns 1 if a statement
+ * was found, or 0 at end of input.
+ */
+int
+bsdconf_scan(const char *buf, size_t len, size_t *ip, uint32_t *linep,
+ int bequals, int bsemicolon, int strict_equals, int operator_equals,
+ struct bsdconf_stmt *st)
+{
+ size_t i = *ip;
+ size_t j;
+ uint32_t line = *linep;
+ int comment = 0;
+ int novalue = 0;
+ int quote;
+
+ /* Skip whitespace and comments to the beginning of a directive */
+ st->line_start = i;
+ while (i < len && (isspace((unsigned char)buf[i]) || buf[i] == '#' ||
+ comment || (bsemicolon && buf[i] == ';'))) {
+ if (buf[i] == '#')
+ comment = 1;
+ else if (buf[i] == '\n') {
+ comment = 0;
+ line++;
+ st->line_start = i + 1;
+ }
+ i++;
+ }
+ if (i >= len) {
+ *ip = i;
+ *linep = line;
+ return (0);
+ }
+
+ st->line = line;
+ st->dir_start = i;
+ st->have_equals = 0;
+ st->op = BSDCONF_OP_DEFAULT;
+ st->eq = 0;
+
+ /* Find the end of the directive */
+ while (i < len) {
+ if (isspace((unsigned char)buf[i]))
+ break;
+ if (bequals && buf[i] == '=') {
+ st->have_equals = 1;
+ break;
+ }
+ if (bsemicolon && buf[i] == ';')
+ break;
+ i++;
+ }
+ st->dir_end = i;
+ st->op_start = i;
+
+ /*
+ * Split a make(1)-style operator (`+=' `?=' `:=' `!=') off the tail
+ * of the directive if requested; the operator character rode along
+ * with the directive because only the `=' terminates the scan.
+ */
+ if (st->have_equals) {
+ st->eq = i;
+ st->op = BSDCONF_OP_ASSIGN;
+ if (operator_equals && st->dir_end - st->dir_start > 1) {
+ switch (buf[st->dir_end - 1]) {
+ case '+': st->op = BSDCONF_OP_APPEND; break;
+ case '?': st->op = BSDCONF_OP_COND; break;
+ case ':': st->op = BSDCONF_OP_EXPAND; break;
+ case '!': st->op = BSDCONF_OP_SHELL; break;
+ }
+ if (st->op != BSDCONF_OP_ASSIGN) {
+ st->dir_end--;
+ st->op_start = st->dir_end;
+ }
+ }
+ }
+
+ /* Step over an `=' acting as the directive terminator */
+ if (bequals && i < len && buf[i] == '=') {
+ i++;
+ if (strict_equals && i < len &&
+ isspace((unsigned char)buf[i]) && buf[i] != '\n')
+ novalue = 1; /* strict `=' then space: no value */
+ }
+
+ /* Advance across separating whitespace to the value */
+ if (!novalue && !(bsemicolon && i < len && buf[i] == ';') &&
+ !(strict_equals && i < len && buf[i] == '=')) {
+ while (i < len && isspace((unsigned char)buf[i]) &&
+ buf[i] != '\n')
+ i++;
+ }
+
+ /* Consume an `=' surrounded by whitespace (non-strict) */
+ if (!novalue && i < len && bequals && buf[i] == '=' &&
+ !strict_equals) {
+ st->have_equals = 1;
+ st->eq = i;
+ if (st->op == BSDCONF_OP_DEFAULT)
+ st->op = BSDCONF_OP_ASSIGN;
+ i++;
+ while (i < len && isspace((unsigned char)buf[i]) &&
+ buf[i] != '\n')
+ i++;
+ }
+
+ /* A directive with no value */
+ if (novalue || i >= len || buf[i] == '\n' || buf[i] == '#' ||
+ (bsemicolon && buf[i] == ';')) {
+ st->have_value = 0;
+ st->val_start = st->val_end = i;
+ } else {
+ st->have_value = 1;
+ st->val_start = i;
+ quote = 0;
+ while (i < len) {
+ char c = buf[i];
+
+ if (c != '"' && c != '#' && c != '\n' &&
+ (!bsemicolon || c != ';')) {
+ i++;
+ continue;
+ }
+
+ /* Count the backslashes immediately preceding `c' */
+ j = i;
+ while (j > st->val_start && buf[j - 1] == '\\')
+ j--;
+
+ if (((i - j) & 1) == 0) { /* not escaped */
+ if (c == '"') {
+ quote = !quote;
+ i++;
+ continue;
+ }
+ if (c == '#') {
+ if (!quote)
+ break;
+ i++;
+ continue;
+ }
+ if (c == '\n')
+ break;
+ if (c == ';') {
+ if (!quote && bsemicolon)
+ break;
+ i++;
+ continue;
+ }
+ } else { /* escaped: part of the value */
+ if (c == '\n')
+ line++;
+ i++;
+ continue;
+ }
+ }
+ /* Trim trailing whitespace from the value */
+ st->val_end = i;
+ while (st->val_end > st->val_start &&
+ isspace((unsigned char)buf[st->val_end - 1]))
+ st->val_end--;
+ }
+
+ /* Record the terminator position and the end of the physical line */
+ st->term = i;
+ st->line_end = i;
+ while (st->line_end < len && buf[st->line_end] != '\n')
+ st->line_end++;
+ if (st->line_end < len)
+ st->line_end++;
+
+ *ip = i;
+ *linep = line;
+ return (1);
+}
+
+/*
+ * Search for a config option (struct bsdconf_option) in the array of config
+ * options and stage the value of the struct whose directive matches the
+ * given parameter. On success, returns 1, otherwise 0.
+ */
+int
+bsdconf_set_option(struct bsdconf_option options[], const char *directive,
+ union bsdconf_value *value)
+{
+ uint32_t n;
+
+ /* Check arguments */
+ if (options == NULL || directive == NULL || value == NULL)
+ return (0);
+
+ /* Loop through the array, staging the first match */
+ for (n = 0; options[n].directive != NULL; n++) {
+ if (strcmp(options[n].directive, directive) == 0) {
+ options[n].value = *value;
+ return (1);
+ }
+ }
+
+ return (0);
+}
diff --git a/lib/libbsdconf/bsdconf_string.c b/lib/libbsdconf/bsdconf_string.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_string.c
@@ -0,0 +1,270 @@
+/*
+ * Copyright (c) 2001-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "bsdconf_internal.h"
+
+/*
+ * Counts the number of occurrences of one string that appear in the source
+ * string. Return value is the total count.
+ *
+ * An example use would be if you need to know how large a buffer needs to be
+ * for a bsdconf_replaceall() series.
+ */
+unsigned int
+bsdconf_strcount(const char *source, const char *find)
+{
+ const char *p = source;
+ size_t flen;
+ unsigned int n = 0;
+
+ /* Both parameters are required */
+ if (source == NULL || find == NULL)
+ return (0);
+
+ /* Cache the length of find element */
+ flen = strlen(find);
+ if (strlen(source) == 0 || flen == 0)
+ return (0);
+
+ /* Loop until the end of the string */
+ while (*p != '\0') {
+ if (strncmp(p, find, flen) == 0) { /* found an instance */
+ p += flen;
+ n++;
+ } else
+ p++;
+ }
+
+ return (n);
+}
+
+/*
+ * Replaces all occurrences of `find' in `buf' with `replace'.
+ *
+ * `buf' must point to a mutable buffer of at least `buflen' bytes (including
+ * space for the terminating NUL). A string constant will not compile as the
+ * first argument without a cast; do not cast one in. A local or global
+ * non-const array is fine.
+ *
+ * If `replace' is longer than `find', the result grows. Pass a `buflen' large
+ * enough for the expanded result (bsdconf_strcount() can size it); if the
+ * result would not fit, -1 is returned with errno set to ENOSPC and `buf' is
+ * left unmodified. On success the return value is the length (in bytes) of
+ * the result, not counting the terminating NUL.
+ *
+ * When an error occurs, -1 is returned and the global variable errno is set
+ * accordingly.
+ */
+int
+bsdconf_replaceall(char *buf, size_t buflen, const char *find,
+ const char *replace)
+{
+ char *dst, *temp;
+ const char *src;
+ size_t flen, need, rlen, slen;
+ unsigned int n;
+
+ /* Check that we have non-null parameters */
+ if (buf == NULL)
+ return (0);
+ if (find == NULL)
+ return (strlen(buf));
+
+ /* Cache the length of the strings */
+ slen = strlen(buf);
+ flen = strlen(find);
+ rlen = replace ? strlen(replace) : 0;
+
+ /* Cases where no replacements need to be made */
+ if (slen == 0 || flen == 0 || slen < flen)
+ return (slen);
+
+ /*
+ * Ensure the result fits. Shrinking replacements always fit in the
+ * existing string; growing ones need room for each expansion.
+ */
+ if (rlen > flen) {
+ n = bsdconf_strcount(buf, find);
+ need = slen + (size_t)n * (rlen - flen) + 1;
+ } else
+ need = slen + 1;
+ if (need > buflen) {
+ errno = ENOSPC;
+ return (-1);
+ }
+
+ /* If replace is longer than find, we'll need to create a temp copy */
+ if (rlen > flen) {
+ temp = malloc(slen + 1);
+ if (temp == NULL) /* could not allocate memory */
+ return (-1);
+ memcpy(temp, buf, slen + 1);
+ } else
+ temp = buf;
+
+ /* Reconstruct the string with the replacements */
+ dst = buf;
+ src = temp;
+
+ while (*src != '\0') {
+ if (strncmp(src, find, flen) == 0) {
+ /* found an occurrence */
+ for (n = 0; replace && replace[n]; n++)
+ *dst++ = replace[n];
+ src += flen;
+ } else
+ *dst++ = *src++; /* copy character and increment */
+ }
+
+ /* Terminate the string */
+ *dst = '\0';
+
+ /* Free the temporary allocated memory */
+ if (temp != buf)
+ free(temp);
+
+ /* Return the length of the completed string */
+ return (strlen(buf));
+}
+
+/*
+ * Unexpands (collapses) C-style escape sequences in `src' into `dst'.
+ *
+ * The result is never longer than the input, so `dst' may be the same buffer
+ * as `src' (cf. strunvis(3)). Do not pass a string constant as `dst'; a local
+ * or global non-const array is fine.
+ *
+ * Interpreted sequences are:
+ *
+ * \NNN character with octal value NNN (1 to 3 digits)
+ * \a alert (BEL)
+ * \b backspace
+ * \f form feed
+ * \n new line
+ * \r carriage return
+ * \t horizontal tab
+ * \v vertical tab
+ * \xNN byte with hexadecimal value NN (1 to 2 digits)
+ *
+ * All other sequences are unescaped (ie. '\"' and '\#'). A trailing backslash
+ * or a `\x' with no following hex digits is emitted verbatim (the backslash
+ * is dropped for `\x', leaving `x').
+ */
+void
+bsdconf_strunexpand(char *dst, const char *src)
+{
+ char *d;
+ const char *s;
+ unsigned int n, v;
+ unsigned char c;
+
+ d = dst;
+ s = src;
+
+ /*
+ * Loop until we hit the end of the input. The stop condition must
+ * track the input cursor (s): collapsing an escape advances s ahead
+ * of the output cursor (d), so once the two diverge *d no longer
+ * reflects where the input terminates.
+ */
+ while (*s != '\0') {
+ if (*s != '\\') {
+ *d++ = *s++;
+ continue;
+ }
+
+ /*
+ * A backslash at the very end of the string escapes nothing
+ * (there is no next character); emit it verbatim and stop
+ * rather than read past the terminator.
+ */
+ if (*(s + 1) == '\0') {
+ *d++ = *s++;
+ break;
+ }
+
+ /* Replace the backslash with the correct character */
+ s++;
+ switch (*s) {
+ case 'a': *d = '\a'; break; /* bell/alert (BEL) */
+ case 'b': *d = '\b'; break; /* backspace */
+ case 'f': *d = '\f'; break; /* form feed */
+ case 'n': *d = '\n'; break; /* new line */
+ case 'r': *d = '\r'; break; /* carriage return */
+ case 't': *d = '\t'; break; /* horizontal tab */
+ case 'v': *d = '\v'; break; /* vertical tab */
+ case 'x': /* hex value (1 to 2 digits)(\xNN) */
+ v = 0;
+ n = 0;
+ while (n < 2) {
+ c = (unsigned char)*(s + 1);
+ if (c >= '0' && c <= '9')
+ v = (v << 4) + (c - '0');
+ else if (c >= 'A' && c <= 'F')
+ v = (v << 4) + (c - 'A' + 10);
+ else if (c >= 'a' && c <= 'f')
+ v = (v << 4) + (c - 'a' + 10);
+ else
+ break;
+ s++;
+ n++;
+ }
+ /* \x with no digits: emit the 'x' (unknown escape) */
+ *d = (n == 0) ? 'x' : (char)v;
+ break;
+ default: /* octal (\NNN, 1 to 3 digits) or unknown sequence */
+ if (*s >= '0' && *s <= '7') {
+ v = (unsigned int)(*s - '0');
+ n = 1;
+ while (n < 3 && *(s + 1) >= '0' &&
+ *(s + 1) <= '7') {
+ s++;
+ v = (v << 3) +
+ (unsigned int)(*s - '0');
+ n++;
+ }
+ *d = (char)v;
+ } else
+ *d = *s;
+ break;
+ }
+
+ /* Increment to next offset, possible next escape sequence */
+ d++;
+ s++;
+ }
+
+ /*
+ * Terminate at the (possibly earlier) output cursor. When any
+ * escape was collapsed the string shrank, so the trailing bytes
+ * between d and s are now stale and must be cut off here.
+ */
+ *d = '\0';
+}
+
+/*
+ * Convert a string to lower case. Pass a mutable buffer (a local or global
+ * non-const array is fine); do not pass a string constant.
+ */
+void
+bsdconf_strtolower(char *buf)
+{
+ char *p = buf;
+
+ if (buf == NULL)
+ return;
+
+ while (*p != '\0') {
+ *p = (char)tolower((unsigned char)*p);
+ p++;
+ }
+}
diff --git a/share/mk/bsd.libnames.mk b/share/mk/bsd.libnames.mk
--- a/share/mk/bsd.libnames.mk
+++ b/share/mk/bsd.libnames.mk
@@ -30,6 +30,7 @@
LIBBLOCKLIST?= ${LIBDESTDIR}${LIBDIR_BASE}/libblocklist.a
LIBBLOCKSRUNTIME?= ${LIBDESTDIR}${LIBDIR_BASE}/libBlocksRuntime.a
LIBBLUETOOTH?= ${LIBDESTDIR}${LIBDIR_BASE}/libbluetooth.a
+LIBBSDCONF?= ${LIBDESTDIR}${LIBDIR_BASE}/libbsdconf.a
LIBBSDXML?= ${LIBDESTDIR}${LIBDIR_BASE}/libbsdxml.a
LIBBSM?= ${LIBDESTDIR}${LIBDIR_BASE}/libbsm.a
LIBBSNMP?= ${LIBDESTDIR}${LIBDIR_BASE}/libbsnmp.a
diff --git a/share/mk/src.libnames.mk b/share/mk/src.libnames.mk
--- a/share/mk/src.libnames.mk
+++ b/share/mk/src.libnames.mk
@@ -118,6 +118,7 @@
be \
begemot \
bluetooth \
+ bsdconf \
bsdxml \
bsm \
bsnmp \
diff --git a/usr.sbin/Makefile b/usr.sbin/Makefile
--- a/usr.sbin/Makefile
+++ b/usr.sbin/Makefile
@@ -86,6 +86,7 @@
snapinfo \
spi \
spray \
+ sysconf \
syslogd \
sysrc \
tcpdrop \
diff --git a/usr.sbin/sysconf/Makefile b/usr.sbin/sysconf/Makefile
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/Makefile
@@ -0,0 +1,32 @@
+.include <src.opts.mk>
+
+PACKAGE= utilities
+PROG= sysconf
+MAN= sysconf.8 \
+ sysconf-generic.8 \
+ sysconf-loader.8 \
+ sysconf-make.8 \
+ sysconf-rc.8 \
+ sysconf-src.8 \
+ sysconf-sysctl.8 \
+ sysconf-targets.8
+
+SRCS= sysconf.c \
+ sysconf_edit_assign.c \
+ sysconf_edit_make.c \
+ sysconf_edit_write.c \
+ sysconf_print.c \
+ sysconf_query_desc.c \
+ sysconf_query_sysctl.c \
+ sysconf_query_value.c \
+ sysconf_resolve.c \
+ sysconf_scan.c
+
+CFLAGS+= -I${SRCTOP}/lib/libbsdconf -I${.CURDIR}
+
+LIBADD= bsdconf jail
+
+HAS_TESTS=
+SUBDIR.${MK_TESTS}+= tests
+
+.include <bsd.prog.mk>
diff --git a/usr.sbin/sysconf/Makefile.depend b/usr.sbin/sysconf/Makefile.depend
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/Makefile.depend
@@ -0,0 +1,17 @@
+# Autogenerated - do NOT edit!
+
+DIRDEPS = \
+ include \
+ include/xlocale \
+ lib/${CSU_DIR} \
+ lib/libbsdconf \
+ lib/libc \
+ lib/libcompiler_rt \
+ lib/libjail \
+
+
+.include <dirdeps.mk>
+
+.if ${DEP_RELDIR} == ${_DEP_RELDIR}
+# local dependencies - needed for -jN in clean tree
+.endif
diff --git a/usr.sbin/sysconf/sysconf-generic.8 b/usr.sbin/sysconf/sysconf-generic.8
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf-generic.8
@@ -0,0 +1,49 @@
+.\" Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+.\" Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.Dd August 2, 2026
+\
+.Dt SYSCONF-GENERIC 8
+.Os
+.Sh NAME
+.Nm "sysconf generic"
+.Nd configure a file of no built-in format
+.Sh SYNOPSIS
+.Nm sysconf
+.Cm generic
+.Fl f Ar file
+.Op Fl ceinNqvx
+.Op Fl j Ar jail | Fl R Ar dir
+.Ar name Ns Op Ns Oo +|- Oc Ns = Ns Ar value
+.Ar ...
+.Sh DESCRIPTION
+The
+.Cm generic
+target of
+.Xr sysconf 8
+exists for files matching no known format
+.Pq quote values only when required to round-trip .
+Having no default files of its own,
+it always requires
+.Fl f .
+The format is never inferred from the path;
+see
+.Xr sysconf-targets 8 .
+Shared options are in
+.Xr sysconf 8 .
+.Sh EXAMPLES
+.Dl sysconf generic -f /usr/local/etc/myapp.conf loglevel=debug
+.Sh SEE ALSO
+.Xr bsdconf_format 3 ,
+.Xr sysconf 8 ,
+.Xr sysconf-targets 8
+.Sh HISTORY
+The
+.Nm sysconf
+utility first appeared in
+.Fx 16.0 .
+.Sh AUTHORS
+.An Devin Teske Aq Mt dteske@FreeBSD.org
+.An Faraz Vahedi Aq Mt kfv@kfv.io
diff --git a/usr.sbin/sysconf/sysconf-loader.8 b/usr.sbin/sysconf/sysconf-loader.8
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf-loader.8
@@ -0,0 +1,151 @@
+.\" Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+.\" Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.Dd August 2, 2026
+\
+.Dt SYSCONF-LOADER 8
+.Os
+.Sh NAME
+.Nm "sysconf loader"
+.Nd configure boot loader directives
+.Sh SYNOPSIS
+.Nm sysconf
+.Cm loader
+.Op Fl AcDeFinNqvVx
+.Op Fl j Ar jail | Fl R Ar dir
+.Op Fl f Ar file
+.Ar name Ns Op Ns Oo +|- Oc Ns = Ns Ar value
+.Ar ...
+.Nm sysconf
+.Cm loader
+.Op Fl ADeFnNqvV
+.Op Fl f Ar file
+.Fl a
+.Nm sysconf
+.Cm loader
+.Op Fl AaDinNq
+.Fl d
+.Op Ar name ...
+.Nm sysconf
+.Cm loader
+.Op Fl E
+.Fl l | L
+.Sh DESCRIPTION
+The
+.Cm loader
+target of
+.Xr sysconf 8
+reads and modifies
+.Xr loader.conf 5
+configuration.
+Shared assignment syntax and options are documented in
+.Xr sysconf 8 ;
+multi-file write policy in
+.Xr sysconf-targets 8 .
+.Pp
+The file list is not hardcoded:
+just as the boot loader itself hardcodes only
+.Pa /boot/defaults/loader.conf ,
+.Nm sysconf
+names that defaults file and the
+.Va loader_conf_files ,
+.Va loader_conf_dirs ,
+and
+.Va local_loader_conf_files
+directives so that discovery matches the loader
+.Pq see Xr bsdconf_format 3 and Xr loader.conf 5 .
+On a stock system this resolves to
+.Pa /boot/device.hints ,
+.Pa /boot/loader.conf ,
+.Pa /boot/loader.conf.d/*.conf ,
+and
+.Pa /boot/loader.conf.local ,
+in that order;
+when the defaults file is missing,
+that stock list is assumed.
+.Pp
+Values written for this target are always enclosed in quotes
+.Pq quoted or unquoted input is accepted
+with no whitespace around the equals sign,
+as demanded by the boot loader's reader.
+.Pp
+The defaults file
+.Pa /boot/defaults/loader.conf
+.Pq or Ev LOADER_DEFAULTS
+additionally supports
+.Fl d ,
+.Fl D ,
+and
+.Fl A
+.Pq see Xr sysconf-targets 8 .
+Space-separated list directives such as
+.Va kernels
+use
+.Ar name Ns += Ns Ar word
+and
+.Ar name Ns -= Ns Ar word
+as described in
+.Xr sysconf 8 .
+.Sh ENVIRONMENT
+.Bl -tag -width "LOADER_DEFAULTS"
+.It Ev LOADER_DEFAULTS
+Defaults file consulted in place of
+.Pa /boot/defaults/loader.conf
+for file-list discovery and for
+.Fl d ,
+.Fl D ,
+and
+.Fl A
+.Po
+in the vein of
+.Ev RC_DEFAULTS
+in
+.Xr sysrc 8
+.Pc .
+The value is used verbatim
+.Pq not subject to Fl R .
+.El
+.Sh FILES
+.Bl -tag -width "/boot/defaults/loader.conf" -compact
+.It Pa /boot/defaults/loader.conf
+Defaults and the authority on the file list.
+.It Pa /boot/device.hints
+First in the stock
+.Va loader_conf_files
+list.
+.It Pa /boot/loader.conf
+Stock write target.
+.It Pa /boot/loader.conf.d/
+Drop-in directory.
+.It Pa /boot/loader.conf.local
+Site-local overrides,
+sourced last.
+.El
+.Sh EXAMPLES
+.Dl sysconf loader zfs_load
+.Dl sysconf loader zfs_load=\(dqYES\(dq
+.Dl sysconf loader -l
+.Dl sysconf loader -d verbose_loading
+.Dl sysconf loader -Ad
+.Dl sysconf loader -D verbose_loading
+.Dl sysconf loader -aD
+.Dl sysconf loader -A
+.Dl sysconf loader kernels+=kernel_test
+.Dl sysconf loader kernels-=kernel_test
+.Dl sysconf -a loader
+.Dl sysconf loader -f /tmp/loader.conf.new zfs_load=\(dqYES\(dq
+.Sh SEE ALSO
+.Xr bsdconf_format 3 ,
+.Xr loader.conf 5 ,
+.Xr sysconf 8 ,
+.Xr sysconf-targets 8
+.Sh HISTORY
+The
+.Nm sysconf
+utility first appeared in
+.Fx 16.0 .
+.Sh AUTHORS
+.An Devin Teske Aq Mt dteske@FreeBSD.org
+.An Faraz Vahedi Aq Mt kfv@kfv.io
diff --git a/usr.sbin/sysconf/sysconf-make.8 b/usr.sbin/sysconf/sysconf-make.8
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf-make.8
@@ -0,0 +1,231 @@
+.\" Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+.\" Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.Dd August 2, 2026
+\
+.Dt SYSCONF-MAKE 8
+.Os
+.Sh NAME
+.Nm "sysconf make"
+.Nd configure make.conf directives
+.Sh SYNOPSIS
+.Nm sysconf
+.Cm make
+.Op Fl ceFinNqsvVx
+.Op Fl j Ar jail | Fl R Ar dir
+.Op Fl f Ar file
+.Ar name Ns Op Ns Oo +|- Oc Ns = Ns Ar value
+.Ar ...
+.Nm sysconf
+.Cm make
+.Op Fl eFnNqvV
+.Op Fl f Ar file
+.Fl a
+.Nm sysconf
+.Cm make
+.Op Fl E
+.Fl l | L
+.Sh DESCRIPTION
+The
+.Cm make
+target of
+.Xr sysconf 8
+reads and writes
+.Xr make 1 Ns -syntax
+configuration on
+.Pa /etc/make.conf
+by default,
+or on any single file named with
+.Fl f .
+Shared options are in
+.Xr sysconf 8 ;
+make(1) limitations and multi-file policy for other targets in
+.Xr sysconf-targets 8 .
+.Pp
+.Ql +=
+is the
+.Xr make 1
+append operator:
+a new
+.Ql name+=value
+line is appended
+.Pq prior assignments for the same name are left intact ,
+and reads report the cumulative effective value as
+.Nm make Fl V
+would.
+Assignment modifiers may also be requested by suffixing the name,
+as in
+.Ql CFLAGS+=-DDEBUG .
+.Pp
+.Ql -=
+is not a
+.Xr make 1
+operator;
+it strikes each given word from the
+.Em last
+assignment whose value contains that word as a whole whitespace-separated
+token
+.Pq rewriting the statement, or deleting it when the value becomes empty ,
+so successive strikes peel list fragments from the end of the assignment
+chain without collapsing earlier
+.Ql +=
+lines.
+.Pp
+.Ql WITH_*
+and
+.Ql WITHOUT_*
+presence knobs may be set with
+.Fl s
+.Pq preferred; writes Ql name=
+or
+.Ar name Ns = ;
+reads report
+.Ql name (present)
+for any assigned value
+.Po
+.Fl e
+prints the real assignment, e.g.\&
+.Ql name=
+or
+.Ql name=t ;
+.Fl n
+prints that same value alone
+.Pc .
+.Fl s
+is a no-op when the knob is already defined
+.Pq any value ;
+an explicit
+.Ar name Ns =
+.Pq including empty
+always writes, and a change of placeholder echoes
+.Ql name: old -> new .
+.Fl s
+does not clear an inverse
+.Ql WITH_
+or
+.Ql WITHOUT_
+for the same option
+.Po
+use
+.Fl V
+and clean up manually if needed
+.Pc .
+.Pp
+Writing
+.Ql WITH_ Ns Ar foo Ns =no
+.Pq exact lowercase Ql no
+still stores the assignment but warns to use
+.Ql WITHOUT_ Ns Ar foo Ns =1 ,
+matching the
+.Ql .warning
+in
+.Pa share/mk/bsd.mkopt.mk .
+.Nm
+emits that advisory on write and on read
+.Pq named queries and Fl a ,
+with
+.Ql file:line:
+of the authoritative statement when known
+.Pq suppressed by Fl q ,
+because
+.Xr make.conf 5
+is read by
+.Pa /usr/src
+builds: a line such as
+.Va WITH_MANCOMPRESS Ns =no
+here is what produces
+.Pp
+.Dl warning: Use WITHOUT_MANCOMPRESS=1 instead of WITH_MANCOMPRESS=no
+.Pp
+when running
+.Xr make 1
+in
+.Pa /usr/src
+.Pq after Pa bsd.opts.mk reloads Pa bsd.mkopt.mk for common options .
+The read-time form matters when the default presence display hides the
+.Ql =no
+value behind
+.Ql (present) .
+The same line stays silent under a bare
+.Ql make -C /tmp
+or ports, which never evaluate that option list.
+Neither
+.Nm
+nor make warns for
+.Ql WITHOUT_* Ns =no
+or
+.Ql WITH_* Ns =NO :
+presence of
+.Ql WITHOUT_
+still disables, and
+.Ql WITH_
+with a value other than lowercase
+.Ql no
+does not match the check.
+.Pp
+.Va WITHOUT_MODULES
+is not a presence knob:
+its value is a module reject list, so
+.Fl s
+is rejected and reads print the module list rather than
+.Ql (present) .
+Set it with a normal assignment
+.Pq e.g. Va WITHOUT_MODULES Ns = Ns Ar plip .
+.Pp
+This target has no defaults file;
+.Fl d ,
+.Fl D ,
+and
+.Fl A
+are errors.
+For the
+.Pa /usr/src
+build triad see
+.Xr sysconf-src 8 .
+.Sh FILES
+.Bl -tag -width "/etc/make.conf" -compact
+.It Pa /etc/make.conf
+Default file for the
+.Cm make
+target.
+.El
+.Sh EXAMPLES
+.Dl sysconf make CFLAGS+=-DDEBUG
+.Dl sysconf make -s WITHOUT_MANCOMPRESS
+.Dl sysconf make WITHOUT_MANCOMPRESS
+.Pp
+Strike a word from the last
+.Ql +=
+fragment that contains it
+.Pq here removing Ql 3 from the final line only :
+.Bd -literal -offset indent
+bar+=1
+bar+=1 2
+bar+=1 2 3
+.Ed
+.Pp
+.Dl sysconf make bar-=3
+.Pp
+leaves
+.Ql bar+=1 2
+on the last line.
+Repeating with
+.Ql bar-=2
+and
+.Ql bar-=1
+peels that line further, then removes it when emptied.
+.Sh SEE ALSO
+.Xr make 1 ,
+.Xr sysconf 8 ,
+.Xr sysconf-src 8 ,
+.Xr sysconf-targets 8
+.Sh HISTORY
+The
+.Nm sysconf
+utility first appeared in
+.Fx 16.0 .
+.Sh AUTHORS
+.An Devin Teske Aq Mt dteske@FreeBSD.org
+.An Faraz Vahedi Aq Mt kfv@kfv.io
diff --git a/usr.sbin/sysconf/sysconf-rc.8 b/usr.sbin/sysconf/sysconf-rc.8
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf-rc.8
@@ -0,0 +1,56 @@
+.\" Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+.\" Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.Dd August 2, 2026
+\
+.Dt SYSCONF-RC 8
+.Os
+.Sh NAME
+.Nm "sysconf rc"
+.Nd pass configuration requests through to sysrc
+.Sh SYNOPSIS
+.Nm sysconf
+.Cm rc
+.Op Ar sysrc_argument ...
+.Sh DESCRIPTION
+The
+.Cm rc
+target of
+.Xr sysconf 8
+is a pure pass-through:
+.Xr rc.conf 5
+is
+.Xr sysrc 8 Ns 's
+domain,
+and its feature set is a superset of
+.Nm sysconf Ns 's ,
+so no option or argument is validated,
+interpreted,
+or reordered
+.Po
+.Ql --help
+and
+.Ql --version
+included
+.Pc .
+Every argument except the
+.Cm rc
+keyword itself is handed to
+.Xr sysrc 8
+verbatim.
+.Sh EXAMPLES
+.Dl sysconf rc kld_list+=i915kms
+.Sh SEE ALSO
+.Xr rc.conf 5 ,
+.Xr sysconf 8 ,
+.Xr sysrc 8
+.Sh HISTORY
+The
+.Nm sysconf
+utility first appeared in
+.Fx 16.0 .
+.Sh AUTHORS
+.An Devin Teske Aq Mt dteske@FreeBSD.org
+.An Faraz Vahedi Aq Mt kfv@kfv.io
diff --git a/usr.sbin/sysconf/sysconf-src.8 b/usr.sbin/sysconf/sysconf-src.8
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf-src.8
@@ -0,0 +1,211 @@
+.\" Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+.\" Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.Dd August 2, 2026
+\
+.Dt SYSCONF-SRC 8
+.Os
+.Sh NAME
+.Nm "sysconf src"
+.Nd configure the src-env.conf / make.conf / src.conf triad
+.Sh SYNOPSIS
+.Nm sysconf
+.Cm src
+.Op Fl ceFinNqsvVx
+.Op Fl j Ar jail | Fl R Ar dir
+.Op Fl f Ar file
+.Ar name Ns Op Ns Oo +|- Oc Ns = Ns Ar value
+.Ar ...
+.Nm sysconf
+.Cm src
+.Op Fl eFnNqvV
+.Op Fl f Ar file
+.Fl a
+.Nm sysconf
+.Cm src
+.Op Fl E
+.Fl l | L
+.Sh DESCRIPTION
+The
+.Cm src
+target of
+.Xr sysconf 8
+is the
+.Pa /usr/src
+build audience:
+it consults the same triad
+.Xr make 1
+uses when building the source tree
+.Po
+.Pa src-env.conf ,
+then
+.Pa make.conf ,
+then
+.Pa src.conf
+.Pc ,
+honoring
+.Ev SRC_ENV_CONF ,
+.Ev __MAKE_CONF ,
+and
+.Ev SRCCONF
+when set.
+See
+.Xr bsdconf_format 3
+and
+.Xr src.conf 5 .
+Shared options are in
+.Xr sysconf 8 ;
+authoritative-file and removal policy in
+.Xr sysconf-targets 8 ;
+.Ql +=
+and
+.Ql -=
+semantics match
+.Xr sysconf-make 8 .
+.Pp
+New directives are written to
+.Pa src.conf
+.Pq or the path in Ev SRCCONF ;
+an existing definition is rewritten in the file that last set it.
+There is no separate
+.Ql src-env
+keyword;
+use
+.Cm src
+or
+.Fl f
+for surgical edits of
+.Pa src-env.conf .
+.Pp
+.Ql WITH_*
+and
+.Ql WITHOUT_*
+are presence knobs
+.Po
+.Xr src.conf 5
+ignores the value; empty
+.Ql name=
+is the preferred spelling
+.Pc .
+Prefer
+.Fl s
+.Ar name
+over
+.Ar name Ns =
+when only presence matters;
+.Fl s
+leaves an already-defined knob
+.Pq any value
+unchanged and echoes
+.Ql (present) (unchanged) .
+An explicit
+.Ar name Ns =
+.Pq including empty
+always writes the requested value, and a change of placeholder echoes
+.Ql name: old -> new .
+.Fl s
+appends to the preferred write file and does not remove an inverse
+.Ql WITH_
+or
+.Ql WITHOUT_
+for the same option.
+Reads show
+.Ql name (present)
+for any assigned value;
+with
+.Fl e ,
+the real assignment
+.Po
+e.g.
+.Ql name=
+or
+.Ql name=t
+.Pc ;
+with
+.Fl n ,
+that same value alone
+.Pq empty when the assignment is Ql name= .
+Writing
+.Ql WITH_ Ns Ar foo Ns =no
+warns as on the
+.Cm make
+target
+.Pq see Xr sysconf-make 8 ;
+.Pa /usr/src
+make evaluates such assignments via
+.Pa bsd.opts.mk
+and
+.Pa src.opts.mk .
+.Pp
+.Va WITHOUT_MODULES
+is not a presence knob:
+its value is a module reject list, so
+.Fl s
+is rejected and reads print the module list rather than
+.Ql (present) .
+Set it with a normal assignment
+.Pq e.g. Va WITHOUT_MODULES Ns = Ns Ar plip .
+.Pp
+This target has no defaults file;
+.Fl d ,
+.Fl D ,
+and
+.Fl A
+are errors.
+.Sh ENVIRONMENT
+.Bl -tag -width "SRC_ENV_CONF"
+.It Ev SRC_ENV_CONF
+Path of
+.Pa src-env.conf
+.Pq default Pa /etc/src-env.conf ;
+verbatim,
+not subject to
+.Fl R .
+.It Ev __MAKE_CONF
+Path of
+.Pa make.conf
+.Pq default Pa /etc/make.conf ;
+verbatim,
+not subject to
+.Fl R .
+.It Ev SRCCONF
+Path of
+.Pa src.conf
+.Pq default Pa /etc/src.conf ;
+verbatim,
+not subject to
+.Fl R .
+Also the preferred write file for new directives.
+.El
+.Sh FILES
+.Bl -tag -width "/etc/src-env.conf" -compact
+.It Pa /etc/src-env.conf
+First file of the triad.
+.It Pa /etc/make.conf
+Middle file of the triad.
+.It Pa /etc/src.conf
+Last file and preferred write target.
+.El
+.Sh EXAMPLES
+.Dl sysconf src -s WITHOUT_LLDB
+.Dl sysconf src WITHOUT_LLDB=
+.Dl sysconf src WITHOUT_LLDB
+.Dl sysconf src -x WITHOUT_LLDB
+.Dl sysconf src -V CFLAGS
+.Sh SEE ALSO
+.Xr make 1 ,
+.Xr bsdconf_format 3 ,
+.Xr src.conf 5 ,
+.Xr sysconf 8 ,
+.Xr sysconf-make 8 ,
+.Xr sysconf-targets 8
+.Sh HISTORY
+The
+.Nm sysconf
+utility first appeared in
+.Fx 16.0 .
+.Sh AUTHORS
+.An Devin Teske Aq Mt dteske@FreeBSD.org
+.An Faraz Vahedi Aq Mt kfv@kfv.io
diff --git a/usr.sbin/sysconf/sysconf-sysctl.8 b/usr.sbin/sysconf/sysconf-sysctl.8
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf-sysctl.8
@@ -0,0 +1,146 @@
+.\" Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+.\" Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.Dd August 2, 2026
+\
+.Dt SYSCONF-SYSCTL 8
+.Os
+.Sh NAME
+.Nm "sysconf sysctl"
+.Nd configure boot-time sysctl.conf directives
+.Sh SYNOPSIS
+.Nm sysconf
+.Cm sysctl
+.Op Fl AcDeFinNqvVx
+.Op Fl j Ar jail | Fl R Ar dir
+.Op Fl f Ar file | Fl k Ar module
+.Ar name Ns Op Ns Oo +|- Oc Ns = Ns Ar value
+.Ar ...
+.Nm sysconf
+.Cm sysctl
+.Op Fl ADeFnNqvV
+.Op Fl f Ar file | Fl k Ar module
+.Fl a
+.Nm sysconf
+.Cm sysctl
+.Op Fl AaDinNq
+.Fl d
+.Op Ar name ...
+.Nm sysconf
+.Cm sysctl
+.Op Fl E
+.Op Fl k Ar module
+.Fl l | L
+.Sh DESCRIPTION
+The
+.Cm sysctl
+target of
+.Xr sysconf 8
+reads and modifies
+.Xr sysctl.conf 5
+configuration.
+Shared assignment syntax and options are documented in
+.Xr sysconf 8 ;
+multi-file write policy in
+.Xr sysconf-targets 8 .
+.Pp
+The stock file list is
+.Pa /etc/sysctl.conf ,
+.Pa /etc/sysctl.conf.local ,
+then
+.Pa /etc/sysctl.kld.d/ Ns Ar module Ns Pa .conf
+when
+.Fl k Ar module
+is given
+.Pq applied by Xr rc.subr 8 when the module loads .
+.Pp
+Values are quoted on write only when the value requires it
+.Pq embedded whitespace .
+.Pp
+When writing to the running system,
+each
+.Ar name Ns = Ns Ar value
+is validated against the kernel's sysctl tree first.
+An OID that cannot be set at run time
+.Pq non-leaf, read-only, or loader-only tunable
+is reported and skipped,
+while the remaining assignments are still applied.
+In particular,
+a loader-only tunable is reported as requiring the
+.Cm loader
+target instead
+.Pq see Xr sysconf-loader 8 .
+Numeric values are also checked against the OID's
+.Dv CTLTYPE
+.Po
+.Dv CTLTYPE_INT ,
+.Dv CTLTYPE_U64 ,
+and the other integer widths
+.Pc
+so an overflowing or non-numeric assignment cannot land in
+.Xr sysctl.conf 5
+and fail
+.Pq or apply truncated
+when
+.Xr sysctl 8
+loads the file at boot.
+.Pp
+An unknown OID
+.Pq often from a module that is not yet loaded
+is reported with a warning but still written,
+matching how
+.Xr init 8
+treats such lines in
+.Xr sysctl.conf 5 ;
+.Fl i
+and
+.Fl q
+suppress that warning.
+Validation is skipped entirely under
+.Fl R ,
+where the target system's kernel is not the running kernel.
+.Pp
+.Fl d
+descriptions for this target come from the running kernel
+.Pq as with Xr sysctl 8 Fl d ,
+not from a defaults file.
+.Sh FILES
+.Bl -tag -width "/etc/sysctl.conf.local" -compact
+.It Pa /etc/sysctl.conf
+Write target for new directives.
+.It Pa /etc/sysctl.conf.local
+Site-local overrides.
+.It Pa /etc/sysctl.kld.d/
+Module-specific drop-ins
+.Pq see Fl k .
+.El
+.Sh EXAMPLES
+.Dl sysconf sysctl kern.maxfiles=65536
+.Dl sysconf sysctl -F vfs.usermount
+.Dl sysconf sysctl -d kern.maxfiles
+.Dl sysconf sysctl -L
+.Dl sysconf sysctl -LE
+.Dl sysconf sysctl -k ipfw net.inet.ip.fw.verbose=1
+.Dl sysconf -x sysctl vfs.usermount
+.Dl sysconf -j www sysctl kern.ipc.somaxconn=1024
+.Pp
+An overflowing assignment is refused before the file is touched
+.Pq here an integer OID given a value beyond INT_MAX :
+.Pp
+.Dl sysconf sysctl security.bsd.see_other_uids=99999999999
+.Sh SEE ALSO
+.Xr sysctl.conf 5 ,
+.Xr sysconf 8 ,
+.Xr sysconf-loader 8 ,
+.Xr sysconf-targets 8 ,
+.Xr sysctl 8
+.Sh HISTORY
+The
+.Nm sysconf
+utility first appeared in
+.Fx 16.0 .
+.Sh AUTHORS
+.An Devin Teske Aq Mt dteske@FreeBSD.org
+.An Faraz Vahedi Aq Mt kfv@kfv.io
diff --git a/usr.sbin/sysconf/sysconf-targets.8 b/usr.sbin/sysconf/sysconf-targets.8
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf-targets.8
@@ -0,0 +1,174 @@
+.\" Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+.\" Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.Dd August 2, 2026
+\
+.Dt SYSCONF-TARGETS 8
+.Os
+.Sh NAME
+.Nm sysconf-targets
+.Nd sysconf multi-file sourcing and write policy
+.Sh SYNOPSIS
+.Nm sysconf
+.Ar target
+.Op options
+.Op Ar name Ns Op Ns Oo +|- Oc Ns = Ns Ar value ...
+.Sh DESCRIPTION
+This page documents rules shared by every
+.Xr sysconf 8
+target that is backed by more than one configuration file.
+Per-target file lists and syntax quirks are in
+.Xr sysconf-loader 8 ,
+.Xr sysconf-sysctl 8 ,
+.Xr sysconf-make 8 ,
+.Xr sysconf-src 8 ,
+.Xr sysconf-rc 8 ,
+and
+.Xr sysconf-generic 8 .
+Shared options remain in
+.Xr sysconf 8 .
+.Ss Authoritative file
+Most targets consult several files in the deterministic order the system
+sources them at boot.
+A directive in a later file overrides the same directive in an earlier one,
+so the last file listing a directive is the authoritative source of its
+value.
+.Pp
+Reads report that authoritative value.
+Writes are applied to the authoritative file
+.Pq so the change survives a reboot
+or appended to the target's default write file
+.Pq the first modifiable file listed
+when the directive is set nowhere or only in the read-only defaults.
+Removals
+.Pq Fl x
+are applied to every file listing the directive,
+lest deleting the authoritative definition merely unmask an earlier one.
+.Ss Defaults files
+For a target with a defaults file
+.Po
+presently
+.Ql loader ;
+see
+.Xr sysconf-loader 8
+.Pc ,
+the boot-time consumer sources the defaults before everything else,
+and so does
+.Nm sysconf :
+a directive no conf file mentions still reads back its default
+.Pq the value that does, in fact, take effect at boot ,
+with
+.Fl F
+truthfully naming the defaults file as its source.
+The defaults file is nevertheless read-only bookkeeping:
+it is never chosen as a write target,
+never satisfies a removal,
+and is never listed by
+.Fl l
+or
+.Fl L
+.Pq which enumerate only files Nm sysconf may modify .
+.Pp
+Those targets additionally support querying defaults:
+.Fl d
+reports directive descriptions,
+.Fl D
+consults the defaults alone,
+and
+.Fl A
+widens dumps to include directives still at their system default
+.Po
+a named
+.Ar name
+read always reflects the defaults already;
+.Fl A
+changes only
+.Fl a
+scope,
+or dumps everything when given with no
+.Ar name
+arguments
+.Pc .
+Attempting any of these against a target with no defaults
+.Po
+.Cm make
+and
+.Cm src
+have neither a defaults file nor directive descriptions
+.Pc
+is an error.
+The one exception is
+.Ql sysctl -d ,
+whose descriptions come from the running kernel rather than any file
+.Pq see Xr sysconf-sysctl 8 .
+.Ss Explicit file selection
+An arbitrary configuration file may be selected with
+.Fl f ,
+which overrides
+.Em which
+file is consulted and modified
+.Pq the named file alone; multi-file processing is disabled
+but never
+.Em how :
+the format is always stated explicitly by the
+.Ar target
+keyword and is never inferred from the file.
+This composes freely,
+so a
+.Xr loader.conf 5 Ns -formatted
+file may be staged anywhere
+.Pq Ql sysconf loader -f /tmp/loader.conf.new ...
+and,
+conversely,
+any format may be deliberately applied to any file.
+.Pp
+How each built-in format discovers or lists its backing files is
+implemented by
+.Xr bsdconf_format 3 .
+.Sh LIMITATIONS
+.Nm sysconf
+does not evaluate
+.Xr make 1
+conditionals
+.Pq Ql .if , .elif , .else , .endif
+or other makefile control flow that may appear in
+.Pa make.conf ,
+.Pa src.conf ,
+or
+.Pa src-env.conf .
+.Pp
+It also does not consult
+.Pa sys.mk ,
+.Pa local.sys.mk ,
+or other
+.Xr make 1
+defaults under
+.Pa /usr/share/mk .
+.Pp
+Effective values from
+.Nm sysconf
+therefore match
+.Nm make Fl V
+for straight-line assignment and
+.Ql +=
+chains in those conf files,
+but may differ when the value depends on conditionals or mk defaults.
+.Sh SEE ALSO
+.Xr bsdconf_format 3 ,
+.Xr sysconf 8 ,
+.Xr sysconf-generic 8 ,
+.Xr sysconf-loader 8 ,
+.Xr sysconf-make 8 ,
+.Xr sysconf-rc 8 ,
+.Xr sysconf-src 8 ,
+.Xr sysconf-sysctl 8
+.Sh HISTORY
+The
+.Nm sysconf
+utility first appeared in
+.Fx 16.0 .
+.Sh AUTHORS
+.An Devin Teske Aq Mt dteske@FreeBSD.org
+.An Faraz Vahedi Aq Mt kfv@kfv.io
diff --git a/usr.sbin/sysconf/sysconf.8 b/usr.sbin/sysconf/sysconf.8
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf.8
@@ -0,0 +1,704 @@
+.\" Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+.\" Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.Dd August 2, 2026
+\
+.Dt SYSCONF 8
+.Os
+.Sh NAME
+.Nm sysconf
+.Nd read and modify system configuration files
+.Sh SYNOPSIS
+.Nm
+.Ar target
+.Op Fl AcDeFinNqsvVx
+.Op Fl j Ar jail | Fl R Ar dir
+.Op Fl f Ar file | Fl k Ar module
+.Ar name Ns Op Ns Oo +|- Oc Ns = Ns Ar value
+.Ar ...
+.Nm
+.Ar target
+.Op Fl ADeFnNqvV
+.Op Fl j Ar jail | Fl R Ar dir
+.Op Fl f Ar file | Fl k Ar module
+.Fl a
+.Nm
+.Ar target
+.Op Fl AaDinNq
+.Op Fl j Ar jail | Fl R Ar dir
+.Fl d
+.Op Ar name ...
+.Nm
+.Ar target
+.Op Fl E
+.Op Fl j Ar jail | Fl R Ar dir
+.Op Fl k Ar module
+.Fl l | L
+.Nm
+.Cm rc
+.Op Ar sysrc_argument ...
+.Sh DESCRIPTION
+The
+.Nm
+utility reads and modifies the system configuration files of the base
+system,
+completing the native configuration trinity alongside
+.Xr sysctl 8
+.Pq kernel state
+and
+.Xr sysrc 8
+.Pq Xr rc.conf 5 .
+The first positional argument selects a
+.Ar target
+keyword;
+options are equally accepted before it.
+Each target is documented on its own page
+.Pq see Sx TARGETS ;
+shared options and assignment syntax are documented here.
+.Ss Assignment syntax
+A
+.Ar name
+alone reads the current value of the directive,
+while
+.Ar name Ns = Ns Ar value
+atomically writes it.
+Writes echo
+.Ql name: old -> new
+when the stored value changes,
+or
+.Ql name: value (unchanged)
+when it is already set as requested
+.Pq the file is left untouched in that case .
+With
+.Fl e
+those echoes use
+.Ql name=...
+instead of
+.Ql name: ...
+.Pq matching read output .
+The echo is suppressed by
+.Fl q ;
+.Fl c
+writes nothing to stdout.
+A directive not previously set anywhere shows an empty old value.
+.Pp
+On the
+.Cm make
+and
+.Cm src
+targets, a
+.Ql WITH_*
+or
+.Ql WITHOUT_*
+build knob
+.Pq presence matters; Xr src.conf 5 ignores the value
+is shown as
+.Ql name (present)
+when defined
+.Po
+any value, including
+.Ql =
+or
+.Ql =t
+.Pc ,
+rather than printing the value after the colon;
+.Fl e
+prints the real assignment
+.Po
+e.g.
+.Ql name=
+or
+.Ql name=t
+.Pc ,
+and
+.Fl n
+prints that same value alone
+.Pq empty when the assignment is Ql name= .
+Setting such a knob with
+.Fl s
+echoes
+.Ql name: (not present) -> (present)
+.Po
+or
+.Ql (present) (unchanged)
+when it is already defined
+.Pc .
+An explicit
+.Ar name Ns =
+.Pq including empty
+always writes the requested value;
+a change of placeholder while the knob stays defined echoes
+.Ql name: old -> new
+like other assignments.
+A write of
+.Ql WITH_ Ns Ar foo Ns =no
+.Pq exact lowercase Ql no
+is still performed but warns to use
+.Ql WITHOUT_ Ns Ar foo Ns =1
+instead.
+That message matches
+.Pa share/mk/bsd.mkopt.mk
+and is emitted here as a precursor so a bad
+.Xr make.conf 5
+or
+.Xr src.conf 5
+assignment is caught at write time.
+The same advisory is printed on reads
+.Po
+named queries and
+.Fl a
+dump
+.Pc ,
+with
+.Ql file:line:
+when the authoritative statement is known, so an existing
+.Ql WITH_* Ns =no
+is not overlooked when presence display shows only
+.Ql (present) .
+.Fl q
+suppresses the read-time form.
+.Xr make 1
+itself typically warns later during a
+.Pa /usr/src
+build, when
+.Pa bsd.opts.mk
+.Pq or Pa src.opts.mk
+reloads
+.Pa bsd.mkopt.mk
+for recognized options such as
+.Va MANCOMPRESS .
+The same
+.Xr make.conf 5
+line stays silent for a bare
+.Xr make 1
+.Pq e.g. Fl C Pa /tmp
+or for ports, which do not run that option pass.
+.Ql WITHOUT_* Ns =no ,
+.Ql WITH_* Ns =NO ,
+and other values do not warn here or there.
+.Va WITHOUT_MODULES
+is not a presence knob
+.Pq its value is a module list
+and is excluded from
+.Fl s
+and presence display.
+.Pp
+For directives whose value is a space-separated list
+.Po
+.Va kld_list Ns -style ;
+see for example
+.Va kernels
+in
+.Xr loader.conf 5
+.Pc ,
+.Ar name Ns += Ns Ar word
+appends each given word not already present and
+.Ar name Ns -= Ns Ar word
+strikes each given word,
+after the fashion of
+.Xr sysrc 8 .
+Striking words from a directive set nowhere is reported like any other
+unknown directive.
+.Pp
+For
+.Xr make 1 Ns -syntax
+targets
+.Po
+.Cm make
+and
+.Cm src ;
+see
+.Xr sysconf-make 8
+and
+.Xr sysconf-src 8
+.Pc ,
+.Ql +=
+is instead the
+.Xr make 1
+append operator and
+.Ql -=
+strikes words from the last matching assignment fragment.
+.Pp
+Files are never modified in place:
+updates are streamed to a temporary file which is flushed to stable storage
+and atomically renamed over the target
+.Pq see Xr bsdconf_put 3 .
+How multi-file targets choose the file to read or rewrite is documented in
+.Xr sysconf-targets 8 .
+.Sh TARGETS
+The following target keywords are supported:
+.Bl -tag -width indent
+.It Cm loader
+Boot loader configuration
+.Pq Xr loader.conf 5 .
+See
+.Xr sysconf-loader 8 .
+.It Cm sysctl
+Kernel state set at boot
+.Pq Xr sysctl.conf 5 .
+See
+.Xr sysconf-sysctl 8 .
+.It Cm make
+System-wide
+.Xr make 1
+settings
+.Pq Pa /etc/make.conf .
+See
+.Xr sysconf-make 8 .
+.It Cm src
+The
+.Pa /usr/src
+build triad
+.Pq Pa src-env.conf , make.conf , src.conf .
+See
+.Xr sysconf-src 8 .
+.It Cm rc
+Pass-through to
+.Xr sysrc 8 .
+See
+.Xr sysconf-rc 8 .
+.It Cm generic
+Foreign or unknown format;
+requires
+.Fl f .
+See
+.Xr sysconf-generic 8 .
+.El
+.Pp
+Cross-cutting rules for multi-file sourcing,
+defaults,
+and where writes versus removals land are in
+.Xr sysconf-targets 8 .
+Built-in format descriptors and file-list discovery are in
+.Xr bsdconf_format 3 .
+.Sh OPTIONS
+The options are as follows:
+.Bl -tag -width indent
+.It Fl A
+Widen dump scope to include directives still at their system default
+.Po
+named reads always reflect the defaults;
+see
+.Xr sysconf-targets 8
+.Pc .
+With no
+.Ar name
+arguments,
+dumps everything
+.Po
+as
+.Fl a ,
+defaults included
+.Pc .
+.Pp
+Combined with
+.Fl D ,
+the scoping of
+.Fl D
+wins and
+.Fl A
+retains only its dump-implying role,
+so
+.Ql -ADd
+describes all defaults and only defaults,
+as in
+.Xr sysrc 8 .
+Only valid for targets with a defaults file
+.Pq see Xr sysconf-loader 8 .
+.It Fl a
+Dump all configured directives from the target:
+the union of the target's files,
+wherein directives in later files override earlier ones.
+Directives whose value still comes from the defaults file alone are out
+of scope
+.Pq widen with Fl A .
+.It Fl c
+Check mode.
+For each
+.Ar name Ns = Ns Ar value
+argument,
+compare the effective value against
+.Ar value
+without modifying any file
+.Po
+with
+.Fl x ,
+check that
+.Ar name
+is absent
+.Pc .
+Exit with success if no changes would be required,
+else error.
+.It Fl D
+Consult only the target's defaults file,
+reporting the system default value of each
+.Ar name
+.Po
+or, with
+.Fl a ,
+dumping every default
+.Pc .
+Defaults are read-only;
+combining
+.Fl D
+with an assignment is an error.
+Only valid for targets with a defaults file
+.Pq see Xr sysconf-loader 8 .
+.It Fl d
+Show the description of each
+.Ar name
+instead of its value.
+.Pp
+For targets with a defaults file,
+the description is the inline comment trailing the directive's default
+assignment,
+continued across subsequent lines of whitespace followed by a comment
+character.
+A commented-out default
+.Pq e.g., Ql #comconsole_speed=\(dq115200\(dq # Set the ...
+still yields its description.
+For the
+.Ql sysctl
+target,
+descriptions come from the running kernel
+.Po
+as with
+.Xr sysctl 8
+.Fl d ;
+see
+.Xr sysconf-sysctl 8
+.Pc .
+.Pp
+Combined with
+.Fl a ,
+.Fl A ,
+or
+.Fl D ,
+dumps the description of every directive in scope
+.Po
+of the configured directives,
+of everything defaults included,
+or of the defaults alone,
+respectively
+.Pc ,
+after the fashion of
+.Xr sysrc 8
+.Fl \&Ad .
+A directive the defaults never mention prints an empty description.
+.It Fl E
+With
+.Fl l
+or
+.Fl L ,
+list only files that exist on disk.
+.It Fl e
+Print values in
+.Ar name Ns = Ns Ar value
+format,
+suitable for feeding back into the configuration file.
+Applies to both reads and the informational echo from assignments
+.Po
+replacing the default
+.Ql name: value
+form;
+changed writes use
+.Ql name=old # -> new ,
+matching
+.Xr sysrc 8
+.Pc .
+For the
+.Ql loader
+target the value is quoted.
+.It Fl F
+Show the pathname of the file holding each directive's effective
+.Pq authoritative
+value instead of the value itself.
+For a directive still at its system default,
+this truthfully names the defaults file.
+.It Fl f Ar file
+Operate on
+.Ar file ,
+in the format of the
+.Ar target
+keyword,
+instead of the target's standard files;
+mutually exclusive with
+.Fl k .
+For read operations
+.Ar file
+may be a non-seekable stream
+.Po
+a fifo or
+.Pa /dev/stdin ,
+for example,
+spooled to a temporary automatically
+.Pc ;
+write operations require a regular file.
+A
+.Ar file
+of
+.Ql -
+means standard input
+.Pq a synonym for Pa /dev/stdin ,
+exempt from
+.Fl R .
+See
+.Xr sysconf-targets 8
+and
+.Xr sysconf-generic 8 .
+.It Fl h , Fl Fl help
+Print a short usage statement
+.Pq or, for the long form, the full option summary
+to stderr and exit.
+.It Fl Fl version
+Print version information to stdout and exit.
+.It Fl i
+Ignore unknown names
+.Pq and suppress the warning when writing an unknown sysctl OID .
+.It Fl j Ar jail
+Operate within the jail
+.Ar jail
+.Pq name or numeric id ;
+mutually exclusive with
+.Fl R .
+.It Fl k Ar module
+Include the kernel module drop-in file for
+.Ar module
+in the target's file list
+.Pq e.g., Pa /etc/sysctl.kld.d/ Ns Ar module Ns Pa .conf .
+Only valid for targets with a module drop-in directory
+.Pq see Xr sysconf-sysctl 8 .
+.It Fl l
+List the pathnames of the files backing the target,
+in sourcing order,
+and exit.
+The read-only defaults file is never listed
+.Po
+.Fl l
+enumerates only files
+.Nm sysconf
+may modify;
+see
+.Xr sysconf-targets 8
+.Pc .
+.It Fl L
+Like
+.Fl l ,
+but additionally list every drop-in candidate:
+for targets with a module drop-in directory,
+one candidate per loaded kernel module
+.Pq whether or not the file exists yet
+plus any drop-in already on disk.
+The read-only defaults file is likewise never listed.
+.It Fl n
+Show only directive values,
+not their names.
+.It Fl N
+Show only directive names,
+not their values.
+.It Fl q
+Quiet.
+Suppress warnings about unknown directives and the
+.Ql old -> new
+echo of write operations.
+.It Fl R Ar dir
+Operate within the root directory
+.Ar dir
+rather than
+.Pa / ;
+mutually exclusive with
+.Fl j .
+.It Fl s
+Set empty
+.Ql WITH_*
+and
+.Ql WITHOUT_*
+build knobs on the
+.Cm make
+and
+.Cm src
+targets
+.Po
+equivalent to
+.Ar name Ns =
+with no value;
+see
+.Xr sysconf-make 8
+and
+.Xr sysconf-src 8
+.Pc .
+Each bare
+.Ar name
+must begin with
+.Ql WITH_
+or
+.Ql WITHOUT_ .
+An already-present knob
+.Pq any value
+is left unchanged by
+.Fl s .
+An explicit
+.Ar name Ns =
+including empty always writes.
+Mutually exclusive with
+.Fl x .
+.It Fl v
+Verbose.
+Print the pathname of the configuration file holding the final effective
+value
+.Pq the last file in sourcing order that assigned the directive .
+.It Fl V
+Trail.
+For each named read
+.Pq and for Fl a ,
+print every assignment step that contributed to the effective value:
+.Ql file:line: nameopfragment ,
+and when the running effective value differs from the fragment,
+.Ql -> effective .
+.Pp
+For writes, print the same
+.Ql file:line:
+shape for the change:
+.Ql file:line: name=old -> name=new ,
+.Ql name=value (unchanged) ,
+.Ql name=value (added)
+when a new statement is created,
+or
+.Ql nameopfragment (removed)
+when a statement is deleted,
+using the statement operator
+.Ql = / += / ...
+as appropriate .
+.Pp
+Uses the same accumulation model as ordinary reads
+.Pq make(1) `+=' et al. ,
+so the last effective value matches
+.Fl v
+and,
+for linear conf files without
+.Ql .if ,
+.Nm make Fl V
+.Pq see Xr sysconf-make 8 and Xr sysconf-src 8 .
+.It Fl x
+Remove the named directive(s) from every file of the target listing them
+.Pq see Xr sysconf-targets 8 .
+With
+.Fl v
+or
+.Fl V ,
+a successful removal echoes
+.Ql name (removed)
+.Pq or a trail form with Fl V .
+.El
+.Sh ENVIRONMENT
+.Bl -tag -width "LOADER_DEFAULTS"
+.It Ev LOADER_DEFAULTS
+Defaults file for the
+.Ql loader
+target;
+see
+.Xr sysconf-loader 8 .
+.It Ev SRC_ENV_CONF , Ev __MAKE_CONF , Ev SRCCONF
+Paths for the
+.Cm src
+triad;
+see
+.Xr sysconf-src 8 .
+.El
+.Sh FILES
+See the per-target manuals
+.Pq Xr sysconf-loader 8 , Xr sysconf-sysctl 8 , Xr sysconf-make 8 , Xr sysconf-src 8
+for the files each keyword consults.
+Common paths include
+.Pa /boot/loader.conf ,
+.Pa /etc/sysctl.conf ,
+.Pa /etc/make.conf ,
+and
+.Pa /etc/src.conf .
+.Sh EXIT STATUS
+.Ex -std
+In check mode
+.Pq Fl c ,
+an exit status of zero means no changes are required.
+.Sh EXAMPLES
+Read and set a boot loader directive:
+.Pp
+.Dl sysconf loader zfs_load
+.Dl sysconf loader zfs_load=\(dqYES\(dq
+.Pp
+Raise a sysctl applied at boot:
+.Pp
+.Dl sysconf sysctl kern.maxfiles=65536
+.Pp
+List files backing the loader target:
+.Pp
+.Dl sysconf loader -l
+.Pp
+Append to a space-separated list:
+.Pp
+.Dl sysconf loader kernels+=kernel_test
+.Pp
+Append a
+.Xr make 1
+flag and set a
+.Pa src.conf
+knob:
+.Pp
+.Dl sysconf make CFLAGS+=-DDEBUG
+.Dl sysconf src -s WITHOUT_LLDB
+.Dl sysconf src WITHOUT_LLDB=
+.Pp
+Further examples for each target appear in
+.Xr sysconf-loader 8 ,
+.Xr sysconf-sysctl 8 ,
+.Xr sysconf-make 8 ,
+.Xr sysconf-src 8 ,
+.Xr sysconf-rc 8 ,
+and
+.Xr sysconf-generic 8 .
+.Sh SEE ALSO
+.Xr bsdconf 3 ,
+.Xr bsdconf_format 3 ,
+.Xr bsdconf_put 3 ,
+.Xr loader.conf 5 ,
+.Xr src.conf 5 ,
+.Xr sysctl.conf 5 ,
+.Xr jail 8 ,
+.Xr sysconf-generic 8 ,
+.Xr sysconf-loader 8 ,
+.Xr sysconf-make 8 ,
+.Xr sysconf-rc 8 ,
+.Xr sysconf-src 8 ,
+.Xr sysconf-sysctl 8 ,
+.Xr sysconf-targets 8 ,
+.Xr sysctl 8 ,
+.Xr sysrc 8
+.Sh HISTORY
+The
+.Nm
+utility first appeared in
+.Fx 16.0 .
+.Sh AUTHORS
+.An Devin Teske Aq Mt dteske@FreeBSD.org
+.An Faraz Vahedi Aq Mt kfv@kfv.io
+.Sh SECURITY CONSIDERATIONS
+Read-only invocations
+.Po
+including
+.Fl c
+checks, which never modify files
+.Pc
+run inside a
+.Xr capsicum 4
+sandbox:
+the backing files are opened first and the process then relinquishes all
+other capabilities before any file content is parsed.
+.Pp
+Write operations replace files atomically and never in place;
+see the
+.Sx SECURITY CONSIDERATIONS
+section of
+.Xr bsdconf_put 3
+for the properties of the write transaction.
diff --git a/usr.sbin/sysconf/sysconf.c b/usr.sbin/sysconf/sysconf.c
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf.c
@@ -0,0 +1,761 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * sysconf(8): CLI entry, option parsing, and program state.
+ */
+
+#include "sysconf_priv.h"
+
+/* Requests to process (one per name argument) */
+struct request *reqs; /* set by main() */
+unsigned int nreqs; /* set by main() */
+
+/* Union of all directives, for `-a' against multi-file targets */
+struct dumpent *dumps; /* set by scan_cb() */
+size_t ndumps; /* set by scan_cb() */
+size_t dumpsize; /* set by scan_cb() */
+
+/* Resolved target state */
+char **conf_files; /* ordered backing files */
+size_t nconf_files;
+size_t conf_scanidx; /* file being parsed (scan_pass) */
+size_t write_idx; /* default write target index */
+int defaults_idx = -1; /* defaults file index in conf_files
+ * (read-only, never listed); -1 if none */
+enum bsdconf_format format = BSDCONF_FORMAT_GENERIC;
+
+/* Extra display information */
+const char *pgm; /* set to argv[0] by main() */
+uint8_t check = 0; /* `-c' */
+uint8_t defaults_only = 0; /* `-D' */
+uint8_t dump_all = 0; /* `-a' */
+uint8_t existing_only = 0; /* `-E' */
+uint8_t ignore_unknown = 0; /* `-i' */
+uint8_t list_all = 0; /* `-L' */
+uint8_t list_files = 0; /* `-l' */
+uint8_t name_only = 0; /* `-N' */
+uint8_t quiet = 0; /* `-q' */
+uint8_t remove_mode = 0; /* `-x' */
+uint8_t set_knobs = 0; /* `-s' WITH_/WITHOUT_ presence */
+uint8_t show_desc = 0; /* `-d' */
+uint8_t show_equals = 0; /* `-e' */
+uint8_t show_file = 0; /* `-F' */
+uint8_t value_only = 0; /* `-n' */
+uint8_t verbose = 0; /* `-v' */
+uint8_t verbose_trail = 0; /* `-V' */
+uint8_t with_defaults = 0; /* `-A' */
+
+/* Option arguments */
+const char *file = NULL; /* `-f file' */
+uint8_t file_stdin = 0; /* `-f -' given */
+const char *jailname = NULL; /* `-j jail' */
+const char *module = NULL; /* `-k module' */
+const char *rootdir = ""; /* `-R dir' */
+
+/*
+ * Locate the target keyword in `argv' without consuming or validating
+ * anything: the first argument that is neither an option cluster nor the
+ * argument of one (per OPTSTRING; an unknown option letter is assumed to
+ * take no argument). A `--' ends option scanning as usual. Returns the
+ * argv index of the target, or -1 when there is none. Used before any
+ * getopt(3) processing so that the `rc' pass-through target (see
+ * exec_sysrc() below) can be detected while the command line is still
+ * pristine.
+ *
+ * Attached arguments (`-fPATH', `-R/altroot') are self-contained: the rest
+ * of the cluster is the option's argument, so the following argv element is
+ * not consumed. Only a trailing option letter that takes an argument and
+ * has nothing after it in the cluster (`-f PATH') skips the next element.
+ */
+static int
+find_target(int argc, char *argv[])
+{
+ int n;
+ const char *o;
+ const char *p;
+
+ for (n = 1; n < argc; n++) {
+ if (strcmp(argv[n], "--") == 0)
+ return (n + 1 < argc ? n + 1 : -1);
+ if (argv[n][0] == '-' && argv[n][1] != '\0') {
+ for (p = argv[n] + 1; *p != '\0'; p++) {
+ o = strchr(OPTSTRING, *p);
+ if (o != NULL && o[1] == ':') {
+ /* Takes an argument */
+ if (p[1] != '\0') {
+ ;
+ /* attached: rest of argv[n] */
+ } else if (n + 1 < argc) {
+ n++;
+ /* separate argv */
+ }
+ break;
+ }
+ }
+ continue;
+ }
+ return (n);
+ }
+
+ return (-1);
+}
+
+/*
+ * The `rc' pass-through: rc.conf(5) is sysrc(8)'s domain, and its feature
+ * set is a superset of ours, so no option or argument is validated,
+ * interpreted, or reordered here -- every argument except the target
+ * keyword itself (at argv index `skip') is handed to sysrc(8) verbatim.
+ * Never returns.
+ */
+static void
+exec_sysrc(int argc, char *argv[], int skip)
+{
+ int n;
+ int nargc = 0;
+ char **nargv;
+
+ if ((nargv = calloc((size_t)argc + 1, sizeof(*nargv))) == NULL)
+ err(EXIT_FAILURE, NULL);
+ nargv[nargc++] = (char *)(uintptr_t)"sysrc";
+ for (n = 1; n < argc; n++) {
+ if (n == skip)
+ continue;
+ nargv[nargc++] = argv[n];
+ }
+ nargv[nargc] = NULL;
+
+ execvp("sysrc", nargv);
+ err(EXIT_FAILURE, "sysrc");
+}
+
+/*
+ * Run the getopt(3) loop over `argv', setting the option globals. Called
+ * once for the options preceding the target keyword and once for those
+ * following it. Returns the number of arguments consumed (optind).
+ */
+static int
+parse_options(int argc, char *argv[])
+{
+ int ch;
+
+ while ((ch = getopt(argc, argv, OPTSTRING)) != -1) {
+ switch (ch) {
+ case 'A': /* dump all directives, defaults included */
+ with_defaults = 1;
+ break;
+ case 'a': /* dump all directives */
+ dump_all = 1;
+ break;
+ case 'c': /* check only; do not modify */
+ check = 1;
+ break;
+ case 'd': /* show directive descriptions */
+ show_desc = 1;
+ break;
+ case 'D': /* consult the defaults file only */
+ defaults_only = 1;
+ break;
+ case 'E': /* list existing files only (with -l/-L) */
+ existing_only = 1;
+ break;
+ case 'e': /* show name=value */
+ show_equals = 1;
+ break;
+ case 'F': /* show authoritative file, not value */
+ show_file = 1;
+ break;
+ case 'f': /* explicit file (`-' is standard input) */
+ if (strcmp(optarg, "-") == 0) {
+ file = "/dev/stdin";
+ file_stdin = 1;
+ } else {
+ file = optarg;
+ file_stdin = 0;
+ }
+ break;
+ case 'h': /* help/usage */
+ usage();
+ break;
+ case 'i': /* ignore unknown names */
+ ignore_unknown = 1;
+ break;
+ case 'j': /* jail name or id */
+ jailname = optarg;
+ break;
+ case 'k': /* kernel module drop-in */
+ module = optarg;
+ break;
+ case 'l': /* list backing files */
+ list_files = 1;
+ break;
+ case 'L': /* list all candidate files */
+ list_all = 1;
+ break;
+ case 'n': /* value only */
+ value_only = 1;
+ break;
+ case 'N': /* name only */
+ name_only = 1;
+ break;
+ case 'q': /* quiet */
+ quiet = 1;
+ break;
+ case 'R': /* root dir */
+ rootdir = optarg;
+ break;
+ case 's': /* set empty WITH_/WITHOUT_ knobs (make/src) */
+ set_knobs = 1;
+ break;
+ case 'v': /* verbose (last file + final value) */
+ verbose = 1;
+ break;
+ case 'V': /* verbose trail (every assignment step) */
+ verbose_trail = 1;
+ break;
+ case 'x': /* remove */
+ remove_mode = 1;
+ break;
+ case '?': /* unknown argument (based on optstring) */
+ default: /* unhandled argument (based on switch) */
+ usage();
+ }
+ }
+
+ return (optind);
+}
+
+/*
+ * The native configuration trinity's third member: read and modify
+ * loader.conf(5), sysctl.conf(5), make.conf(5), and arbitrary configuration
+ * files with sysctl(8) assignment syntax.
+ */
+int
+main(int argc, char *argv[])
+{
+ int have_reads = 0;
+ int have_writes = 0;
+ int n;
+ int rv = EXIT_SUCCESS;
+ const char *target = NULL;
+
+ pgm = argv[0]; /* store a copy of invocation name */
+
+ /*
+ * The `rc' target is a pure pass-through to sysrc(8), detected
+ * while the command line is still pristine so that nothing --
+ * `--help' and `--version' included -- is intercepted on its way
+ * there (see exec_sysrc()).
+ */
+ n = find_target(argc, argv);
+ if (n > 0 && strcmp(argv[n], "rc") == 0)
+ exec_sysrc(argc, argv, n); /* never returns */
+
+ /*
+ * Honor `--help' and `--version' wherever they appear (getopt(3)
+ * knows nothing of long options and would reject them as illegal
+ * short options). A bare `--' ends option processing as usual.
+ */
+ for (n = 1; n < argc; n++) {
+ if (strcmp(argv[n], "--") == 0)
+ break;
+ if (strcmp(argv[n], "--help") == 0)
+ help();
+ if (strcmp(argv[n], "--version") == 0) {
+ puts(SYSCONF_VERSION);
+ exit(EXIT_SUCCESS);
+ }
+ }
+
+ /*
+ * Process command-line options
+ */
+ n = parse_options(argc, argv);
+ argc -= n;
+ argv += n;
+
+ /*
+ * Consume the (required) target keyword and allow further options
+ * to follow it, mirroring the option placement freedom of
+ * sysctl(8) (e.g., `sysconf sysctl -L'). getopt(3) skips over its
+ * first argument, so hand it the target keyword's slot as the
+ * program name.
+ */
+ if (argc > 0) {
+ target = argv[0];
+ argc--;
+ argv++;
+#ifdef __FreeBSD__
+ optreset = 1;
+ optind = 1;
+#else
+ optind = 0; /* glibc and musl re-initialize at zero */
+#endif
+ n = parse_options(argc + 1, argv - 1) - 1;
+ argc -= n;
+ argv += n;
+ }
+
+ if (jailname != NULL && *rootdir != '\0') {
+ warnx("-j and -R are mutually exclusive");
+ usage();
+ }
+ if (file != NULL && module != NULL) {
+ warnx("-f and -k are mutually exclusive");
+ usage();
+ }
+ if (existing_only && !list_files && !list_all) {
+ warnx("-E requires -l or -L");
+ usage();
+ }
+ if (set_knobs && remove_mode) {
+ warnx("-s and -x are mutually exclusive");
+ usage();
+ }
+
+#ifdef __FreeBSD__
+ if (jailname != NULL) {
+ int jid;
+
+ if ((jid = jail_getid(jailname)) < 0)
+ errx(EXIT_FAILURE, "%s", jail_errmsg);
+ if (jail_attach(jid) != 0)
+ err(EXIT_FAILURE, "jail_attach(%d)", jid);
+ }
+#else
+ if (jailname != NULL)
+ errx(EXIT_FAILURE, "-j is not supported on this platform");
+#endif
+
+ /* Resolve the target format and its backing files */
+ resolve_target(target);
+
+ /* `-k' requires a target with a module drop-in directory */
+ if (module != NULL) {
+ const struct bsdconf_format_def *def;
+ const struct bsdconf_source *source;
+ int has_moddir = 0;
+
+ def = bsdconf_format_lookup(format);
+ if (def != NULL && def->sources != NULL)
+ for (source = def->sources; source->path != NULL;
+ source++)
+ if (source->type == BSDCONF_SOURCE_MODDIR)
+ has_moddir = 1;
+ if (!has_moddir) {
+ warnx("target does not support -k");
+ usage();
+ }
+ }
+
+ /*
+ * `-d', `-D', and `-A' consult a body of defaults; only targets
+ * that have one support them (make.conf and friends have no
+ * defaults file, and no directive descriptions to give). The
+ * exception is `sysctl -d', whose descriptions come from the
+ * running kernel rather than any file.
+ */
+ {
+ const struct bsdconf_format_def *def;
+ int has_defaults;
+
+ def = bsdconf_format_lookup(format);
+ has_defaults = def != NULL && def->defaults != NULL;
+
+ if (show_desc) {
+ if (format != BSDCONF_FORMAT_SYSCTL &&
+ !has_defaults) {
+ warnx("target does not support -d");
+ usage();
+ }
+ if (file != NULL) {
+ warnx("-d and -f are mutually exclusive");
+ usage();
+ }
+ if (list_files || list_all || remove_mode || check ||
+ set_knobs) {
+ warnx("-d cannot be combined with "
+ "-c/-l/-L/-s/-x");
+ usage();
+ }
+#ifdef __FreeBSD__
+ if (format == BSDCONF_FORMAT_SYSCTL &&
+ *rootdir != '\0')
+ errx(EXIT_FAILURE, "sysctl descriptions "
+ "come from the running kernel; "
+ "-R does not apply");
+#endif
+ }
+ if ((defaults_only || with_defaults) && !has_defaults) {
+ warnx("target does not support -%c",
+ defaults_only ? 'D' : 'A');
+ usage();
+ }
+ if ((defaults_only || with_defaults) && file != NULL) {
+ warnx("-%c and -f are mutually exclusive",
+ defaults_only ? 'D' : 'A');
+ usage();
+ }
+ }
+
+ /* File listings take no names and perform no other work */
+ if (list_files || list_all) {
+ if (argc != 0 || dump_all) {
+ warnx("-l/-L take no names");
+ usage();
+ }
+ exit(do_list());
+ }
+
+ /* `-A' with no names dumps everything, defaults included */
+ if (with_defaults && argc == 0)
+ dump_all = 1;
+
+ /* Display usage and exit if not given at least one name */
+ if (argc == 0 && !dump_all) {
+ warnx("no names provided");
+ usage();
+ }
+ if (argc != 0 && dump_all) {
+ warnx("-a does not take names");
+ usage();
+ }
+
+ /* Classify the remaining arguments */
+ nreqs = (unsigned int)argc;
+ if (nreqs > 0 &&
+ (reqs = calloc(nreqs, sizeof(*reqs))) == NULL)
+ err(EXIT_FAILURE, NULL);
+ if (set_knobs) {
+ if (format != BSDCONF_FORMAT_MAKE &&
+ format != BSDCONF_FORMAT_SRC) {
+ warnx("-s is only valid for make and src");
+ usage();
+ }
+ if (argc == 0) {
+ warnx("-s requires at least one WITH_/WITHOUT_ name");
+ usage();
+ }
+ }
+ for (n = 0; n < argc; n++) {
+ split_request(argv[n], &reqs[n], remove_mode);
+ if (set_knobs) {
+ if (reqs[n].newvalue != NULL) {
+ warnx("-s does not take a value "
+ "('%s')", argv[n]);
+ usage();
+ }
+ if (!make_knob_name_p(reqs[n].name)) {
+ if (strcmp(reqs[n].name,
+ "WITHOUT_MODULES") == 0)
+ warnx("WITHOUT_MODULES takes a "
+ "module list; use "
+ "WITHOUT_MODULES=...");
+ else
+ warnx("-s requires a WITH_ or "
+ "WITHOUT_ name ('%s')",
+ reqs[n].name);
+ usage();
+ }
+ reqs[n].newvalue = "";
+ }
+ if (reqs[n].newvalue != NULL && !reqs[n].remove)
+ warn_with_equals_no(reqs[n].name,
+ reqs[n].newvalue, NULL, 0);
+ if (reqs[n].remove || reqs[n].newvalue != NULL)
+ have_writes = 1;
+ else
+ have_reads = 1;
+ }
+ if (check && !have_writes) {
+ warnx("-c requires at least one name=value or -s name");
+ usage();
+ }
+
+ /* Descriptions are read-only */
+ if (show_desc) {
+ if (have_writes) {
+ warnx("-d does not take values");
+ usage();
+ }
+#ifndef __FreeBSD__
+ if (format == BSDCONF_FORMAT_SYSCTL)
+ errx(EXIT_FAILURE,
+ "sysctl descriptions require FreeBSD");
+#endif
+ /* Explicit names need no file scan at all */
+ if (!dump_all) {
+#ifdef __FreeBSD__
+ if (format == BSDCONF_FORMAT_SYSCTL)
+ exit(describe_sysctl());
+#endif
+ exit(describe_defaults());
+ }
+
+ /*
+ * Dumps (-ad, -Ad, -aDd) pair each scanned directive with
+ * its description; harvest the descriptions before the
+ * read sandbox slams shut.
+ */
+ if (format != BSDCONF_FORMAT_SYSCTL)
+ load_descriptions();
+ }
+
+ /* Defaults can be read and checked against, never rewritten */
+ if ((defaults_only || with_defaults) && have_writes && !check)
+ errx(EXIT_FAILURE, "defaults are read-only");
+
+ /* Standard input can be read and checked, never rewritten */
+ if (file_stdin && have_writes && !check)
+ errx(EXIT_FAILURE, "cannot write to standard input");
+
+#if defined(__FreeBSD__)
+ /*
+ * Refuse up front to set sysctl OIDs that can never take effect
+ * from sysctl.conf(5) (non-leaf, read-only, loader-only tunables)
+ * or whose values cannot fit the OID's CTLTYPE (so an overflowing
+ * assignment cannot surprise sysctl(8) at boot). Unknown OIDs are
+ * warned about but still written -- the module may not be loaded
+ * yet, matching init(8)'s handling of sysctl.conf(5). Only
+ * meaningful against the running system's sysctl tree, so skipped
+ * under -R. -i/-q quiet the unknown-OID warning; -k remains the
+ * module drop-in selector.
+ */
+ if (format == BSDCONF_FORMAT_SYSCTL && have_writes && !check &&
+ *rootdir == '\0') {
+ int quiet_unknown = ignore_unknown || quiet;
+ unsigned int u;
+
+ for (u = 0; u < nreqs; u++) {
+ if (reqs[u].newvalue == NULL)
+ continue;
+ if (sysctl_writable(reqs[u].name, reqs[u].newvalue,
+ quiet_unknown) != 0) {
+ /* Convert to a no-op; count the failure */
+ reqs[u].newvalue = NULL;
+ reqs[u].remove = 0;
+ reqs[u].name = "";
+ rv = EXIT_FAILURE;
+ }
+ }
+ have_writes = 0;
+ have_reads = 0;
+ for (u = 0; u < nreqs; u++) {
+ if (reqs[u].remove || reqs[u].newvalue != NULL)
+ have_writes = 1;
+ else if (reqs[u].name[0] != '\0')
+ have_reads = 1;
+ }
+ if (rv != EXIT_SUCCESS && !have_writes && !have_reads)
+ exit(rv); /* nothing settable remains */
+ }
+#endif
+
+ /*
+ * Scan the backing files to locate each request's authoritative
+ * definition. Pure reads and -c checks (do_checks() never calls
+ * put) run the scan inside a Capsicum sandbox -- except a sysctl
+ * description dump, whose kernel queries the sandbox would deny.
+ * Classify by whether we will actually write, not by whether the
+ * argv looked like name=value (`-c' sets have_writes for checks).
+ */
+ scan_pass((check || !have_writes) &&
+ !(show_desc && format == BSDCONF_FORMAT_SYSCTL));
+
+ /* Materialize list edits now that effective values are known */
+ if (merge_list_requests() != EXIT_SUCCESS)
+ rv = EXIT_FAILURE;
+
+ if (check) {
+ if (do_make_strikes(1) != EXIT_SUCCESS)
+ rv = EXIT_FAILURE;
+ if (do_checks() != EXIT_SUCCESS)
+ rv = EXIT_FAILURE;
+ exit(rv);
+ }
+
+ /* Apply writes first so that subsequent reads see the result */
+ if (have_writes) {
+ int wrv;
+
+ if ((wrv = do_make_strikes(0)) != EXIT_SUCCESS)
+ rv = wrv;
+ if ((wrv = do_writes()) != EXIT_SUCCESS)
+ rv = wrv;
+
+ /* Refresh the scan for any trailing read requests */
+ if (have_reads || dump_all) {
+ for (n = 0; (unsigned int)n < nreqs; n++) {
+ size_t ai;
+
+ trail_free(reqs[n].trail, reqs[n].ntrail);
+ reqs[n].trail = NULL;
+ reqs[n].ntrail = 0;
+ for (ai = 0; ai < reqs[n].nassigns; ai++)
+ free(reqs[n].assigns[ai].piece);
+ free(reqs[n].assigns);
+ reqs[n].assigns = NULL;
+ reqs[n].nassigns = 0;
+ reqs[n].found = 0;
+ reqs[n].append_present = 0;
+ reqs[n].srcidx = -1;
+ reqs[n].line = 0;
+ memset(reqs[n].infile, 0, nconf_files);
+ }
+ scan_pass(0);
+ }
+ }
+
+ if (have_reads || dump_all) {
+ int rrv;
+
+ if ((rrv = do_reads()) != EXIT_SUCCESS)
+ rv = rrv;
+ }
+
+ exit(rv);
+}
+
+/*
+ * Print short usage statement to stderr and exit with error status.
+ */
+void
+usage(void)
+{
+
+ fprintf(stderr,
+ "usage: %s target [-AcDeFinNqsvVx] [-j jail | -R dir]"
+ " [-f file | -k module]\n"
+ " name[[+|-]=value] ...\n", pgm);
+ fprintf(stderr,
+ " %s target [-ADeFnNqvV] [-j jail | -R dir]"
+ " [-f file | -k module] -a\n", pgm);
+ fprintf(stderr,
+ " %s target [-AaDinNq] [-j jail | -R dir]"
+ " -d [name ...]\n", pgm);
+ fprintf(stderr,
+ " %s target [-E] [-j jail | -R dir] [-k module]"
+ " -l | -L\n", pgm);
+ fprintf(stderr,
+ " %s rc [sysrc(8) argument ...]\n", pgm);
+ fprintf(stderr, "Try `%s --help' for more information.\n", pgm);
+ exit(EXIT_FAILURE);
+}
+
+/*
+ * Print long usage statement to stderr and exit with error status.
+ */
+void
+help(void)
+{
+
+ fprintf(stderr,
+ "usage: %s target [-AcDeFinNqsvVx] [-j jail | -R dir]"
+ " [-f file | -k module] name[[+|-]=value] ...\n", pgm);
+ fprintf(stderr, "TARGETS:\n");
+#define TGTFMT "\t%-9s %s\n"
+ fprintf(stderr, TGTFMT, "loader",
+ "files named by loader_conf_files et al. in");
+ fprintf(stderr, TGTFMT, "",
+ "/boot/defaults/loader.conf (typically /boot/loader.conf,");
+ fprintf(stderr, TGTFMT, "",
+ "loader.conf.d/*.conf, loader.conf.local)");
+ fprintf(stderr, TGTFMT, "sysctl",
+ "/etc/sysctl.conf, sysctl.conf.local, sysctl.kld.d/*.conf");
+ fprintf(stderr, TGTFMT, "make",
+ "/etc/make.conf (or -f for any make-syntax file)");
+ fprintf(stderr, TGTFMT, "src",
+ "/usr/src build triad: src-env.conf, make.conf, src.conf");
+ fprintf(stderr, TGTFMT, "",
+ "(new writes prefer src.conf; env: SRC_ENV_CONF,");
+ fprintf(stderr, TGTFMT, "",
+ "__MAKE_CONF, SRCCONF)");
+ fprintf(stderr, TGTFMT, "rc",
+ "pass-through: all other arguments go to sysrc(8) verbatim");
+ fprintf(stderr, TGTFMT, "generic",
+ "no default files; requires -f file");
+ fprintf(stderr, "OPTIONS:\n");
+#define OPTFMT "\t%-9s %s\n"
+ fprintf(stderr, OPTFMT, "-A",
+ "Include the target's defaults file in the sourcing order");
+ fprintf(stderr, OPTFMT, "",
+ "(alone, dumps everything, defaults included).");
+ fprintf(stderr, OPTFMT, "-a",
+ "Dump the union of all directives from the target's files.");
+ fprintf(stderr, OPTFMT, "-c",
+ "Check. Return success if no changes needed, else error.");
+ fprintf(stderr, OPTFMT, "-D",
+ "Consult only the target's defaults file.");
+ fprintf(stderr, OPTFMT, "-d",
+ "Show directive descriptions (from the defaults file's");
+ fprintf(stderr, OPTFMT, "",
+ "comments; for sysctl, from the running kernel). With");
+ fprintf(stderr, OPTFMT, "",
+ "-a, -A, or -D, describe every directive in scope.");
+ fprintf(stderr, OPTFMT, "-E",
+ "With -l or -L, list only files that exist on disk.");
+ fprintf(stderr, OPTFMT, "-e",
+ "Separate name and value with `=' (reads and write echoes).");
+ fprintf(stderr, OPTFMT, "-F",
+ "Show the file holding each directive's effective value.");
+ fprintf(stderr, OPTFMT, "-f file",
+ "Operate on file, in the target's format, instead of the");
+ fprintf(stderr, OPTFMT, "",
+ "target's standard files (`-' means standard input).");
+ fprintf(stderr, OPTFMT, "-h",
+ "Print a short usage statement to stderr and exit.");
+ fprintf(stderr, OPTFMT, "--help",
+ "Print this message to stderr and exit.");
+ fprintf(stderr, OPTFMT, "--version",
+ "Print version information to stdout and exit.");
+ fprintf(stderr, OPTFMT, "-i",
+ "Ignore unknown names (and quiet unknown sysctl OID warnings).");
+ fprintf(stderr, OPTFMT, "-j jail",
+ "Operate within the jail `jail' (name or numeric id).");
+ fprintf(stderr, OPTFMT, "-k module",
+ "Include the kernel module drop-in file for `module'.");
+ fprintf(stderr, OPTFMT, "-l",
+ "List the pathnames of the target's backing files.");
+ fprintf(stderr, OPTFMT, "-L",
+ "List all candidate files, including module drop-ins.");
+ fprintf(stderr, OPTFMT, "-n",
+ "Show only directive values, not their names.");
+ fprintf(stderr, OPTFMT, "-N",
+ "Show only directive names, not their values.");
+ fprintf(stderr, OPTFMT, "-q",
+ "Quiet. Suppress unknown-directive warnings and the");
+ fprintf(stderr, OPTFMT, "",
+ "`old -> new' echo of writes.");
+ fprintf(stderr, OPTFMT, "-R dir",
+ "Operate within the root directory `dir' rather than `/'.");
+ fprintf(stderr, OPTFMT, "-s",
+ "Set empty WITH_/WITHOUT_ knobs (make and src only).");
+ fprintf(stderr, OPTFMT, "-v",
+ "Verbose. Print the pathname of the configuration file");
+ fprintf(stderr, OPTFMT, "",
+ "holding the final effective value.");
+ fprintf(stderr, OPTFMT, "-V",
+ "Trail. Reads: each assignment step (file:line, operator,");
+ fprintf(stderr, OPTFMT, "",
+ "fragment, running effective). Writes: file:line:");
+ fprintf(stderr, OPTFMT, "",
+ "name=old -> name=new (or name=value (unchanged)).");
+ fprintf(stderr, OPTFMT, "-x",
+ "Remove name(s) from the target's files.");
+ fprintf(stderr, "ENVIRONMENT:\n");
+ fprintf(stderr, OPTFMT, "LOADER_DEFAULTS",
+ "Defaults file for the loader target (in place of");
+ fprintf(stderr, OPTFMT, "",
+ "/boot/defaults/loader.conf).");
+ fprintf(stderr, OPTFMT, "SRC_ENV_CONF",
+ "src-env.conf for the src target (default /etc/src-env.conf).");
+ fprintf(stderr, OPTFMT, "__MAKE_CONF",
+ "make.conf for the src target (default /etc/make.conf).");
+ fprintf(stderr, OPTFMT, "SRCCONF",
+ "src.conf for the src target (default /etc/src.conf).");
+ exit(EXIT_FAILURE);
+}
diff --git a/usr.sbin/sysconf/sysconf_edit_assign.c b/usr.sbin/sysconf/sysconf_edit_assign.c
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf_edit_assign.c
@@ -0,0 +1,221 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * CLI assignment parsing and sysrc(8)-style list word edits.
+ */
+
+#include <ctype.h>
+
+#include "sysconf_priv.h"
+
+/*
+ * Split a `name[=value]' argument into a request. With `remove' set, the
+ * argument is a bare name to delete. For make(1)-style targets, a trailing
+ * operator character on the name (`+', `?', `:', `!') selects the
+ * corresponding assignment modifier: `+=' appends a new `name+=value' line
+ * (make accumulates; we never rewrite a prior assignment), while `=', `?=',
+ * `:=', and `!=' rewrite or add a single assignment as today. `name-=word'
+ * is a list strike: the last assignment whose value contains each word is
+ * rewritten (or removed if emptied); see do_make_strikes(). For every other
+ * target, `name+=word' and `name-=word' are sysrc(8)-style list edits
+ * against the effective value (see merge_list_requests() below).
+ */
+void
+split_request(char *arg, struct request *req, int remove)
+{
+ char *eq;
+ size_t n;
+
+ req->name = arg;
+ req->newvalue = NULL;
+ req->op = BSDCONF_OP_DEFAULT;
+ req->listop = '\0';
+ req->value = NULL;
+ req->found = 0;
+ req->append_present = 0;
+ req->remove = remove;
+ req->srcidx = -1;
+ req->line = 0;
+ req->trail = NULL;
+ req->ntrail = 0;
+ req->assigns = NULL;
+ req->nassigns = 0;
+ if ((req->infile = calloc(nconf_files, 1)) == NULL)
+ err(EXIT_FAILURE, NULL);
+
+ if (remove) {
+ if (strchr(arg, '=') != NULL)
+ errx(EXIT_FAILURE,
+ "%s: -x does not take a value", arg);
+ return;
+ }
+
+ if ((eq = strchr(arg, '=')) == NULL)
+ return;
+
+ *eq = '\0';
+
+ /*
+ * Strip one balanced layer of double-quotes from the value as a
+ * courtesy (e.g., a literal zfs_load="YES" from a quoted shell
+ * word); the library re-applies the target format's quoting rules
+ * on output.
+ */
+ req->newvalue = bsdconf_unquote(eq + 1);
+
+ /* Split an operator character off the tail of the name */
+ n = strlen(arg);
+ if ((bsdconf_format_processing(format) &
+ BSDCONF_OPERATOR_EQUALS) != 0 && n > 1) {
+ switch (arg[n - 1]) {
+ case '+': req->op = BSDCONF_OP_APPEND; break;
+ case '?': req->op = BSDCONF_OP_COND; break;
+ case ':': req->op = BSDCONF_OP_EXPAND; break;
+ case '!': req->op = BSDCONF_OP_SHELL; break;
+ case '-':
+ /* make(1) has no `-='; treat as a list strike */
+ req->listop = '-';
+ arg[n - 1] = '\0';
+ break;
+ }
+ if (req->op != BSDCONF_OP_DEFAULT)
+ arg[n - 1] = '\0';
+ } else if (n > 1 && (arg[n - 1] == '+' || arg[n - 1] == '-')) {
+ req->listop = arg[n - 1];
+ arg[n - 1] = '\0';
+ }
+
+ if (*arg == '\0')
+ errx(EXIT_FAILURE, "empty directive name");
+}
+
+/* True when `req' is a make/src `name-=word' strike (not a sysrc list edit). */
+/*
+ * Test whether the whitespace-separated `list' contains `word' (of length
+ * `wlen') as a whole word.
+ */
+int
+list_has(const char *list, const char *word, size_t wlen)
+{
+ size_t len;
+
+ if (list == NULL)
+ return (0);
+ while (*list != '\0') {
+ list += strspn(list, " \t");
+ if ((len = strcspn(list, " \t")) == 0)
+ break;
+ if (len == wlen && strncmp(list, word, wlen) == 0)
+ return (1);
+ list += len;
+ }
+
+ return (0);
+}
+
+/*
+ * Merge the whitespace-separated `words' into (op `+') or out of (op `-')
+ * the whitespace-separated list `current' (NULL for an unset directive),
+ * appending only words not already present and joining the survivors with
+ * single spaces. Returns newly allocated storage.
+ */
+char *
+list_merge(const char *current, const char *words, int op)
+{
+ size_t len;
+ size_t rlen = 0;
+ size_t size;
+ char *result;
+ const char *token;
+
+ if (current == NULL)
+ current = "";
+ size = strlen(current) + strlen(words) + 2;
+ if ((result = malloc(size)) == NULL)
+ err(EXIT_FAILURE, NULL);
+ result[0] = '\0';
+
+ /* Copy the current words, striking matches under `-' */
+ token = current;
+ while (*token != '\0') {
+ token += strspn(token, " \t");
+ if ((len = strcspn(token, " \t")) == 0)
+ break;
+ if (op == '-' && list_has(words, token, len)) {
+ token += len;
+ continue;
+ }
+ if (rlen > 0)
+ result[rlen++] = ' ';
+ memcpy(result + rlen, token, len);
+ rlen += len;
+ result[rlen] = '\0';
+ token += len;
+ }
+
+ /* Append the new words not already present under `+' */
+ if (op == '+') {
+ token = words;
+ while (*token != '\0') {
+ token += strspn(token, " \t");
+ if ((len = strcspn(token, " \t")) == 0)
+ break;
+ if (!list_has(result, token, len)) {
+ if (rlen > 0)
+ result[rlen++] = ' ';
+ memcpy(result + rlen, token, len);
+ rlen += len;
+ result[rlen] = '\0';
+ }
+ token += len;
+ }
+ }
+
+ return (result);
+}
+
+/*
+ * Materialize sysrc(8)-style list-edit requests (`name+=word ...' and
+ * `name-=word ...') against the effective values recorded by scan_pass(),
+ * rewriting each request's pending value in place so the ordinary write,
+ * check, and echo machinery sees a plain assignment. Make/src `name-=word'
+ * strikes are handled separately by do_make_strikes(). Appending to an
+ * unset directive creates it; striking words from one is reported like any
+ * other unknown directive (there is nothing to remove) and the request is
+ * neutralized. Returns EXIT_SUCCESS or EXIT_FAILURE.
+ */
+int
+merge_list_requests(void)
+{
+ int rv = EXIT_SUCCESS;
+ unsigned int n;
+
+ for (n = 0; n < nreqs; n++) {
+ if (reqs[n].listop == '\0' || reqs[n].newvalue == NULL)
+ continue;
+ /* make/src `-=` is a per-statement strike, not a list merge */
+ if (make_strike_p(&reqs[n]))
+ continue;
+ if (reqs[n].listop == '-' && !reqs[n].found) {
+ if (!ignore_unknown) {
+ if (!quiet)
+ warnx("unknown directive '%s'",
+ reqs[n].name);
+ rv = EXIT_FAILURE;
+ }
+ /* Convert to a no-op */
+ reqs[n].newvalue = NULL;
+ reqs[n].name = "";
+ continue;
+ }
+ reqs[n].newvalue = list_merge(reqs[n].value,
+ reqs[n].newvalue, reqs[n].listop);
+ }
+
+ return (rv);
+}
diff --git a/usr.sbin/sysconf/sysconf_edit_make.c b/usr.sbin/sysconf/sysconf_edit_make.c
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf_edit_make.c
@@ -0,0 +1,274 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * make(1)/src triad word-removal across += assignment fragments.
+ */
+
+#include <ctype.h>
+
+#include "sysconf_priv.h"
+
+int
+make_strike_p(const struct request *req)
+{
+
+ return ((bsdconf_format_processing(format) &
+ BSDCONF_OPERATOR_EQUALS) != 0 &&
+ req->listop == '-' && req->newvalue != NULL);
+}
+
+int
+make_assign_push(struct request *req, enum bsdconf_op op,
+ const char *piece, uint32_t line)
+{
+ struct make_assign *na;
+ char *pcopy;
+
+ if (piece == NULL)
+ piece = "";
+ if ((pcopy = strdup(piece)) == NULL)
+ return (-1);
+ na = realloc(req->assigns, (req->nassigns + 1) * sizeof(*na));
+ if (na == NULL) {
+ free(pcopy);
+ return (-1);
+ }
+ req->assigns = na;
+ na[req->nassigns].fileidx = conf_scanidx;
+ na[req->nassigns].line = line;
+ na[req->nassigns].op = op;
+ na[req->nassigns].piece = pcopy;
+ req->nassigns++;
+ return (0);
+}
+
+/*
+ * Apply make/src `name-=word' strikes: for each whitespace-separated word,
+ * find the last assignment whose piece contains it and remove the word from
+ * that piece (delete the statement if the piece becomes empty). With
+ * `check_only', report whether any change would be required without writing.
+ * Echoes effective old/new (or `(unchanged)') unless -q. Neutralizes each
+ * handled request so do_writes() skips it. Returns EXIT_SUCCESS or
+ * EXIT_FAILURE.
+ */
+int
+do_make_strikes(int check_only)
+{
+ char **work;
+ char *new_eff;
+ char *token;
+ char *wordscopy;
+ char *wp;
+ const char *old_eff;
+ int differs;
+ int rv = EXIT_SUCCESS;
+ size_t i;
+ size_t j;
+ size_t wlen;
+ unsigned int n;
+
+ for (n = 0; n < nreqs; n++) {
+ if (!make_strike_p(&reqs[n]))
+ continue;
+ if (!reqs[n].found || reqs[n].nassigns == 0) {
+ if (!ignore_unknown) {
+ if (!quiet)
+ warnx("unknown directive '%s'",
+ reqs[n].name);
+ rv = EXIT_FAILURE;
+ }
+ reqs[n].newvalue = NULL;
+ reqs[n].name = "";
+ continue;
+ }
+
+ if ((work = calloc(reqs[n].nassigns, sizeof(*work))) == NULL)
+ err(EXIT_FAILURE, NULL);
+ for (i = 0; i < reqs[n].nassigns; i++) {
+ work[i] = strdup(reqs[n].assigns[i].piece);
+ if (work[i] == NULL)
+ err(EXIT_FAILURE, NULL);
+ }
+
+ differs = 0;
+ if ((wordscopy = strdup(reqs[n].newvalue)) == NULL)
+ err(EXIT_FAILURE, NULL);
+ wp = wordscopy;
+ while ((token = strsep(&wp, " \t")) != NULL) {
+ if (*token == '\0')
+ continue;
+ wlen = strlen(token);
+ for (i = reqs[n].nassigns; i > 0; i--) {
+ j = i - 1;
+ if (!list_has(work[j], token, wlen))
+ continue;
+ {
+ char *merged;
+
+ merged = list_merge(work[j], token,
+ '-');
+ free(work[j]);
+ work[j] = merged;
+ differs = 1;
+ }
+ break;
+ }
+ }
+ free(wordscopy);
+
+ if (check_only) {
+ if (differs) {
+ if (!quiet)
+ warnx("%s: value differs",
+ reqs[n].name);
+ rv = EXIT_FAILURE;
+ }
+ goto neutralize;
+ }
+
+ old_eff = reqs[n].value != NULL ? reqs[n].value : "";
+ if (!differs) {
+ if (!quiet)
+ print_write_echo(
+ reqs[n].srcidx >= 0 ?
+ conf_files[reqs[n].srcidx] : NULL,
+ 0, reqs[n].name, BSDCONF_OP_ASSIGN,
+ old_eff, old_eff, 0, 0, 0);
+ goto neutralize;
+ }
+
+ /* Effective value after applying the struck pieces */
+ new_eff = NULL;
+ for (i = 0; i < reqs[n].nassigns; i++) {
+ char *merged;
+
+ if (work[i][0] == '\0')
+ continue;
+ merged = make_apply(reqs[n].assigns[i].op, new_eff,
+ work[i]);
+ if (merged == NULL)
+ err(EXIT_FAILURE, NULL);
+ free(new_eff);
+ new_eff = merged;
+ }
+ if (new_eff == NULL) {
+ if ((new_eff = strdup("")) == NULL)
+ err(EXIT_FAILURE, NULL);
+ }
+
+ /* Put each changed statement (line-targeted) */
+ for (i = 0; i < nconf_files; i++) {
+ struct bsdconf_option *options;
+ unsigned int nopts = 0;
+ size_t k;
+
+ for (k = 0; k < reqs[n].nassigns; k++)
+ if (reqs[n].assigns[k].fileidx == i &&
+ strcmp(work[k],
+ reqs[n].assigns[k].piece) != 0)
+ nopts++;
+ if (nopts == 0)
+ continue;
+ options = calloc(nopts + 1, sizeof(*options));
+ if (options == NULL)
+ err(EXIT_FAILURE, NULL);
+ nopts = 0;
+ for (k = 0; k < reqs[n].nassigns; k++) {
+ struct bsdconf_option *opt;
+
+ if (reqs[n].assigns[k].fileidx != i ||
+ strcmp(work[k],
+ reqs[n].assigns[k].piece) == 0)
+ continue;
+ opt = &options[nopts++];
+ memset(opt, 0, sizeof(*opt));
+ opt->type = BSDCONF_TYPE_STR;
+ opt->directive = reqs[n].name;
+ opt->match_line = reqs[n].assigns[k].line;
+ opt->op = reqs[n].assigns[k].op;
+ if (work[k][0] == '\0') {
+ opt->action = BSDCONF_ACTION_REMOVE;
+ opt->value.str = NULL;
+ } else {
+ opt->action = BSDCONF_ACTION_SET_VALUE;
+ opt->value.str = work[k];
+ }
+ }
+ {
+ int fd;
+
+ if ((fd = open(conf_files[i],
+ O_WRONLY | O_CREAT | O_EXCL, 0644)) >= 0)
+ close(fd);
+ else if (errno != EEXIST)
+ err(EXIT_FAILURE, "%s", conf_files[i]);
+ }
+ if (bsdconf_put(options, conf_files[i],
+ bsdconf_format_processing(format),
+ bsdconf_format_put(format)) != 0)
+ err(EXIT_FAILURE, "%s", conf_files[i]);
+ if (!quiet && verbose_trail) {
+ for (k = 0; k < nopts; k++) {
+ struct bsdconf_option *opt;
+ const char *oval;
+ const char *nval;
+
+ opt = &options[k];
+ /* Recover old piece from assigns */
+ oval = "";
+ nval = opt->value.str != NULL ?
+ opt->value.str : "";
+ for (j = 0; j < reqs[n].nassigns;
+ j++) {
+ if (reqs[n].assigns[j].fileidx
+ != i ||
+ reqs[n].assigns[j].line !=
+ opt->match_line)
+ continue;
+ oval =
+ reqs[n].assigns[j].piece;
+ break;
+ }
+ if (opt->action ==
+ BSDCONF_ACTION_REMOVE)
+ printf("%s:%u: %s%s%s "
+ "(removed)\n",
+ conf_files[i],
+ opt->match_line,
+ reqs[n].name,
+ op_str(opt->op), oval);
+ else
+ print_write_echo(conf_files[i],
+ opt->match_line,
+ reqs[n].name, opt->op,
+ oval, nval, 1, 0, 0);
+ }
+ }
+ free(options);
+ }
+
+ if (!quiet && !verbose_trail)
+ print_write_echo(
+ reqs[n].srcidx >= 0 ?
+ conf_files[reqs[n].srcidx] : NULL,
+ 0, reqs[n].name, BSDCONF_OP_ASSIGN, old_eff,
+ new_eff, 1, 0, 0);
+ free(reqs[n].value);
+ reqs[n].value = new_eff;
+
+neutralize:
+ for (i = 0; i < reqs[n].nassigns; i++)
+ free(work[i]);
+ free(work);
+ reqs[n].newvalue = NULL;
+ reqs[n].listop = '\0';
+ /* Keep name for any trailing read of the same argv slot */
+ }
+
+ return (rv);
+}
diff --git a/usr.sbin/sysconf/sysconf_edit_write.c b/usr.sbin/sysconf/sysconf_edit_write.c
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf_edit_write.c
@@ -0,0 +1,264 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * 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,
+ 0644)) >= 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);
+}
diff --git a/usr.sbin/sysconf/sysconf_print.c b/usr.sbin/sysconf/sysconf_print.c
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf_print.c
@@ -0,0 +1,320 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Display helpers: name/value pairs, `-V' trails, write echoes.
+ */
+
+#include <ctype.h>
+
+#include "sysconf_priv.h"
+
+/*
+ * True when `name' is a presence-only WITH_/WITHOUT_ build knob
+ * (share/mk/bsd.mkopt.mk tests defined(), and src.conf(5) ignores the
+ * value). WITHOUT_MODULES is excluded: its value is a module reject list.
+ */
+int
+make_knob_name_p(const char *name)
+{
+
+ if (strcmp(name, "WITHOUT_MODULES") == 0)
+ return (0);
+ return (strncmp(name, "WITH_", 5) == 0 ||
+ strncmp(name, "WITHOUT_", 8) == 0);
+}
+
+/*
+ * Precursor to share/mk/bsd.mkopt.mk (seen under /usr/src after
+ * bsd.opts.mk / src.opts.mk, not bare make or ports): WITH_FOO=no
+ * does not disable FOO (presence still counts). Warn only for WITH_*
+ * with the exact value "no" (not WITHOUT_*, not "NO"). On reads,
+ * `path'/`line' locate the authoritative assignment when known.
+ */
+void
+warn_with_equals_no(const char *name, const char *value, const char *path,
+ uint32_t line)
+{
+ const char *opt;
+
+ if (format != BSDCONF_FORMAT_MAKE && format != BSDCONF_FORMAT_SRC)
+ return;
+ if (name == NULL || value == NULL || strcmp(value, "no") != 0)
+ return;
+ /* WITH_ but not WITHOUT_ */
+ if (strncmp(name, "WITH_", 5) != 0 ||
+ strncmp(name, "WITHOUT_", 8) == 0)
+ return;
+ opt = name + 5;
+ if (path != NULL && line != 0)
+ warnx("%s:%u: Use WITHOUT_%s=1 instead of WITH_%s=no",
+ path, line, opt, opt);
+ else
+ warnx("Use WITHOUT_%s=1 instead of WITH_%s=no", opt, opt);
+}
+
+/*
+ * True when `name' is a presence-only WITH_/WITHOUT_ knob on make/src
+ * (bsd.mkopt.mk tests defined(); src.conf(5) ignores the value).
+ */
+int
+make_knob_p(const char *name)
+{
+
+ if (format != BSDCONF_FORMAT_MAKE && format != BSDCONF_FORMAT_SRC)
+ return (0);
+ return (make_knob_name_p(name));
+}
+
+/*
+ * Print a directive/value pair according to the display flags. `path' is
+ * the configuration file holding the authoritative definition (or NULL).
+ */
+void
+print_pair(const char *path, const char *directive, const char *value)
+{
+ int knob;
+
+ if (show_file)
+ value = path != NULL ? path : "(none)";
+ if (verbose && path != NULL && !show_file)
+ printf("%s: ", path);
+ if (name_only) {
+ puts(directive);
+ return;
+ }
+ /* Presence knobs: any assigned value means present (defined()). */
+ knob = !show_file && make_knob_p(directive);
+ if (value_only) {
+ puts(value != NULL ? value : "");
+ return;
+ }
+ /*
+ * Presence display is for the human-readable form only. With -e,
+ * emit the real assignment (`NAME=') so the spelling is valid make
+ * and matches what -s writes.
+ */
+ if (knob && !show_equals) {
+ printf("%s (present)\n", directive);
+ return;
+ }
+ if (show_equals && !show_file) {
+ if ((bsdconf_format_put(format) &
+ BSDCONF_PUT_QUOTE_ALWAYS) != 0)
+ printf("%s=\"%s\"\n", directive,
+ value != NULL ? value : "");
+ else
+ printf("%s=%s\n", directive,
+ value != NULL ? value : "");
+ return;
+ }
+ printf("%s: %s\n", directive, value != NULL ? value : "");
+}
+
+/*
+ * Spelling of a make(1) (or plain) assignment operator for `-V' trails.
+ */
+const char *
+op_str(enum bsdconf_op op)
+{
+
+ switch (op) {
+ case BSDCONF_OP_APPEND:
+ return ("+=");
+ case BSDCONF_OP_COND:
+ return ("?=");
+ case BSDCONF_OP_EXPAND:
+ return (":=");
+ case BSDCONF_OP_SHELL:
+ return ("!=");
+ case BSDCONF_OP_ASSIGN:
+ case BSDCONF_OP_DEFAULT:
+ default:
+ return ("=");
+ }
+}
+
+/*
+ * Append one assignment step to a `-V' trail. No-op when `-V' is unset.
+ * Returns zero on success; -1 (errno set) on allocation failure.
+ */
+int
+trail_push(struct trail_step **trailp, size_t *ntrailp,
+ size_t fileidx, uint32_t line, enum bsdconf_op op,
+ const char *piece, const char *effective)
+{
+ struct trail_step *nt;
+ char *ecopy;
+ char *pcopy;
+
+ if (!verbose_trail)
+ return (0);
+ if (piece == NULL)
+ piece = "";
+ if (effective == NULL)
+ effective = "";
+ if ((pcopy = strdup(piece)) == NULL)
+ return (-1);
+ if ((ecopy = strdup(effective)) == NULL) {
+ free(pcopy);
+ return (-1);
+ }
+ nt = realloc(*trailp, (*ntrailp + 1) * sizeof(*nt));
+ if (nt == NULL) {
+ free(pcopy);
+ free(ecopy);
+ return (-1);
+ }
+ *trailp = nt;
+ nt[*ntrailp].fileidx = fileidx;
+ nt[*ntrailp].line = line;
+ nt[*ntrailp].op = op;
+ nt[*ntrailp].piece = pcopy;
+ nt[*ntrailp].effective = ecopy;
+ (*ntrailp)++;
+ return (0);
+}
+
+void
+trail_free(struct trail_step *trail, size_t ntrail)
+{
+ size_t i;
+
+ for (i = 0; i < ntrail; i++) {
+ free(trail[i].piece);
+ free(trail[i].effective);
+ }
+ free(trail);
+}
+
+/*
+ * Print every recorded assignment step for `name'. Each line is
+ * `file:line: nameoppiece' and, when the running effective value differs
+ * from the fragment, ` -> effective'.
+ */
+void
+print_trail(const char *name, const struct trail_step *trail, size_t ntrail)
+{
+ const char *path;
+ size_t i;
+
+ for (i = 0; i < ntrail; i++) {
+ path = conf_files[trail[i].fileidx];
+ if (strcmp(trail[i].piece, trail[i].effective) != 0)
+ printf("%s:%u: %s%s%s -> %s\n", path, trail[i].line,
+ name, op_str(trail[i].op), trail[i].piece,
+ trail[i].effective);
+ else
+ printf("%s:%u: %s%s%s\n", path, trail[i].line, name,
+ op_str(trail[i].op), trail[i].piece);
+ }
+}
+
+/*
+ * Echo a write result. With -V: `file:line: name=old -> name=new',
+ * `name=value (unchanged)', or `name=value (added)', matching read-trail
+ * shape. With -v alone: prefix the pathname. Otherwise: `name: old -> new'
+ * / `name: value (unchanged)' (additions still use old -> new with an
+ * empty old value). With `-e', the non-trail form uses `=' (and quotes
+ * for formats that always quote), matching print_pair() on reads --
+ * except make/src WITH_/WITHOUT_ presence knobs, which use
+ * `(not present) -> (present)' when newly defined,
+ * `(present) (unchanged)' when `-s' finds them already defined or
+ * an explicit write leaves the value unchanged, and a normal `old ->
+ * new' echo when the placeholder value changes while still present
+ * (also for `-e'). Removals are only echoed when the caller passes
+ * `-v'/`-V' (not bare `-x'), using the same `name (removed)' / trail
+ * form as other directives.
+ */
+void
+print_write_echo(const char *path, uint32_t line, const char *name,
+ enum bsdconf_op op, const char *oldv, const char *newv, int changed,
+ int added, int removed)
+{
+ const char *ops;
+ const char *shown;
+ int knob;
+ int quote;
+ int was_present;
+
+ /* NULL oldv: absent; "" : present with empty assignment */
+ was_present = (oldv != NULL);
+ if (oldv == NULL)
+ oldv = "";
+ if (newv == NULL)
+ newv = "";
+ ops = op_str(op);
+ shown = oldv[0] != '\0' ? oldv : newv;
+ knob = make_knob_p(name) && op != BSDCONF_OP_APPEND;
+
+ if (verbose_trail && path != NULL) {
+ if (removed) {
+ printf("%s:%u: %s (removed)\n", path, line, name);
+ return;
+ }
+ if (knob) {
+ if (!was_present || added) {
+ printf("%s:%u: %s: (not present) -> "
+ "(present)\n", path, line, name);
+ return;
+ }
+ if (strcmp(oldv, newv) == 0) {
+ printf("%s:%u: %s: (present) (unchanged)\n",
+ path, line, name);
+ return;
+ }
+ /* value change while present: fall through */
+ }
+ if (added)
+ printf("%s:%u: %s%s%s (added)\n", path, line, name,
+ ops, newv);
+ else if (changed)
+ printf("%s:%u: %s%s%s -> %s%s%s\n", path, line, name,
+ ops, oldv, name, ops, newv);
+ else
+ printf("%s:%u: %s%s%s (unchanged)\n", path, line, name,
+ ops, shown);
+ return;
+ }
+ if (verbose && path != NULL)
+ printf("%s: ", path);
+ if (removed) {
+ printf("%s (removed)\n", name);
+ return;
+ }
+ if (knob) {
+ if (!was_present || added) {
+ printf("%s: (not present) -> (present)\n", name);
+ return;
+ }
+ if (strcmp(oldv, newv) == 0) {
+ printf("%s: (present) (unchanged)\n", name);
+ return;
+ }
+ /* value change while present: fall through */
+ }
+ quote = show_equals && !show_file &&
+ (bsdconf_format_put(format) & BSDCONF_PUT_QUOTE_ALWAYS) != 0;
+ if (show_equals && !show_file) {
+ if (changed || added) {
+ /* sysrc(8)-style: `name=old # -> new' with `-e' */
+ if (quote)
+ printf("%s=\"%s\" # -> \"%s\"\n", name, oldv,
+ newv);
+ else
+ printf("%s=%s # -> %s\n", name, oldv, newv);
+ } else if (quote)
+ printf("%s=\"%s\" (unchanged)\n", name, shown);
+ else
+ printf("%s=%s (unchanged)\n", name, shown);
+ return;
+ }
+ if (changed || added)
+ printf("%s: %s -> %s\n", name, oldv, newv);
+ else
+ printf("%s: %s (unchanged)\n", name, shown);
+}
diff --git a/usr.sbin/sysconf/sysconf_priv.h b/usr.sbin/sysconf/sysconf_priv.h
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf_priv.h
@@ -0,0 +1,202 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#ifndef _SYSCONF_PRIV_H_
+#define _SYSCONF_PRIV_H_
+
+#ifdef __FreeBSD__
+#include <sys/param.h>
+#include <sys/capsicum.h>
+#include <sys/jail.h>
+#include <sys/linker.h>
+#include <sys/sysctl.h>
+
+#include <capsicum_helpers.h>
+#include <jail.h>
+#endif
+
+#include <bsdconf.h>
+#include <dirent.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#ifndef __unused
+#define __unused /* not all compilers support attributes */
+#endif
+
+#define SYSCONF_VERSION "1.0 Aug-2,2026"
+
+/* getopt(3) optstring; shared by parse_options() and find_target() */
+#define OPTSTRING "AacdDEeFf:hij:k:lLnNqR:svVx"
+
+/* One assignment step recorded for `-V' trails */
+struct trail_step {
+ size_t fileidx; /* index into conf_files */
+ uint32_t line; /* 1-based line in that file */
+ enum bsdconf_op op; /* operator of this statement */
+ char *piece; /* unquoted fragment */
+ char *effective; /* value after applying the op */
+};
+
+/*
+ * One make(1) assignment statement for `name-=word' strikes: the last
+ * statement whose piece contains the word is rewritten (or removed if
+ * the piece becomes empty).
+ */
+struct make_assign {
+ size_t fileidx;
+ uint32_t line;
+ enum bsdconf_op op;
+ char *piece;
+};
+
+/* Requests to process (one per name argument) */
+struct request {
+ const char *name; /* directive name */
+ const char *newvalue; /* value to write (NULL for reads) */
+ enum bsdconf_op op; /* assignment operator (writes) */
+ char listop; /* `+' append / `-' strike words */
+ char *value; /* value read from file(s) */
+ int found; /* directive seen while parsing */
+ int append_present; /* identical make `+=value' line */
+ int remove; /* nonzero for `-x' removals */
+ int srcidx; /* conf file that last set the value
+ (authoritative); -1 if unset */
+ uint32_t line; /* 1-based line of that definition */
+ unsigned char *infile; /* per-file presence map */
+ struct trail_step *trail; /* `-V' assignment steps */
+ size_t ntrail;
+ struct make_assign *assigns; /* make `-=': per-statement pieces */
+ size_t nassigns;
+};
+
+/* Union of all directives, for `-a' against multi-file targets */
+struct dumpent {
+ char *name; /* directive name */
+ char *value; /* last value seen */
+ int srcidx; /* conf file that last set the value */
+ uint32_t line; /* 1-based line of that definition */
+ struct trail_step *trail; /* `-V' assignment steps */
+ size_t ntrail;
+};
+
+/* Request / dump state (defined in sysconf.c) */
+extern struct request *reqs;
+extern unsigned int nreqs;
+extern struct dumpent *dumps;
+extern size_t ndumps;
+extern size_t dumpsize;
+
+/* Resolved target state */
+extern char **conf_files;
+extern size_t nconf_files;
+extern size_t conf_scanidx;
+extern size_t write_idx;
+extern int defaults_idx;
+extern enum bsdconf_format format;
+
+/* Display / mode flags */
+extern const char *pgm;
+extern uint8_t check;
+extern uint8_t defaults_only;
+extern uint8_t dump_all;
+extern uint8_t existing_only;
+extern uint8_t ignore_unknown;
+extern uint8_t list_all;
+extern uint8_t list_files;
+extern uint8_t name_only;
+extern uint8_t quiet;
+extern uint8_t remove_mode;
+extern uint8_t set_knobs;
+extern uint8_t show_desc;
+extern uint8_t show_equals;
+extern uint8_t show_file;
+extern uint8_t value_only;
+extern uint8_t verbose;
+extern uint8_t verbose_trail;
+extern uint8_t with_defaults;
+
+/* Option arguments */
+extern const char *file;
+extern uint8_t file_stdin;
+extern const char *jailname;
+extern const char *module;
+extern const char *rootdir;
+
+/* sysconf_print.c */
+int make_knob_name_p(const char *_name);
+int make_knob_p(const char *_name);
+void warn_with_equals_no(const char *_name, const char *_value,
+ const char *_path, uint32_t _line);
+void print_pair(const char *_path, const char *_directive,
+ const char *_value);
+const char *op_str(enum bsdconf_op _op);
+int trail_push(struct trail_step **_trailp, size_t *_ntrailp,
+ size_t _fileidx, uint32_t _line, enum bsdconf_op _op,
+ const char *_piece, const char *_effective);
+void trail_free(struct trail_step *_trail, size_t _ntrail);
+void print_trail(const char *_name,
+ const struct trail_step *_trail, size_t _ntrail);
+void print_write_echo(const char *_path, uint32_t _line,
+ const char *_name, enum bsdconf_op _op, const char *_oldv,
+ const char *_newv, int _changed, int _added, int _removed);
+
+/* sysconf_resolve.c */
+char *defaults_path(void);
+void resolve_target(const char *_target);
+int do_list(void);
+
+/* sysconf_scan.c */
+char *make_apply(enum bsdconf_op _op, const char *_old,
+ const char *_piece);
+int scan_pass(int _sandbox);
+
+/* sysconf_edit_assign.c */
+void split_request(char *_arg, struct request *_req, int _remove);
+int list_has(const char *_list, const char *_word, size_t _wlen);
+char *list_merge(const char *_current, const char *_words, int _op);
+int merge_list_requests(void);
+
+/* sysconf_edit_make.c */
+int make_strike_p(const struct request *_req);
+int make_assign_push(struct request *_req, enum bsdconf_op _op,
+ const char *_piece, uint32_t _line);
+int do_make_strikes(int _check_only);
+
+/* sysconf_edit_write.c */
+int do_checks(void);
+int do_writes(void);
+
+/* sysconf_query_value.c */
+int do_reads(void);
+
+/* sysconf_query_desc.c */
+void load_descriptions(void);
+void print_desc(const char *_directive);
+int describe_defaults(void);
+
+/* sysconf_query_sysctl.c */
+#if defined(__FreeBSD__)
+int sysctl_descr(const char *_name, char *_buf, size_t _bufsize);
+int describe_sysctl(void);
+int sysctl_writable(const char *_name, const char *_value,
+ int _quiet_unknown);
+#endif
+
+/* sysconf.c */
+void usage(void);
+void help(void);
+
+#endif /* !_SYSCONF_PRIV_H_ */
diff --git a/usr.sbin/sysconf/sysconf_query_desc.c b/usr.sbin/sysconf/sysconf_query_desc.c
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf_query_desc.c
@@ -0,0 +1,209 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Directive descriptions harvested from defaults-file comments (-d).
+ */
+
+#include "sysconf_priv.h"
+
+/*
+ * Directive descriptions harvested from the defaults file by
+ * load_descriptions() below.
+ */
+struct descent {
+ char *name; /* directive name */
+ char *desc; /* description (may be empty) */
+};
+
+static struct descent *descs; /* set by load_descriptions() */
+static size_t ndescs;
+static size_t descsize;
+
+/*
+ * Harvest every directive description from the defaults file, where a
+ * description is the inline comment trailing the directive's default
+ * assignment, continued across any subsequent lines of whitespace
+ * followed by a comment character. A commented-out default
+ * (`#comconsole_speed="115200" # Set the ...') yields its directive and
+ * description all the same -- deliberately unlike sysrc(8), which goes
+ * blind when the default itself is disabled. Comment prose is never
+ * mistaken for a directive: a name must directly abut the optional
+ * leading `#' and run unbroken to its `='.
+ */
+void
+load_descriptions(void)
+{
+ int current = -1; /* entry whose description is growing */
+ FILE *fp;
+ ssize_t linelen;
+ size_t len;
+ size_t linesize = 0;
+ size_t n;
+ char *defaults;
+ char *line = NULL;
+ char *p;
+ char *v;
+ struct descent *tmp;
+
+ if ((defaults = defaults_path()) == NULL)
+ errx(EXIT_FAILURE, "target has no defaults file");
+ if ((fp = fopen(defaults, "r")) == NULL)
+ err(EXIT_FAILURE, "%s", defaults);
+
+ while ((linelen = getline(&line, &linesize, fp)) != -1) {
+ int quoted = 0;
+
+ if (linelen > 0 && line[linelen - 1] == '\n')
+ line[--linelen] = '\0';
+
+ /* Whitespace then `#' continues the open description */
+ p = line;
+ if (*p == ' ' || *p == '\t') {
+ p += strspn(p, " \t");
+ if (*p == '#' && current >= 0) {
+ char *grown;
+
+ p++;
+ p += strspn(p, " \t");
+ if (*p == '\0')
+ continue;
+ len = strlen(descs[current].desc) +
+ strlen(p) + 2;
+ if ((grown = malloc(len)) == NULL)
+ err(EXIT_FAILURE, NULL);
+ snprintf(grown, len, "%s%s%s",
+ descs[current].desc,
+ *descs[current].desc != '\0' ? " " : "",
+ p);
+ free(descs[current].desc);
+ descs[current].desc = grown;
+ continue;
+ }
+ current = -1;
+ continue;
+ }
+ current = -1;
+
+ /* A commented-out default still describes its directive */
+ if (*p == '#')
+ p++;
+
+ /* A directive name runs unbroken to its `=' */
+ len = strcspn(p, " \t\"#=");
+ if (len == 0 || p[len] != '=')
+ continue;
+
+ /* First description wins on a repeated directive */
+ for (n = 0; n < ndescs; n++)
+ if (strncmp(descs[n].name, p, len) == 0 &&
+ descs[n].name[len] == '\0')
+ break;
+ if (n < ndescs)
+ continue;
+
+ /* Skip the value (quotes may hide a `#') */
+ for (v = p + len + 1; *v != '\0'; v++) {
+ if (*v == '"')
+ quoted = !quoted;
+ else if (*v == '#' && !quoted)
+ break;
+ }
+ if (*v == '#') {
+ v++;
+ v += strspn(v, " \t");
+ }
+
+ if (ndescs >= descsize) {
+ descsize = (descsize == 0) ? 64 : descsize << 1;
+ tmp = realloc(descs, descsize * sizeof(*descs));
+ if (tmp == NULL)
+ err(EXIT_FAILURE, NULL);
+ descs = tmp;
+ }
+ descs[ndescs].name = strndup(p, len);
+ descs[ndescs].desc = strdup(v);
+ if (descs[ndescs].name == NULL ||
+ descs[ndescs].desc == NULL)
+ err(EXIT_FAILURE, NULL);
+ current = (int)ndescs;
+ ndescs++;
+ }
+ free(line);
+ fclose(fp);
+ free(defaults);
+}
+
+/*
+ * Return the harvested description of `name' or NULL if the defaults file
+ * does not mention it.
+ */
+static const char *
+desc_find(const char *name)
+{
+ size_t n;
+
+ for (n = 0; n < ndescs; n++)
+ if (strcmp(descs[n].name, name) == 0)
+ return (descs[n].desc);
+ return (NULL);
+}
+
+/*
+ * Print the description of `directive' according to the display flags: for
+ * the sysctl target, from the running kernel; otherwise from the table
+ * harvested off the defaults file (a directive the defaults never mention
+ * prints an empty description).
+ */
+void
+print_desc(const char *directive)
+{
+ const char *desc = NULL;
+#if defined(__FreeBSD__)
+ char buf[BUFSIZ];
+
+ if (format == BSDCONF_FORMAT_SYSCTL) {
+ (void)sysctl_descr(directive, buf, sizeof(buf));
+ desc = buf;
+ }
+#endif
+ if (desc == NULL && (desc = desc_find(directive)) == NULL)
+ desc = "";
+ if (name_only)
+ puts(directive);
+ else if (value_only)
+ puts(desc);
+ else
+ printf("%s: %s\n", directive, desc);
+}
+
+/*
+ * Process `-d' with explicit names for a defaults-backed target. Returns
+ * EXIT_SUCCESS or EXIT_FAILURE.
+ */
+int
+describe_defaults(void)
+{
+ int rv = EXIT_SUCCESS;
+ unsigned int n;
+
+ load_descriptions();
+ for (n = 0; n < nreqs; n++) {
+ if (desc_find(reqs[n].name) == NULL) {
+ if (ignore_unknown)
+ continue;
+ if (!quiet)
+ warnx("unknown directive '%s'",
+ reqs[n].name);
+ rv = EXIT_FAILURE;
+ continue;
+ }
+ print_desc(reqs[n].name);
+ }
+
+ return (rv);
+}
diff --git a/usr.sbin/sysconf/sysconf_query_sysctl.c b/usr.sbin/sysconf/sysconf_query_sysctl.c
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf_query_sysctl.c
@@ -0,0 +1,289 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Running-kernel sysctl OID descriptions and writability checks.
+ */
+
+#include "sysconf_priv.h"
+
+#if defined(__FreeBSD__)
+
+/*
+ * Fetch the running kernel's description of the sysctl OID `name' into
+ * `buf' via CTL_SYSCTL_OIDDESCR (the same information sysctl(8) prints
+ * with its own -d; an OID with no description yields the empty string).
+ * Returns zero on success; -1 when the OID does not resolve.
+ */
+int
+sysctl_descr(const char *name, char *buf, size_t bufsize)
+{
+ int mib[CTL_MAXNAME];
+ int qoid[CTL_MAXNAME + 2];
+ size_t len;
+ size_t size;
+
+ buf[0] = '\0';
+
+ qoid[0] = CTL_SYSCTL;
+ qoid[1] = CTL_SYSCTL_NAME2OID;
+ size = sizeof(mib);
+ if (sysctl(qoid, 2, mib, &size, name, strlen(name)) != 0)
+ return (-1);
+ len = size / sizeof(int);
+
+ qoid[1] = CTL_SYSCTL_OIDDESCR;
+ memcpy(qoid + 2, mib, len * sizeof(int));
+ size = bufsize - 1;
+ if (sysctl(qoid, (u_int)len + 2, buf, &size, 0, 0) != 0)
+ buf[0] = '\0';
+ buf[bufsize - 1] = '\0';
+
+ return (0);
+}
+
+/*
+ * Process `-d' with explicit names for the sysctl target: report each
+ * requested OID's description from the running kernel. Returns
+ * EXIT_SUCCESS or EXIT_FAILURE.
+ */
+int
+describe_sysctl(void)
+{
+ int rv = EXIT_SUCCESS;
+ unsigned int n;
+ char buf[BUFSIZ];
+
+ for (n = 0; n < nreqs; n++) {
+ if (sysctl_descr(reqs[n].name, buf, sizeof(buf)) != 0) {
+ if (ignore_unknown)
+ continue;
+ if (!quiet)
+ warnx("unknown oid '%s'", reqs[n].name);
+ rv = EXIT_FAILURE;
+ continue;
+ }
+ if (name_only)
+ puts(reqs[n].name);
+ else if (value_only)
+ puts(buf);
+ else
+ printf("%s: %s\n", reqs[n].name, buf);
+ }
+
+ return (rv);
+}
+
+/*
+ * Return a short name for a CTLTYPE_* value (for warnings).
+ */
+static const char *
+sysctl_typename(u_int kind)
+{
+
+ switch (kind & CTLTYPE) {
+ case CTLTYPE_INT: return ("integer");
+ case CTLTYPE_UINT: return ("unsigned integer");
+ case CTLTYPE_LONG: return ("long integer");
+ case CTLTYPE_ULONG: return ("unsigned long");
+ case CTLTYPE_S8: return ("int8_t");
+ case CTLTYPE_S16: return ("int16_t");
+ case CTLTYPE_S32: return ("int32_t");
+ case CTLTYPE_S64: return ("int64_t");
+ case CTLTYPE_U8: return ("uint8_t");
+ case CTLTYPE_U16: return ("uint16_t");
+ case CTLTYPE_U32: return ("uint32_t");
+ case CTLTYPE_U64: return ("uint64_t");
+ case CTLTYPE_STRING: return ("string");
+ case CTLTYPE_OPAQUE: return ("opaque");
+ default: return ("unknown");
+ }
+}
+
+/*
+ * True if `value' can be represented as a signed integer of the given
+ * inclusive [min, max] range. Requires a full-string numeric parse.
+ */
+static int
+sysctl_signed_ok(const char *value, intmax_t min, intmax_t max)
+{
+ char *end;
+ intmax_t v;
+
+ errno = 0;
+ v = strtoimax(value, &end, 0);
+ if (errno != 0 || end == value || *end != '\0')
+ return (0);
+ return (v >= min && v <= max);
+}
+
+/*
+ * True if `value' can be represented as an unsigned integer <= max.
+ * Leading whitespace is not accepted (sysctl.conf values are trimmed by
+ * the caller before we see them); a leading minus is rejected so that
+ * strtoumax() cannot wrap a negative into a large positive.
+ */
+static int
+sysctl_unsigned_ok(const char *value, uintmax_t max)
+{
+ char *end;
+ uintmax_t v;
+
+ if (value[0] == '\0' || value[0] == '-')
+ return (0);
+ errno = 0;
+ v = strtoumax(value, &end, 0);
+ if (errno != 0 || end == value || *end != '\0')
+ return (0);
+ return (v <= max);
+}
+
+/*
+ * Validate that `value' fits the numeric CTLTYPE described by `kind'.
+ * `fmt' is the OIDFMT format string (may start with "IK" for Kelvin
+ * temperatures); those are left to sysctl(8) at apply time. String and
+ * opaque OIDs are accepted as-is. Returns zero if acceptable; -1 if not.
+ */
+static int
+sysctl_value_ok(u_int kind, const char *fmt, const char *value)
+{
+
+ if (value == NULL)
+ return (0);
+
+ switch (kind & CTLTYPE) {
+ case CTLTYPE_STRING:
+ case CTLTYPE_OPAQUE:
+ return (0);
+ case CTLTYPE_INT:
+ /*
+ * Temperature OIDs use an "IK" / "IK<digit>" display format
+ * with a specialized parser in sysctl(8); do not apply a
+ * plain integer range here.
+ */
+ if (fmt != NULL && strncmp(fmt, "IK", 2) == 0)
+ return (0);
+ if (sysctl_signed_ok(value, INT_MIN, INT_MAX))
+ return (0);
+ break;
+ case CTLTYPE_UINT:
+ if (sysctl_unsigned_ok(value, UINT_MAX))
+ return (0);
+ break;
+ case CTLTYPE_LONG:
+ if (sysctl_signed_ok(value, LONG_MIN, LONG_MAX))
+ return (0);
+ break;
+ case CTLTYPE_ULONG:
+ if (sysctl_unsigned_ok(value, ULONG_MAX))
+ return (0);
+ break;
+ case CTLTYPE_S8:
+ if (sysctl_signed_ok(value, INT8_MIN, INT8_MAX))
+ return (0);
+ break;
+ case CTLTYPE_S16:
+ if (sysctl_signed_ok(value, INT16_MIN, INT16_MAX))
+ return (0);
+ break;
+ case CTLTYPE_S32:
+ if (sysctl_signed_ok(value, INT32_MIN, INT32_MAX))
+ return (0);
+ break;
+ case CTLTYPE_S64:
+ if (sysctl_signed_ok(value, INT64_MIN, INT64_MAX))
+ return (0);
+ break;
+ case CTLTYPE_U8:
+ if (sysctl_unsigned_ok(value, UINT8_MAX))
+ return (0);
+ break;
+ case CTLTYPE_U16:
+ if (sysctl_unsigned_ok(value, UINT16_MAX))
+ return (0);
+ break;
+ case CTLTYPE_U32:
+ if (sysctl_unsigned_ok(value, UINT32_MAX))
+ return (0);
+ break;
+ case CTLTYPE_U64:
+ if (sysctl_unsigned_ok(value, UINT64_MAX))
+ return (0);
+ break;
+ default:
+ /* NODE already rejected; unknown types: do not block */
+ return (0);
+ }
+ return (-1);
+}
+
+/*
+ * Determine whether the sysctl OID `name' can take effect when set from
+ * sysctl.conf(5) to `value': if the OID resolves it must be a leaf and
+ * writable at run time, and `value' must fit the OID's CTLTYPE (so an
+ * overflowing assignment cannot land in the conf file and surprise
+ * sysctl(8) at boot). An OID that is only tunable from the boot loader is
+ * reported as such (pointing at the loader target). An OID that does not
+ * resolve is still written -- sysctl.conf(5) commonly names OIDs from
+ * modules that are not yet loaded, and init(8) only warns -- so we warn
+ * (unless `quiet_unknown') and allow the assignment through; flag and
+ * range checks are only possible against a resolvable OID. Returns zero
+ * when the value should be written; -1 (after warning) otherwise.
+ */
+int
+sysctl_writable(const char *name, const char *value, int quiet_unknown)
+{
+ int mib[CTL_MAXNAME];
+ int qoid[CTL_MAXNAME + 2];
+ size_t len;
+ size_t size;
+ u_int kind;
+ u_char buf[BUFSIZ];
+ const char *fmt;
+
+ qoid[0] = CTL_SYSCTL;
+ qoid[1] = CTL_SYSCTL_NAME2OID;
+ size = sizeof(mib);
+ if (sysctl(qoid, 2, mib, &size, name, strlen(name)) != 0) {
+ if (!quiet_unknown)
+ warnx("WARNING: unknown oid '%s'", name);
+ return (0);
+ }
+ len = size / sizeof(int);
+
+ qoid[1] = CTL_SYSCTL_OIDFMT;
+ memcpy(qoid + 2, mib, len * sizeof(int));
+ size = sizeof(buf);
+ if (sysctl(qoid, (u_int)len + 2, buf, &size, 0, 0) != 0) {
+ warnx("couldn't find format of oid '%s'", name);
+ return (-1);
+ }
+ kind = *(u_int *)(void *)buf;
+ fmt = (const char *)(buf + sizeof(u_int));
+
+ if ((kind & CTLTYPE) == CTLTYPE_NODE) {
+ warnx("oid '%s' isn't a leaf node", name);
+ return (-1);
+ }
+ if ((kind & CTLFLAG_WR) == 0) {
+ if ((kind & CTLFLAG_TUN) != 0) {
+ warnx("oid '%s' is a read only tunable", name);
+ warnx("use: %s loader %s=<value>", pgm, name);
+ } else
+ warnx("oid '%s' is read only", name);
+ return (-1);
+ }
+ if (sysctl_value_ok(kind, fmt, value) != 0) {
+ warnx("oid '%s' value '%s' is invalid or out of range for %s",
+ name, value, sysctl_typename(kind));
+ return (-1);
+ }
+
+ return (0);
+}
+
+#endif /* __FreeBSD__ */
diff --git a/usr.sbin/sysconf/sysconf_query_value.c b/usr.sbin/sysconf/sysconf_query_value.c
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf_query_value.c
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Report effective directive values (named reads and -a).
+ */
+
+#include "sysconf_priv.h"
+
+/*
+ * Report read requests (and `-a') from the results of a completed
+ * scan_pass(). Returns EXIT_SUCCESS or EXIT_FAILURE.
+ */
+int
+do_reads(void)
+{
+ int rv = EXIT_SUCCESS;
+ size_t d;
+ unsigned int n;
+
+ if (dump_all) {
+ for (d = 0; d < ndumps; d++) {
+ /*
+ * Without -A, a plain -a dumps only what the conf
+ * files themselves configure; a directive whose
+ * value still comes from the (always-scanned)
+ * defaults file is out of scope.
+ */
+ if (!with_defaults && defaults_idx >= 0 &&
+ dumps[d].srcidx == defaults_idx)
+ continue;
+ if (!quiet)
+ warn_with_equals_no(dumps[d].name,
+ dumps[d].value,
+ conf_files[dumps[d].srcidx],
+ dumps[d].line);
+ if (show_desc)
+ print_desc(dumps[d].name);
+ else if (verbose_trail)
+ print_trail(dumps[d].name, dumps[d].trail,
+ dumps[d].ntrail);
+ else
+ print_pair(conf_files[dumps[d].srcidx],
+ dumps[d].name, dumps[d].value);
+ }
+ return (EXIT_SUCCESS);
+ }
+
+ /* Report the results in the order requested */
+ for (n = 0; n < nreqs; n++) {
+ if (reqs[n].newvalue != NULL || reqs[n].remove ||
+ reqs[n].name[0] == '\0') /* neutralized by validation */
+ continue;
+ if (!reqs[n].found) {
+ if (ignore_unknown)
+ continue;
+ if (!quiet)
+ warnx("unknown directive '%s'",
+ reqs[n].name);
+ rv = EXIT_FAILURE;
+ continue;
+ }
+ if (!quiet)
+ warn_with_equals_no(reqs[n].name, reqs[n].value,
+ conf_files[reqs[n].srcidx], reqs[n].line);
+ if (verbose_trail)
+ print_trail(reqs[n].name, reqs[n].trail,
+ reqs[n].ntrail);
+ else
+ print_pair(conf_files[reqs[n].srcidx], reqs[n].name,
+ reqs[n].value);
+ }
+
+ return (rv);
+}
diff --git a/usr.sbin/sysconf/sysconf_resolve.c b/usr.sbin/sysconf/sysconf_resolve.c
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf_resolve.c
@@ -0,0 +1,337 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Backing-file discovery and file-list reporting (-l/-L).
+ */
+
+#include "sysconf_priv.h"
+
+static void apply_src_env_overrides(void);
+
+/*
+ * Resolve the pathname of the target format's defaults file: the format's
+ * environment override (e.g., LOADER_DEFAULTS) verbatim when set and
+ * non-empty, otherwise the descriptor's defaults file prefixed with the
+ * `-R dir' root. Returns a newly allocated path, or NULL when the format
+ * has no defaults file.
+ */
+char *
+defaults_path(void)
+{
+ char *env;
+ char *path;
+ const struct bsdconf_format_def *def;
+ char buf[PATH_MAX];
+
+ if ((def = bsdconf_format_lookup(format)) == NULL ||
+ def->defaults == NULL)
+ return (NULL);
+ if (def->defaults_env != NULL &&
+ (env = getenv(def->defaults_env)) != NULL && *env != '\0') {
+ if ((path = strdup(env)) == NULL)
+ err(EXIT_FAILURE, NULL);
+ return (path);
+ }
+ if (snprintf(buf, sizeof(buf), "%s%s", rootdir, def->defaults) >=
+ (int)sizeof(buf))
+ errx(EXIT_FAILURE, "%s%s: %s", rootdir, def->defaults,
+ strerror(ENAMETOOLONG));
+ if ((path = strdup(buf)) == NULL)
+ err(EXIT_FAILURE, NULL);
+ return (path);
+}
+
+/*
+ * Resolve the target format and its ordered list of backing files from the
+ * command line: either the `-f file' override (a single file; the format is
+ * guessed from its name) or the leading `target' keyword (`loader',
+ * `sysctl', `make'), whose multi-file list -- optionally including the
+ * `-k module' drop-in -- comes from the format abstraction layer (which,
+ * for targets carrying a defaults file, performs the same discovery the
+ * boot-time consumer does; see bsdconf_format_files(3)). All paths honor
+ * the `-R dir' (or `-j jail') root. A target with a defaults file always
+ * has it prepended to the scan order (defaults first, mirroring the boot
+ * loader, so reads report the effective value even for directives no conf
+ * file mentions), but the slot is read-only bookkeeping: see defaults_idx.
+ * With `-D', the defaults file replaces the list outright; `-A' widens
+ * dump scope to include directives still at their default. Combined
+ * (`-AD'), the scoping of `-D' wins and `-A' retains only its
+ * dump-implying role, so `-ADd' describes all defaults and only defaults,
+ * as in sysrc(8).
+ */
+void
+resolve_target(const char *target)
+{
+ char *defaults;
+ const char *env = NULL;
+ const struct bsdconf_format_def *def;
+
+ if (target == NULL) {
+ warnx("no target provided");
+ usage();
+ }
+ if (bsdconf_format_find(target, &format) != 0) {
+ if (strcmp(target, "src-env") == 0)
+ warnx("unknown target `src-env'; use `src' "
+ "(includes src-env.conf in the build triad) "
+ "or `-f' for a specific file");
+ else
+ warnx("unknown target `%s'", target);
+ usage();
+ }
+
+ if (file != NULL) {
+ char path[PATH_MAX];
+ const char *prefix;
+
+ /*
+ * `-f file' overrides which file(s) are consulted and
+ * modified, but never the format; that is always stated
+ * explicitly by the target keyword. Standard input
+ * (`-f -') is the caller's own and is not subject to -R.
+ */
+ prefix = file_stdin ? "" : rootdir;
+ if ((conf_files = calloc(1, sizeof(*conf_files))) == NULL)
+ err(EXIT_FAILURE, NULL);
+ if (snprintf(path, sizeof(path), "%s%s", prefix, file) >=
+ (int)sizeof(path))
+ errx(EXIT_FAILURE, "%s%s: %s", prefix, file,
+ strerror(ENAMETOOLONG));
+ if ((conf_files[0] = strdup(path)) == NULL)
+ err(EXIT_FAILURE, NULL);
+ nconf_files = 1;
+ write_idx = 0;
+ return;
+ }
+
+ def = bsdconf_format_lookup(format);
+ if (def == NULL || (def->path == NULL && def->sources == NULL))
+ errx(EXIT_FAILURE,
+ "target `%s' has no default files; specify one with -f",
+ target);
+
+ /* `-D' consults the defaults file alone */
+ if (defaults_only) {
+ if ((conf_files = calloc(1, sizeof(*conf_files))) == NULL)
+ err(EXIT_FAILURE, NULL);
+ conf_files[0] = defaults_path();
+ nconf_files = 1;
+ write_idx = 0;
+ return;
+ }
+
+ if (def->defaults_env != NULL &&
+ (env = getenv(def->defaults_env)) != NULL && *env == '\0')
+ env = NULL;
+ if (bsdconf_format_files(format, rootdir, module, env, &conf_files,
+ &nconf_files, &write_idx) != 0 || nconf_files == 0)
+ err(EXIT_FAILURE, "%s", target);
+
+ /*
+ * The src triad honors the same environment overrides make(1)
+ * does when building /usr/src (verbatim, not subject to -R).
+ */
+ if (format == BSDCONF_FORMAT_SRC)
+ apply_src_env_overrides();
+
+ /*
+ * A target with a defaults file sources it first at boot, so the
+ * effective value of a directive may come from the defaults alone
+ * even when no conf file mentions it. Prepend the defaults file
+ * to the scan order (defaults first, so every override lands
+ * later) and remember its index: the defaults are read-only, so
+ * that slot is never chosen as a write target, never satisfies a
+ * removal, and is never shown by -l/-L (see defaults_idx users).
+ */
+ if ((defaults = defaults_path()) != NULL) {
+ char **tmp;
+ size_t n;
+
+ tmp = calloc(nconf_files + 1, sizeof(*tmp));
+ if (tmp == NULL)
+ err(EXIT_FAILURE, NULL);
+ tmp[0] = defaults;
+ for (n = 0; n < nconf_files; n++)
+ tmp[n + 1] = conf_files[n];
+ free(conf_files);
+ conf_files = tmp;
+ nconf_files++;
+ defaults_idx = 0;
+ write_idx++;
+ }
+}
+
+/*
+ * Replace src triad paths with SRC_ENV_CONF / __MAKE_CONF / SRCCONF when
+ * set and non-empty (same variables share/mk consults). Values are used
+ * verbatim, matching LOADER_DEFAULTS.
+ */
+static void
+apply_src_env_overrides(void)
+{
+ static const char *const envs[] = {
+ "SRC_ENV_CONF",
+ "__MAKE_CONF",
+ "SRCCONF",
+ };
+ const char *env;
+ char *path;
+ size_t i;
+
+ if (nconf_files != sizeof(envs) / sizeof(envs[0]))
+ return;
+ for (i = 0; i < nconf_files; i++) {
+ if ((env = getenv(envs[i])) == NULL || *env == '\0')
+ continue;
+ if ((path = strdup(env)) == NULL)
+ err(EXIT_FAILURE, NULL);
+ free(conf_files[i]);
+ conf_files[i] = path;
+ }
+}
+
+/*
+ * Print a candidate path unless `-E' excludes it for not existing or it
+ * was already printed (`seen' is a NULL-terminated list of prior paths).
+ */
+static void
+list_candidate(const char *path, char *seen[], size_t nseen)
+{
+ size_t n;
+
+ for (n = 0; n < nseen; n++)
+ if (seen[n] != NULL && strcmp(seen[n], path) == 0)
+ return;
+ if (existing_only && access(path, F_OK) != 0)
+ return;
+ puts(path);
+}
+
+/*
+ * Record `path' in the `seen' list (for suppressing duplicates) after
+ * printing it as a candidate.
+ */
+static void
+list_candidate_seen(const char *path, char **seen, size_t *nseen,
+ size_t maxseen)
+{
+
+ list_candidate(path, seen, *nseen);
+ if (*nseen < maxseen)
+ seen[(*nseen)++] = strdup(path);
+}
+
+/*
+ * Process `-l' (list the files backing the target) and `-L' (additionally
+ * list every drop-in candidate; for targets with a module drop-in
+ * directory, one candidate per loaded kernel module plus any `*.conf'
+ * already on disk). With `-E', only files that exist are shown. Returns
+ * EXIT_SUCCESS.
+ */
+int
+do_list(void)
+{
+ size_t n;
+ size_t nseen = 0;
+ char **seen;
+ const char *moddir = NULL;
+ const struct bsdconf_format_def *def;
+ const struct bsdconf_source *source;
+#define LIST_MAXSEEN 512
+ char dpath[PATH_MAX];
+ char path[PATH_MAX];
+
+ if ((seen = calloc(LIST_MAXSEEN, sizeof(*seen))) == NULL)
+ err(EXIT_FAILURE, NULL);
+
+ for (n = 0; n < nconf_files; n++) {
+ /* The read-only defaults file is never a write candidate */
+ if ((int)n == defaults_idx)
+ continue;
+ list_candidate_seen(conf_files[n], seen, &nseen,
+ LIST_MAXSEEN);
+ }
+
+ if (!list_all)
+ goto done;
+
+ /* Locate the module drop-in directory (if the format has one) */
+ def = bsdconf_format_lookup(format);
+ if (def != NULL && def->sources != NULL)
+ for (source = def->sources; source->path != NULL; source++)
+ if (source->type == BSDCONF_SOURCE_MODDIR)
+ moddir = source->path;
+ if (moddir == NULL)
+ goto done;
+
+#ifdef __FreeBSD__
+ /*
+ * One candidate per loaded kernel module, whether or not the file
+ * exists yet (the module being loaded is what makes the file
+ * eligible for sourcing by rc.subr(8)).
+ */
+ {
+ char name[MAXPATHLEN];
+ char *dot;
+ int fileid;
+ struct kld_file_stat stat;
+
+ for (fileid = kldnext(0); fileid > 0;
+ fileid = kldnext(fileid)) {
+ stat.version = sizeof(stat);
+ if (kldstat(fileid, &stat) != 0)
+ continue;
+ if (strcmp(stat.name, "kernel") == 0)
+ continue;
+ if (strlcpy(name, stat.name, sizeof(name)) >=
+ sizeof(name))
+ continue;
+ if ((dot = strrchr(name, '.')) != NULL &&
+ strcmp(dot, ".ko") == 0)
+ *dot = '\0';
+ if (snprintf(path, sizeof(path), "%s%s/%s.conf",
+ rootdir, moddir, name) >= (int)sizeof(path))
+ continue;
+ list_candidate_seen(path, seen, &nseen,
+ LIST_MAXSEEN);
+ }
+ }
+#endif
+
+ /* Plus any drop-in already on disk (module not currently loaded) */
+ {
+ int n2;
+ int nentries;
+ struct dirent **entries;
+
+ if (snprintf(dpath, sizeof(dpath), "%s%s", rootdir,
+ moddir) >= (int)sizeof(dpath))
+ goto done;
+ nentries = scandir(dpath, &entries, NULL, alphasort);
+ for (n2 = 0; n2 < nentries; n2++) {
+ size_t len = strlen(entries[n2]->d_name);
+
+ if (entries[n2]->d_name[0] != '.' && len > 5 &&
+ strcmp(entries[n2]->d_name + len - 5,
+ ".conf") == 0 &&
+ snprintf(path, sizeof(path), "%s/%s", dpath,
+ entries[n2]->d_name) < (int)sizeof(path))
+ list_candidate_seen(path, seen, &nseen,
+ LIST_MAXSEEN);
+ free(entries[n2]);
+ }
+ if (nentries >= 0)
+ free(entries);
+ }
+
+done:
+ for (n = 0; n < nseen; n++)
+ free(seen[n]);
+ free(seen);
+ return (EXIT_SUCCESS);
+}
diff --git a/usr.sbin/sysconf/sysconf_scan.c b/usr.sbin/sysconf/sysconf_scan.c
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf_scan.c
@@ -0,0 +1,288 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Ordered conf-file scanning into effective values, trails, and dumps.
+ */
+
+#include <ctype.h>
+
+#include "sysconf_priv.h"
+
+/*
+ * Apply a make(1)-style assignment operator to the running effective value
+ * of a directive. `+=' appends with a single space (as make(1) does);
+ * `?=' keeps the existing value when already defined; `=', `:=', and `!='
+ * replace. Returns newly allocated storage, or NULL on allocation failure.
+ * `old' may be NULL when the directive has not been seen yet.
+ */
+char *
+make_apply(enum bsdconf_op op, const char *old, const char *piece)
+{
+ size_t len;
+ char *out;
+
+ if (piece == NULL)
+ piece = "";
+
+ switch (op) {
+ case BSDCONF_OP_APPEND:
+ if (old == NULL || *old == '\0')
+ return (strdup(piece));
+ if (*piece == '\0')
+ return (strdup(old));
+ len = strlen(old) + 1 + strlen(piece) + 1;
+ if ((out = malloc(len)) == NULL)
+ return (NULL);
+ snprintf(out, len, "%s %s", old, piece);
+ return (out);
+ case BSDCONF_OP_COND:
+ if (old != NULL)
+ return (strdup(old));
+ return (strdup(piece));
+ case BSDCONF_OP_EXPAND:
+ case BSDCONF_OP_SHELL:
+ case BSDCONF_OP_ASSIGN:
+ case BSDCONF_OP_DEFAULT:
+ default:
+ return (strdup(piece));
+ }
+}
+
+/*
+ * Record `directive'/`value' in the dump union (for `-a'). For formats
+ * without make(1) operators a later assignment overrides an earlier one
+ * while retaining its original position. With BSDCONF_OPERATOR_EQUALS the
+ * operator is honored: `+=' accumulates, `?=' keeps a prior definition,
+ * and `=' / `:=' / `!=' replace -- matching make(1)'s effective value.
+ * Returns zero on success; -1 (errno set) on allocation failure.
+ */
+static int
+dump_record(const char *directive, const char *value, enum bsdconf_op op,
+ uint32_t line)
+{
+ char *merged;
+ char *vcopy;
+ size_t n;
+ struct dumpent *tmp;
+
+ if ((vcopy = strdup(value)) == NULL)
+ return (-1);
+ bsdconf_unquote(vcopy);
+
+ for (n = 0; n < ndumps; n++) {
+ if (strcmp(dumps[n].name, directive) != 0)
+ continue;
+ if ((bsdconf_format_processing(format) &
+ BSDCONF_OPERATOR_EQUALS) != 0) {
+ merged = make_apply(op, dumps[n].value, vcopy);
+ if (merged == NULL) {
+ free(vcopy);
+ return (-1);
+ }
+ free(dumps[n].value);
+ dumps[n].value = merged;
+ } else {
+ free(dumps[n].value);
+ dumps[n].value = vcopy;
+ vcopy = NULL; /* owned by dumps[n].value */
+ }
+ dumps[n].srcidx = (int)conf_scanidx;
+ dumps[n].line = line;
+ if (trail_push(&dumps[n].trail, &dumps[n].ntrail,
+ conf_scanidx, line, op,
+ vcopy != NULL ? vcopy : dumps[n].value,
+ dumps[n].value) != 0) {
+ free(vcopy);
+ return (-1);
+ }
+ free(vcopy);
+ return (0);
+ }
+
+ if (ndumps >= dumpsize) {
+ dumpsize = (dumpsize == 0) ? 64 : dumpsize << 1;
+ tmp = realloc(dumps, dumpsize * sizeof(*dumps));
+ if (tmp == NULL) {
+ free(vcopy);
+ return (-1);
+ }
+ dumps = tmp;
+ }
+ if ((dumps[ndumps].name = strdup(directive)) == NULL) {
+ free(vcopy);
+ return (-1);
+ }
+ dumps[ndumps].value = vcopy;
+ dumps[ndumps].srcidx = (int)conf_scanidx;
+ dumps[ndumps].line = line;
+ dumps[ndumps].trail = NULL;
+ dumps[ndumps].ntrail = 0;
+ if (trail_push(&dumps[ndumps].trail, &dumps[ndumps].ntrail,
+ conf_scanidx, line, op, vcopy, vcopy) != 0)
+ return (-1);
+ ndumps++;
+ return (0);
+}
+
+/*
+ * bsdconf_fparse() call-back; record each statement against the requests
+ * (and, with `-a', the dump union). For loader/sysctl/generic a later
+ * assignment overrides an earlier one -- both within a file and across the
+ * ordered file list. For make(1)-syntax targets the statement's operator
+ * (passed via option->op) drives accumulation so `foo=1' followed by
+ * `foo+=2' yields the effective value `1 2', as make -V reports.
+ */
+static int
+scan_cb(struct bsdconf_option *option, uint32_t line,
+ char *directive, char *value)
+{
+ char *copy;
+ char *merged;
+ enum bsdconf_op file_op;
+ unsigned int n;
+
+ file_op = (option != NULL) ? option->op : BSDCONF_OP_ASSIGN;
+ if (file_op == BSDCONF_OP_DEFAULT)
+ file_op = BSDCONF_OP_ASSIGN;
+
+ for (n = 0; n < nreqs; n++) {
+ if (strcmp(reqs[n].name, directive) != 0)
+ continue;
+ /*
+ * The defaults file is read-only: it informs the effective
+ * value seen by reads, checks, and list edits, but can
+ * never satisfy a removal (nothing there may be removed).
+ */
+ if (reqs[n].remove && (int)conf_scanidx == defaults_idx)
+ continue;
+ if ((copy = strdup(value)) == NULL)
+ return (-1);
+ bsdconf_unquote(copy);
+
+ /*
+ * An identical make(1) `+=value' line already in the file
+ * satisfies a pending `name+=value' write or check.
+ */
+ if (reqs[n].op == BSDCONF_OP_APPEND &&
+ reqs[n].newvalue != NULL &&
+ file_op == BSDCONF_OP_APPEND &&
+ strcmp(copy, reqs[n].newvalue) == 0)
+ reqs[n].append_present = 1;
+
+ if ((bsdconf_format_processing(format) &
+ BSDCONF_OPERATOR_EQUALS) != 0) {
+ if (make_strike_p(&reqs[n]) &&
+ make_assign_push(&reqs[n], file_op, copy,
+ line) != 0) {
+ free(copy);
+ return (-1);
+ }
+ merged = make_apply(file_op, reqs[n].value, copy);
+ if (merged == NULL) {
+ free(copy);
+ return (-1);
+ }
+ free(reqs[n].value);
+ reqs[n].value = merged;
+ if (trail_push(&reqs[n].trail, &reqs[n].ntrail,
+ conf_scanidx, line, file_op, copy,
+ reqs[n].value) != 0) {
+ free(copy);
+ return (-1);
+ }
+ free(copy);
+ } else {
+ free(reqs[n].value);
+ reqs[n].value = copy;
+ if (trail_push(&reqs[n].trail, &reqs[n].ntrail,
+ conf_scanidx, line, file_op, copy, copy) != 0)
+ return (-1);
+ }
+ reqs[n].found = 1;
+ reqs[n].srcidx = (int)conf_scanidx;
+ reqs[n].line = line;
+ reqs[n].infile[conf_scanidx] = 1;
+ }
+
+ if (dump_all && dump_record(directive, value, file_op, line) != 0)
+ return (-1);
+
+ return (0);
+}
+
+/*
+ * Parse every existing backing file in deterministic order, feeding each
+ * statement through scan_cb(). With `sandbox' set (pure read operations),
+ * all descriptors are opened up front and the remainder of the pass runs
+ * inside a Capsicum sandbox on FreeBSD. Returns EXIT_SUCCESS or exits on
+ * hard errors.
+ */
+int
+scan_pass(int sandbox)
+{
+ int *fds;
+ size_t n;
+ uint16_t processing;
+
+ if ((fds = calloc(nconf_files, sizeof(*fds))) == NULL)
+ err(EXIT_FAILURE, NULL);
+
+ for (n = 0; n < nconf_files; n++) {
+ fds[n] = open(conf_files[n], O_RDONLY);
+ if (fds[n] < 0 && errno != ENOENT)
+ err(EXIT_FAILURE, "%s", conf_files[n]);
+
+ /*
+ * Spool input that cannot seek (a fifo or /dev/stdin named
+ * by `-f') before the sandbox slams shut: the library would
+ * otherwise spool lazily inside bsdconf_fparse(), where
+ * creating the temporary is no longer permitted.
+ */
+ if (fds[n] >= 0 && lseek(fds[n], 0, SEEK_CUR) == -1) {
+ int sfd;
+
+ if (errno != ESPIPE ||
+ (sfd = bsdconf_spool(fds[n])) == -1)
+ err(EXIT_FAILURE, "%s", conf_files[n]);
+ close(fds[n]);
+ fds[n] = sfd;
+ }
+ }
+
+#ifdef __FreeBSD__
+ if (sandbox) {
+ cap_rights_t rights;
+
+ if (caph_limit_stdio() < 0)
+ err(EXIT_FAILURE, "capsicum");
+ cap_rights_init(&rights, CAP_READ, CAP_FSTAT, CAP_SEEK);
+ for (n = 0; n < nconf_files; n++) {
+ if (fds[n] >= 0 &&
+ caph_rights_limit(fds[n], &rights) < 0)
+ err(EXIT_FAILURE, "capsicum");
+ }
+ if (caph_enter() < 0)
+ err(EXIT_FAILURE, "capsicum");
+ }
+#else
+ (void)sandbox;
+#endif
+
+ processing = bsdconf_format_processing(format);
+ for (n = 0; n < nconf_files; n++) {
+ if (fds[n] < 0)
+ continue;
+ conf_scanidx = n;
+ if (bsdconf_fparse(NULL, fds[n], scan_cb, processing) != 0)
+ err(EXIT_FAILURE, "%s", conf_files[n]);
+ close(fds[n]);
+ }
+
+ free(fds);
+ return (EXIT_SUCCESS);
+}
diff --git a/usr.sbin/sysconf/tests/Makefile b/usr.sbin/sysconf/tests/Makefile
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/tests/Makefile
@@ -0,0 +1,11 @@
+ATF_TESTS_SH+= sysconf_test
+
+.include <bsd.test.mk>
+
+#
+# SCRIPTS install before DIRS in bsd.prog.mk; create TESTSDIR first so a
+# leaf `make install' works before installworld has applied BSD.tests.dist.
+#
+beforeinstall: _sysconf_testdir
+_sysconf_testdir: .PHONY
+ ${INSTALL} -d -o ${BINOWN} -g ${BINGRP} -m 0755 ${DESTDIR}${TESTSDIR}
diff --git a/usr.sbin/sysconf/tests/sysconf_test.sh b/usr.sbin/sysconf/tests/sysconf_test.sh
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/tests/sysconf_test.sh
@@ -0,0 +1,431 @@
+#
+# Copyright (c) 2026 Devin Teske <dteske@FreeBSD.org>
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+
+#
+# Resolve the sysconf binary: honor SYSCONF if set, else the sibling
+# build product (objdir layout: .../sysconf/tests next to .../sysconf/sysconf),
+# else PATH. Require a regular file: when installed under
+# /usr/tests/usr.sbin/sysconf, `../sysconf' is this directory itself and
+# `[ -x dir ]' is true for searchable directories.
+#
+find_sysconf()
+{
+ if [ -n "$SYSCONF" ] && [ -f "$SYSCONF" ] && [ -x "$SYSCONF" ]; then
+ return
+ fi
+ if [ -f "$( atf_get_srcdir )/../sysconf" ] &&
+ [ -x "$( atf_get_srcdir )/../sysconf" ]
+ then
+ SYSCONF="$( atf_get_srcdir )/../sysconf"
+ elif [ -f "$( pwd )/../sysconf" ] && [ -x "$( pwd )/../sysconf" ]; then
+ SYSCONF="$( pwd )/../sysconf"
+ else
+ SYSCONF=$( command -v sysconf ) ||
+ atf_skip "sysconf binary not found"
+ fi
+}
+
+setup_root()
+{
+ find_sysconf
+ ROOT="$( pwd )/root"
+ mkdir -p "$ROOT/etc" "$ROOT/boot/defaults"
+}
+
+cleanup_root()
+{
+ rm -rf "$( pwd )/root"
+}
+
+atf_test_case make_append_accumulate cleanup
+make_append_accumulate_head()
+{
+ atf_set "descr" \
+ "make += accumulates on read and appends a line on write"
+}
+make_append_accumulate_body()
+{
+ setup_root
+ printf 'foo=1\n' > "$ROOT/etc/make.conf"
+ atf_check -o inline:'foo: 1\n' "$SYSCONF" make -R "$ROOT" foo
+ printf 'foo+=2\n' >> "$ROOT/etc/make.conf"
+ atf_check -o inline:'foo: 1 2\n' "$SYSCONF" make -R "$ROOT" foo
+ atf_check -o inline:'foo: 1 2 -> 1 2 -O2\n' \
+ "$SYSCONF" make -R "$ROOT" 'foo+=-O2'
+ atf_check -o inline:'foo: 1 2 -O2\n' "$SYSCONF" make -R "$ROOT" foo
+ atf_check -o match:'foo\+=-O2' cat "$ROOT/etc/make.conf"
+ # -V on a new += statement reports (added), not `+= -> +=value'
+ atf_check -o match:'make.conf:[0-9]*: foo\+=-O3 \(added\)' \
+ "$SYSCONF" make -R "$ROOT" -V 'foo+=-O3'
+ atf_check -o inline:'foo: 1 2 -O2 -O3\n' "$SYSCONF" make -R "$ROOT" foo
+ # Identical += is a no-op (append_present)
+ before=$( stat -f '%m' "$ROOT/etc/make.conf" )
+ atf_check "$SYSCONF" make -R "$ROOT" -c 'foo+=-O2'
+ after=$( stat -f '%m' "$ROOT/etc/make.conf" )
+ atf_check_equal "$before" "$after"
+}
+make_append_accumulate_cleanup()
+{
+ cleanup_root
+}
+
+atf_test_case src_triad_and_trail cleanup
+src_triad_and_trail_head()
+{
+ atf_set "descr" "src triad order, -V trail, writes prefer src.conf"
+}
+src_triad_and_trail_body()
+{
+ setup_root
+ printf 'CFLAGS=-O0\n' > "$ROOT/etc/src-env.conf"
+ printf 'CFLAGS+=-g\n' > "$ROOT/etc/make.conf"
+ printf 'CFLAGS+=-pipe\n' > "$ROOT/etc/src.conf"
+ atf_check -o inline:'CFLAGS: -O0 -g -pipe\n' \
+ "$SYSCONF" src -R "$ROOT" CFLAGS
+ atf_check -o match:'src-env.conf:1: CFLAGS=-O0' \
+ -o match:'make.conf:1: CFLAGS\+=-g -> -O0 -g' \
+ -o match:'src.conf:1: CFLAGS\+=-pipe -> -O0 -g -pipe' \
+ "$SYSCONF" src -R "$ROOT" -V CFLAGS
+ atf_check -o match:'src.conf' \
+ "$SYSCONF" src -R "$ROOT" -v CFLAGS
+ # New directive lands in src.conf
+ atf_check "$SYSCONF" src -R "$ROOT" -q WITHOUT_FOO=
+ atf_check -o match:'WITHOUT_FOO=' cat "$ROOT/etc/src.conf"
+ atf_check -o not-match:'WITHOUT_FOO' cat "$ROOT/etc/make.conf"
+}
+src_triad_and_trail_cleanup()
+{
+ cleanup_root
+}
+
+atf_test_case src_env_rejected
+src_env_rejected_head()
+{
+ atf_set "descr" "src-env keyword is rejected with a hint toward src"
+}
+src_env_rejected_body()
+{
+ find_sysconf
+ atf_check -s not-exit:0 -e match:'src-env' -e match:'src' \
+ "$SYSCONF" src-env foo
+}
+
+atf_test_case env_overrides cleanup
+env_overrides_head()
+{
+ atf_set "descr" "SRC_ENV_CONF / __MAKE_CONF / SRCCONF remap the triad"
+}
+env_overrides_body()
+{
+ setup_root
+ mkdir -p "$ROOT/alt"
+ printf 'X=from-env\n' > "$ROOT/alt/se.conf"
+ printf 'X+=from-make\n' > "$ROOT/alt/mk.conf"
+ printf 'X+=from-src\n' > "$ROOT/alt/sc.conf"
+ # Empty stock files so -R still resolves the triad slots
+ :> "$ROOT/etc/src-env.conf"
+ :> "$ROOT/etc/make.conf"
+ :> "$ROOT/etc/src.conf"
+ export SRC_ENV_CONF="$ROOT/alt/se.conf"
+ export __MAKE_CONF="$ROOT/alt/mk.conf"
+ export SRCCONF="$ROOT/alt/sc.conf"
+ atf_check -o inline:'X: from-env from-make from-src\n' \
+ "$SYSCONF" src -R "$ROOT" X
+ unset SRC_ENV_CONF __MAKE_CONF SRCCONF
+}
+env_overrides_cleanup()
+{
+ cleanup_root
+}
+
+atf_test_case attached_f_passthrough cleanup
+attached_f_passthrough_head()
+{
+ atf_set "descr" \
+ "Attached -fPATH does not steal the next argv (rc as name)"
+}
+attached_f_passthrough_body()
+{
+ setup_root
+ mkdir -p "$ROOT/boot"
+ printf 'zfs_load="YES"\n' > "$ROOT/boot/loader.conf"
+ #
+ # If find_target consumed 'loader' as -f's argument, 'rc' would become
+ # the target and exec sysrc(8). Correctly, target is loader and 'rc'
+ # is an unknown directive name.
+ #
+ atf_check -s not-exit:0 -e match:'unknown directive' \
+ "$SYSCONF" -R "$ROOT" -f/boot/loader.conf loader rc
+}
+attached_f_passthrough_cleanup()
+{
+ cleanup_root
+}
+
+atf_test_case verbatim_backslash cleanup
+verbatim_backslash_head()
+{
+ atf_set "descr" \
+ "verbatim_ok re-examines the byte after an even \\\\ run"
+}
+verbatim_backslash_body()
+{
+ setup_root
+ printf 'foo=1\n' > "$ROOT/etc/make.conf"
+ # Even-length \\ before # must not skip #: put must reject (Faraz)
+ atf_check -s not-exit:0 -e match:'Invalid argument' \
+ "$SYSCONF" make -R "$ROOT" 'foo=a\\#b'
+ # A value without a comment marker still writes.
+ atf_check -o match:'foo:' \
+ "$SYSCONF" make -R "$ROOT" 'foo=a\\b'
+ atf_check -o match:'foo=a\\\\b' cat "$ROOT/etc/make.conf"
+}
+verbatim_backslash_cleanup()
+{
+ cleanup_root
+}
+
+atf_test_case make_strike_last_assign cleanup
+make_strike_last_assign_head()
+{
+ atf_set "descr" \
+ "make -= strikes the last assignment containing the word"
+}
+make_strike_last_assign_body()
+{
+ setup_root
+ printf 'bar+=1\nbar+=1 2\nbar+=1 2 3\n' > "$ROOT/etc/make.conf"
+ atf_check -o match:'->' "$SYSCONF" make -R "$ROOT" 'bar-=3'
+ atf_check -o inline:'bar+=1\nbar+=1 2\nbar+=1 2\n' \
+ cat "$ROOT/etc/make.conf"
+ atf_check -o match:'->' "$SYSCONF" make -R "$ROOT" 'bar-=2'
+ atf_check -o inline:'bar+=1\nbar+=1 2\nbar+=1\n' \
+ cat "$ROOT/etc/make.conf"
+ atf_check -o match:'->' "$SYSCONF" make -R "$ROOT" 'bar-=1'
+ atf_check -o inline:'bar+=1\nbar+=1 2\n' cat "$ROOT/etc/make.conf"
+ atf_check -o match:'->' "$SYSCONF" make -R "$ROOT" 'bar-=1'
+ atf_check -o inline:'bar+=1\nbar+=2\n' cat "$ROOT/etc/make.conf"
+ atf_check -o match:'->' "$SYSCONF" make -R "$ROOT" 'bar-=2'
+ atf_check -o inline:'bar+=1\n' cat "$ROOT/etc/make.conf"
+ atf_check -o inline:'bar: 1 (unchanged)\n' \
+ "$SYSCONF" make -R "$ROOT" 'bar-=9'
+ # Emptying the last word deletes the statement: (removed), like (added)
+ printf 'z+=only\n' >>"$ROOT/etc/make.conf"
+ atf_check -o match:'make.conf:[0-9]*: z\+=only \(removed\)' \
+ "$SYSCONF" make -R "$ROOT" -V 'z-=only'
+ atf_check -o not-match:'^z\+=' cat "$ROOT/etc/make.conf"
+}
+make_strike_last_assign_cleanup()
+{
+ cleanup_root
+}
+
+atf_test_case equal_value_nocheck_mtime cleanup
+equal_value_nocheck_mtime_head()
+{
+ atf_set "descr" "Equal-value write and -c leave mtime alone"
+}
+equal_value_nocheck_mtime_body()
+{
+ setup_root
+ printf 'bar=1\n' > "$ROOT/etc/make.conf"
+ sleep 1
+ before=$( stat -f '%m' "$ROOT/etc/make.conf" )
+ # Equal-value write: informational echo, file untouched
+ atf_check -o inline:'bar: 1 (unchanged)\n' \
+ "$SYSCONF" make -R "$ROOT" bar=1
+ # -e uses `=' for write echoes the same as for reads
+ atf_check -o inline:'bar=1 (unchanged)\n' \
+ "$SYSCONF" make -R "$ROOT" -e bar=1
+ after=$( stat -f '%m' "$ROOT/etc/make.conf" )
+ atf_check_equal "$before" "$after"
+ # -q suppresses the echo; -c is silent and does not write
+ atf_check -o empty "$SYSCONF" make -R "$ROOT" -q bar=1
+ atf_check -o empty "$SYSCONF" make -R "$ROOT" -c bar=1
+ after2=$( stat -f '%m' "$ROOT/etc/make.conf" )
+ atf_check_equal "$before" "$after2"
+ atf_check -o inline:'bar=1 # -> 2\n' \
+ "$SYSCONF" make -R "$ROOT" -e bar=2
+ atf_check -o inline:'bar=2\n' \
+ "$SYSCONF" make -R "$ROOT" -e bar
+}
+equal_value_nocheck_mtime_cleanup()
+{
+ cleanup_root
+}
+
+atf_test_case sysctl_oid_range cleanup
+sysctl_oid_range_head()
+{
+ atf_set "descr" \
+ "sysctl target rejects values that overflow the OID CTLTYPE"
+}
+sysctl_oid_range_body()
+{
+ find_sysconf
+ case "$( uname -s )" in
+ FreeBSD) ;;
+ *) atf_skip "requires FreeBSD sysctl OIDFMT" ;;
+ esac
+ # -f alone (no -R) still validates against the running kernel.
+ conf="$( pwd )/sysctl.conf"
+ :> "$conf"
+ atf_check -s exit:1 -e match:'out of range for integer' \
+ "$SYSCONF" sysctl -f "$conf" \
+ security.bsd.see_other_uids=99999999999
+ atf_check -o empty cat "$conf"
+ atf_check -s exit:1 -e match:'out of range for unsigned integer' \
+ "$SYSCONF" sysctl -f "$conf" kern.ipc.somaxconn=-1
+ atf_check -s exit:1 -e match:'out of range for unsigned integer' \
+ "$SYSCONF" sysctl -f "$conf" kern.ipc.somaxconn=abc
+ atf_check -o match:'see_other_uids' \
+ "$SYSCONF" sysctl -f "$conf" security.bsd.see_other_uids=0
+ atf_check -o match:'see_other_uids=0' cat "$conf"
+}
+sysctl_oid_range_cleanup()
+{
+ rm -f "$( pwd )/sysctl.conf"
+}
+
+atf_test_case make_src_knobs cleanup
+make_src_knobs_head()
+{
+ atf_set "descr" \
+ "-s WITH_/WITHOUT_ presence, query display, unchanged, -x"
+}
+make_src_knobs_body()
+{
+ setup_root
+ :> "$ROOT/etc/make.conf"
+ :> "$ROOT/etc/sysctl.conf"
+ :> "$ROOT/etc/src-env.conf"
+ :> "$ROOT/etc/src.conf"
+
+ # -s only on make/src
+ atf_check -s exit:1 -e match:'only valid for make and src' \
+ "$SYSCONF" sysctl -R "$ROOT" -s WITHOUT_FOO
+ # name must be WITH_/WITHOUT_
+ atf_check -s exit:1 -e match:'WITH_ or WITHOUT_' \
+ "$SYSCONF" make -R "$ROOT" -s CFLAGS
+ # no value with -s
+ atf_check -s exit:1 -e match:'does not take a value' \
+ "$SYSCONF" make -R "$ROOT" -s 'WITHOUT_FOO='
+
+ atf_check -o inline:'WITHOUT_FOO: (not present) -> (present)\n' \
+ "$SYSCONF" make -R "$ROOT" -s WITHOUT_FOO
+ atf_check -o match:'^WITHOUT_FOO=$' cat "$ROOT/etc/make.conf"
+ atf_check -o inline:'WITHOUT_FOO (present)\n' \
+ "$SYSCONF" make -R "$ROOT" WITHOUT_FOO
+ atf_check -o inline:'WITHOUT_FOO=\n' \
+ "$SYSCONF" make -R "$ROOT" -e WITHOUT_FOO
+ atf_check -o inline:'\n' \
+ "$SYSCONF" make -R "$ROOT" -n WITHOUT_FOO
+
+ before=$( stat -f '%m' "$ROOT/etc/make.conf" )
+ atf_check -o inline:'WITHOUT_FOO: (present) (unchanged)\n' \
+ "$SYSCONF" make -R "$ROOT" -s WITHOUT_FOO
+ after=$( stat -f '%m' "$ROOT/etc/make.conf" )
+ atf_check_equal "$before" "$after"
+
+ # Non-empty placeholder (=t) still means present (defined())
+ atf_check -o inline:'WITHOUT_BAR: (not present) -> (present)\n' \
+ "$SYSCONF" make -R "$ROOT" 'WITHOUT_BAR=t'
+ atf_check -o match:'^WITHOUT_BAR=t$' cat "$ROOT/etc/make.conf"
+ atf_check -o inline:'WITHOUT_BAR (present)\n' \
+ "$SYSCONF" make -R "$ROOT" WITHOUT_BAR
+ atf_check -o inline:'WITHOUT_BAR=t\n' \
+ "$SYSCONF" make -R "$ROOT" -e WITHOUT_BAR
+ atf_check -o inline:'t\n' \
+ "$SYSCONF" make -R "$ROOT" -n WITHOUT_BAR
+ before=$( stat -f '%m' "$ROOT/etc/make.conf" )
+ atf_check -o inline:'WITHOUT_BAR: (present) (unchanged)\n' \
+ "$SYSCONF" make -R "$ROOT" -s WITHOUT_BAR
+ after=$( stat -f '%m' "$ROOT/etc/make.conf" )
+ atf_check_equal "$before" "$after"
+ atf_check -o match:'^WITHOUT_BAR=t$' cat "$ROOT/etc/make.conf"
+
+ # Explicit name= always writes (incl. empty); echo value changes
+ atf_check -o inline:'WITHOUT_BAR: t -> \n' \
+ "$SYSCONF" make -R "$ROOT" 'WITHOUT_BAR='
+ atf_check -o match:'^WITHOUT_BAR=$' cat "$ROOT/etc/make.conf"
+ atf_check -o inline:'WITHOUT_BAR: -> 123\n' \
+ "$SYSCONF" make -R "$ROOT" 'WITHOUT_BAR=123'
+ atf_check -o match:'^WITHOUT_BAR=123$' cat "$ROOT/etc/make.conf"
+ atf_check -o inline:'WITHOUT_BAR: 123 -> 456\n' \
+ "$SYSCONF" make -R "$ROOT" 'WITHOUT_BAR=456'
+ atf_check -o match:'^WITHOUT_BAR=456$' cat "$ROOT/etc/make.conf"
+ # -s still no-op while present with a value
+ before=$( stat -f '%m' "$ROOT/etc/make.conf" )
+ atf_check -o inline:'WITHOUT_BAR: (present) (unchanged)\n' \
+ "$SYSCONF" make -R "$ROOT" -s WITHOUT_BAR
+ after=$( stat -f '%m' "$ROOT/etc/make.conf" )
+ atf_check_equal "$before" "$after"
+ atf_check -o match:'^WITHOUT_BAR=456$' cat "$ROOT/etc/make.conf"
+
+ # src prefers src.conf
+ atf_check -o inline:'WITHOUT_LLDB: (not present) -> (present)\n' \
+ "$SYSCONF" src -R "$ROOT" -s WITHOUT_LLDB
+ atf_check -o match:'^WITHOUT_LLDB=$' cat "$ROOT/etc/src.conf"
+ # -x is quiet unless -v/-V (same as non-knob removals)
+ atf_check -o empty \
+ "$SYSCONF" src -R "$ROOT" -x WITHOUT_LLDB
+ atf_check -o empty cat "$ROOT/etc/src.conf"
+ atf_check -o inline:'WITHOUT_LLDB: (not present) -> (present)\n' \
+ "$SYSCONF" src -R "$ROOT" -s WITHOUT_LLDB
+ atf_check -o match:'WITHOUT_LLDB \(removed\)' \
+ "$SYSCONF" src -R "$ROOT" -vx WITHOUT_LLDB
+ atf_check -o empty cat "$ROOT/etc/src.conf"
+
+ # WITH_*=no (exact lowercase) warns like bsd.mkopt.mk; WITHOUT_*=no
+ # and WITH_*=NO do not
+ atf_check -e match:'Use WITHOUT_FOO=1 instead of WITH_FOO=no' \
+ -o match:'WITH_FOO' \
+ "$SYSCONF" make -R "$ROOT" 'WITH_FOO=no'
+ atf_check -o match:'^WITH_FOO=no$' cat "$ROOT/etc/make.conf"
+ atf_check -e empty -o match:'WITH_BAR' \
+ "$SYSCONF" make -R "$ROOT" 'WITH_BAR=NO'
+ atf_check -e empty -o match:'WITHOUT_BAZ' \
+ "$SYSCONF" make -R "$ROOT" 'WITHOUT_BAZ=no'
+
+ # Reads remind with file:line (presence display hides =no)
+ atf_check -e match:'make.conf:[0-9]+: Use WITHOUT_FOO=1 instead of WITH_FOO=no' \
+ -o inline:'WITH_FOO (present)\n' \
+ "$SYSCONF" make -R "$ROOT" WITH_FOO
+ atf_check -e match:'make.conf:[0-9]+: Use WITHOUT_FOO=1 instead of WITH_FOO=no' \
+ -o match:'WITH_FOO \(present\)' \
+ "$SYSCONF" make -R "$ROOT" -a
+ atf_check -e match:'make.conf:[0-9]+: Use WITHOUT_FOO=1 instead of WITH_FOO=no' \
+ -o inline:'WITH_FOO=no\n' \
+ "$SYSCONF" make -R "$ROOT" -e WITH_FOO
+ atf_check -e empty -o inline:'WITH_FOO (present)\n' \
+ "$SYSCONF" make -R "$ROOT" -q WITH_FOO
+
+ # WITHOUT_MODULES is not a presence knob
+ atf_check -s exit:1 -e match:'WITHOUT_MODULES takes a module list' \
+ "$SYSCONF" make -R "$ROOT" -s WITHOUT_MODULES
+ atf_check -o match:'WITHOUT_MODULES: -> plip' \
+ "$SYSCONF" make -R "$ROOT" 'WITHOUT_MODULES=plip'
+ atf_check -o inline:'WITHOUT_MODULES: plip\n' \
+ "$SYSCONF" make -R "$ROOT" WITHOUT_MODULES
+}
+make_src_knobs_cleanup()
+{
+ cleanup_root
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case make_src_knobs
+ atf_add_test_case make_append_accumulate
+ atf_add_test_case make_strike_last_assign
+ atf_add_test_case src_triad_and_trail
+ atf_add_test_case src_env_rejected
+ atf_add_test_case env_overrides
+ atf_add_test_case attached_f_passthrough
+ atf_add_test_case verbatim_backslash
+ atf_add_test_case equal_value_nocheck_mtime
+ atf_add_test_case sysctl_oid_range
+}

File Metadata

Mime Type
text/plain
Expires
Sun, Aug 16, 12:52 PM (13 h, 56 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
36747678
Default Alt Text
D58066.id183354.diff (284 KB)

Event Timeline