Changeset View
Standalone View
usr.sbin/syslogd/syslogd_cap_log.c
- This file was added.
/*- | |||||
* SPDX-License-Identifier: BSD-2-Clause | |||||
* | |||||
* Copyright (c) 2023 The FreeBSD Foundation | |||||
* | |||||
* This software was developed by Jake Freeland <jfree@FreeBSD.org> | |||||
* under sponsorship from the FreeBSD Foundation. | |||||
* | |||||
* Redistribution and use in source and binary forms, with or without | |||||
* modification, are permitted provided that the following conditions | |||||
* are met: | |||||
* 1. Redistributions of source code must retain the above copyright | |||||
* notice, this list of conditions and the following disclaimer. | |||||
* 2. Redistributions in binary form must reproduce the above copyright | |||||
* notice, this list of conditions and the following disclaimer in the | |||||
* documentation and/or other materials provided with the distribution. | |||||
* | |||||
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND | |||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE | |||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS | |||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) | |||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT | |||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY | |||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF | |||||
* SUCH DAMAGE. | |||||
*/ | |||||
#include <assert.h> | |||||
#include <err.h> | |||||
#include <string.h> | |||||
#include "syslogd_cap.h" | |||||
struct cfiled_list cfiled_head; | |||||
static void | |||||
iovec_to_nvlist(const struct iovec *iov, nvlist_t *nvl) | |||||
{ | |||||
nvlist_add_string(nvl, "iov_base", iov->iov_base); | |||||
nvlist_add_number(nvl, "iov_len", iov->iov_len); | |||||
} | |||||
static void | |||||
nvlist_to_iovec(const nvlist_t *nvl, struct iovec *iov) | |||||
{ | |||||
iov->iov_base = strdup(nvlist_get_string(nvl, "iov_base")); | |||||
markj: There's no need to malloc this temporary copy. You can return the iovec by value. | |||||
markjUnsubmitted Done Inline ActionsCan you use nvlist_take_string() to avoid the extra memory allocation here? markj: Can you use nvlist_take_string() to avoid the extra memory allocation here? | |||||
jfreeAuthorUnsubmitted Done Inline ActionsThe nvlist_to_iovec() routine does not consume/alter the nvlist that is passed in. This is indicated by nvl being const. Using nvlist_take_string() would modify the state of nvl. jfree: The `nvlist_to_iovec()` routine does not consume/alter the nvlist that is passed in. This is… | |||||
iov->iov_len = nvlist_get_number(nvl, "iov_len"); | |||||
markjUnsubmitted Done Inline ActionsWhat's the purpose of passing a length when we can already get that from the string? I'd probably just avoid pretending that we're passing iovecs around at all. It feels a bit strange, since that sounds like we're passing pointers between processes, which of course doesn't work. Or, at least add a comment explaining that we're abusing the notion of iovec here a bit. markj: What's the purpose of passing a length when we can already get that from the string? I'd… | |||||
jfreeAuthorUnsubmitted Done Inline Actions
I agree. This is incredibly confusing; I even had a hard time understanding what was going on and I wrote it. Changing this now... jfree: > What's the purpose of passing a length when we can already get that from the string? I'd… | |||||
} | |||||
int | |||||
cap_p_open(cap_channel_t *chan, size_t filed_idx, const char *prog, | |||||
int *procdesc) | |||||
{ | |||||
nvlist_t *nvl = nvlist_create(0); | |||||
int error, pipedesc_w; | |||||
nvlist_add_string(nvl, "cmd", "p_open"); | |||||
nvlist_add_number(nvl, "filed_idx", filed_idx); | |||||
nvlist_add_string(nvl, "prog", prog); | |||||
nvl = cap_xfer_nvlist(chan, nvl); | |||||
if (nvl == NULL) { | |||||
logerror("Failed to xfer p_open nvlist"); | |||||
exit(1); | |||||
} | |||||
error = nvlist_get_number(nvl, "error"); | |||||
if (error != 0) { | |||||
errno = error; | |||||
logerror("Failed to open piped command"); | |||||
} | |||||
pipedesc_w = dnvlist_take_descriptor(nvl, "pipedesc_w", -1); | |||||
*procdesc = dnvlist_take_descriptor(nvl, "procdesc", -1); | |||||
nvlist_destroy(nvl); | |||||
return (pipedesc_w); | |||||
} | |||||
int | |||||
casper_p_open(nvlist_t *nvlin, nvlist_t *nvlout) | |||||
{ | |||||
struct cap_filed *cfiled; | |||||
size_t filed_idx; | |||||
int pipedesc_w, procdesc = -1; | |||||
const char *prog; | |||||
filed_idx = nvlist_get_number(nvlin, "filed_idx"); | |||||
prog = nvlist_get_string(nvlin, "prog"); | |||||
SLIST_FOREACH(cfiled, &cfiled_head, next) { | |||||
if (cfiled->idx != filed_idx) | |||||
continue; | |||||
if (strcmp(cfiled->pipe_cmd, prog) != 0) | |||||
return (-1); | |||||
pipedesc_w = p_open(filed_idx, prog, &procdesc); | |||||
if (pipedesc_w == -1) | |||||
return (-1); | |||||
nvlist_move_descriptor(nvlout, "pipedesc_w", pipedesc_w); | |||||
nvlist_move_descriptor(nvlout, "procdesc", procdesc); | |||||
return (0); | |||||
} | |||||
return (-1); | |||||
} | |||||
const char * | |||||
cap_ttymsg(cap_channel_t *chan, struct iovec *iov, int iovcnt, | |||||
const char *line, int tmout) | |||||
{ | |||||
nvlist_t *nvl = nvlist_create(0); | |||||
int error; | |||||
static char errbuf[1024]; | |||||
char *ret = NULL; | |||||
nvlist_add_string(nvl, "cmd", "ttymsg"); | |||||
for (int i = 0; i < iovcnt; ++i) { | |||||
nvlist_t *nvl_iov = nvlist_create(0); | |||||
iovec_to_nvlist(iov + i, nvl_iov); | |||||
nvlist_append_nvlist_array(nvl, "iovec_list", nvl_iov); | |||||
nvlist_destroy(nvl_iov); | |||||
} | |||||
nvlist_add_string(nvl, "line", line); | |||||
nvlist_add_number(nvl, "tmout", tmout); | |||||
nvl = cap_xfer_nvlist(chan, nvl); | |||||
if (nvl == NULL) { | |||||
logerror("Failed to xfer ttymsg nvlist"); | |||||
exit(1); | |||||
} | |||||
error = nvlist_get_number(nvl, "error"); | |||||
if (error != 0) { | |||||
errno = error; | |||||
logerror("Failed to ttymsg"); | |||||
} | |||||
if (nvlist_exists_string(nvl, "errstr")) { | |||||
const char *errstr = nvlist_get_string(nvl, "errstr"); | |||||
(void)strlcpy(errbuf, errstr, sizeof(errbuf)); | |||||
ret = errbuf; | |||||
} | |||||
nvlist_destroy(nvl); | |||||
return (ret); | |||||
} | |||||
int | |||||
casper_ttymsg(nvlist_t *nvlin, nvlist_t *nvlout) | |||||
{ | |||||
const nvlist_t * const *nvl_iov; | |||||
struct iovec *iov; | |||||
size_t iovcnt; | |||||
int tmout; | |||||
const char *line; | |||||
nvl_iov = nvlist_get_nvlist_array(nvlin, "iovec_list", &iovcnt); | |||||
assert(iovcnt <= TTYMSG_IOV_MAX); | |||||
iov = calloc(iovcnt, sizeof(*iov)); | |||||
for (size_t i = 0; i < iovcnt; ++i) | |||||
nvlist_to_iovec(nvl_iov[i], iov + i); | |||||
line = nvlist_get_string(nvlin, "line"); | |||||
tmout = nvlist_get_number(nvlin, "tmout"); | |||||
line = ttymsg(iov, iovcnt, line, tmout); | |||||
if (line != NULL) | |||||
nvlist_add_string(nvlout, "errstr", line); | |||||
free(iov); | |||||
return (0); | |||||
} | |||||
void | |||||
cap_wallmsg(cap_channel_t *chan, const struct filed *f, struct iovec *iov, | |||||
int iovcnt) | |||||
{ | |||||
nvlist_t *nvl = nvlist_create(0); | |||||
int error; | |||||
nvlist_add_string(nvl, "cmd", "wallmsg"); | |||||
/* | |||||
* The filed_to_nvlist() function is not needed | |||||
* here because wallmsg() only uses f_type and | |||||
* fu_uname members, which are both inline. | |||||
*/ | |||||
nvlist_add_binary(nvl, "filed", f, sizeof(*f)); | |||||
for (int i = 0; i < iovcnt; ++i) { | |||||
nvlist_t *nvl_iov = nvlist_create(0); | |||||
iovec_to_nvlist(iov + i, nvl_iov); | |||||
nvlist_append_nvlist_array(nvl, "iovec_list", nvl_iov); | |||||
nvlist_destroy(nvl_iov); | |||||
} | |||||
nvl = cap_xfer_nvlist(chan, nvl); | |||||
if (nvl == NULL) { | |||||
logerror("Failed to xfer wallmsg nvlist"); | |||||
exit(1); | |||||
} | |||||
error = nvlist_get_number(nvl, "error"); | |||||
if (error != 0) { | |||||
errno = error; | |||||
logerror("Failed to wallmsg"); | |||||
} | |||||
nvlist_destroy(nvl); | |||||
} | |||||
int | |||||
casper_wallmsg(nvlist_t *nvlin) | |||||
{ | |||||
const struct filed *f; | |||||
const nvlist_t * const *nvl_iov; | |||||
struct iovec *iov; | |||||
size_t sz; | |||||
Done Inline ActionsRather than passing the whole filed, syslogd should instead use an integer (say, index in the filed list) to refer to it. Then 1) the whole filed doesn't need to be copied, 2) we don't have to worry about validating the filed structure. Basically, the same thing you did with p_open(). markj: Rather than passing the whole filed, syslogd should instead use an integer (say, index in the… | |||||
Done Inline Actions
This would require storing the f_uname and f_type fields in cap_filed. Is this okay? jfree: > Rather than passing the whole filed, syslogd should instead use an integer (say, index in the… | |||||
f = nvlist_get_binary(nvlin, "filed", &sz); | |||||
assert(sz == sizeof(*f)); | |||||
nvl_iov = nvlist_get_nvlist_array(nvlin, "iovec_list", &sz); | |||||
Done Inline Actionsnvlist_get_nvlist_array() will tell you how many array elements are present. That is, the line iovcnt = nvlist_get_number(nvlin, "iovec_count"); does nothing, I believe. markj: nvlist_get_nvlist_array() will tell you how many array elements are present. That is, the line… | |||||
assert(sz <= TTYMSG_IOV_MAX); | |||||
markjUnsubmitted Not Done Inline Actionsassert() can be compiled out. sz is untrusted (it comes from the sandbox), so we should validate it properly. The same applies to some other assertions in this file. markj: assert() can be compiled out. `sz` is untrusted (it comes from the sandbox), so we should… | |||||
iov = calloc(sz, sizeof(*iov)); | |||||
for (size_t i = 0; i < sz; ++i) | |||||
nvlist_to_iovec(nvl_iov[i], iov + i); | |||||
wallmsg(f, iov, sz); | |||||
for (size_t i = 0; i < sz; ++i) | |||||
free(iov[i].iov_base); | |||||
Done Inline ActionsIn principle we should avoid trusting the values syslogd gives us, since syslogd might be compromised somehow. In particular, we shouldn assume that iovcnt * sizeof(*iov) could overflow. A simple solution here is to use calloc() instead. markj: In principle we should avoid trusting the values syslogd gives us, since syslogd might be… | |||||
Done Inline Actions
If syslogd manipulates iovcnt to be less than the true number of iovecs, then we will convert iovcnt number of nvlists into iovecs and have some extra, unused space from the memory allocation. It doesn't matter if that unused space is unitialized because libcasper just passes the same iovcnt into wallmsg(). That extra space would never be touched. When syslogd manipulates iovcnt to be more than the true number of iovecs, libcasper will allocate more memory to store more iovecs, but we will end up indexing out of bounds with nvl_iov[]. We would just end up fetching data from non-existent nvlists, which would likely crash the program. calloc() fixes situations where the iovecs have been corrupted on syslogd's side and iovcnt has not been manipulated, but there doesn't seem to be a good solution to fixing situations where iovcnt is manipulated. I don't think there is a good solution, but this prompts the question: how far can we go without trusting syslogd? jfree: > In principle we should avoid trusting the values syslogd gives us, since syslogd might be… | |||||
Done Inline Actions
Most likely, but not necessarily. This kind of overflow is a classic source of security holes (hence mallocarray() from OpenBSD). We should, at least, make sure that syslogd cannot trigger memory safety bugs in a casper service. The service should crash deterministically or return an error in response to invalid input. markj: > but we will end up indexing out of bounds with nvl_iov[]. We would just end up fetching data… | |||||
Done Inline Actions
How do you determine if input is invalid? And I am very confident that the program would crash. Anytime an nvlist element is accessed that does not exist, the program will terminate from an assertion. I'm also fairly confident that accessing an uninitialized nvlist would raise some assertions itself. jfree: > Most likely, but not necessarily. This kind of overflow is a classic source of security holes… | |||||
Done Inline Actions
I don't have a general definition. A value for iovcnt where iovcnt * sizeof(*iov) overflows is certainly invalid though.
Yes, we can be fairly sure that the program will crash if we take a random address and treat it as a pointer to an nvlist, but the point is that we are not certain (what if, for example, that address is a pointer to a recently freed nvlist?), and that uncertainty is where security holes lie. If we handle the possibility of overflow properly, then there's no need to reason about what might happen if the code starts accessing uninitialized memory. This example is a bit pedantic. My point is just that the boundary between syslogd and its casper services is a privilege boundary, so code which transfers data across that boundary requires extra scrutiny. markj: > How do you determine if input is invalid?
I don't have a general definition. A value for… | |||||
Done Inline Actions
Fair enough. Like you said, these are the kinds of uncertainties that can lead to security exploits. I totally acknowledge that the data transfer between syslogd and libcasper is a point of security concern, I just still have no idea how you would validate iovcnt. Even checking for arithmetic overflow like mallocarray() does not ensure that syslogd isn't just lying to us by passing an invalid iovcnt. jfree: > Yes, we can be fairly sure that the program will crash if we take a random address and treat… | |||||
free(iov); | |||||
return (0); | |||||
} |
There's no need to malloc this temporary copy. You can return the iovec by value.