git: 74cab6e2371f - main - libbsdconf: parse from a bounded in-memory buffer

From: Devin Teske <dteske_at_FreeBSD.org>
Date: Tue, 22 Sep 2026 22:34:05 UTC
The branch main has been updated by dteske:

URL: https://cgit.FreeBSD.org/src/commit/?id=74cab6e2371fc35b58d5d3d018c7c3ad52809153

commit 74cab6e2371fc35b58d5d3d018c7c3ad52809153
Author:     Devin Teske <dteske@FreeBSD.org>
AuthorDate: 2026-09-22 22:31:20 +0000
Commit:     Devin Teske <dteske@FreeBSD.org>
CommitDate: 2026-09-22 22:31:20 +0000

    libbsdconf: parse from a bounded in-memory buffer
    
    Copy the descriptor into a buffer of at most 64 MiB (raise it with
    BSDCONF_MAX_BYTES) and tokenize with bsdconf_scan(), the walker
    bsdconf_put() already uses.  Input above the cap fails with EFBIG.
    Bump libbsdconf to 1.2.0 and sysconf(8) to 2.0.
    
    Suggested by:   fuz
    Reviewed by:    fuz
    Differential Revision:  https://reviews.freebsd.org/D59751
---
 lib/libbsdconf/bsdconf.3               |  62 +++--
 lib/libbsdconf/bsdconf.c               | 459 +++------------------------------
 lib/libbsdconf/bsdconf.h               |  12 +-
 lib/libbsdconf/bsdconf_internal.h      |   2 +
 lib/libbsdconf/bsdconf_put.3           |   6 +-
 lib/libbsdconf/bsdconf_put.c           |   2 +-
 lib/libbsdconf/bsdconf_stmt.c          | 133 ++++++++++
 usr.sbin/sysconf/sysconf.8             |  11 +-
 usr.sbin/sysconf/sysconf.c             |   2 +
 usr.sbin/sysconf/sysconf_priv.h        |   2 +-
 usr.sbin/sysconf/sysconf_scan.c        |  16 --
 usr.sbin/sysconf/tests/sysconf_test.sh |  23 ++
 12 files changed, 267 insertions(+), 463 deletions(-)

diff --git a/lib/libbsdconf/bsdconf.3 b/lib/libbsdconf/bsdconf.3
index a1452e43adfd..2ff9381ad1ac 100644
--- a/lib/libbsdconf/bsdconf.3
+++ b/lib/libbsdconf/bsdconf.3
@@ -3,7 +3,7 @@
 .\"
 .\" SPDX-License-Identifier: BSD-2-Clause
 .\"
-.Dd August 2, 2026
+.Dd September 16, 2026
 .Dt BSDCONF 3
 .Os
 .Sh NAME
@@ -261,15 +261,23 @@ except that it operates on an already-open file descriptor
 .Fa fd ,
 which remains open on return
 .Pq the caller retains ownership .
+The descriptor is read into a bounded in-memory buffer and then scanned
+as an array of characters
+.Pq the same tokenizer used by Xr bsdconf_put 3 .
+The buffer is capped at
+.Dv BSDCONF_MAX_BYTES_DEFAULT
+bytes
+.Pq 67108864; 64 MiB ;
+.Ev BSDCONF_MAX_BYTES
+overrides that ceiling
+.Pq see Sx ENVIRONMENT .
+Input larger than the cap fails with
+.Er EFBIG .
+The descriptor need not be seekable;
+a pipe or socket is read to EOF subject to the same cap.
 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
@@ -280,13 +288,8 @@ 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.
+It is exported for callers that need a seekable snapshot of a pipe or
+socket.
 .Pp
 .Fn bsdconf_get_option
 traverses the options-array and returns the option that matches via
@@ -311,6 +314,11 @@ otherwise -1
 is returned and the global variable
 .Va errno
 is set to indicate the error.
+Input that exceeds the
+.Ev BSDCONF_MAX_BYTES
+cap fails with
+.Er EFBIG .
+.Pp
 .Fn bsdconf_spool
 returns a new seekable file descriptor on success;
 otherwise -1 with
@@ -321,6 +329,20 @@ returns a pointer to the matching option,
 or
 .Dv NULL
 when none matches.
+.Sh ENVIRONMENT
+.Bl -tag -width "BSDCONF_MAX_BYTES"
+.It Ev BSDCONF_MAX_BYTES
+Maximum number of bytes
+.Fn bsdconf_fparse
+and
+.Fn bsdconf_put
+will read from a configuration file or stream.
+The default is
+.Dv BSDCONF_MAX_BYTES_DEFAULT
+.Pq 67108864; 64 MiB .
+A larger value is an opt-in pain threshold for unusual files.
+Unset, empty, zero, or unparseable values restore the default.
+.El
 .Sh EXAMPLES
 Read two known directives from a
 .Ql name=value
@@ -417,16 +439,20 @@ Write-path limitations for cumulative directives are discussed in the
 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
+reads the descriptor into a bounded in-memory buffer rather than
+trusting untrusted length fields,
+and 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.
+The
+.Ev BSDCONF_MAX_BYTES
+cap
+.Pq see Sx ENVIRONMENT
+is the bound on that buffer.
 Write-path hardening is documented in
 .Xr bsdconf_put 3 .
diff --git a/lib/libbsdconf/bsdconf.c b/lib/libbsdconf/bsdconf.c
index 17f272f3a3e3..e739bd85fb05 100644
--- a/lib/libbsdconf/bsdconf.c
+++ b/lib/libbsdconf/bsdconf.c
@@ -5,7 +5,6 @@
  * SPDX-License-Identifier: BSD-2-Clause
  */
 
-#include <ctype.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <fnmatch.h>
@@ -66,10 +65,9 @@ bsdconf_unquote(char *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.
+ * Exported for callers that need a seekable snapshot of a pipe or socket.
+ * Returns the new descriptor on success; otherwise returns -1 and errno
+ * should be consulted.
  */
 int
 bsdconf_spool(int fd)
@@ -113,109 +111,6 @@ fail:
 	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
@@ -233,126 +128,6 @@ bsdconf_call_unknown(int (*unknown)(struct bsdconf_option *option,
 	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);
-		if (*r < 0)
-			return (-1);
-	}
-	return (0);
-}
-
 /*
  * Parse the configuration data on the open file descriptor `fd' and execute
  * the `parse' call-back functions for any directives defined by the array of
@@ -361,12 +136,13 @@ bsdconf_scan_value_end(int fd, char *p, ssize_t *r, uint32_t *line,
  * 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).
+ * The descriptor is read into a bounded in-memory buffer (see
+ * bsdconf_slurp()) and then scanned as an array of characters with
+ * bsdconf_scan(), the same tokenizer used by bsdconf_put(). The descriptor
+ * need not be seekable; a pipe or socket is read to EOF subject to the
+ * BSDCONF_MAX_BYTES cap. 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.
@@ -379,30 +155,26 @@ bsdconf_fparse(struct bsdconf_option options[], int fd,
 	bool bequals;
 	bool bsemicolon;
 	bool case_sensitive;
+	bool found;
 	bool operator_equals;
 	bool require_equals;
 	bool strict_equals;
-	uint8_t comment = 0;
-	uint8_t ecomment;
-	uint8_t found;
-	uint8_t have_equals = 0;
-	char p[2];
+	char *buf = NULL;
 	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;
+	int sverrno;
+	size_t buflen = 0;
+	size_t dsize = 0;
+	size_t i = 0;
+	size_t n;
+	size_t vsize = 0;
+	struct bsdconf_stmt st;
 	uint32_t line = 1;
-	uint32_t n;
-	uint32_t vsize = 0;
-	uint32_t x;
-	off_t charpos;
-	off_t curpos;
+	unsigned int x;
 
 	/* Sanity check: if no options and no unknown function, return */
 	if (options == NULL && unknown == NULL) {
@@ -410,14 +182,8 @@ bsdconf_fparse(struct bsdconf_option options[], int fd,
 		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;
-	}
+	if ((buf = bsdconf_slurp(fd, &buflen)) == NULL)
+		return (-1);
 
 	/* Processing options */
 	bequals = processing_options & BSDCONF_BREAK_ON_EQUALS;
@@ -427,185 +193,39 @@ bsdconf_fparse(struct bsdconf_option options[], int fd,
 	require_equals = processing_options & BSDCONF_REQUIRE_EQUALS;
 	strict_equals = processing_options & BSDCONF_STRICT_EQUALS;
 
-	/* Read the file until EOF */
-	while (r > 0) {
-		r = bsdconf_read1(fd, p);
-		if (r < 0)
-			goto fail;
-
-		/* Skip to the beginning of a directive */
-		while (r > 0 && (isspace((unsigned char)*p) || *p == '#' ||
-		    comment || (bsemicolon && *p == ';'))) {
-			if (*p == '#')
-				comment = 1;
-			else if (*p == '\n') {
-				comment = 0;
-				line++;
-			}
-			r = bsdconf_read1(fd, p);
-			if (r < 0)
-				goto fail;
-		}
-		/* Test for EOF; if EOF then no directive was found */
-		if (r == 0)
-			goto cleanup;
-
-		/* Record the line number the directive appears on */
-		dline = line;
-
-		/* Get the current offset */
-		if ((curpos = lseek(fd, 0, SEEK_CUR)) == -1)
-			goto fail;
-		curpos--;
-
-		/* Find the length of the directive */
-		for (n = 0; r > 0; n++) {
-			if (isspace((unsigned char)*p))
-				break;
-			if (bequals && *p == '=') {
-				have_equals = 1;
-				break;
-			}
-			if (bsemicolon && *p == ';')
-				break;
-			r = bsdconf_read1(fd, p);
-			if (r < 0)
-				goto fail;
-		}
-
-		/* Test for EOF, if EOF then no directive was found */
-		if (n == 0 && r == 0)
-			goto cleanup;
-
-		/* Go back to the beginning of the directive */
-		if (lseek(fd, curpos, SEEK_SET) == -1)
-			goto fail;
-
-		/*
-		 * Allocate and read the directive into memory. The buffer
-		 * must be grown on the first pass (directive == NULL) even
-		 * when the name is empty (a line beginning with `='), lest
-		 * the string terminator below store through a NULL pointer.
-		 */
+	while (bsdconf_scan(buf, buflen, &i, &line, bequals, bsemicolon,
+	    strict_equals, operator_equals, &st)) {
+		n = st.dir_end - st.dir_start;
 		if (directive == NULL || n > dsize) {
 			if ((t = realloc(directive, n + 1)) == NULL)
 				goto fail;
 			directive = t;
 			dsize = n;
 		}
-		if (bsdconf_readn(fd, directive, n) != 0)
-			goto fail;
-
-		/* Advance beyond the equals sign if appropriate/desired */
-		if (bequals && *p == '=') {
-			if (lseek(fd, 1, SEEK_CUR) != -1) {
-				r = bsdconf_read1(fd, p);
-				if (r < 0)
-					goto fail;
-			}
-			if (strict_equals && isspace((unsigned char)*p))
-				*p = '\n';
-		}
-
-		/* Terminate the string */
+		memcpy(directive, buf + st.dir_start, n);
 		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 */
+		op = st.op;
 		if (!case_sensitive)
 			bsdconf_strtolower(directive);
 
-		/* Move to what may be the start of the value */
-		if (!(bsemicolon && *p == ';') &&
-		    !(strict_equals && *p == '=')) {
-			if (bsdconf_skip_hspace(fd, p, &r) != 0)
-				goto fail;
-		}
-
-		/* An equals sign may have stopped us, should we eat it? */
-		if (r > 0 && bequals && *p == '=' && !strict_equals) {
-			have_equals = 1;
-			r = bsdconf_read1(fd, p);
-			if (r < 0)
-				goto fail;
-			if (bsdconf_skip_hspace(fd, p, &r) != 0)
-				goto fail;
-		}
-
-		/* If no value, allocate a dummy value and jump to action */
-		if (r == 0 || *p == '\n' || *p == '#' ||
-		    (bsemicolon && *p == ';')) {
-			/* Count the consumed terminator if a newline */
-			if (r > 0 && *p == '\n')
-				line++;
-			/* Flag a trailing comment so it is skipped */
-			if (r > 0 && *p == '#')
-				comment = 1;
-			/* Initialize the value if not already done */
+		if (!st.have_value) {
 			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 */
-		if (bsdconf_scan_value_end(fd, p, &r, &line, &comment,
-		    &ecomment, bsemicolon) != 0)
-			goto fail;
-
-		/* Get the current offset */
-		if ((charpos = lseek(fd, 0, SEEK_CUR)) == -1)
-			goto fail;
-
-		/* Get the length of the value */
-		n = (uint32_t)(charpos - curpos);
-		if (r > 0) /* more to read, but don't read ending key */
-			n--;
-
-		/* Move offset back to the beginning of the value */
-		if (lseek(fd, curpos, SEEK_SET) == -1)
-			goto fail;
-
-		/* Allocate and read the value into memory */
-		if (n > vsize) {
+		n = st.val_end - st.val_start;
+		if (value == NULL || n > vsize) {
 			if ((t = realloc(value, n + 1)) == NULL)
 				goto fail;
 			value = t;
 			vsize = n;
 		}
-		if (bsdconf_readn(fd, value, n) != 0)
-			goto fail;
-
-		/* Terminate the string */
+		memcpy(value, buf + st.val_start, n);
 		value[n] = '\0';
 
-		/* Cut trailing whitespace and a trailing `#' / `;' key */
-		t = bsdconf_rtrim_ws(value, value + n);
-		t = bsdconf_trim_value_key(value, t, ecomment != 0,
-		    bsemicolon);
-
 		/* Escape the escaped quotes (see bsdconf_string.c) */
 		x = bsdconf_strcount(value, "\\\"");
 		if (x != 0 && (n + x) > vsize) {
@@ -626,12 +246,12 @@ bsdconf_fparse(struct bsdconf_option options[], int fd,
 
 call_function:
 		/* Abort if we're seeking only assignments */
-		if (require_equals && !have_equals) {
+		if (require_equals && !st.have_equals) {
 			errno = EINVAL;
 			goto fail;
 		}
 
-		found = have_equals = 0; /* reset */
+		found = 0;
 
 		/*
 		 * Report the statement's assignment operator through a
@@ -639,7 +259,7 @@ call_function:
 		 * (there is no matched options[] slot to hang it on).
 		 */
 		if (options == NULL && unknown != NULL) {
-			error = bsdconf_call_unknown(unknown, op, dline,
+			error = bsdconf_call_unknown(unknown, op, st.line,
 			    directive, value);
 			if (error != 0) {
 				rv = error;
@@ -658,7 +278,7 @@ call_function:
 				options[n].op = op;
 				if (options[n].parse != NULL) {
 					error = options[n].parse(&options[n],
-					    dline, directive, value);
+					    st.line, directive, value);
 					if (error != 0) {
 						rv = error;
 						goto cleanup;
@@ -675,7 +295,7 @@ call_function:
 			 * No match was found for the value we read from the
 			 * file; call function designated for unknown values.
 			 */
-			error = bsdconf_call_unknown(unknown, op, dline,
+			error = bsdconf_call_unknown(unknown, op, st.line,
 			    directive, value);
 			if (error != 0) {
 				rv = error;
@@ -690,12 +310,11 @@ fail:
 	rv = -1;
 
 cleanup:
-	x = errno; /* preserve errno across free(3) and close(2) */
-	if (spoolfd != -1)
-		close(spoolfd);
+	sverrno = errno; /* preserve errno across free(3) */
+	free(buf);
 	free(directive);
 	free(value);
-	errno = x;
+	errno = sverrno;
 
 	return (rv);
 }
diff --git a/lib/libbsdconf/bsdconf.h b/lib/libbsdconf/bsdconf.h
index 00a5d6e07905..d55d4ca84de4 100644
--- a/lib/libbsdconf/bsdconf.h
+++ b/lib/libbsdconf/bsdconf.h
@@ -34,10 +34,16 @@
 /*
  * Library version info
  */
-#define BSDCONF_VERSION		"1.1.1 2026-09-16"
+#define BSDCONF_VERSION		"1.2.0 2026-09-21"
 #define BSDCONF_VERSION_MAJOR	1
-#define BSDCONF_VERSION_MINOR	1
-#define BSDCONF_VERSION_PATCH	1
+#define BSDCONF_VERSION_MINOR	2
+#define BSDCONF_VERSION_PATCH	0
+
+/*
+ * Ceiling on bytes read from a configuration file or stream. Override with
+ * the BSDCONF_MAX_BYTES environment variable (an unsigned decimal count).
+ */
+#define BSDCONF_MAX_BYTES_DEFAULT	(64U * 1024U * 1024U)
 
 /*
  * Union for storing various types of data in a single common container.
diff --git a/lib/libbsdconf/bsdconf_internal.h b/lib/libbsdconf/bsdconf_internal.h
index 511850761837..c1dac177e80b 100644
--- a/lib/libbsdconf/bsdconf_internal.h
+++ b/lib/libbsdconf/bsdconf_internal.h
@@ -54,6 +54,8 @@ struct bsdconf_stmt {
 const char	*bsdconf_op_token(enum bsdconf_op _op);
 int		 bsdconf_writeall(int _fd, const void *_data, size_t _len);
 char		*bsdconf_readfile(int _fd, size_t _size, size_t *_lenp);
+size_t		 bsdconf_max_bytes(void);
+char		*bsdconf_slurp(int _fd, size_t *_lenp);
 int		 bsdconf_emit(int _fd, const void *_data, size_t _len,
 		    int *_last);
 int		 bsdconf_ensure_tmp(int *_tmpfdp, char *_tpath, size_t _tpathsz,
diff --git a/lib/libbsdconf/bsdconf_put.3 b/lib/libbsdconf/bsdconf_put.3
index 665c2a8f3a96..caa48fc39c94 100644
--- a/lib/libbsdconf/bsdconf_put.3
+++ b/lib/libbsdconf/bsdconf_put.3
@@ -3,7 +3,7 @@
 .\"
 .\" SPDX-License-Identifier: BSD-2-Clause
 .\"
-.Dd August 2, 2026
+.Dd September 16, 2026
 .Dt BSDCONF_PUT 3
 .Os
 .Sh NAME
@@ -97,6 +97,10 @@ no temporary is created,
 .Va mtime
 is not bumped,
 and hard links are not severed.
+The original is read in full, subject to the
+.Ev BSDCONF_MAX_BYTES
+cap documented in
+.Xr bsdconf 3 .
 Otherwise the file is replaced atomically:
 output is streamed to a temporary file created with
 .Xr mkstemp 3
diff --git a/lib/libbsdconf/bsdconf_put.c b/lib/libbsdconf/bsdconf_put.c
index a26ab33d0c70..2f266e1498e9 100644
--- a/lib/libbsdconf/bsdconf_put.c
+++ b/lib/libbsdconf/bsdconf_put.c
@@ -151,7 +151,7 @@ bsdconf_put(struct bsdconf_option options[], const char *path,
 	}
 
 	/* Slurp the original into memory */
-	if ((buf = bsdconf_readfile(fd, (size_t)sb.st_size, &buflen)) == NULL)
+	if ((buf = bsdconf_slurp(fd, &buflen)) == NULL)
 		goto cleanup;
 
 	/*
diff --git a/lib/libbsdconf/bsdconf_stmt.c b/lib/libbsdconf/bsdconf_stmt.c
index 4970f84e5126..1105fa3144aa 100644
--- a/lib/libbsdconf/bsdconf_stmt.c
+++ b/lib/libbsdconf/bsdconf_stmt.c
@@ -15,6 +15,7 @@
 
 #include <ctype.h>
 #include <errno.h>
+#include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -109,6 +110,138 @@ bsdconf_readfile(int fd, size_t size, size_t *lenp)
 	return (buf);
 }
 
+/*
+ * Ceiling on a slurp of configuration data. Unset, empty, zero, or
+ * unparseable BSDCONF_MAX_BYTES restores BSDCONF_MAX_BYTES_DEFAULT.
+ */
+size_t
+bsdconf_max_bytes(void)
+{
+	const char *s;
+	char *end;
+	unsigned long n;
+
+	s = getenv("BSDCONF_MAX_BYTES");
+	if (s == NULL || *s == '\0')
+		return (BSDCONF_MAX_BYTES_DEFAULT);
+	errno = 0;
+	n = strtoul(s, &end, 10);
+	if (errno != 0 || end == s || *end != '\0' || n == 0)
+		return (BSDCONF_MAX_BYTES_DEFAULT);
+	if (n > SIZE_MAX)
+		return (SIZE_MAX);
+	return ((size_t)n);
+}
+
+/*
+ * Read the remaining contents of `fd' into a freshly allocated,
+ * NUL-terminated buffer, stopping at EOF or the BSDCONF_MAX_BYTES cap.
+ * Regular files whose remaining length already exceeds the cap fail with
+ * EFBIG without reading. Returns the buffer on success (which the caller
+ * must free) or NULL (with errno set) on error.
+ */
+char *
+bsdconf_slurp(int fd, size_t *lenp)
+{
+	struct stat sb;
+	char *buf;
+	char *t;
+	int sverrno;
+	size_t cap;
+	size_t maxb;
+	size_t off;
+	ssize_t r;
+
+	maxb = bsdconf_max_bytes();
+	if (maxb == 0) {
+		errno = EFBIG;
+		return (NULL);
+	}
+
+	/*
+	 * Known remaining length on a regular file: fail fast if the cap
+	 * cannot admit it, otherwise one exact allocation.
+	 */
+	if (fstat(fd, &sb) == 0 && S_ISREG(sb.st_mode)) {
+		off_t cur;
+
+		cur = lseek(fd, 0, SEEK_CUR);
+		if (cur != -1 && sb.st_size >= cur) {
+			uintmax_t remain;
+
+			remain = (uintmax_t)(sb.st_size - cur);
+			if (remain > maxb) {
+				errno = EFBIG;
+				return (NULL);
+			}
+			return (bsdconf_readfile(fd, (size_t)remain, lenp));
+		}
+	}
+
+	/* Unknown length (pipe, socket, device): grow up to the cap */
+	cap = maxb < 8192 ? maxb : 8192;
+	if ((buf = malloc(cap + 1)) == NULL)
+		return (NULL);
+	off = 0;
+	for (;;) {
+		if (off == cap) {
+			size_t ncap;
+
+			if (cap >= maxb) {
+				char probe;
+
+				do {
+					r = read(fd, &probe, 1);
+				} while (r < 0 && errno == EINTR);
+				if (r < 0) {
+					sverrno = errno;
+					free(buf);
+					errno = sverrno;
+					return (NULL);
+				}
+				if (r > 0) {
+					free(buf);
+					errno = EFBIG;
+					return (NULL);
+				}
+				break;
+			}
+			ncap = cap * 2;
+			if (ncap <= cap || ncap > maxb)
+				ncap = maxb;
+			if (ncap <= cap) {
+				free(buf);
+				errno = EFBIG;
+				return (NULL);
+			}
+			if ((t = realloc(buf, ncap + 1)) == NULL) {
+				sverrno = errno;
+				free(buf);
+				errno = sverrno;
+				return (NULL);
+			}
+			buf = t;
+			cap = ncap;
+		}
+		r = read(fd, buf + off, cap - off);
+		if (r < 0) {
+			if (errno == EINTR)
+				continue;
+			sverrno = errno;
+			free(buf);
+			errno = sverrno;
+			return (NULL);
+		}
+		if (r == 0)
+			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
diff --git a/usr.sbin/sysconf/sysconf.8 b/usr.sbin/sysconf/sysconf.8
index 09ab2cccd8d7..c026d40f68f9 100644
--- a/usr.sbin/sysconf/sysconf.8
+++ b/usr.sbin/sysconf/sysconf.8
@@ -3,7 +3,7 @@
 .\"
 .\" SPDX-License-Identifier: BSD-2-Clause
 .\"
-.Dd September 15, 2026
+.Dd September 16, 2026
 .Dt SYSCONF 8
 .Os
 .Sh NAME
@@ -431,8 +431,7 @@ may be a non-seekable stream
 .Po
 a fifo or
*** 112 LINES SKIPPED ***