git: 3fe5961a0b70 - main - Add sysconf(8) and libbsdconf(3)

From: Devin Teske <dteske_at_FreeBSD.org>
Date: Wed, 16 Sep 2026 21:31:28 UTC
The branch main has been updated by dteske:

URL: https://cgit.FreeBSD.org/src/commit/?id=3fe5961a0b708da599d42cbb6b5e4f030c28ea45

commit 3fe5961a0b708da599d42cbb6b5e4f030c28ea45
Author:     Devin Teske <dteske@FreeBSD.org>
AuthorDate: 2026-09-16 04:29:32 +0000
Commit:     Devin Teske <dteske@FreeBSD.org>
CommitDate: 2026-09-16 21:28:14 +0000

    Add sysconf(8) and libbsdconf(3)
    
    Complete the native configuration trinity: sysctl(8) for live kernel
    state, sysrc(8) for rc.conf(5), and sysconf(8) for the remaining base
    configuration -- loader.conf(5), sysctl.conf(5), and the make.conf(5)
    family -- atop libbsdconf(3).
    
    libbsdconf resurrects figpar as a unified reader/writer.  Callbacks own
    semantics; statements may span multiple lines via backslash continuation;
    non-seekable input is spooled; writes are atomic (mkstemp, fsync, rename)
    with mode/owner preservation.  Format descriptors name each target, its
    files, and quoting rules without private parsers.  Multi-file targets
    follow boot sourcing order; loader chases loader_conf_files as the boot
    loader does.
    
    sysconf(8) is the operator-facing tool: name / name=value on a required
    target, sysrc-style list edits, make append and list-strike where they
    belong, jail/altroot, and a capsicum sandbox for read-only use.
    
    Sysctl writes validate against the running kernel first -- unknown and
    read-only OIDs, CTLFLAG_TUN (pointing at the loader target), and CTLTYPE
    range checks -- so a typo or overflow does not land in sysctl.conf.
    
    Make and src treat WITH_/WITHOUT_ as presence knobs (as bsd.mkopt.mk /
    src.conf(5) do) and warn on the WITH_*=no form that does not disable the
    option, so a bad assignment is caught before an /usr/src build surfaces
    it.
    
    The rc target passes through to sysrc(8).
    
    Defaults querying (-d/-D/-A) mirrors sysrc for dumps and descriptions on
    targets that have a defaults file; named reads already see defaults, and
    -A only widens dump scope.
    
    Manuals are split pkg(8)-style (bsdconf/put/format; sysconf plus
    per-target pages).  ATF coverage exercises the frontend.
    
    Co-authored-by: Faraz Vahedi <kfv@FreeBSD.org>
    Reviewed by:    fuz, kfv
    Differential Revision:  https://reviews.freebsd.org/D58066
---
 contrib/mandoc/lib.in                   |   1 +
 etc/mtree/BSD.tests.dist                |   2 +
 lib/Makefile                            |   1 +
 lib/libbsdconf/Makefile                 |  30 ++
 lib/libbsdconf/Makefile.depend          |  15 +
 lib/libbsdconf/bsdconf.3                | 432 ++++++++++++++++++
 lib/libbsdconf/bsdconf.c                | 733 ++++++++++++++++++++++++++++++
 lib/libbsdconf/bsdconf.h                | 281 ++++++++++++
 lib/libbsdconf/bsdconf_format.3         | 560 +++++++++++++++++++++++
 lib/libbsdconf/bsdconf_format.c         | 693 +++++++++++++++++++++++++++++
 lib/libbsdconf/bsdconf_format_generic.c |  32 ++
 lib/libbsdconf/bsdconf_format_loader.c  |  51 +++
 lib/libbsdconf/bsdconf_format_make.c    |  31 ++
 lib/libbsdconf/bsdconf_format_src.c     |  38 ++
 lib/libbsdconf/bsdconf_format_sysctl.c  |  36 ++
 lib/libbsdconf/bsdconf_formats.h        |  37 ++
 lib/libbsdconf/bsdconf_internal.h       |  71 +++
 lib/libbsdconf/bsdconf_put.3            | 290 ++++++++++++
 lib/libbsdconf/bsdconf_put.c            | 537 ++++++++++++++++++++++
 lib/libbsdconf/bsdconf_stmt.c           | 547 +++++++++++++++++++++++
 lib/libbsdconf/bsdconf_string.c         | 245 ++++++++++
 share/mk/bsd.libnames.mk                |   1 +
 share/mk/src.libnames.mk                |   1 +
 usr.sbin/Makefile                       |   1 +
 usr.sbin/sysconf/Makefile               |  32 ++
 usr.sbin/sysconf/Makefile.depend        |  17 +
 usr.sbin/sysconf/sysconf-generic.8      |  49 ++
 usr.sbin/sysconf/sysconf-loader.8       | 151 +++++++
 usr.sbin/sysconf/sysconf-make.8         | 231 ++++++++++
 usr.sbin/sysconf/sysconf-rc.8           |  56 +++
 usr.sbin/sysconf/sysconf-src.8          | 211 +++++++++
 usr.sbin/sysconf/sysconf-sysctl.8       | 146 ++++++
 usr.sbin/sysconf/sysconf-targets.8      | 174 ++++++++
 usr.sbin/sysconf/sysconf.8              | 704 +++++++++++++++++++++++++++++
 usr.sbin/sysconf/sysconf.c              | 761 ++++++++++++++++++++++++++++++++
 usr.sbin/sysconf/sysconf_edit_assign.c  | 221 ++++++++++
 usr.sbin/sysconf/sysconf_edit_make.c    | 274 ++++++++++++
 usr.sbin/sysconf/sysconf_edit_write.c   | 264 +++++++++++
 usr.sbin/sysconf/sysconf_print.c        | 320 ++++++++++++++
 usr.sbin/sysconf/sysconf_priv.h         | 203 +++++++++
 usr.sbin/sysconf/sysconf_query_desc.c   | 209 +++++++++
 usr.sbin/sysconf/sysconf_query_sysctl.c | 285 ++++++++++++
 usr.sbin/sysconf/sysconf_query_value.c  |  79 ++++
 usr.sbin/sysconf/sysconf_resolve.c      | 337 ++++++++++++++
 usr.sbin/sysconf/sysconf_scan.c         | 288 ++++++++++++
 usr.sbin/sysconf/tests/Makefile         |  11 +
 usr.sbin/sysconf/tests/sysconf_test.sh  | 431 ++++++++++++++++++
 47 files changed, 10120 insertions(+)

diff --git a/contrib/mandoc/lib.in b/contrib/mandoc/lib.in
index 04d86f9add1e..da343264c6d4 100644
--- a/contrib/mandoc/lib.in
+++ b/contrib/mandoc/lib.in
@@ -31,6 +31,7 @@ LINE("libarm",		"ARM Architecture Library (libarm, \\-larm)")
 LINE("libarm32",	"ARM32 Architecture Library (libarm32, \\-larm32)")
 LINE("libbe",		"Boot Environment Library (libbe, \\-lbe)")
 LINE("libbluetooth",	"Bluetooth Library (libbluetooth, \\-lbluetooth)")
+LINE("libbsdconf",	"Configuration File Library (libbsdconf, \\-lbsdconf)")
 LINE("libbsdxml",	"eXpat XML parser library (libbsdxml, \\-lbsdxml)")
 LINE("libbsm",		"Basic Security Module Library (libbsm, \\-lbsm)")
 LINE("libc",		"Standard C\\~Library (libc, \\-lc)")
diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist
index 2ab781fe4b6c..84bd2f09e7cc 100644
--- a/etc/mtree/BSD.tests.dist
+++ b/etc/mtree/BSD.tests.dist
@@ -1323,6 +1323,8 @@
         ..
         sa
         ..
+        sysconf
+        ..
         syslogd
         ..
         sysrc
diff --git a/lib/Makefile b/lib/Makefile
index 3391f276c7ef..78b48382e12e 100644
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -36,6 +36,7 @@ SUBDIR=	${SUBDIR_BOOTSTRAP} \
 	libarchive \
 	libbegemot \
 	libblocksruntime \
+	libbsdconf \
 	libbsddialog \
 	libbsdstat \
 	libbsm \
diff --git a/lib/libbsdconf/Makefile b/lib/libbsdconf/Makefile
new file mode 100644
index 000000000000..f9f74c82318f
--- /dev/null
+++ b/lib/libbsdconf/Makefile
@@ -0,0 +1,30 @@
+PACKAGE=	utilities
+LIB=		bsdconf
+SHLIB_MAJOR=	1
+INCS=		bsdconf.h
+MAN=		bsdconf.3 bsdconf_format.3 bsdconf_put.3
+MLINKS=		bsdconf.3 bsdconf_fparse.3			\
+		bsdconf.3 bsdconf_get_option.3			\
+		bsdconf.3 bsdconf_parse.3			\
+		bsdconf.3 bsdconf_spool.3			\
+		bsdconf.3 bsdconf_unquote.3			\
+		bsdconf_format.3 bsdconf_format_derive.3	\
+		bsdconf_format.3 bsdconf_format_files.3		\
+		bsdconf_format.3 bsdconf_format_files_free.3	\
+		bsdconf_format.3 bsdconf_format_find.3		\
+		bsdconf_format.3 bsdconf_format_guess.3		\
+		bsdconf_format.3 bsdconf_format_lookup.3	\
+		bsdconf_format.3 bsdconf_format_path.3		\
+		bsdconf_format.3 bsdconf_format_processing.3	\
+		bsdconf_format.3 bsdconf_format_put.3		\
+		bsdconf_format.3 bsdconf_format_register.3	\
+		bsdconf_put.3 bsdconf_set_option.3
+
+CFLAGS+=	-I${.CURDIR}
+
+SRCS=		bsdconf.c bsdconf_format.c bsdconf_format_generic.c	\
+		bsdconf_format_loader.c bsdconf_format_make.c		\
+		bsdconf_format_src.c bsdconf_format_sysctl.c		\
+		bsdconf_put.c bsdconf_stmt.c bsdconf_string.c
+
+.include <bsd.lib.mk>
diff --git a/lib/libbsdconf/Makefile.depend b/lib/libbsdconf/Makefile.depend
new file mode 100644
index 000000000000..6ef78fac5cbf
--- /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.3 b/lib/libbsdconf/bsdconf.3
new file mode 100644
index 000000000000..a1452e43adfd
--- /dev/null
+++ b/lib/libbsdconf/bsdconf.3
@@ -0,0 +1,432 @@
+.\" Copyright (c) 2013-2026 Devin Teske <dteske@FreeBSD.org>
+.\" Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.Dd August 2, 2026
+.Dt BSDCONF 3
+.Os
+.Sh NAME
+.Nm bsdconf ,
+.Nm bsdconf_parse ,
+.Nm bsdconf_fparse ,
+.Nm bsdconf_get_option ,
+.Nm bsdconf_spool ,
+.Nm bsdconf_unquote
+.Nd configuration file reading library
+.Sh LIBRARY
+.Lb libbsdconf
+.Sh SYNOPSIS
+.In bsdconf.h
+.Ft int
+.Fo bsdconf_parse
+.Fa "struct bsdconf_option options[]"
+.Fa "const char *path"
+.Fa "int \*[lp]*unknown\*[rp]\*[lp]struct bsdconf_option *option"
+.Fa "uint32_t line"
+.Fa "char *directive"
+.Fa "char *value\*[rp]"
+.Fa "uint16_t processing_options"
+.Fc
+.Ft int
+.Fo bsdconf_fparse
+.Fa "struct bsdconf_option options[]"
+.Fa "int fd"
+.Fa "int \*[lp]*unknown\*[rp]\*[lp]struct bsdconf_option *option"
+.Fa "uint32_t line"
+.Fa "char *directive"
+.Fa "char *value\*[rp]"
+.Fa "uint16_t processing_options"
+.Fc
+.Ft "struct bsdconf_option *"
+.Fo bsdconf_get_option
+.Fa "struct bsdconf_option options[]"
+.Fa "const char *directive"
+.Fc
+.Ft int
+.Fo bsdconf_spool
+.Fa "int fd"
+.Fc
+.Ft "char *"
+.Fo bsdconf_unquote
+.Fa "char *value"
+.Fc
+.Sh DESCRIPTION
+The
+.Nm
+library provides a light-weight,
+portable framework for reading and writing configuration
+files.
+It is the successor to the retired
+.Nm figpar
+library,
+extending the original read-only token parser into a unified,
+resilient reader/writer engine.
+.Pp
+Due to the fact that configuration files may have basic syntax differences,
+the library does not attempt to impose any structure on the data but instead
+provides raw data to a set of callback functions.
+These callback functions can in-turn initiate abort through their return
+value,
+allowing custom syntax validation during parsing.
+.Pp
+Syntax differences between well-known file formats are described by format
+descriptors:
+a format descriptor is a small read-only table of properties
+.Pq Vt struct bsdconf_format_def ; see Xr bsdconf_format 3
+naming a format's target keyword,
+backing files,
+and tokenizing/quoting rules,
+which parameterize a single shared engine.
+.Pp
+Despite the name, it bears no relation to a file descriptor;
+it is closer in spirit to a driver's method table:
+static data describing behavior,
+consulted rather than executed.
+Writing is documented in
+.Xr bsdconf_put 3 ;
+built-in formats and discovery in
+.Xr bsdconf_format 3 .
+.Pp
+Configuration directives,
+types,
+and callback functions are provided through data structures defined in
+.In bsdconf.h :
+.Bd -literal -offset indent
+struct bsdconf_option {
+    enum bsdconf_type		type;	/* value type */
+    const char			*directive; /* keyword */
+    union bsdconf_value		value;	/* value */
+    enum bsdconf_op		op;	/* assignment operator */
+    uint8_t			action;	/* bsdconf_put() action */
+    uint16_t			result;	/* set by bsdconf_put() */
+    uint32_t			line;	/* set by bsdconf_put() */
+    uint32_t			match_line;
+					/* put: 0=any, else line */
+
+    /* Pointer to function used when directive is found */
+    int (*parse)(struct bsdconf_option *option, uint32_t line,
+        char *directive, char *value);
+};
+
+enum bsdconf_type {
+    BSDCONF_TYPE_NONE      = 0x0000, /* directives with no value */
+    BSDCONF_TYPE_BOOL      = 0x0001, /* boolean */
+    BSDCONF_TYPE_INT       = 0x0002, /* signed 32-bit integer */
+    BSDCONF_TYPE_UINT      = 0x0004, /* unsigned 32-bit integer */
+    BSDCONF_TYPE_STR       = 0x0008, /* string pointer */
+    BSDCONF_TYPE_STRARRAY  = 0x0010, /* string array pointer */
+    BSDCONF_TYPE_DATA1     = 0x0020, /* void data type-1 (open) */
+    BSDCONF_TYPE_DATA2     = 0x0040, /* void data type-2 (open) */
+    BSDCONF_TYPE_DATA3     = 0x0080, /* void data type-3 (open) */
+    BSDCONF_TYPE_INT64     = 0x0100, /* signed 64-bit integer */
+    BSDCONF_TYPE_UINT64    = 0x0200, /* unsigned 64-bit integer */
+    BSDCONF_TYPE_RESERVED  = 0x0400, /* reserved */
+};
+
+union bsdconf_value {
+    void	*data;      /* Opaque pointer (DATA1..DATA3) */
+    char	*str;       /* Pointer to NUL-terminated string */
+    char	**strarray; /* Pointer to an array of strings */
+    int32_t	num;        /* Signed 32-bit integer value */
+    uint32_t	u_num;      /* Unsigned 32-bit integer value */
+    int64_t	num64;      /* Signed 64-bit integer value */
+    uint64_t	u_num64;    /* Unsigned 64-bit integer value */
+    bool	boolean;    /* Boolean value */
+};
+.Ed
+.Pp
+The
+.Fa processing_options
+argument to
+.Fn bsdconf_parse ,
+.Fn bsdconf_fparse ,
+and
+.Xr bsdconf_put 3
+is a mask of bit fields which indicate various processing options.
+The possible flags are:
+.Bl -tag -width BSDCONF_BREAK_ON_SEMICOLON
+.It Dv BSDCONF_BREAK_ON_EQUALS
+An equals sign
+.Pq Ql =
+is normally considered part of the directive.
+This flag enables terminating the directive at the equals sign.
+Also makes equals sign optional and transient.
+.It Dv BSDCONF_BREAK_ON_SEMICOLON
+A semicolon
+.Pq Ql \&;
+is normally considered part of the value.
+This flag enables terminating the value at the semicolon.
+Also allows multiple statements on a single line separated by semicolon.
+.It Dv BSDCONF_CASE_SENSITIVE
+Normally directives are matched case insensitively using
+.Xr fnmatch 3 .
+This flag enables directive matching to be case sensitive.
+.It Dv BSDCONF_REQUIRE_EQUALS
+If a directive is not followed by an equals,
+processing is aborted.
+.It Dv BSDCONF_STRICT_EQUALS
+Equals must be part of the directive
+.Pq no whitespace before or after
+to be considered a delimiter between directive and value.
+Required by file formats whose readers reject whitespace around the equals
+sign,
+such as the
+.Fx
+boot loader's processing of
+.Xr loader.conf 5 .
+.It Dv BSDCONF_OPERATOR_EQUALS
+Recognize
+.Xr make 1
+style assignment modifiers
+.Po
+.Ql += ,
+.Ql ?= ,
+.Ql := ,
+and
+.Ql !=
+.Pc
+and split them off the tail of the directive.
+The parsed operator is reported through the
+.Va op
+member of the matched option
+.Pq one of Dv BSDCONF_OP_ASSIGN , BSDCONF_OP_APPEND , BSDCONF_OP_COND , BSDCONF_OP_EXPAND , No or Dv BSDCONF_OP_SHELL .
+.El
+.Pp
+The
+.Fa options
+struct array pointer can be NULL and every directive will run the
+.Fn unknown
+function argument.
+.Pp
+The directive for each bsdconf_option item in the
+.Fn bsdconf_parse
+options argument is matched against each parsed directive using
+.Xr fnmatch 3
+until a match is found.
+If a match is found,
+the
+.Fn parse
+function for that bsdconf_option directive is run with the line number,
+directive,
+and value.
+Otherwise if no match,
+the
+.Fn unknown
+function is run
+.Pq with the same arguments .
+When
+.Dv BSDCONF_OPERATOR_EQUALS
+is set,
+.Fn unknown
+receives a non-NULL
+.Fa option
+whose
+.Va op
+member holds the statement's assignment operator
+.Pq there is no matched options-array slot to hang it on ;
+otherwise
+.Fa option
+may be
+.Dv NULL .
+.Pp
+If either
+.Fn parse
+or
+.Fn unknown
+return non-zero,
+.Fn bsdconf_parse
+aborts reading the file and returns the error value to its caller.
+.Pp
+A value normally ends at the first unescaped newline,
+but a statement may span multiple lines:
+a backslash immediately preceding the newline continues the value on the
+next line,
+in the manner of
+.Xr make 1
+.Pq essential to Pa make.conf and its siblings .
+The backslash-newline pairs are removed from the value delivered to the
+callbacks
+.Pq surrounding whitespace is preserved verbatim ,
+and reported line numbers are those of each statement's first line.
+.Xr bsdconf_put 3
+recognizes the same continuations when locating a value;
+rewriting a continued value replaces all of its lines with the single new
+value.
+.Pp
+.Fn bsdconf_fparse
+is identical to
+.Fn bsdconf_parse
+except that it operates on an already-open file descriptor
+.Fa fd ,
+which remains open on return
+.Pq the caller retains ownership .
+This allows the caller to constrain the process
+.Pq for example with Xr capsicum 4
+before parsing begins.
+The scanner requires a seekable descriptor;
+input that cannot seek
+.Pq a pipe or socket, standard input included
+is detected up front and transparently spooled through
+.Fn bsdconf_spool ,
+at the cost of one transient copy of the data.
+.Pp
+.Fn bsdconf_spool
+copies the remaining contents of
+.Fa fd
+to an unlinked temporary file
+.Pq created with Xr tmpfile 3
+and returns a seekable descriptor referencing it,
+which the caller must
+.Xr close 2
+.Pq the backing storage is reclaimed then .
+It is exported for callers that must adapt non-seekable input themselves
+before revoking their own ability to create files,
+as
+.Xr sysconf 8
+does before entering its
+.Xr capsicum 4
+sandbox.
+.Pp
+.Fn bsdconf_get_option
+traverses the options-array and returns the option that matches via
+.Xr strcmp 3 ,
+or
+.Dv NULL
+if none matches.
+.Pp
+.Fn bsdconf_unquote
+strips one layer of surrounding double-quotes from
+.Fa value
+in place and returns it.
+Parsed values retain their quotes so that data round-trips losslessly;
+this helper is for display and comparison purposes.
+.Sh RETURN VALUES
+.Fn bsdconf_parse
+and
+.Fn bsdconf_fparse
+return zero on success;
+otherwise -1
+.Pq or the non-zero result of a callback
+is returned and the global variable
+.Va errno
+is set to indicate the error.
+.Fn bsdconf_spool
+returns a new seekable file descriptor on success;
+otherwise -1 with
+.Va errno
+set to indicate the error.
+.Fn bsdconf_get_option
+returns a pointer to the matching option,
+or
+.Dv NULL
+when none matches.
+.Sh EXAMPLES
+Read two known directives from a
+.Ql name=value
+file,
+routing every statement through a callback:
+.Bd -literal -offset indent
+#include <err.h>
+#include <stdio.h>
+#include <bsdconf.h>
+
+static int
+show(struct bsdconf_option *option, uint32_t line,
+    char *directive, char *value)
+{
+	printf("%u: %s is %s\en", line, directive,
+	    bsdconf_unquote(value));
+	return (0);
+}
+
+static struct bsdconf_option options[] = {
+	{ .directive = "hostname", .parse = show },
+	{ .directive = "timeout",  .parse = show },
+	{ .directive = NULL }
+};
+
+int
+main(void)
+{
+	if (bsdconf_parse(options, "/usr/local/etc/myapp.conf",
+	    NULL, BSDCONF_BREAK_ON_EQUALS) != 0)
+		err(1, "myapp.conf");
+	return (0);
+}
+.Ed
+.Pp
+Callbacks own the semantics,
+so a directive that legitimately repeats is accumulated rather than
+overwritten;
+no format descriptor is involved.
+The file here is Apache-style
+.Pq space-separated, no equals sign ,
+so
+.Dv BSDCONF_BREAK_ON_EQUALS
+is simply omitted:
+.Bd -literal -offset indent
+static char *servers[16];
+static size_t nservers;
+
+static int
+addserver(struct bsdconf_option *option, uint32_t line,
+    char *directive, char *value)
+{
+	if (nservers >= 16 ||
+	    (servers[nservers] = strdup(value)) == NULL)
+		return (-1); /* abort the parse */
+	nservers++;
+	return (0);
+}
+
+static struct bsdconf_option cumulative[] = {
+	{ .directive = "server", .parse = addserver },
+	{ .directive = NULL }
+};
+
+	...
+	if (bsdconf_parse(cumulative, path, NULL, 0) != 0)
+		err(1, "%s", path);
+.Ed
+.Sh SEE ALSO
+.Xr bsdconf_format 3 ,
+.Xr bsdconf_put 3 ,
+.Xr loader.conf 5 ,
+.Xr sysctl.conf 5 ,
+.Xr sysconf 8
+.Sh HISTORY
+The
+.Nm
+library first appeared in
+.Fx 16.0 .
+It supersedes the
+.Nm figpar
+library which first appeared in
+.Fx 10.2
+and was retired to the ports tree.
+.Sh AUTHORS
+.An Devin Teske Aq Mt dteske@FreeBSD.org
+.An Faraz Vahedi Aq Mt kfv@FreeBSD.org
+.Sh BUGS
+This is the first implementation of the library,
+and the interface may be subject to refinement.
+.Pp
+Write-path limitations for cumulative directives are discussed in the
+.Sx LIMITATIONS
+section of
+.Xr bsdconf_put 3 .
+.Sh SECURITY CONSIDERATIONS
+Parsing allocates buffers sized by the longest directive and value
+encountered rather than by untrusted length fields,
+and
+.Fn bsdconf_fparse
+accepts an already-open descriptor precisely so that a caller may
+sandbox itself
+.Pq for example with Xr capsicum 4
+before touching untrusted input,
+as
+.Xr sysconf 8
+does for its read-only operations.
+Write-path hardening is documented in
+.Xr bsdconf_put 3 .
diff --git a/lib/libbsdconf/bsdconf.c b/lib/libbsdconf/bsdconf.c
new file mode 100644
index 000000000000..341fad9b841c
--- /dev/null
+++ b/lib/libbsdconf/bsdconf.c
@@ -0,0 +1,733 @@
+/*
+ * Copyright (c) 2002-2026 Devin Teske <dteske@FreeBSD.org>
+ * Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <fnmatch.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "bsdconf.h"
+#include "bsdconf_internal.h"
+
+/*
+ * 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, NULL 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;
+
+	if (options == NULL || directive == NULL)
+		return (NULL);
+
+	for (n = 0; options[n].directive != NULL; n++)
+		if (strcmp(options[n].directive, directive) == 0)
+			return (&options[n]);
+
+	return (NULL);
+}
+
+/*
+ * Strip one layer of surrounding double-quotes from `value' in place (the
+ * parser preserves quotes so that values round-trip losslessly; see
+ * bsdconf_fparse() below). Returns `value' for convenience. If the value is
+ * not a quoted string, it is returned unmodified.
+ */
+char *
+bsdconf_unquote(char *value)
+{
+	size_t len;
+
+	if (value == NULL || (len = strlen(value)) < 2)
+		return (value);
+	if (value[0] != '"' || value[len - 1] != '"')
+		return (value);
+
+	memmove(value, value + 1, len - 2);
+	value[len - 2] = '\0';
+
+	return (value);
+}
+
+/*
+ * Copy the remaining contents of the open file descriptor `fd' to an
+ * unlinked temporary file and return a seekable descriptor referencing it
+ * (which the caller must close(2); the backing storage is reclaimed then).
+ * This adapts input that cannot seek -- a pipe or socket, standard input
+ * included -- for the scanner in bsdconf_fparse() below, which seeks
+ * liberally. Returns the new descriptor on success; otherwise returns -1
+ * and errno should be consulted.
+ */
+int
+bsdconf_spool(int fd)
+{
+	FILE *tmp;
+	int error;
+	int newfd;
+	int tfd;
+	ssize_t r;
+	char buf[8192];
+
+	if ((tmp = tmpfile()) == NULL)
+		return (-1);
+	tfd = fileno(tmp);
+
+	for (;;) {
+		r = read(fd, buf, sizeof(buf));
+		if (r < 0) {
+			if (errno == EINTR)
+				continue;
+			goto fail;
+		}
+		if (r == 0)
+			break;
+		if (bsdconf_writeall(tfd, buf, (size_t)r) != 0)
+			goto fail;
+	}
+	if (lseek(tfd, 0, SEEK_SET) == -1)
+		goto fail;
+
+	/* Detach the descriptor from the stream before closing it */
+	if ((newfd = dup(tfd)) == -1)
+		goto fail;
+	fclose(tmp);
+	return (newfd);
+
+fail:
+	error = errno; /* preserve errno across fclose(3) */
+	fclose(tmp);
+	errno = error;
+	return (-1);
+}
+
+/*
+ * Read one byte into `*p', restarting on EINTR. Returns 1 on success, 0 on
+ * EOF, or -1 on error (with errno set). Callers must treat a negative return
+ * as failure: a loop conditioned only on `r != 0' spins forever on error
+ * because read(2) returns -1, and a length counter in such a loop can grow
+ * without bound (see the directive scan in bsdconf_fparse() below).
+ */
+static ssize_t
+bsdconf_read1(int fd, char *p)
+{
+	ssize_t r;
+
+	do {
+		r = read(fd, p, 1);
+	} while (r < 0 && errno == EINTR);
+	return (r);
+}
+
+/*
+ * Read exactly `n' bytes into `buf', restarting on EINTR. Returns 0 on
+ * success, or -1 on error / premature EOF (with errno set; EIO for a short
+ * read after the caller measured a length on a seekable descriptor).
+ */
+static int
+bsdconf_readn(int fd, void *buf, size_t n)
+{
+	char *p = buf;
+	size_t off = 0;
+	ssize_t r;
+
+	while (off < n) {
+		r = read(fd, p + off, n - off);
+		if (r < 0) {
+			if (errno == EINTR)
+				continue;
+			return (-1);
+		}
+		if (r == 0) {
+			errno = EIO;
+			return (-1);
+		}
+		off += (size_t)r;
+	}
+	return (0);
+}
+
+/*
+ * Advance past horizontal whitespace (spaces and tabs, not newline).
+ * Updates `*r' and the byte in `*p'. Returns 0 on success, or -1 on
+ * read error (errno set).
+ */
+static int
+bsdconf_skip_hspace(int fd, char *p, ssize_t *r)
+{
+
+	while (*r > 0 && isspace((unsigned char)*p) && *p != '\n') {
+		*r = bsdconf_read1(fd, p);
+		if (*r < 0)
+			return (-1);
+	}
+	return (0);
+}
+
+/*
+ * Truncate trailing whitespace from a NUL-terminated string whose end
+ * (the NUL) is at `end'. Returns a pointer to the last remaining
+ * character, or to `value' when the string is empty.
+ */
+static char *
+bsdconf_rtrim_ws(char *value, char *end)
+{
+	char *t = end;
+
+	while (t > value && isspace((unsigned char)*--t))
+		*t = '\0';
+	return (t);
+}
+
+/*
+ * Drop a trailing inline `#' or unescaped `;' that rode along with the
+ * value (historic figpar behavior), then trim again. `ecomment' is set
+ * when the end-key scan stopped on an unquoted `#'.
+ */
+static char *
+bsdconf_trim_value_key(char *value, char *t, bool ecomment, bool bsemicolon)
+{
+	uint32_t x;
+
+	if (ecomment && t > value && *t == '#') {
+		*t = '\0';
+		return (bsdconf_rtrim_ws(value, t));
+	}
+	if (bsemicolon && t > value && *t == ';') {
+		for (x = 0; t - x > value && *(t - x - 1) == '\\'; x++)
+			;
+		if ((x & 1) == 0) {
+			*t = '\0';
+			return (bsdconf_rtrim_ws(value, t));
+		}
+	}
+	return (t);
+}
+
+/*
+ * Invoke the unknown-directive call-back with a stack-local option that
+ * carries the statement's assignment operator (there is no matched
+ * options[] slot to hang it on). Returns the call-back's result.
+ */
+static int
+bsdconf_call_unknown(int (*unknown)(struct bsdconf_option *option,
+    uint32_t line, char *directive, char *value), enum bsdconf_op op,
+    uint32_t dline, char *directive, char *value)
+{
+	struct bsdconf_option unk;
+
+	memset(&unk, 0, sizeof(unk));
+	unk.op = op;
+	return (unknown(&unk, dline, directive, value));
+}
+
+/*
+ * Scan from the current byte in `*p' to the end of the value. Handles
+ * quotes, escaped end-keys, inline comments, and semicolon terminators.
+ * On return, `*p' holds the terminating key (or is at EOF), and `*r',
+ * `*line', `*comment', and `*ecomment' are updated. Returns 0 on
+ * success, or -1 on seek/read error (errno set).
+ */
+static int
+bsdconf_scan_value_end(int fd, char *p, ssize_t *r, uint32_t *line,
+    uint8_t *comment, uint8_t *ecomment, bool bsemicolon)
+{
+	uint8_t end = 0;
+	uint8_t quote = 0;
+	uint32_t n;
+	off_t charpos;
+
+	*ecomment = 0;
+	while (*r > 0 && end == 0) {
+		/* Advance to the next character if we know we can */
+		if (*p != '\"' && *p != '#' && *p != '\n' &&
+		    (!bsemicolon || *p != ';')) {
+			*r = bsdconf_read1(fd, p);
+			if (*r < 0)
+				return (-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)
+			return (-1);
+		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)
+			return (-1);
+		*r = bsdconf_read1(fd, p);
+		if (*r < 0)
+			return (-1);
+
+		/*
+		 * Count how many backslashes there are (an odd number means
+		 * the key is escaped, even means otherwise).
+		 */
+		for (n = 1; *r > 0 && *p == '\\'; n++) {
+			/* Move back another offset to read */
+			if (lseek(fd, -2, SEEK_CUR) == -1)
+				return (-1);
+			*r = bsdconf_read1(fd, p);
+			if (*r < 0)
+				return (-1);
+		}
+
+		/* Move offset back to the key and read it */
+		if (lseek(fd, charpos, SEEK_SET) == -1)
+			return (-1);
+		*r = bsdconf_read1(fd, p);
+		if (*r < 0)
+			return (-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 = bsdconf_read1(fd, p);
*** 9545 LINES SKIPPED ***