mirror of
https://github.com/freebsd/freebsd-src.git
synced 2026-11-07 04:36:52 -10:00
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:
1 parent
7b7ef798bf
commit
74cab6e237
12 files changed
+267
-463
No files matched your search
+44
-18
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
/*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, "",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user