From nobody Wed Sep 16 21:31:28 2026 X-Original-To: dev-commits-src-main@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4hlXBm5h7Pz6sMMh for ; Wed, 16 Sep 2026 21:31:28 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "YR2" (not verified)) by mx1.freebsd.org (Postfix) with ESMTPS id 4hlXBm5Jfxz4ss4 for ; Wed, 16 Sep 2026 21:31:28 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1789594288; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=tIPJ9x3DcRvBcD+hs6vloWRug2irJRa6zgikQofc7Eg=; b=UZXMH1RJfUKXUZDAfOwD3N5uY5GEjaSXMkd2d9twlnRB0m7bYh9N4+F7Z3yt21otT4ciAW /sEzrUvaXWyzJoYYqRDbuFUP4ZpG1RoAnsRYyQvb5muMiIt6Yh5dNnbYFOnoH5s7gu/uTL +tDMmruXNlfU7d4aWuDuA8tLlFEoY4I/kn/lWzAMbgZGPjF5puvcmfwc/fMTH/OD/d8An/ ir+oNbYcS8mU5pzLBEyRSqbdoUEqlkaNdzJC02eSVIcMRtgbPiogRua2chlOxAJU/GgtVF 5ekWnMzwP23v9M64f8oCd0hbNUWoJtfc83BR7hWKHIuHtZHB5MFbYsblLixRpw== ARC-Seal: i=1; a=rsa-sha256; d=freebsd.org; s=dkim; cv=none; t=1789594288; b=N8NG8Zde3mA/yf6a6/VfBxV42KOKK1qrgL+NcK+wPy/U8PY1Hxaz6OgIqs1+q+ADN7YjrO JM3V4d5amd9jamT0j6OuSv8ZZ/4fJ0NNcq/feWN+yoIId4DrsyPHgD8ZogdwRF5fmFoxId UeQ1XF7tJLo+lPtMWukksuUn8yneyHzHMy9OxGYy2P1ktrHA3vwNjnY5PjAQ9jz8Lbblq6 +ReeHns8GH4wDABUiaNG2i7gtRhFGYIVovL78F/Q8SGUaN2jSzdKytqtljib32HV4D/Wwg K4532y60/O2H9vl7HKSBcOid8IeSjlOTEUGeiEeANCOBegvDH6ZSCZmNL7jlMA== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1789594288; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=tIPJ9x3DcRvBcD+hs6vloWRug2irJRa6zgikQofc7Eg=; b=cj/Yb3gtzR22EMnjuiFlYJRganDy7GrK68ermpi3eBys2zlXoRi44/rsbG1zMwTLF/rATE xCKRRdgtaT81eMKQ2kECkdO7gRNxjEYcZqmPgt6nUbE15E9+ERkYwgmn7JD5HdKxwypy48 q/29jjG/frVp5efPMuoS+fdkJBSyWiN0OpzJL/A7F+LocKf0YjTbG/2my6Z/uR/WEhUlWk 2Y7A2GxQhE6amHe3/uFkbLLKrwA2qWlDrypCsxSmV9ZigtLc2gi6bIPvu0McIRoJmPzq4H 8p6/DFagUoqkPqi0mN3uaB59OS1RL8vit/2oamXAwAacebQu6kFlXRJgg1H8Kg== ARC-Authentication-Results: i=1; mx1.freebsd.org; none Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) by mxrelay.nyi.freebsd.org (Postfix) with ESMTP id 4hlXBm4Mqrz14Fm for ; Wed, 16 Sep 2026 21:31:28 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from git (uid 1279) (envelope-from git@FreeBSD.org) id 449ca by gitrepo.freebsd.org (DragonFly Mail Agent v0.13+ on gitrepo.freebsd.org); Wed, 16 Sep 2026 21:31:28 +0000 To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org From: Devin Teske Subject: git: 3fe5961a0b70 - main - Add sysconf(8) and libbsdconf(3) List-Id: Commit messages for the main branch of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-main List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-main@freebsd.org Sender: owner-dev-commits-src-main@FreeBSD.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: dteske X-Git-Repository: src X-Git-Refname: refs/heads/main X-Git-Reftype: branch X-Git-Commit: 3fe5961a0b708da599d42cbb6b5e4f030c28ea45 Auto-Submitted: auto-generated Date: Wed, 16 Sep 2026 21:31:28 +0000 Message-Id: <6aab0ab0.449ca.14bad696@gitrepo.freebsd.org> The branch main has been updated by dteske: URL: https://cgit.FreeBSD.org/src/commit/?id=3fe5961a0b708da599d42cbb6b5e4f030c28ea45 commit 3fe5961a0b708da599d42cbb6b5e4f030c28ea45 Author: Devin Teske AuthorDate: 2026-09-16 04:29:32 +0000 Commit: Devin Teske 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 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 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 + +.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 +.\" Copyright (c) 2021-2026 Faraz Vahedi +.\" +.\" 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 +#include +#include + +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 + * Copyright (c) 2021-2026 Faraz Vahedi + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#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 ***