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
This commit is contained in:
Devin Teske committed 2026-09-22 15:31:20 -07:00
1 parent 7b7ef798bf
commit 74cab6e237
12 files changed
+267 -463

No files matched your search

+44 -18
View File
@@ -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 .
+39 -420
View File
@@ -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);
}
+9 -3
View File
@@ -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.
+2
View File
@@ -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,
+5 -1
View File
@@ -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
+1 -1
View File
@@ -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;
/*
+133
View File
@@ -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
+8 -3
View File
@@ -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
.Pa /dev/stdin ,
for example,
spooled to a temporary automatically
for example
.Pc ;
write operations require a regular file.
A
@@ -597,6 +596,12 @@ a successful removal echoes
.El
.Sh ENVIRONMENT
.Bl -tag -width "LOADER_DEFAULTS"
.It Ev BSDCONF_MAX_BYTES
Maximum bytes
.Nm
will read from a configuration file or stream;
see
.Xr bsdconf 3 .
.It Ev LOADER_DEFAULTS
Defaults file for the
.Ql loader
+2
View File
@@ -748,6 +748,8 @@ help(void)
fprintf(stderr, OPTFMT, "-x",
"Remove name(s) from the target's files.");
fprintf(stderr, "ENVIRONMENT:\n");
fprintf(stderr, OPTFMT, "BSDCONF_MAX_BYTES",
"Cap on bytes read from a configuration file (see bsdconf(3)).");
fprintf(stderr, OPTFMT, "LOADER_DEFAULTS",
"Defaults file for the loader target (in place of");
fprintf(stderr, OPTFMT, "",
+1 -1
View File
@@ -37,7 +37,7 @@
#define __unused /* not all compilers support attributes */
#endif
#define SYSCONF_VERSION "1.1 2026-09-15"
#define SYSCONF_VERSION "2.0 2026-09-16"
/* getopt(3) optstring; shared by parse_options() and find_target() */
#define OPTSTRING "AacdDEeFf:hij:k:lLnNqR:svVx"
-16
View File
@@ -236,22 +236,6 @@ scan_pass(int sandbox)
fds[n] = open(conf_files[n], O_RDONLY);
if (fds[n] < 0 && errno != ENOENT)
err(EXIT_FAILURE, "%s", conf_files[n]);
/*
* Spool input that cannot seek (a fifo or /dev/stdin named
* by `-f') before the sandbox slams shut: the library would
* otherwise spool lazily inside bsdconf_fparse(), where
* creating the temporary is no longer permitted.
*/
if (fds[n] >= 0 && lseek(fds[n], 0, SEEK_CUR) == -1) {
int sfd;
if (errno != ESPIPE ||
(sfd = bsdconf_spool(fds[n])) == -1)
err(EXIT_FAILURE, "%s", conf_files[n]);
close(fds[n]);
fds[n] = sfd;
}
}
#ifdef __FreeBSD__
+23
View File
@@ -416,6 +416,28 @@ make_src_knobs_cleanup()
cleanup_root
}
atf_test_case max_bytes_cap cleanup
max_bytes_cap_head()
{
atf_set "descr" "BSDCONF_MAX_BYTES bounds parse input"
}
max_bytes_cap_body()
{
setup_root
printf 'foo=ok\n' > "$ROOT/etc/make.conf"
atf_check -o inline:'foo: ok\n' "$SYSCONF" make -R "$ROOT" foo
# A tight cap rejects an otherwise valid file
atf_check -s not-exit:0 -e match:'File too large' \
env BSDCONF_MAX_BYTES=4 "$SYSCONF" make -R "$ROOT" foo
got=$( printf 'bar=piped\n' | "$SYSCONF" generic -f - bar ) ||
atf_fail "pipe parse failed"
atf_check_equal "bar: piped" "$got"
}
max_bytes_cap_cleanup()
{
cleanup_root
}
atf_init_test_cases()
{
atf_add_test_case make_src_knobs
@@ -428,4 +450,5 @@ atf_init_test_cases()
atf_add_test_case verbatim_backslash
atf_add_test_case equal_value_nocheck_mtime
atf_add_test_case sysctl_oid_range
atf_add_test_case max_bytes_cap
}