diff --git a/usr.sbin/inetd/builtins.c b/usr.sbin/inetd/builtins.c --- a/usr.sbin/inetd/builtins.c +++ b/usr.sbin/inetd/builtins.c @@ -37,6 +37,7 @@ #include #include +#include #include #include #include @@ -73,12 +74,12 @@ struct biltin biltins[] = { /* Echo received data */ - { "echo", SOCK_STREAM, BIF_FORK, -1, echo_stream }, + { "echo", SOCK_STREAM, BIF_CAPENTER, -1, echo_stream }, { "echo", SOCK_DGRAM, 0, 1, echo_dg }, /* Internet /dev/null */ - { "discard", SOCK_STREAM, BIF_FORK, -1, discard_stream }, - { "discard", SOCK_DGRAM, 0, 1, discard_dg }, + { "discard", SOCK_STREAM, BIF_CAPENTER, -1, discard_stream }, + { "discard", SOCK_DGRAM, BIF_CAPENTER, 1, discard_dg }, /* Return 32 bit time since 1900 */ { "time", SOCK_STREAM, 0, -1, machtime_stream }, @@ -89,9 +90,14 @@ { "daytime", SOCK_DGRAM, 0, 1, daytime_dg }, /* Familiar character generator */ - { "chargen", SOCK_STREAM, BIF_FORK, -1, chargen_stream }, + { "chargen", SOCK_STREAM, BIF_CAPENTER, -1, chargen_stream }, { "chargen", SOCK_DGRAM, 0, 1, chargen_dg }, + /* + * tcpmux cap_enter is effectively ignored; we explicitly check if the + * service is tcpmux, then we invoke it manually and proceed as if the + * service it resolved to was selected. + */ { "tcpmux", SOCK_STREAM, BIF_FORK, -1, (bi_fn_t *)tcpmux }, { "auth", SOCK_STREAM, BIF_FORK, -1, ident_stream }, diff --git a/usr.sbin/inetd/inetd.h b/usr.sbin/inetd/inetd.h --- a/usr.sbin/inetd/inetd.h +++ b/usr.sbin/inetd/inetd.h @@ -36,6 +36,7 @@ #include +#include #include #define BUFSIZE 8192 @@ -151,6 +152,11 @@ extern struct biltin biltins[]; #define BIF_FORK 0x0001 /* should fork before call */ +#define BIF_CAPENTER 0x0002 /* enter capability mode upon fork */ #define SERVTAB_FORK(sep) \ - ((sep)->se_bi == NULL || ((sep)->se_bi->bi_flags & BIF_FORK) != 0) + ((sep)->se_bi == NULL || \ + ((sep)->se_bi->bi_flags & (BIF_FORK | BIF_CAPENTER)) != 0) + +#define SERVTAB_CAPENTER(sep) \ + ((sep)->se_bi != NULL && ((sep)->se_bi->bi_flags & BIF_CAPENTER) != 0) diff --git a/usr.sbin/inetd/inetd.c b/usr.sbin/inetd/inetd.c --- a/usr.sbin/inetd/inetd.c +++ b/usr.sbin/inetd/inetd.c @@ -96,6 +96,7 @@ * #endif */ #include +#include #include #include #include @@ -110,6 +111,7 @@ #include #include +#include #include #include #include @@ -280,6 +282,9 @@ static LIST_HEAD(, procinfo) proctable[PERIPSIZE]; +static cap_rights_t ctrl_rights, dgram_svc_rights, svc_rights; +static unsigned long *ctrl_ioctls, nctrl_ioctls; + static int getvalue(const char *arg, int *value, const char *whine) { @@ -313,6 +318,101 @@ } #endif +static void +setup_sigpipe(void) +{ + const unsigned long sigpipe_cmds[] = { FIONREAD }; + cap_rights_t sigrxpipe_rights, sigtxpipe_rights; + + cap_rights_init(&sigtxpipe_rights, CAP_WRITE); + cap_rights_init(&sigrxpipe_rights, CAP_READ, CAP_IOCTL, CAP_EVENT); + if (pipe2(signalpipe, O_CLOEXEC) != 0) { + syslog(LOG_ERR, "pipe: %m"); + exit(EX_OSERR); + } + if (caph_rights_limit(signalpipe[1], &sigtxpipe_rights) == -1) { + syslog(LOG_ERR, "failed to limit tx signalpipe: %m"); + exit(EX_OSERR); + } + if (caph_rights_limit(signalpipe[0], &sigrxpipe_rights) == -1 || + caph_ioctls_limit(signalpipe[0], sigpipe_cmds, + nitems(sigpipe_cmds)) == -1) { + syslog(LOG_ERR, "failed to limit rx signalpipe: %m"); + exit(EX_OSERR); + } +} + +static void +prepare_ctrl_caps(void) +{ + const unsigned long std_ctrl_ioctls[] = { FIONBIO, FIONREAD }; + size_t i, j; + + /* + * The rights we're imposing on these sockets will be passed down to + * inetd services as stdio, so we need to both be somewhat respectful of + * what they may reasonably attempt to do and we need to make sure we + * apply a superset of the standard rights we grant to stdio. + */ + caph_stream_rights(&ctrl_rights, CAPH_READ | CAPH_WRITE); + caph_stream_rights(&svc_rights, CAPH_READ | CAPH_WRITE); + + /* Control rights need to be a superset of service rights. */ + cap_rights_set(&ctrl_rights, CAP_ACCEPT, CAP_BIND, CAP_CONNECT, + CAP_LISTEN, CAP_SETSOCKOPT, CAP_GETSOCKNAME); + cap_rights_set(&svc_rights, CAP_SETSOCKOPT, CAP_GETSOCKNAME); +#ifdef LIBWRAP + cap_rights_set(&ctrl_rights, CAP_GETPEERNAME); + cap_rights_set(&svc_rights, CAP_GETPEERNAME); +#endif + /* Now build dgram_svc_rights as based on svc_rights + CAP_CONNECT. */ + cap_rights_init(&dgram_svc_rights); + cap_rights_merge(&dgram_svc_rights, &svc_rights); + cap_rights_set(&dgram_svc_rights, CAP_CONNECT); + + nctrl_ioctls = nitems(caph_stream_cmds) + nitems(std_ctrl_ioctls); + ctrl_ioctls = calloc(nctrl_ioctls, sizeof(*ctrl_ioctls)); + if (ctrl_ioctls == NULL) { + syslog(LOG_ERR, "calloc: %m"); + exit(EX_OSERR); + } + + for (i = 0, j = 0; i < nitems(caph_stream_cmds); ++i, ++j) + ctrl_ioctls[j] = caph_stream_cmds[i]; + for (i = 0; i < nitems(std_ctrl_ioctls); ++i, ++j) + ctrl_ioctls[j] = std_ctrl_ioctls[i]; +} + +/* + * Service sockets are created via accept(2), thus inheriting the rights of the + * control socket. Therefore, control caps must be a superset of those needed + * by the services. Apply rights here for both service sockets and control + * socket to make it easier to audit what is happening. + * + * rights(4) are applied based both on whether we're a control or service + * socket, and whether we're UDP or TCP for service sockets. ioctl limitations + * are applied purely for control sockets. + */ +static void +setup_ctrl_caps(int fd, struct servtab *sep) +{ + cap_rights_t *rights; + + if (sep != NULL && sep->se_socktype == SOCK_DGRAM) + rights = &dgram_svc_rights; + else if (sep != NULL) + rights = &svc_rights; + else + rights = &ctrl_rights; + + if (caph_rights_limit(fd, rights) == -1 || (sep == NULL && + caph_ioctls_limit(fd, ctrl_ioctls, nctrl_ioctls) == -1)) { + syslog(LOG_ERR, "failed to limit %s sock: %m", + sep != NULL ? "service" : "control"); + exit(EX_OSERR); + } +} + int main(int argc, char **argv) { @@ -320,7 +420,7 @@ struct passwd *pwd; struct group *grp; struct sigaction sa, saalrm, sachld, sahup, sapipe; - int ch, dofork; + int ch; pid_t pid; char buf[50]; #ifdef LOGIN_CAP @@ -337,8 +437,17 @@ const char *servname; int error; struct conninfo *conn; + bool dofork; openlog("inetd", LOG_PID | LOG_NOWAIT | LOG_PERROR, LOG_DAEMON); + /* Relies on syslog(3). */ + prepare_ctrl_caps(); + + caph_cache_catpages(); + if (caph_limit_stdio() == -1) { + syslog(LOG_ERR, "caph_limit_stdio: %m"); + exit(EX_OSERR); + } while ((ch = getopt(argc, argv, "dlwWR:a:c:C:p:s:")) != -1) switch(ch) { @@ -543,10 +652,7 @@ (void)setenv("inetd_dummy", dummy, 1); } - if (pipe2(signalpipe, O_CLOEXEC) != 0) { - syslog(LOG_ERR, "pipe: %m"); - exit(EX_OSERR); - } + setup_sigpipe(); FD_SET(signalpipe[0], &allsock); #ifdef SANITY_CHECK nsock++; @@ -629,6 +735,13 @@ close(ctrl); continue; } + + /* + * This will limit the service effectively to + * read/write/select for all spawned processes, both + * builtin and external. + */ + setup_ctrl_caps(ctrl, sep); i = 0; if (ioctl(sep->se_fd, FIONBIO, &i) < 0) syslog(LOG_ERR, "ioctl1(FIONBIO, 0): %m"); @@ -644,8 +757,18 @@ close(ctrl); continue; } - } else + } else { + /* + * "wait" sockets and datagram sockets won't have + * any further limiting applied to them. For the + * former, the service needs to be able to accept(2) + * other sockets. The latter has already been + * limited as far as it can go, since we won't be + * needing any further bind(2). + */ ctrl = sep->se_fd; + } + if (dolog && !ISWRAP(sep)) { char pname[NI_MAXHOST] = "unknown"; socklen_t sl; @@ -741,6 +864,7 @@ _exit(0); } } + #ifdef LIBWRAP if (ISWRAP(sep)) { inetd_setproctitle("wrapping", ctrl); @@ -771,7 +895,13 @@ } } #endif + if (sep->se_bi) { + if (SERVTAB_CAPENTER(sep) && + caph_enter() == -1) { + syslog(LOG_ERR, "cap_enter: %m"); + _exit(0); + } (*sep->se_bi->bi_fn)(ctrl, sep); } else { if (debug) @@ -1253,6 +1383,8 @@ sep->se_service, sep->se_proto); return; } + + setup_ctrl_caps(sep->se_fd, NULL); #define turnon(fd, opt) \ setsockopt(fd, SOL_SOCKET, opt, (char *)&on, sizeof (on)) if (strcmp(sep->se_proto, "tcp") == 0 && (options & SO_DEBUG) && @@ -1368,6 +1500,8 @@ } if (sep->se_socktype == SOCK_STREAM) listen(sep->se_fd, -1); + else if (sep->se_socktype == SOCK_DGRAM) + setup_ctrl_caps(sep->se_fd, sep); enable(sep); if (debug) { warnx("registered %s on %d",