mirror of
https://github.com/opnsense/src.git
synced 2026-05-28 04:12:45 -04:00
libcasper: switch from select(2) to poll(2)
The previous implementation used FD_SET() on a stack-allocated fd_set, which is an out-of-bounds write whenever the socket fd is >= FD_SETSIZE (1024). poll(2) takes an array indexed by slot rather than by fd value, so it has no FD_SETSIZE limit. Approved by: so Security: FreeBSD-SA-26:22.libcasper Security: CVE-2026-39461 Reported by: Joshua Rogers Reviewed by: markj Differential Revision: https://reviews.freebsd.org/D56695
This commit is contained in:
parent
ed2557cfa3
commit
9bb455f9c9
5 changed files with 263 additions and 142 deletions
|
|
@ -54,6 +54,8 @@ void service_message(struct service *service,
|
|||
void service_start(struct service *service, int sock, int procfd);
|
||||
const char *service_name(struct service *service);
|
||||
int service_get_channel_flags(struct service *service);
|
||||
bool service_have_connections(void);
|
||||
bool service_poll_dispatch(void);
|
||||
|
||||
/* Private service connection functions. */
|
||||
struct service_connection *service_connection_add(struct service *service,
|
||||
|
|
@ -64,10 +66,6 @@ void service_connection_remove(
|
|||
int service_connection_clone(
|
||||
struct service *service,
|
||||
struct service_connection *sconn);
|
||||
struct service_connection *service_connection_first(
|
||||
struct service *service);
|
||||
struct service_connection *service_connection_next(
|
||||
struct service_connection *sconn);
|
||||
cap_channel_t *service_connection_get_chan(
|
||||
const struct service_connection *sconn);
|
||||
int service_connection_get_sock(
|
||||
|
|
|
|||
|
|
@ -223,10 +223,6 @@ service_register_core(int fd)
|
|||
void
|
||||
casper_main_loop(int fd)
|
||||
{
|
||||
fd_set fds;
|
||||
struct casper_service *casserv;
|
||||
struct service_connection *sconn, *sconntmp;
|
||||
int sock, maxfd, ret;
|
||||
|
||||
if (zygote_init() < 0)
|
||||
_exit(1);
|
||||
|
|
@ -236,55 +232,10 @@ casper_main_loop(int fd)
|
|||
*/
|
||||
service_register_core(fd);
|
||||
|
||||
for (;;) {
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(fd, &fds);
|
||||
maxfd = -1;
|
||||
TAILQ_FOREACH(casserv, &casper_services, cs_next) {
|
||||
/* We handle only core services. */
|
||||
if (!CSERVICE_IS_CORE(casserv))
|
||||
continue;
|
||||
for (sconn = service_connection_first(casserv->cs_service);
|
||||
sconn != NULL;
|
||||
sconn = service_connection_next(sconn)) {
|
||||
sock = service_connection_get_sock(sconn);
|
||||
FD_SET(sock, &fds);
|
||||
maxfd = sock > maxfd ? sock : maxfd;
|
||||
}
|
||||
}
|
||||
if (maxfd == -1) {
|
||||
/* Nothing to do. */
|
||||
_exit(0);
|
||||
}
|
||||
maxfd++;
|
||||
|
||||
|
||||
assert(maxfd <= (int)FD_SETSIZE);
|
||||
ret = select(maxfd, &fds, NULL, NULL, NULL);
|
||||
assert(ret == -1 || ret > 0); /* select() cannot timeout */
|
||||
if (ret == -1) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
while (service_have_connections()) {
|
||||
if (!service_poll_dispatch())
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
TAILQ_FOREACH(casserv, &casper_services, cs_next) {
|
||||
/* We handle only core services. */
|
||||
if (!CSERVICE_IS_CORE(casserv))
|
||||
continue;
|
||||
for (sconn = service_connection_first(casserv->cs_service);
|
||||
sconn != NULL; sconn = sconntmp) {
|
||||
/*
|
||||
* Prepare for connection to be removed from
|
||||
* the list on failure.
|
||||
*/
|
||||
sconntmp = service_connection_next(sconn);
|
||||
sock = service_connection_get_sock(sconn);
|
||||
if (FD_ISSET(sock, &fds)) {
|
||||
service_message(casserv->cs_service,
|
||||
sconn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_exit(0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,8 +30,7 @@
|
|||
* SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include <sys/cdefs.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/param.h>
|
||||
#include <sys/queue.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/nv.h>
|
||||
|
|
@ -42,6 +41,7 @@
|
|||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <paths.h>
|
||||
#include <poll.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
|
@ -72,7 +72,8 @@ struct service_connection {
|
|||
int sc_magic;
|
||||
cap_channel_t *sc_chan;
|
||||
nvlist_t *sc_limits;
|
||||
TAILQ_ENTRY(service_connection) sc_next;
|
||||
struct service *sc_service;
|
||||
size_t sc_pollidx;
|
||||
};
|
||||
|
||||
#define SERVICE_MAGIC 0x5e91ce
|
||||
|
|
@ -82,9 +83,90 @@ struct service {
|
|||
uint64_t s_flags;
|
||||
service_limit_func_t *s_limit;
|
||||
service_command_func_t *s_command;
|
||||
TAILQ_HEAD(, service_connection) s_connections;
|
||||
};
|
||||
|
||||
#define POLLSET_CHUNK 8
|
||||
static struct pollfd *pollset_pfds;
|
||||
static struct service_connection **pollset_conns;
|
||||
static size_t pollset_cap;
|
||||
static size_t pollset_size;
|
||||
|
||||
static int
|
||||
pollset_add(struct service_connection *sconn, int sock)
|
||||
{
|
||||
size_t i, newcap;
|
||||
void *p;
|
||||
|
||||
for (i = 0; i < pollset_size; i++) {
|
||||
if (pollset_pfds[i].fd < 0)
|
||||
break;
|
||||
}
|
||||
if (i == pollset_size) {
|
||||
newcap = roundup2(pollset_size + 1, POLLSET_CHUNK);
|
||||
if (newcap > pollset_cap) {
|
||||
p = reallocarray(pollset_pfds, newcap,
|
||||
sizeof(*pollset_pfds));
|
||||
if (p == NULL)
|
||||
return (-1);
|
||||
pollset_pfds = p;
|
||||
p = reallocarray(pollset_conns, newcap,
|
||||
sizeof(*pollset_conns));
|
||||
if (p == NULL)
|
||||
return (-1);
|
||||
pollset_conns = p;
|
||||
pollset_cap = newcap;
|
||||
}
|
||||
pollset_size++;
|
||||
}
|
||||
pollset_pfds[i].fd = sock;
|
||||
pollset_pfds[i].events = POLLIN;
|
||||
pollset_pfds[i].revents = 0;
|
||||
pollset_conns[i] = sconn;
|
||||
sconn->sc_pollidx = i;
|
||||
return (0);
|
||||
}
|
||||
|
||||
static void
|
||||
pollset_remove(struct service_connection *sconn)
|
||||
{
|
||||
|
||||
pollset_pfds[sconn->sc_pollidx].fd = -1;
|
||||
pollset_conns[sconn->sc_pollidx] = NULL;
|
||||
}
|
||||
|
||||
bool
|
||||
service_have_connections(void)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < pollset_size; i++) {
|
||||
if (pollset_pfds[i].fd >= 0)
|
||||
return (true);
|
||||
}
|
||||
return (false);
|
||||
}
|
||||
|
||||
bool
|
||||
service_poll_dispatch(void)
|
||||
{
|
||||
size_t i;
|
||||
int ret;
|
||||
|
||||
do {
|
||||
ret = poll(pollset_pfds, pollset_size, -1);
|
||||
} while (ret == -1 && errno == EINTR);
|
||||
if (ret == -1)
|
||||
return (false);
|
||||
|
||||
for (i = 0; i < pollset_size; i++) {
|
||||
if (pollset_pfds[i].revents == 0)
|
||||
continue;
|
||||
service_message(pollset_conns[i]->sc_service,
|
||||
pollset_conns[i]);
|
||||
}
|
||||
return (true);
|
||||
}
|
||||
|
||||
struct service *
|
||||
service_alloc(const char *name, service_limit_func_t *limitfunc,
|
||||
service_command_func_t *commandfunc, uint64_t flags)
|
||||
|
|
@ -102,7 +184,6 @@ service_alloc(const char *name, service_limit_func_t *limitfunc,
|
|||
service->s_limit = limitfunc;
|
||||
service->s_command = commandfunc;
|
||||
service->s_flags = flags;
|
||||
TAILQ_INIT(&service->s_connections);
|
||||
service->s_magic = SERVICE_MAGIC;
|
||||
|
||||
return (service);
|
||||
|
|
@ -111,13 +192,16 @@ service_alloc(const char *name, service_limit_func_t *limitfunc,
|
|||
void
|
||||
service_free(struct service *service)
|
||||
{
|
||||
struct service_connection *sconn;
|
||||
size_t i;
|
||||
|
||||
assert(service->s_magic == SERVICE_MAGIC);
|
||||
|
||||
service->s_magic = 0;
|
||||
while ((sconn = service_connection_first(service)) != NULL)
|
||||
service_connection_remove(service, sconn);
|
||||
for (i = 0; i < pollset_size; i++) {
|
||||
if (pollset_conns[i] != NULL &&
|
||||
pollset_conns[i]->sc_service == service)
|
||||
service_connection_remove(service, pollset_conns[i]);
|
||||
}
|
||||
free(service->s_name);
|
||||
free(service);
|
||||
}
|
||||
|
|
@ -154,8 +238,16 @@ service_connection_add(struct service *service, int sock,
|
|||
return (NULL);
|
||||
}
|
||||
}
|
||||
sconn->sc_service = service;
|
||||
if (pollset_add(sconn, sock) == -1) {
|
||||
serrno = errno;
|
||||
nvlist_destroy(sconn->sc_limits);
|
||||
(void)cap_unwrap(sconn->sc_chan, NULL);
|
||||
free(sconn);
|
||||
errno = serrno;
|
||||
return (NULL);
|
||||
}
|
||||
sconn->sc_magic = SERVICE_CONNECTION_MAGIC;
|
||||
TAILQ_INSERT_TAIL(&service->s_connections, sconn, sc_next);
|
||||
return (sconn);
|
||||
}
|
||||
|
||||
|
|
@ -167,7 +259,7 @@ service_connection_remove(struct service *service,
|
|||
assert(service->s_magic == SERVICE_MAGIC);
|
||||
assert(sconn->sc_magic == SERVICE_CONNECTION_MAGIC);
|
||||
|
||||
TAILQ_REMOVE(&service->s_connections, sconn, sc_next);
|
||||
pollset_remove(sconn);
|
||||
sconn->sc_magic = 0;
|
||||
nvlist_destroy(sconn->sc_limits);
|
||||
cap_close(sconn->sc_chan);
|
||||
|
|
@ -197,31 +289,6 @@ service_connection_clone(struct service *service,
|
|||
return (sock[1]);
|
||||
}
|
||||
|
||||
struct service_connection *
|
||||
service_connection_first(struct service *service)
|
||||
{
|
||||
struct service_connection *sconn;
|
||||
|
||||
assert(service->s_magic == SERVICE_MAGIC);
|
||||
|
||||
sconn = TAILQ_FIRST(&service->s_connections);
|
||||
assert(sconn == NULL ||
|
||||
sconn->sc_magic == SERVICE_CONNECTION_MAGIC);
|
||||
return (sconn);
|
||||
}
|
||||
|
||||
struct service_connection *
|
||||
service_connection_next(struct service_connection *sconn)
|
||||
{
|
||||
|
||||
assert(sconn->sc_magic == SERVICE_CONNECTION_MAGIC);
|
||||
|
||||
sconn = TAILQ_NEXT(sconn, sc_next);
|
||||
assert(sconn == NULL ||
|
||||
sconn->sc_magic == SERVICE_CONNECTION_MAGIC);
|
||||
return (sconn);
|
||||
}
|
||||
|
||||
cap_channel_t *
|
||||
service_connection_get_chan(const struct service_connection *sconn)
|
||||
{
|
||||
|
|
@ -330,14 +397,6 @@ service_message(struct service *service, struct service_connection *sconn)
|
|||
nvlist_destroy(nvlout);
|
||||
}
|
||||
|
||||
static int
|
||||
fd_add(fd_set *fdsp, int maxfd, int fd)
|
||||
{
|
||||
|
||||
FD_SET(fd, fdsp);
|
||||
return (fd > maxfd ? fd : maxfd);
|
||||
}
|
||||
|
||||
const char *
|
||||
service_name(struct service *service)
|
||||
{
|
||||
|
|
@ -418,9 +477,6 @@ service_clean(int *sockp, int *procfdp, uint64_t flags)
|
|||
void
|
||||
service_start(struct service *service, int sock, int procfd)
|
||||
{
|
||||
struct service_connection *sconn, *sconntmp;
|
||||
fd_set fds;
|
||||
int maxfd, nfds;
|
||||
|
||||
assert(service != NULL);
|
||||
assert(service->s_magic == SERVICE_MAGIC);
|
||||
|
|
@ -430,43 +486,9 @@ service_start(struct service *service, int sock, int procfd)
|
|||
if (service_connection_add(service, sock, NULL) == NULL)
|
||||
_exit(1);
|
||||
|
||||
for (;;) {
|
||||
FD_ZERO(&fds);
|
||||
maxfd = -1;
|
||||
for (sconn = service_connection_first(service); sconn != NULL;
|
||||
sconn = service_connection_next(sconn)) {
|
||||
maxfd = fd_add(&fds, maxfd,
|
||||
service_connection_get_sock(sconn));
|
||||
}
|
||||
|
||||
assert(maxfd >= 0);
|
||||
assert(maxfd + 1 <= (int)FD_SETSIZE);
|
||||
nfds = select(maxfd + 1, &fds, NULL, NULL, NULL);
|
||||
if (nfds < 0) {
|
||||
if (errno != EINTR)
|
||||
_exit(1);
|
||||
continue;
|
||||
} else if (nfds == 0) {
|
||||
/* Timeout. */
|
||||
abort();
|
||||
}
|
||||
|
||||
for (sconn = service_connection_first(service); sconn != NULL;
|
||||
sconn = sconntmp) {
|
||||
/*
|
||||
* Prepare for connection to be removed from the list
|
||||
* on failure.
|
||||
*/
|
||||
sconntmp = service_connection_next(sconn);
|
||||
if (FD_ISSET(service_connection_get_sock(sconn), &fds))
|
||||
service_message(service, sconn);
|
||||
}
|
||||
if (service_connection_first(service) == NULL) {
|
||||
/*
|
||||
* No connections left, exiting.
|
||||
*/
|
||||
break;
|
||||
}
|
||||
while (service_have_connections()) {
|
||||
if (!service_poll_dispatch())
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
_exit(0);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
.include <src.opts.mk>
|
||||
|
||||
.PATH: ${SRCTOP}/tests
|
||||
KYUAFILE= yes
|
||||
PACKAGE= tests
|
||||
|
||||
ATF_TESTS_C= cap_main_test
|
||||
|
||||
.if ${MK_CASPER} != "no"
|
||||
LIBADD+= casper
|
||||
CFLAGS+= -DWITH_CASPER
|
||||
.endif
|
||||
LIBADD+= nv
|
||||
|
||||
.include <bsd.test.mk>
|
||||
|
|
|
|||
142
lib/libcasper/tests/cap_main_test.c
Normal file
142
lib/libcasper/tests/cap_main_test.c
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
/*-
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*
|
||||
* Copyright (c) 2026 Mariusz Zaborski <oshogbo@FreeBSD.org>
|
||||
*
|
||||
* 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 AUTHORS 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 AUTHORS 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 <sys/resource.h>
|
||||
#include <sys/select.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <inttypes.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <libcasper.h>
|
||||
|
||||
#include <atf-c.h>
|
||||
|
||||
#define NCONNECTIONS (FD_SETSIZE + 64)
|
||||
#define FD_HEADROOM 64
|
||||
|
||||
/* Test that file descriptors past FD_SETSIZE (1024) work. */
|
||||
ATF_TC_WITHOUT_HEAD(many_connections);
|
||||
ATF_TC_BODY(many_connections, tc)
|
||||
{
|
||||
struct rlimit rl;
|
||||
cap_channel_t *chan;
|
||||
cap_channel_t **clones;
|
||||
size_t i;
|
||||
|
||||
if (getrlimit(RLIMIT_NOFILE, &rl) != 0)
|
||||
atf_tc_skip("getrlimit: %s", strerror(errno));
|
||||
if (rl.rlim_max < NCONNECTIONS + FD_HEADROOM)
|
||||
atf_tc_skip("RLIMIT_NOFILE hard cap %ju below required %d",
|
||||
(uintmax_t)rl.rlim_max, NCONNECTIONS + FD_HEADROOM);
|
||||
rl.rlim_cur = rl.rlim_max;
|
||||
ATF_REQUIRE_MSG(setrlimit(RLIMIT_NOFILE, &rl) == 0,
|
||||
"setrlimit: %s", strerror(errno));
|
||||
|
||||
chan = cap_init();
|
||||
ATF_REQUIRE_MSG(chan != NULL, "cap_init failed: %s", strerror(errno));
|
||||
|
||||
clones = calloc(NCONNECTIONS, sizeof(*clones));
|
||||
ATF_REQUIRE(clones != NULL);
|
||||
|
||||
/*
|
||||
* Every cap_clone(3) adds one more connection to the helper.
|
||||
* After this loop the helper is watching more fds than an
|
||||
* fd_set can hold.
|
||||
*/
|
||||
for (i = 0; i < NCONNECTIONS; i++) {
|
||||
clones[i] = cap_clone(chan);
|
||||
ATF_REQUIRE_MSG(clones[i] != NULL,
|
||||
"cap_clone failed at %zu/%d: %s",
|
||||
i, NCONNECTIONS, strerror(errno));
|
||||
}
|
||||
|
||||
for (i = 0; i < NCONNECTIONS; i++)
|
||||
cap_close(clones[i]);
|
||||
free(clones);
|
||||
cap_close(chan);
|
||||
}
|
||||
|
||||
#define CHURN_CONNECTIONS 50
|
||||
#define CHURN_CLOSE_STEP 5
|
||||
|
||||
/* Test that gaps in the file descriptor list do not break casper. */
|
||||
ATF_TC_WITHOUT_HEAD(connection_churn);
|
||||
ATF_TC_BODY(connection_churn, tc)
|
||||
{
|
||||
cap_channel_t *chan, *survivor, *extra;
|
||||
cap_channel_t *clones[CHURN_CONNECTIONS];
|
||||
size_t i, survivor_idx;
|
||||
|
||||
chan = cap_init();
|
||||
ATF_REQUIRE_MSG(chan != NULL, "cap_init failed: %s", strerror(errno));
|
||||
|
||||
for (i = 0; i < CHURN_CONNECTIONS; i++) {
|
||||
clones[i] = cap_clone(chan);
|
||||
ATF_REQUIRE_MSG(clones[i] != NULL,
|
||||
"cap_clone failed at %zu: %s", i, strerror(errno));
|
||||
}
|
||||
|
||||
/*
|
||||
* Close every Nth clone.
|
||||
*/
|
||||
for (i = 0; i < CHURN_CONNECTIONS; i += CHURN_CLOSE_STEP) {
|
||||
cap_close(clones[i]);
|
||||
clones[i] = NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* Force a poll() cycle: the helper handles POLLIN on chan and
|
||||
* POLLHUP on the closed clones in the same walk.
|
||||
*/
|
||||
extra = cap_clone(chan);
|
||||
ATF_REQUIRE_MSG(extra != NULL, "cap_clone after churn failed: %s",
|
||||
strerror(errno));
|
||||
|
||||
/* A surviving clone must still round-trip. */
|
||||
survivor_idx = 1;
|
||||
survivor = cap_clone(clones[survivor_idx]);
|
||||
ATF_REQUIRE_MSG(survivor != NULL,
|
||||
"cap_clone on survivor failed: %s", strerror(errno));
|
||||
|
||||
cap_close(survivor);
|
||||
cap_close(extra);
|
||||
for (i = 0; i < CHURN_CONNECTIONS; i++) {
|
||||
if (clones[i] != NULL)
|
||||
cap_close(clones[i]);
|
||||
}
|
||||
cap_close(chan);
|
||||
}
|
||||
|
||||
ATF_TP_ADD_TCS(tp)
|
||||
{
|
||||
|
||||
ATF_TP_ADD_TC(tp, many_connections);
|
||||
ATF_TP_ADD_TC(tp, connection_churn);
|
||||
return (atf_no_error());
|
||||
}
|
||||
Loading…
Reference in a new issue