From nobody Tue Sep 22 22:34:05 2026 X-Original-To: dev-commits-src-all@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 4hqFJF731rz6tBNv for ; Tue, 22 Sep 2026 22:34:05 +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 4hqFJF6dTPz3Q5k for ; Tue, 22 Sep 2026 22:34:05 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1790116445; 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=6cxHwotcETgurusCh7K7CX1hXwEhDJt8NJofEPr1lDU=; b=nfJzaopmyUCu68XUWhudjzPSXh7HJHbvGCKQNp3K+xPRiN7juaHaC/q9ynW2evTxZG8UWf bsGY8QIiQQSIRmBuqplvEdHP9imiK6NqPLFWluruiesm97rgX+mGXp39rg4P9f4VGw4r/+ NlJODvAk0n1UzqmpKqI4/bv+QpoFclTGQOgJd88tgkuuWrSUxZ5ACD8UWgqwoHUMLhTlkg Ydki3EqXBLGZ2aMcqnzUawUu9EJXJ/PoVKy1b2x+sGlwQQkaWcY6EIy4XLAJtStcr1nMeK Gsf8KDuFKw6zDIwrzKG9X/UXvPp43bv+IaBO+uXoq4ys+BJQhmi3y2g8jp4Czg== ARC-Seal: i=1; a=rsa-sha256; d=freebsd.org; s=dkim; cv=none; t=1790116445; b=P52ICfkRXVK/IZVbMxDUUyjEl5XAS7n+9arRMiQmkdYTvAR9z+rbxnlMf+SmkouHlRkxsg 48UhFv9RwOSWvHfVKpPRJcgP4ejx/QVHWlEol+O4cblrg9r3NowauRhVKlba2OjK7imOW8 lhT80WPdqZDzmxD0s7C0PUbn8Td5UiCtEXavo057hi76IV+ZD5sGKAVkUO3INnlLVTJ591 fd9GppAW8N0Ay6l4ddBknRJDTNVapucLwlfoA/nb+37E0/KJgujbqjFmRkw36jVgEobHZm v8XloKj/v0dQ2lOPqgg/zoa4T7TvHr5bsS2QxmXlk87d7mV2Ah2btHox2MPXnQ== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1790116445; 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=6cxHwotcETgurusCh7K7CX1hXwEhDJt8NJofEPr1lDU=; b=YwSiIbLUFF4o1vZ5G6EN9K2YqLODHLQzDBbXIhlqtMhdpa9BdHTOrKl6bR2hUGxyjIDUo2 2cbQkMvabi9V58klCHzf29QsrpHoLYZduLk2U80ZqWWcjkiI5zsHra7zxpGCQ81B3HAe8w 7jxQwsIDb1Qrik6jFkiJD/xtETnNK8mpSKe54VYA67tBtDlJzxvA4qXF2U0yP1wJqnUX5i L/G42yxT1Ww/38dz45hAAYu2XBLDb5r0TVV0G1j4RiDXuU7aHIODIyZJIYPmhMrc5/z0mj ul/4dvONcn63TnJbuwCra7YewCDFNic2SnyTuYNojy3+kSapv+nzDcJARhBJyg== 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 4hqFJF5gqxzQjc for ; Tue, 22 Sep 2026 22:34:05 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from git (uid 1279) (envelope-from git@FreeBSD.org) id 39f74 by gitrepo.freebsd.org (DragonFly Mail Agent v0.13+ on gitrepo.freebsd.org); Tue, 22 Sep 2026 22:34:05 +0000 To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org From: Devin Teske Subject: git: 74cab6e2371f - main - libbsdconf: parse from a bounded in-memory buffer List-Id: Commit messages for all branches of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-all List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-all@freebsd.org Sender: owner-dev-commits-src-all@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: 74cab6e2371fc35b58d5d3d018c7c3ad52809153 Auto-Submitted: auto-generated Date: Tue, 22 Sep 2026 22:34:05 +0000 Message-Id: <6ab3025d.39f74.16c3f9f4@gitrepo.freebsd.org> The branch main has been updated by dteske: URL: https://cgit.FreeBSD.org/src/commit/?id=74cab6e2371fc35b58d5d3d018c7c3ad52809153 commit 74cab6e2371fc35b58d5d3d018c7c3ad52809153 Author: Devin Teske AuthorDate: 2026-09-22 22:31:20 +0000 Commit: Devin Teske 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 #include #include #include @@ -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 #include +#include #include #include #include @@ -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 ***