Page MenuHomeFreeBSD

D58066.diff
No OneTemporary

D58066.diff

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
+MLINKS= bsdconf.3 bsdconf_fparse.3 \
+ bsdconf.3 bsdconf_format_derive.3 \
+ bsdconf.3 bsdconf_format_files.3 \
+ bsdconf.3 bsdconf_format_files_free.3 \
+ bsdconf.3 bsdconf_format_find.3 \
+ bsdconf.3 bsdconf_format_guess.3 \
+ bsdconf.3 bsdconf_format_lookup.3 \
+ bsdconf.3 bsdconf_format_path.3 \
+ bsdconf.3 bsdconf_format_processing.3 \
+ bsdconf.3 bsdconf_format_put.3 \
+ bsdconf.3 bsdconf_get_option.3 \
+ bsdconf.3 bsdconf_parse.3 \
+ bsdconf.3 bsdconf_put.3 \
+ bsdconf.3 bsdconf_set_option.3 \
+ bsdconf.3 bsdconf_spool.3 \
+ bsdconf.3 bsdconf_unquote.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_src_env.c \
+ bsdconf_format_sysctl.c bsdconf_put.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,264 @@
+/*
+ * 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; /* Pointer to NUL-terminated string */
+ 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 */
+ 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_RESERVED1 = 0x0100, /* reserved data type-1 */
+ BSDCONF_TYPE_RESERVED2 = 0x0200, /* reserved data type-2 */
+ BSDCONF_TYPE_RESERVED3 = 0x0400, /* reserved data type-3 */
+};
+
+/*
+ * 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.conf(5); make(1) syntax */
+ BSDCONF_FORMAT_SRC_ENV, /* src-env.conf(5); 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 /* never quote values on output */
+#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.
+ */
+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() */
+
+ /*
+ * 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,1093 @@
+.\" 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 July 6, 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 ,
+.Nm bsdconf_put ,
+.Nm bsdconf_set_option ,
+.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 file reading and writing 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
+.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
+.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
+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,
+crash-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.
+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 Sx FORMATS
+naming a format's target keyword,
+backing files,
+and tokenizing/quoting rules,
+which parameterize a single shared engine.
+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.
+.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() */
+
+ /* 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_RESERVED1 = 0x0100, /* reserved data type-1 */
+ BSDCONF_TYPE_RESERVED2 = 0x0200, /* reserved data type-2 */
+ BSDCONF_TYPE_RESERVED3 = 0x0400, /* reserved data type-3 */
+};
+
+union bsdconf_value {
+ void *data; /* Pointer to NUL-terminated string */
+ 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 */
+ 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
+.Fn bsdconf_put
+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 .
+.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 physical 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.
+.Fn bsdconf_put
+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 WRITING
+.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 .
+.Pp
+.Fa path
+must name a regular file
+.Pq only a regular file can be atomically replaced ;
+anything else is rejected with
+.Er EINVAL .
+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 Sx FORMATS ,
+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 boot-time 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 values verbatim,
+never quoted
+.Pq embedded whitespace runs to the end of the line .
+A value that could not round-trip verbatim
+.Pq an embedded newline or comment marker
+is rejected with
+.Er EINVAL
+rather than silently corrupting the file.
+.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 FORMATS
+Every supported configuration file format is described by the same small
+format descriptor
+.Pq introduced in Sx DESCRIPTION :
+.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
+.It Dv BSDCONF_FORMAT_SRC_ENV Ta src-env Ta /etc/src-env.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
+.Po
+.Dv BSDCONF_FORMAT_MAKE ,
+.Dv BSDCONF_FORMAT_SRC ,
+and
+.Dv BSDCONF_FORMAT_SRC_ENV
+.Pc
+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
+and
+.Dv BSDCONF_FORMAT_SYSCTL
+.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 , Dv BSDCONF_FORMAT_SRC_ENV
+.Pa /etc/src.conf
+and
+.Pa /etc/src-env.conf ,
+respectively,
+each alone
+.Pq see Xr src.conf 5 .
+.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 any discovered file may revise those lists in turn ,
+and
+.Dv BSDCONF_FORMAT_LOADER
+names that defaults file and those three directives so that
+.Fn bsdconf_format_files
+performs the same discovery the loader does:
+the defaults file is read,
+the file-list directive is chased
+.Pq re-read after every file, since any file may revise it ,
+and the drop-in directories and local files named by the final lists are
+appended.
+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.
+.Xr src.conf 5
+and
+.Pa src-env.conf
+share the
+.Pa make.conf
+syntax exactly
+.Pq all three are read by Xr make 1 itself ,
+including the empty value,
+which for these two files 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
+.Sx EXAMPLES
+.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 a separate and as yet unsolved problem
+.Pq see Sx BUGS .
+.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
+run time 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 FORMATS
+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
+.Pq which a Fn parse callback may split further by its own rules ,
+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_parse ,
+.Fn bsdconf_fparse ,
+and
+.Fn bsdconf_put
+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_set_option
+returns 1 if a matching directive was staged,
+otherwise 0.
+.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
+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
+.Pp
+Bolt on a new format at run time by deriving from the built-in it most
+resembles,
+then set a directive in its file with full crash-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 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
+.Fn bsdconf_put
+models directives as single-valued
+.Pq the last assignment dictates the value ,
+which suits every built-in format.
+Formats whose directives are cumulative
+.Pq legitimately repeating, each occurrence adding to the effect
+parse cleanly with caller-supplied callbacks,
+but no writing primitives for additive semantics
+.Pq insert, remove, or reorder individual occurrences
+are provided yet.
+.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
+.Fa path
+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.
+.Pp
+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.
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,611 @@
+/*
+ * 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);
+}
+
+/*
+ * 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 = read(fd, p, 1);
+
+ /* Skip to the beginning of a directive */
+ while (r != 0 && (isspace(*p) || *p == '#' || comment ||
+ (bsemicolon && *p == ';'))) {
+ if (*p == '#')
+ comment = 1;
+ else if (*p == '\n') {
+ comment = 0;
+ line++;
+ }
+ r = read(fd, p, 1);
+ }
+ /* 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(*p))
+ break;
+ if (bequals && *p == '=') {
+ have_equals = 1;
+ break;
+ }
+ if (bsemicolon && *p == ';')
+ break;
+ r = read(fd, p, 1);
+ }
+
+ /* 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;
+ }
+ r = read(fd, directive, n);
+
+ /* Advance beyond the equals sign if appropriate/desired */
+ if (bequals && *p == '=') {
+ if (lseek(fd, 1, SEEK_CUR) != -1)
+ r = read(fd, p, 1);
+ if (strict_equals && isspace(*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(*p) && *p != '\n')
+ r = read(fd, p, 1);
+ }
+
+ /* An equals sign may have stopped us, should we eat it? */
+ if (r != 0 && bequals && *p == '=' && !strict_equals) {
+ have_equals = 1;
+ r = read(fd, p, 1);
+ while (r != 0 && isspace(*p) && *p != '\n')
+ r = read(fd, p, 1);
+ }
+
+ /* 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 = read(fd, p, 1);
+ 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 = read(fd, p, 1);
+
+ /*
+ * Count how many backslashes there are (an odd number
+ * means the key is escaped, even means otherwise).
+ */
+ for (n = 1; *p == '\\'; n++) {
+ /* Move back another offset to read */
+ if (lseek(fd, -2, SEEK_CUR) == -1)
+ goto fail;
+ r = read(fd, p, 1);
+ }
+
+ /* Move offset back to the key and read it */
+ if (lseek(fd, charpos, SEEK_SET) == -1)
+ goto fail;
+ r = read(fd, p, 1);
+
+ /*
+ * 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 = read(fd, p, 1);
+ }
+
+ /* 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;
+ }
+ r = read(fd, value, n);
+
+ /* Terminate the string */
+ value[n] = '\0';
+
+ /* Cut trailing whitespace off by termination */
+ t = value + n;
+ while (isspace(*--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(*--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(*--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, "\\\"", "\\\\\"") < 0)
+ goto fail;
+
+ /* Remove all escaped newline characters */
+ if (bsdconf_replaceall(value, "\\\n", "") < 0)
+ goto fail;
+
+ /* Resolve escape sequences */
+ bsdconf_strexpand(value);
+
+call_function:
+ /* Abort if we're seeking only assignments */
+ if (require_equals && !have_equals) {
+ errno = EINVAL;
+ goto fail;
+ }
+
+ found = have_equals = 0; /* reset */
+
+ /* If there are no options defined, call unknown and loop */
+ if (options == NULL && unknown != NULL) {
+ error = unknown(NULL, 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) {
+ /*
+ * No match was found for the value we read from the
+ * file; call function designated for unknown values.
+ */
+ error = unknown(NULL, 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.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,
+ [BSDCONF_FORMAT_SRC_ENV] = &bsdconf_format_src_env_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) it defines -- re-reading it after every file, since
+ * any file may revise the lists -- and finally append the drop-in
+ * directory entries and local files named by the other two list
+ * directives. 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,48 @@
+/*
+ * 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,26 @@
+/*
+ * 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.
+ */
+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,26 @@
+/*
+ * 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: src.conf(5).
+ *
+ * Identical syntax to make.conf(5) -- src.conf is read by the make(1)
+ * include machinery in share/mk -- but consulted only when building the
+ * FreeBSD source tree (typically WITH_ and WITHOUT_ knobs, which are
+ * value-less; setting one to an empty value is the idiom, hence
+ * BSDCONF_PUT_ALLOW_EMPTY).
+ */
+const struct bsdconf_format_def bsdconf_format_src_def = {
+ .keyword = "src",
+ .path = "/etc/src.conf",
+ .sources = NULL, /* /etc/src.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_env.c b/lib/libbsdconf/bsdconf_format_src_env.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_format_src_env.c
@@ -0,0 +1,25 @@
+/*
+ * 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_ENV: src-env.conf(5).
+ *
+ * Identical syntax to make.conf(5) and src.conf(5), but read earlier in
+ * the make(1) startup sequence (before the environment is finalized;
+ * see SRC_ENV_CONF in src.conf(5)), which is why it is a separate file
+ * and hence a separate target keyword.
+ */
+const struct bsdconf_format_def bsdconf_format_src_env_def = {
+ .keyword = "src-env",
+ .path = "/etc/src-env.conf",
+ .sources = NULL, /* /etc/src-env.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_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,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
+ */
+
+#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_src_env_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,42 @@
+/*
+ * 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>
+
+/*
+ * 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_strexpand(char *_source);
+void bsdconf_strtolower(char *_source);
+int bsdconf_replaceall(char *_source, const char *_find,
+ const char *_replace);
+unsigned int bsdconf_strcount(const char *_source, const char *_find);
+
+/* Restarting full-length write (bsdconf_put.c) */
+int bsdconf_writeall(int _fd, const void *_data, size_t _len);
+
+#endif /* !_BSDCONF_INTERNAL_H_ */
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,970 @@
+/*
+ * 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 <strings.h>
+#include <unistd.h>
+
+#include "bsdconf.h"
+#include "bsdconf_internal.h"
+
+/*
+ * 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 */
+};
+
+/*
+ * Map an assignment operator to its config file token. BSDCONF_OP_DEFAULT
+ * maps to the plain `=' token.
+ */
+static 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.
+ */
+static 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.
+ */
+static 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);
+}
+
+/*
+ * 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.
+ */
+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 (*p == '\n')
+ return (0);
+ continue; /* escaped character; part of the value */
+ }
+ 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) values are emitted verbatim -- embedded
+ * whitespace included, since the value runs to the end of the line -- and a
+ * value that could not round-trip verbatim (embedded newline or comment
+ * marker) is rejected (errno = EINVAL) rather than silently corrupting the
+ * file. 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.
+ */
+static 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_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.
+ */
+static 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.
+ */
+static 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.
+ */
+static 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);
+}
+
+/*
+ * 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).
+ *
+ * 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;
+
+ /* Build the temporary file path in the target's own directory */
+ if ((slash = strrchr(rpath, '/')) != NULL) {
+ if (snprintf(tpath, sizeof(tpath), "%.*s/.bsdconf.XXXXXXXXXX",
+ (int)(slash - rpath), rpath) >= (int)sizeof(tpath)) {
+ errno = ENAMETOOLONG;
+ goto cleanup;
+ }
+ } else if (snprintf(tpath, sizeof(tpath), ".bsdconf.XXXXXXXXXX") >=
+ (int)sizeof(tpath)) {
+ errno = ENAMETOOLONG;
+ goto cleanup;
+ }
+ if ((tmpfd = mkstemp(tpath)) == -1) {
+ tpath[0] = '\0'; /* nothing to unlink */
+ goto cleanup;
+ }
+
+ /* Give the temporary file the original's mode (and owner if able) */
+ if (fchmod(tmpfd, sb.st_mode & ALLPERMS) != 0)
+ goto cleanup;
+ (void)fchown(tmpfd, sb.st_uid, sb.st_gid); /* best effort */
+
+ /*
+ * Walk the original statement by statement. Bytes are emitted to the
+ * temporary file 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)) {
+ option = &options[n];
+ break;
+ }
+ }
+ if (option == NULL)
+ continue; /* untouched; bytes flushed later */
+
+ /* 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_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 (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;
+ }
+
+ /* Flush the tail of the original */
+ if (bsdconf_emit(tmpfd, buf + wc, buflen - wc, &last_ch) != 0)
+ goto cleanup;
+
+ /*
+ * Append any SET_VALUE directives that were not found, and finalize
+ * the result flags for CHECK directives that were absent.
+ */
+ 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 */
+
+ /* 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;
+ option->result |=
+ BSDCONF_DIRECTIVE_ADDED | BSDCONF_VALUE_CHANGED;
+ }
+
+ /* 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_string.c b/lib/libbsdconf/bsdconf_string.c
new file mode 100644
--- /dev/null
+++ b/lib/libbsdconf/bsdconf_string.c
@@ -0,0 +1,275 @@
+/*
+ * 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 <stdint.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 block of memory
+ * 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 `source' with `replace'.
+ *
+ * You should not pass a string constant as the first parameter, it needs to
+ * be a pointer to an allocated block of memory. The block of memory that
+ * source points to should be large enough to hold the result. If the length
+ * of the replacement string is greater than the length of the find string,
+ * the result will be larger than the original source string. To allocate
+ * enough space for the result, use the function bsdconf_strcount() declared
+ * above to determine the number of occurrences and how much larger the block
+ * size needs to be.
+ *
+ * If source is not large enough, the application will crash. The return value
+ * is the length (in bytes) of the result.
+ *
+ * When an error occurs, -1 is returned and the global variable errno is set
+ * accordingly.
+ */
+int
+bsdconf_replaceall(char *source, const char *find, const char *replace)
+{
+ char *p;
+ char *t;
+ char *temp;
+ size_t flen;
+ size_t rlen;
+ size_t slen;
+ uint32_t n = 0;
+
+ errno = 0; /* reset global error number */
+
+ /* Check that we have non-null parameters */
+ if (source == NULL)
+ return (0);
+ if (find == NULL)
+ return (strlen(source));
+
+ /* Cache the length of the strings */
+ slen = strlen(source);
+ 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);
+
+ /* 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, source, slen + 1);
+ } else
+ temp = source;
+
+ /* Reconstruct the string with the replacements */
+ p = source; t = temp; /* position elements */
+
+ while (*t != '\0') {
+ if (strncmp(t, find, flen) == 0) {
+ /* found an occurrence */
+ for (n = 0; replace && replace[n]; n++)
+ *p++ = replace[n];
+ t += flen;
+ } else
+ *p++ = *t++; /* copy character and increment */
+ }
+
+ /* Terminate the string */
+ *p = '\0';
+
+ /* Free the temporary allocated memory */
+ if (temp != source)
+ free(temp);
+
+ /* Return the length of the completed string */
+ return (strlen(source));
+}
+
+/*
+ * Expands escape sequences in a buffer pointed to by `source'. This function
+ * steps through each character, and converts escape sequences such as "\n",
+ * "\r", "\t" and others into their respective meanings.
+ *
+ * You should not pass a string constant or literal to this function or the
+ * program will likely segmentation fault when it tries to modify the data.
+ *
+ * The string length will either shorten or stay the same depending on whether
+ * any escape sequences were converted but the amount of memory allocated does
+ * not change. The rewrite is done in place: because every escape sequence
+ * consumes at least as many input bytes as it produces, the output cursor
+ * never overtakes the input cursor, so the two may safely share the buffer.
+ *
+ * Interpreted sequences are:
+ *
+ * \0NNN character with octal value NNN (0 to 3 digits)
+ * \N character with octal value N (0 thru 7)
+ * \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 '\#').
+ */
+void
+bsdconf_strexpand(char *source)
+{
+ uint8_t c;
+ char *chr;
+ char *pos;
+ char d[4];
+
+ /* Initialize position elements */
+ pos = chr = source;
+
+ /*
+ * Loop until we hit the end of the input. The stop condition must
+ * track the input cursor (chr): expanding an escape advances chr
+ * ahead of the output cursor (pos), so once the two diverge *pos
+ * no longer reflects where the input terminates.
+ */
+ while (*chr != '\0') {
+ if (*chr != '\\') {
+ *pos = *chr; /* copy character to current offset */
+ pos++;
+ chr++;
+ 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 (*(chr + 1) == '\0') {
+ *pos++ = *chr++;
+ break;
+ }
+
+ /* Replace the backslash with the correct character */
+ switch (*++chr) {
+ case 'a': *pos = '\a'; break; /* bell/alert (BEL) */
+ case 'b': *pos = '\b'; break; /* backspace */
+ case 'f': *pos = '\f'; break; /* form feed */
+ case 'n': *pos = '\n'; break; /* new line */
+ case 'r': *pos = '\r'; break; /* carriage return */
+ case 't': *pos = '\t'; break; /* horizontal tab */
+ case 'v': *pos = '\v'; break; /* vertical tab */
+ case 'x': /* hex value (1 to 2 digits)(\xNN) */
+ d[2] = '\0'; /* pre-terminate the string */
+
+ /* verify next two characters are hex */
+ d[0] = isxdigit(*(chr+1)) ? *++chr : '\0';
+ if (d[0] != '\0')
+ d[1] = isxdigit(*(chr+1)) ? *++chr : '\0';
+
+ /* convert the characters to decimal */
+ c = (uint8_t)strtoul(d, 0, 16);
+
+ /* assign the converted value */
+ *pos = (c != 0 || d[0] == '0') ? c : *++chr;
+ break;
+ case '0': /* octal value (0 to 3 digits)(\0NNN) */
+ d[3] = '\0'; /* pre-terminate the string */
+
+ /* verify next three characters are octal */
+ d[0] = (isdigit(*(chr+1)) && *(chr+1) < '8') ?
+ *++chr : '\0';
+ if (d[0] != '\0')
+ d[1] = (isdigit(*(chr+1)) && *(chr+1) < '8') ?
+ *++chr : '\0';
+ if (d[1] != '\0')
+ d[2] = (isdigit(*(chr+1)) && *(chr+1) < '8') ?
+ *++chr : '\0';
+
+ /* convert the characters to decimal */
+ c = (uint8_t)strtoul(d, 0, 8);
+
+ /* assign the converted value */
+ *pos = c;
+ break;
+ default: /* single octal (\0..7) or unknown sequence */
+ if (isdigit(*chr) && *chr < '8') {
+ d[0] = *chr;
+ d[1] = '\0';
+ *pos = (uint8_t)strtoul(d, 0, 8);
+ } else
+ *pos = *chr;
+ }
+
+ /* Increment to next offset, possible next escape sequence */
+ pos++;
+ chr++;
+ }
+
+ /*
+ * Terminate at the (possibly earlier) output cursor. When any
+ * escape was collapsed the string shrank, so the trailing bytes
+ * between pos and chr are now stale and must be cut off here.
+ */
+ *pos = '\0';
+}
+
+/*
+ * Convert a string to lower case. You should not pass a string constant to
+ * this function. Only pass pointers to allocated memory with null terminated
+ * string data.
+ */
+void
+bsdconf_strtolower(char *source)
+{
+ char *p = source;
+
+ if (source == NULL)
+ return;
+
+ while (*p != '\0') {
+ *p = tolower(*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,9 @@
+PACKAGE= utilities
+PROG= sysconf
+MAN= sysconf.8
+
+CFLAGS+= -I${SRCTOP}/lib/libbsdconf
+
+LIBADD= bsdconf jail
+
+.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.8 b/usr.sbin/sysconf/sysconf.8
new file mode 100644
--- /dev/null
+++ b/usr.sbin/sysconf/sysconf.8
@@ -0,0 +1,732 @@
+.\" 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 July 6, 2026
+.Dt SYSCONF 8
+.Os
+.Sh NAME
+.Nm sysconf
+.Nd read and modify system configuration files
+.Sh SYNOPSIS
+.Nm
+.Ar target
+.Op Fl AcDeFinNqvx
+.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 ADeFnNqv
+.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 .
+.Pp
+The assignment syntax maps directly to
+.Xr sysctl 8 :
+a
+.Ar name
+alone reads the current value of the directive,
+while
+.Ar name Ns = Ns Ar value
+atomically writes it,
+echoing
+.Ql name: old -> new
+in the manner of
+.Xr sysctl 8
+and
+.Xr sysrc 8
+.Pq suppressed by Fl q ;
+a directive not previously set anywhere shows an empty old value.
+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.
+For
+.Xr make 1 Ns -syntax
+targets
+.Ql +=
+is instead the
+.Xr make 1
+append operator,
+written through to the file
+.Pq see below .
+The first positional argument selects the
+.Ar target
+by keyword;
+options are equally accepted before it:
+.Bl -column "keyword" "/etc/sysctl.conf, sysctl.conf.local, sysctl.kld.d/*.conf" -offset indent
+.It Sy keyword Ta Sy files
+.It loader Ta discovered from Pa /boot/defaults/loader.conf No (see below)
+.It sysctl Ta Pa /etc/sysctl.conf , sysctl.conf.local , sysctl.kld.d/*.conf
+.It make Ta Pa /etc/make.conf
+.It src Ta Pa /etc/src.conf
+.It src-env Ta Pa /etc/src-env.conf
+.It rc Ta pass-through to Xr sysrc 8 ; see below
+.It generic Ta none; requires Fl f
+.El
+.Pp
+The
+.Cm rc
+target 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 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.
+.Pp
+Most targets are backed by more than one file,
+consulted automatically in the deterministic order the system sources them
+at boot
+.Pq see Xr loader.conf 5 and Xr sysctl.conf 5 .
+The
+.Ql loader
+target's file list is not hardcoded:
+just as the boot loader itself hardcodes only
+.Pa /boot/defaults/loader.conf ,
+.Nm
+reads that file and chases the
+.Va loader_conf_files ,
+.Va loader_conf_dirs ,
+and
+.Va local_loader_conf_files
+directives it defines
+.Pq re-reading the lists after every file, since any file may revise them ,
+so customized layouts that redirect
+.Va loader_conf_files
+elsewhere are honored automatically.
+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.
+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.
+For a target with a defaults file,
+the boot-time consumer sources the defaults before everything else,
+and so does
+.Nm :
+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 may modify .
+Reads report the 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 are applied to every file listing the directive,
+lest deleting the authoritative definition merely unmask an earlier one.
+For the
+.Ql sysctl
+target,
+.Fl k Ar module
+additionally consults the module-specific drop-in
+.Pa /etc/sysctl.kld.d/ Ns Ar module Ns Pa .conf
+applied by
+.Xr rc.subr 8
+when the module loads.
+.Pp
+An arbitrary configuration file may instead 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.
+The
+.Ql generic
+target
+.Pq quote values only when required to round-trip
+exists for files matching no known format and,
+having no default files of its own,
+always requires
+.Fl f .
+.Pp
+The
+.Nm
+utility understands the syntax particulars of each target:
+values written to
+.Xr loader.conf 5
+are always enclosed in quotes
+.Pq quoted or unquoted input is accepted
+with no whitespace around the equals sign,
+values written to
+.Xr sysctl.conf 5
+are quoted only when the value requires it
+.Pq embedded whitespace ,
+and
+.Pa make.conf
+.Po
+along with its
+.Xr make 1 Ns -syntax
+siblings
+.Pa src.conf
+and
+.Pa src-env.conf
+.Pc
+assignment modifiers are preserved and may be requested by suffixing the
+name with the modifier character,
+as in
+.Ql CFLAGS+=-DDEBUG .
+.Pp
+When writing to the
+.Ql sysctl
+target of the running system,
+each
+.Ar name
+is validated against the kernel's sysctl tree first:
+an unknown OID or an OID that cannot be set at run time is reported and
+skipped,
+while the remaining valid assignments are still applied.
+In particular,
+a loader-only tunable is reported as requiring the
+.Ql loader
+target instead.
+Validation is relaxed for unknown OIDs under
+.Fl i
+or
+.Fl k
+.Pq the module providing the OID may not be loaded
+and skipped entirely under
+.Fl R ,
+where the target system's kernel is not the running kernel.
+.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,
+so that a crash or power loss mid-write cannot truncate or corrupt the
+configuration
+.Pq see Xr bsdconf 3 .
+.Pp
+Targets carrying a body of system-supplied defaults
+.Po
+presently
+.Ql loader ,
+whose defaults file is
+.Pa /boot/defaults/loader.conf
+.Pc
+additionally support querying those defaults:
+.Fl d
+reports directive descriptions,
+.Fl D
+consults the defaults alone,
+and
+.Fl A
+folds the defaults into the sourcing order.
+Attempting any of these against a target with no defaults
+.Po
+.Pa make.conf ,
+.Pa src.conf ,
+and
+.Pa src-env.conf
+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.
+.Pp
+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 above
+.Pc .
+With no
+.Ar name
+arguments,
+dumps everything
+.Pq as Fl a , defaults included .
+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.
+.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
+.Pq with Fl x , check that Ar name is absent .
+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
+.Pq or, with Fl a , dumping every default .
+Defaults are read-only;
+combining
+.Fl D
+with an assignment is an error.
+Only valid for targets with a defaults file.
+.It Fl d
+Show the description of each
+.Ar name
+instead of its value.
+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
+.Pq as with Xr sysctl 8 Fl d .
+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.
+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 .
+.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 unknown sysctl OIDs when writing .
+.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.
+.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
+.Pq it is not a file Nm could or would modify .
+.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 v
+Verbose.
+Print the pathname of the configuration file being operated on.
+.It Fl x
+Remove the named directive(s) from every file of the target listing them.
+.El
+.Sh ENVIRONMENT
+.Bl -tag -width "LOADER_DEFAULTS"
+.It Ev LOADER_DEFAULTS
+Defaults file for the
+.Ql loader
+target,
+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 for the
+.Ql loader
+target and the authority on its file list
+.Pq see Va loader_conf_files .
+.It Pa /boot/device.hints
+Device hints,
+first in the stock
+.Va loader_conf_files
+list.
+.It Pa /boot/loader.conf
+Local boot loader settings;
+the stock write target of the
+.Ql loader
+target.
+.It Pa /boot/loader.conf.d/
+Boot loader drop-in directory.
+.It Pa /boot/loader.conf.local
+Site-local boot loader overrides,
+sourced last.
+.It Pa /etc/sysctl.conf
+Kernel state to set at boot;
+the write target of the
+.Ql sysctl
+target.
+.It Pa /etc/sysctl.conf.local
+Site-local sysctl overrides.
+.It Pa /etc/sysctl.kld.d/
+Module-specific sysctl drop-ins,
+applied when the named kernel module loads
+.Pq see Fl k .
+.It Pa /etc/make.conf
+System-wide
+.Xr make 1
+settings.
+.It Pa /etc/src.conf
+Source tree build options.
+.It Pa /etc/src-env.conf
+Source tree build options read before the environment is finalized.
+.El
+.Sh EXIT STATUS
+.Ex -std
+In check mode
+.Pq Fl c ,
+an exit status of zero means no changes are required.
+.Sh EXAMPLES
+Read the effective value of
+.Va zfs_load
+.Pq from whichever Xr loader.conf 5 file sets it last :
+.Pp
+.Dl sysconf loader zfs_load
+.Pp
+Enable the ZFS module at boot:
+.Pp
+.Dl sysconf loader zfs_load=\(dqYES\(dq
+.Pp
+Raise the maximum number of open files:
+.Pp
+.Dl sysconf sysctl kern.maxfiles=65536
+.Pp
+Show which file authoritatively sets
+.Va vfs.usermount
+for boot:
+.Pp
+.Dl sysconf sysctl -F vfs.usermount
+.Pp
+List the files consulted for the
+.Ql loader
+target
+.Pq as discovered from Pa /boot/defaults/loader.conf :
+.Pp
+.Dl sysconf loader -l
+.Pp
+Show what a directive does,
+straight from the defaults file's comments,
+then dump the description of everything in scope,
+defaults included:
+.Pp
+.Dl sysconf loader -d verbose_loading
+.Dl sysconf loader -Ad
+.Pp
+Show a sysctl OID's description from the running kernel:
+.Pp
+.Dl sysconf sysctl -d kern.maxfiles
+.Pp
+Read the system default of a directive,
+ignoring local configuration,
+then dump every default:
+.Pp
+.Dl sysconf loader -D verbose_loading
+.Dl sysconf loader -aD
+.Pp
+Read a directive's effective value with the defaults folded in
+.Pq useful when the directive is not set locally at all :
+.Pp
+.Dl sysconf loader -A autoboot_delay
+.Pp
+List every
+.Pa sysctl.kld.d
+candidate for the currently loaded kernel modules,
+then narrow the list to those that exist on disk:
+.Pp
+.Dl sysconf sysctl -L
+.Dl sysconf sysctl -LE
+.Pp
+Configure a module-specific sysctl applied when the
+.Ql ipfw
+module loads:
+.Pp
+.Dl sysconf sysctl -k ipfw net.inet.ip.fw.verbose=1
+.Pp
+Maintain a space-separated list,
+appending a word only if absent and then striking it:
+.Pp
+.Dl sysconf loader kernels+=kernel_test
+.Dl sysconf loader kernels-=kernel_test
+.Pp
+Hand an
+.Xr rc.conf 5
+request to
+.Xr sysrc 8
+untouched
+.Pq every argument after the Cm rc keyword passes through verbatim :
+.Pp
+.Dl sysconf rc kld_list+=i915kms
+.Pp
+Append a flag to
+.Va CFLAGS
+in
+.Pa /etc/make.conf
+.Po
+for
+.Xr make 1 Ns -syntax
+targets
+.Ql +=
+is the
+.Xr make 1
+operator,
+not a list edit
+.Pc :
+.Pp
+.Dl sysconf make CFLAGS+=-DDEBUG
+.Pp
+Exclude the LLDB debugger from the next
+.Sy buildworld :
+.Pp
+.Dl sysconf src WITHOUT_LLDB=
+.Pp
+Remove a directive from every file of the
+.Ql sysctl
+target listing it:
+.Pp
+.Dl sysconf -x sysctl vfs.usermount
+.Pp
+Dump the union of everything configured for the
+.Ql loader
+target:
+.Pp
+.Dl sysconf -a loader
+.Pp
+Set a value inside a jail:
+.Pp
+.Dl sysconf -j www sysctl kern.ipc.somaxconn=1024
+.Pp
+Stage a
+.Xr loader.conf 5 Ns -formatted
+file outside
+.Pa /boot ,
+quoted exactly as the boot loader demands:
+.Pp
+.Dl sysconf loader -f /tmp/loader.conf.new zfs_load=\(dqYES\(dq
+.Pp
+Manage a file matching no known format:
+.Pp
+.Dl sysconf generic -f /usr/local/etc/myapp.conf loglevel=debug
+.Sh SEE ALSO
+.Xr bsdconf 3 ,
+.Xr loader.conf 5 ,
+.Xr src.conf 5 ,
+.Xr sysctl.conf 5 ,
+.Xr jail 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 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.
+Write operations replace files atomically and never in place;
+see the
+.Sx SECURITY CONSIDERATIONS
+section of
+.Xr bsdconf 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,1932 @@
+/*
+ * Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#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 <limits.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 Jul-6,2026"
+
+/* getopt(3) optstring; shared by parse_options() and find_target() */
+#define OPTSTRING "AacdDEeFf:hij:k:lLnNqR:vx"
+
+/* 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 remove; /* nonzero for `-x' removals */
+ int srcidx; /* conf file that last set the value
+ (authoritative); -1 if unset */
+ unsigned char *infile; /* per-file presence map */
+};
+
+static struct request *reqs; /* set by main() */
+static unsigned int nreqs; /* set by main() */
+
+/* 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 */
+};
+
+static struct dumpent *dumps; /* set by scan_cb() */
+static size_t ndumps; /* set by scan_cb() */
+static size_t dumpsize; /* set by scan_cb() */
+
+/* Resolved target state */
+static char **conf_files; /* ordered backing files */
+static size_t nconf_files;
+static size_t conf_scanidx; /* file being parsed (scan_pass) */
+static size_t write_idx; /* default write target index */
+static int defaults_idx = -1; /* defaults file index in
+ conf_files (read-only,
+ never listed); -1 if none */
+static enum bsdconf_format format = BSDCONF_FORMAT_GENERIC;
+
+/* Extra display information */
+static const char *pgm; /* set to argv[0] by main() */
+static uint8_t check = 0; /* `-c' */
+static uint8_t defaults_only = 0; /* `-D' */
+static uint8_t dump_all = 0; /* `-a' */
+static uint8_t existing_only = 0; /* `-E' */
+static uint8_t ignore_unknown = 0; /* `-i' */
+static uint8_t list_all = 0; /* `-L' */
+static uint8_t list_files = 0; /* `-l' */
+static uint8_t name_only = 0; /* `-N' */
+static uint8_t quiet = 0; /* `-q' */
+static uint8_t remove_mode = 0; /* `-x' */
+static uint8_t show_desc = 0; /* `-d' */
+static uint8_t show_equals = 0; /* `-e' */
+static uint8_t show_file = 0; /* `-F' */
+static uint8_t value_only = 0; /* `-n' */
+static uint8_t verbose = 0; /* `-v' */
+static uint8_t with_defaults = 0; /* `-A' */
+
+/* Option arguments */
+static const char *file = NULL; /* `-f file' */
+static uint8_t file_stdin = 0; /* `-f -' given */
+static const char *jailname = NULL; /* `-j jail' */
+static const char *module = NULL; /* `-k module' */
+static const char *rootdir = ""; /* `-R dir' */
+
+/* Function prototypes for private functions (see style(9)) */
+static void usage(void);
+static void help(void);
+static int find_target(int argc, char *argv[]);
+static void exec_sysrc(int argc, char *argv[], int skip);
+static int parse_options(int argc, char *argv[]);
+static int list_has(const char *list, const char *word, size_t wlen);
+static char *list_merge(const char *current, const char *words, int op);
+static int merge_list_requests(void);
+static void print_pair(const char *path, const char *directive,
+ const char *value);
+static int scan_cb(struct bsdconf_option *option, uint32_t line,
+ char *directive, char *value);
+static int scan_pass(int sandbox);
+static void resolve_target(const char *target);
+static void split_request(char *arg, struct request *req, int remove);
+static char *defaults_path(void);
+static void load_descriptions(void);
+static const char *desc_find(const char *name);
+static void print_desc(const char *directive);
+static int do_list(void);
+static int do_reads(void);
+static int do_checks(void);
+static int do_writes(void);
+static int describe_defaults(void);
+#if defined(__FreeBSD__)
+static int describe_sysctl(void);
+static int sysctl_descr(const char *name, char *buf, size_t bufsize);
+static int sysctl_writable(const char *name, int tolerate_unknown);
+#endif
+
+/*
+ * 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.
+ */
+static int
+find_target(int argc, char *argv[])
+{
+ int n;
+ char last;
+ 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') {
+ /* Skip the argument of an argument-taking option */
+ last = argv[n][strlen(argv[n]) - 1];
+ if ((p = strchr(OPTSTRING, last)) != NULL &&
+ p[1] == ':')
+ n++;
+ 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 'v': /* verbose */
+ verbose = 1;
+ break;
+ case 'x': /* remove */
+ remove_mode = 1;
+ break;
+ case '?': /* unknown argument (based on optstring) */
+ default: /* unhandled argument (based on switch) */
+ usage();
+ }
+ }
+
+ return (optind);
+}
+
+/*
+ * Print a directive/value pair according to the display flags. `path' is
+ * the configuration file holding the authoritative definition (or NULL).
+ */
+static void
+print_pair(const char *path, const char *directive, const char *value)
+{
+
+ if (show_file)
+ value = path != NULL ? path : "(none)";
+ if (verbose && path != NULL && !show_file)
+ printf("%s: ", path);
+ if (name_only) {
+ puts(directive);
+ return;
+ }
+ if (value_only) {
+ puts(value);
+ return;
+ }
+ if (show_equals && !show_file) {
+ if ((bsdconf_format_put(format) &
+ BSDCONF_PUT_QUOTE_ALWAYS) != 0)
+ printf("%s=\"%s\"\n", directive, value);
+ else
+ printf("%s=%s\n", directive, value);
+ return;
+ }
+ printf("%s: %s\n", directive, value);
+}
+
+/*
+ * Record `directive'/`value' in the dump union (for `-a'). A directive seen
+ * again in a later file overrides the earlier value while retaining its
+ * original position, so the output reflects what the system settles on at
+ * boot. Returns zero on success; -1 (errno set) on allocation failure.
+ */
+static int
+dump_record(const char *directive, const char *value)
+{
+ 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;
+ free(dumps[n].value);
+ dumps[n].value = vcopy;
+ dumps[n].srcidx = (int)conf_scanidx;
+ 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;
+ ndumps++;
+ return (0);
+}
+
+/*
+ * bsdconf_fparse() call-back; record each statement against the requests
+ * (and, with `-a', the dump union). A later assignment overrides an earlier
+ * one -- both within a file and across the ordered file list -- matching
+ * the semantics of the boot loader and of sysctl.conf(5) processing.
+ */
+static int
+scan_cb(struct bsdconf_option *option __unused, uint32_t line __unused,
+ char *directive, char *value)
+{
+ char *copy;
+ unsigned int n;
+
+ 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);
+ free(reqs[n].value);
+ reqs[n].value = bsdconf_unquote(copy);
+ reqs[n].found = 1;
+ reqs[n].srcidx = (int)conf_scanidx;
+ reqs[n].infile[conf_scanidx] = 1;
+ }
+
+ if (dump_all && dump_record(directive, value) != 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.
+ */
+static 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);
+}
+
+/*
+ * 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.
+ */
+static 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).
+ */
+static 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) {
+ 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);
+
+ /*
+ * 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++;
+ }
+}
+
+/*
+ * 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; for every other target, `name+=word'
+ * and `name-=word' are sysrc(8)-style list edits, appending words not
+ * already present or striking those that are (see merge_list_requests()
+ * below).
+ */
+static 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->remove = remove;
+ req->srcidx = -1;
+ 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 '-':
+ arg[n - 1] = '\0';
+ errx(EXIT_FAILURE,
+ "%s: -= is not a make(1) operator", arg);
+ }
+ 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");
+}
+
+/*
+ * Test whether the whitespace-separated `list' contains `word' (of length
+ * `wlen') as a whole word.
+ */
+static 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.
+ */
+static 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 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. 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.
+ */
+static 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;
+ 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);
+}
+
+/*
+ * 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.
+ */
+static 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);
+}
+
+/*
+ * Report read requests (and `-a') from the results of a completed
+ * scan_pass(). Returns EXIT_SUCCESS or EXIT_FAILURE.
+ */
+static 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 (show_desc)
+ print_desc(dumps[d].name);
+ 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;
+ }
+ print_pair(conf_files[reqs[n].srcidx], reqs[n].name,
+ reqs[n].value);
+ }
+
+ return (rv);
+}
+
+/*
+ * 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, and a removal request differs when the directive is still present.
+ * Returns EXIT_SUCCESS when no changes would be required.
+ */
+static 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
+ 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);
+}
+
+/*
+ * 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 `='.
+ */
+static 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);
+}
+
+#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.
+ */
+static 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);
+}
+#endif
+
+/*
+ * 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).
+ */
+static 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.
+ */
+static 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);
+}
+
+#if defined(__FreeBSD__)
+/*
+ * 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.
+ */
+static 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);
+}
+
+/*
+ * Determine whether the sysctl OID `name' can take effect when set from
+ * sysctl.conf(5): the OID must exist, must be a leaf, and must be writable
+ * at run time. An OID that is only tunable from the boot loader is
+ * reported as such (pointing at the loader target). Mirrors the checks
+ * sysctl(8) applies before setting a value. With `tolerate_unknown', an
+ * OID that does not resolve is allowed through (the module providing it
+ * may simply not be loaded); the flag checks are only possible against a
+ * resolvable OID. Returns zero when the value should be written; -1
+ * (after warning) otherwise.
+ */
+static int
+sysctl_writable(const char *name, int tolerate_unknown)
+{
+ int mib[CTL_MAXNAME];
+ int qoid[CTL_MAXNAME + 2];
+ size_t len;
+ size_t size;
+ u_int kind;
+ u_char buf[BUFSIZ];
+
+ qoid[0] = CTL_SYSCTL;
+ qoid[1] = CTL_SYSCTL_NAME2OID;
+ size = sizeof(mib);
+ if (sysctl(qoid, 2, mib, &size, name, strlen(name)) != 0) {
+ if (tolerate_unknown)
+ return (0);
+ warnx("unknown oid '%s'", name);
+ return (-1);
+ }
+ 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;
+
+ 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);
+ }
+
+ return (0);
+}
+#endif
+
+/*
+ * 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.
+ */
+static 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;
+ } 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;
+ if (verbose)
+ printf("%s: %s: removed\n",
+ conf_files[f],
+ opt->directive);
+ }
+ continue;
+ }
+ if (!quiet) {
+ const char *old = owner[n]->value;
+
+ /*
+ * Echo `name: old -> new' in the manner of
+ * sysctl(8) and sysrc(8); a directive not
+ * previously set anywhere has an empty old
+ * value.
+ */
+ if (verbose)
+ printf("%s: ", conf_files[f]);
+ printf("%s: %s -> %s\n", opt->directive,
+ old == NULL ? "" : old,
+ opt->value.str);
+ }
+ }
+ }
+
+ /* 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);
+}
+
+/*
+ * 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();
+ }
+
+#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) {
+ warnx("-d cannot be combined with "
+ "-c/-l/-L/-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);
+ for (n = 0; n < argc; n++) {
+ split_request(argv[n], &reqs[n], remove_mode);
+ 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");
+ 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); apply the remaining (valid) requests. Only
+ * meaningful against the running system's sysctl tree, so skipped
+ * under -R. Unknown OIDs are permitted with -i or -k (a module
+ * drop-in typically configures a module that is not yet loaded).
+ */
+ if (format == BSDCONF_FORMAT_SYSCTL && have_writes && !check &&
+ *rootdir == '\0') {
+ int tolerate = ignore_unknown || module != NULL;
+ unsigned int u;
+
+ for (u = 0; u < nreqs; u++) {
+ if (reqs[u].newvalue == NULL)
+ continue;
+ if (sysctl_writable(reqs[u].name, tolerate) != 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 read operations run the scan (and the rest of
+ * their lives) inside a sandbox -- except a sysctl description
+ * dump, whose kernel queries the sandbox would deny.
+ */
+ scan_pass(!have_writes && !check &&
+ !(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_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_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++) {
+ reqs[n].found = 0;
+ reqs[n].srcidx = -1;
+ 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.
+ */
+static void
+usage(void)
+{
+
+ fprintf(stderr,
+ "usage: %s target [-AcDeFinNqvx] [-j jail | -R dir]"
+ " [-f file | -k module]\n"
+ " name[[+|-]=value] ...\n", pgm);
+ fprintf(stderr,
+ " %s target [-ADeFnNqv] [-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.
+ */
+static void
+help(void)
+{
+
+ fprintf(stderr,
+ "usage: %s target [-AcDeFinNqvx] [-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");
+ fprintf(stderr, TGTFMT, "src", "/etc/src.conf");
+ fprintf(stderr, TGTFMT, "src-env", "/etc/src-env.conf");
+ 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 the name and value of directive(s) with `='.");
+ 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 unknown sysctl OIDs).");
+ 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, "-v",
+ "Verbose. Print the pathname of the configuration file.");
+ 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).");
+ exit(EXIT_FAILURE);
+}

File Metadata

Mime Type
text/plain
Expires
Fri, Jul 10, 7:57 PM (2 h, 41 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
34897487
Default Alt Text
D58066.diff (199 KB)

Event Timeline