From d8cac78d3ded828ac290d48b4bceae795b625c1b Mon Sep 17 00:00:00 2001 From: Can Erkin Acar Date: Fri, 12 Mar 2004 18:40:17 +0000 Subject: Privilege seperation for named. Allows named to handle address/interface changes without restart. If you use non-standard ports in named configuration make sure they are > 1024. Also /var/named/etc/rndc.key (if any) must be readable by group named. Initial work and testing by itojun@, jakob@, hints, help from henning@, avsm@, beck@. ok henning, beck, avsm, deraadt --- usr.sbin/bind/bin/named/main.c | 12 + usr.sbin/bind/bin/named/server.c | 4 +- usr.sbin/bind/lib/isc/Makefile.in | 3 +- usr.sbin/bind/lib/isc/include/isc/socket.h | 2 + usr.sbin/bind/lib/isc/unix/Makefile.in | 6 +- usr.sbin/bind/lib/isc/unix/include/isc/privsep.h | 42 +++ usr.sbin/bind/lib/isc/unix/privsep.c | 404 +++++++++++++++++++++++ usr.sbin/bind/lib/isc/unix/privsep_fdpass.c | 116 +++++++ usr.sbin/bind/lib/isc/unix/socket.c | 13 +- 9 files changed, 597 insertions(+), 5 deletions(-) create mode 100644 usr.sbin/bind/lib/isc/unix/include/isc/privsep.h create mode 100644 usr.sbin/bind/lib/isc/unix/privsep.c create mode 100644 usr.sbin/bind/lib/isc/unix/privsep_fdpass.c diff --git a/usr.sbin/bind/bin/named/main.c b/usr.sbin/bind/bin/named/main.c index 5a3ebbd4024..0eec853876d 100644 --- a/usr.sbin/bind/bin/named/main.c +++ b/usr.sbin/bind/bin/named/main.c @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -511,7 +512,9 @@ setup(void) { } #endif +#if 0 /* Not used due to privsep */ ns_os_chroot(ns_g_chrootdir); +#endif /* * For operating systems which have a capability mechanism, now @@ -538,6 +541,15 @@ setup(void) { if (!ns_g_foreground) ns_os_daemonize(); + /* + * Privilege separation + */ + isc_priv_init(ns_g_logstderr); + isc_drop_privs(ns_g_username); + isc_socket_privsep(1); + + /* process is now unprivileged and inside a chroot */ + isc_log_write(ns_g_lctx, NS_LOGCATEGORY_GENERAL, NS_LOGMODULE_MAIN, ISC_LOG_NOTICE, "starting BIND %s%s", ns_g_version, saved_command_line); diff --git a/usr.sbin/bind/bin/named/server.c b/usr.sbin/bind/bin/named/server.c index fee23ff2c15..e0449007e85 100644 --- a/usr.sbin/bind/bin/named/server.c +++ b/usr.sbin/bind/bin/named/server.c @@ -2069,10 +2069,12 @@ load_configuration(const char *filename, ns_server_t *server, } /* - * Relinquish root privileges. + * Relinquish root privileges. Not used due to privsep */ +#if 0 if (first_time) ns_os_changeuser(); +#endif /* * Configure the logging system. diff --git a/usr.sbin/bind/lib/isc/Makefile.in b/usr.sbin/bind/lib/isc/Makefile.in index 033827e6f61..6947247207d 100644 --- a/usr.sbin/bind/lib/isc/Makefile.in +++ b/usr.sbin/bind/lib/isc/Makefile.in @@ -36,7 +36,8 @@ UNIXOBJS = @ISC_ISCIPV6_O@ \ unix/errno2result.@O@ unix/file.@O@ unix/fsaccess.@O@ \ unix/interfaceiter.@O@ unix/keyboard.@O@ unix/net.@O@ \ unix/os.@O@ unix/resource.@O@ unix/socket.@O@ unix/stdio.@O@ \ - unix/stdtime.@O@ unix/strerror.@O@ unix/syslog.@O@ unix/time.@O@ + unix/stdtime.@O@ unix/strerror.@O@ unix/syslog.@O@ unix/time.@O@ \ + unix/privsep.@O@ unix/privsep_fdpass.@O@ NLSOBJS = nls/msgcat.@O@ diff --git a/usr.sbin/bind/lib/isc/include/isc/socket.h b/usr.sbin/bind/lib/isc/include/isc/socket.h index 9915f3af4d1..2144c81d0b6 100644 --- a/usr.sbin/bind/lib/isc/include/isc/socket.h +++ b/usr.sbin/bind/lib/isc/include/isc/socket.h @@ -326,6 +326,8 @@ isc_socket_bind(isc_socket_t *sock, isc_sockaddr_t *addressp); * ISC_R_UNEXPECTED */ +isc_result_t +isc_socket_privsep(int); isc_result_t isc_socket_listen(isc_socket_t *sock, unsigned int backlog); /* diff --git a/usr.sbin/bind/lib/isc/unix/Makefile.in b/usr.sbin/bind/lib/isc/unix/Makefile.in index 769c14d6857..12d12b00e0f 100644 --- a/usr.sbin/bind/lib/isc/unix/Makefile.in +++ b/usr.sbin/bind/lib/isc/unix/Makefile.in @@ -33,14 +33,16 @@ OBJS = @ISC_IPV6_O@ \ app.@O@ dir.@O@ entropy.@O@ errno2result.@O@ file.@O@ \ fsaccess.@O@ interfaceiter.@O@ keyboard.@O@ net.@O@ \ os.@O@ resource.@O@ socket.@O@ stdio.@O@ stdtime.@O@ \ - strerror.@O@ syslog.@O@ time.@O@ + strerror.@O@ syslog.@O@ time.@O@ \ + privsep.@O@ privsep_fdpass.@O@ # Alphabetically SRCS = @ISC_IPV6_C@ \ app.c dir.c entropy.c errno2result.c file.c \ fsaccess.c interfaceiter.c keyboard.c net.c \ os.c resource.c socket.c stdio.c stdtime.c \ - strerror.c syslog.c time.c + strerror.c syslog.c time.c \ + privsep.c privsep_fdpass.c SUBDIRS = include TARGETS = ${OBJS} diff --git a/usr.sbin/bind/lib/isc/unix/include/isc/privsep.h b/usr.sbin/bind/lib/isc/unix/include/isc/privsep.h new file mode 100644 index 00000000000..5da4e2a6ed3 --- /dev/null +++ b/usr.sbin/bind/lib/isc/unix/include/isc/privsep.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2003 Can Erkin Acar + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef _PRIVSEP_H_ +#define _PRIVSEP_H_ + +enum cmd_types { + PRIV_BIND /* bind to a privileged port */ +}; + +/* Privilege separation */ +int isc_priv_init(int); +int isc_drop_privs(const char *username); + +struct sockaddr; +int isc_priv_bind(int, struct sockaddr *, socklen_t); + +/* File descriptor send/recv */ +void send_fd(int, int); +int receive_fd(int); + +/* communications over the channel */ +int may_read(int, void *, size_t); +void must_read(int, void *, size_t); +void must_write(int, const void *, size_t); + +extern int priv_fd; + +#endif diff --git a/usr.sbin/bind/lib/isc/unix/privsep.c b/usr.sbin/bind/lib/isc/unix/privsep.c new file mode 100644 index 00000000000..22d890aed3f --- /dev/null +++ b/usr.sbin/bind/lib/isc/unix/privsep.c @@ -0,0 +1,404 @@ +/* $OpenBSD: privsep.c,v 1.1 2004/03/12 18:40:15 canacar Exp $ */ + +/* + * Copyright (c) 2004 Henning Brauer + * Copyright (c) 2004 Can Erkin Acar + * Copyright (c) 2003 Anil Madhavapeddy + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +enum priv_state { + STATE_RUN, + STATE_QUIT +}; + +/* allowed privileged port numbers */ +#define NAMED_PORT_DEFAULT 53 +#define RNDC_PORT_DEFAULT 953 +#define LWRES_PORT_DEFAULT 921 + +int debug_level = LOG_DEBUG; +int log_stderr = 1; +int priv_fd = -1; + +static volatile pid_t child_pid = -1; +static volatile sig_atomic_t cur_state = STATE_RUN; + +static int check_bind(const struct sockaddr *, socklen_t); +static void fatal(const char *); +static void logmsg(int, const char *, ...); +static void parent_bind(int); +static void sig_pass_to_chld(int); +static void sig_got_chld(int); +static void write_command(int, int); + +int +isc_priv_init(int lstderr) +{ + int i, socks[2], cmd; + + logmsg(LOG_NOTICE, "Starting privilege seperation"); + + log_stderr = lstderr; + + /* Create sockets */ + if (socketpair(AF_LOCAL, SOCK_STREAM, PF_UNSPEC, socks) == -1) + fatal("socketpair() failed"); + + switch (child_pid = fork()) { + case -1: + fatal("failed to fork() for privsep"); + case 0: + close(socks[0]); + priv_fd = socks[1]; + return (0); + default: + break; + } + + for (i = 1; i < _NSIG; i++) + signal(i, SIG_DFL); + + signal(SIGALRM, sig_pass_to_chld); + signal(SIGTERM, sig_pass_to_chld); + signal(SIGHUP, sig_pass_to_chld); + signal(SIGCHLD, sig_got_chld); + + /* Father - close unneeded sockets */ + for (i = STDERR_FILENO + 1; i < socks[0]; i++) + close(i); + closefrom(socks[0] + 1); + + setproctitle("[priv]"); + + while (cur_state != STATE_QUIT) { + if (may_read(socks[0], &cmd, sizeof(int))) + break; + switch (cmd) { + case PRIV_BIND: + parent_bind(socks[0]); + break; + default: + logmsg(LOG_ERR, "[priv]: unknown command %d", cmd); + _exit(1); + /* NOTREACHED */ + } + } + + _exit(0); +} + +int +isc_drop_privs(const char *username) +{ + struct passwd *pw; + + if ((pw = getpwnam(username)) == NULL) { + logmsg(LOG_ERR, "unknown user %s", username); + exit(1); + } + + if (chroot(pw->pw_dir) == -1) + fatal("chroot failed"); + + if (chdir("/")) + fatal("chdir failed"); + + if (setgroups(1, &pw->pw_gid) || + setegid(pw->pw_gid) || setgid(pw->pw_gid) || + seteuid(pw->pw_uid) || setuid(pw->pw_uid)) + fatal("can't drop privileges"); + + endpwent(); + return (0); +} + +static int +check_bind(const struct sockaddr *sa, socklen_t salen) +{ + const char *pname = child_pid ? "[priv]" : "[child]"; + in_port_t port; + + if (sa == NULL) { + logmsg(LOG_ERR, "%s: NULL address", pname); + return (1); + } + + if (sa->sa_len != salen) { + logmsg(LOG_ERR, "%s: length mismatch: %d %d", pname, + (int) sa->sa_len, (int) salen); + return (1); + } + + switch (sa->sa_family) { + case AF_INET: + if (salen != sizeof(struct sockaddr_in)) { + logmsg(LOG_ERR, "%s: Invalid inet address length", + pname); + return (1); + } + port = ((const struct sockaddr_in *)sa)->sin_port; + break; + case AF_INET6: + if (salen != sizeof(struct sockaddr_in6)) { + logmsg(LOG_ERR, "%s: Invalid inet6 address length", + pname); + return (1); + } + port = ((const struct sockaddr_in6 *)sa)->sin6_port; + break; + default: + logmsg(LOG_ERR, "%s: unknown address family", pname); + return (1); + } + + port = ntohs(port); + + if (port != NAMED_PORT_DEFAULT && port != RNDC_PORT_DEFAULT && + port != LWRES_PORT_DEFAULT) { + if (port || child_pid) + logmsg(LOG_ERR, "%s: disallowed port %u", pname, port); + return (1); + } + + return (0); +} + +static void +parent_bind(int fd) +{ + int sock, status; + struct sockaddr_storage ss; + socklen_t sslen; + int er; + + logmsg(LOG_DEBUG, "[priv]: msg PRIV_BIND received"); + + sock = receive_fd(fd); + must_read(fd, &sslen, sizeof(sslen)); + if (sslen == 0 || sslen > sizeof(ss)) + _exit(1); + + must_read(fd, &ss, sslen); + + if (check_bind((struct sockaddr *) &ss, sslen)) + _exit(1); + + status = bind(sock, (struct sockaddr *)&ss, sslen); + er = errno; + must_write(fd, &er, sizeof(er)); + must_write(fd, &status, sizeof(status)); + + close(sock); +} + +/* Bind to allowed privileged ports using privsep, or try to bind locally */ +int +isc_priv_bind(int fd, struct sockaddr *sa, socklen_t salen) +{ + int status, er; + + if (priv_fd < 0) + errx(1, "%s called from privileged portion", __func__); + + if (check_bind(sa, salen)) { + logmsg(LOG_DEBUG, "Binding locally"); + status = bind(fd, sa, salen); + } else { + logmsg(LOG_DEBUG, "Binding privsep"); + write_command(priv_fd, PRIV_BIND); + send_fd(priv_fd, fd); + must_write(priv_fd, &salen, sizeof(salen)); + must_write(priv_fd, sa, salen); + must_read(priv_fd, &er, sizeof(er)); + must_read(priv_fd, &status, sizeof(status)); + errno = er; + } + + return (status); +} + +/* If priv parent gets a TERM or HUP, pass it through to child instead */ +static void +sig_pass_to_chld(int sig) +{ + int save_err = errno; + + if (child_pid != -1) + kill(child_pid, sig); + errno = save_err; +} + + +/* When child dies, move into the shutdown state */ +static void +sig_got_chld(int sig) +{ + pid_t pid; + int status; + int save_err = errno; + + do { + pid = waitpid(child_pid, &status, WNOHANG); + } while (pid == -1 && errno == EINTR); + + if (pid == child_pid && (WIFEXITED(status) || WIFSIGNALED(status)) && + cur_state < STATE_QUIT) + cur_state = STATE_QUIT; + + errno = save_err; +} + +/* Read all data or return 1 for error. */ +int +may_read(int fd, void *buf, size_t n) +{ + char *s = buf; + ssize_t res; + size_t pos = 0; + + while (n > pos) { + res = read(fd, s + pos, n - pos); + switch (res) { + case -1: + if (errno == EINTR || errno == EAGAIN) + continue; + case 0: + return (1); + default: + pos += res; + } + } + return (0); +} + +/* Read data with the assertion that it all must come through, or + * else abort the process. Based on atomicio() from openssh. */ +void +must_read(int fd, void *buf, size_t n) +{ + char *s = buf; + ssize_t res; + size_t pos = 0; + + while (n > pos) { + res = read(fd, s + pos, n - pos); + switch (res) { + case -1: + if (errno == EINTR || errno == EAGAIN) + continue; + case 0: + _exit(0); + default: + pos += res; + } + } +} + +/* Write data with the assertion that it all has to be written, or + * else abort the process. Based on atomicio() from openssh. */ +void +must_write(int fd, const void *buf, size_t n) +{ + const char *s = buf; + ssize_t res; + size_t pos = 0; + + while (n > pos) { + res = write(fd, s + pos, n - pos); + switch (res) { + case -1: + if (errno == EINTR || errno == EAGAIN) + continue; + case 0: + _exit(0); + default: + pos += res; + } + } +} + + +/* write a command to the peer */ +static void +write_command(int fd, int cmd) +{ + must_write(fd, &cmd, sizeof(cmd)); +} + +static void +logmsg(int pri, const char *message, ...) +{ + va_list ap; + if (pri > debug_level) + return; + + va_start(ap, message); + if (log_stderr) { + vfprintf(stderr, message, ap); + fprintf(stderr, "\n"); + } else + vsyslog(pri, message, ap); + + va_end(ap); +} + +/* from bgpd */ +static void +fatal(const char *emsg) +{ + const char *pname; + + if (child_pid == -1) + pname = "bind"; + else if (child_pid) + pname = "bind [priv]"; + else + pname = "bind [child]"; + + if (emsg == NULL) + logmsg(LOG_CRIT, "fatal in %s: %s", pname, strerror(errno)); + else + if (errno) + logmsg(LOG_CRIT, "fatal in %s: %s: %s", + pname, emsg, strerror(errno)); + else + logmsg(LOG_CRIT, "fatal in %s: %s", pname, emsg); + + if (child_pid) + _exit(1); + else /* parent copes via SIGCHLD */ + exit(1); +} diff --git a/usr.sbin/bind/lib/isc/unix/privsep_fdpass.c b/usr.sbin/bind/lib/isc/unix/privsep_fdpass.c new file mode 100644 index 00000000000..4a99118aa71 --- /dev/null +++ b/usr.sbin/bind/lib/isc/unix/privsep_fdpass.c @@ -0,0 +1,116 @@ +/* $OpenBSD: privsep_fdpass.c,v 1.1 2004/03/12 18:40:15 canacar Exp $ */ + +/* + * Copyright 2001 Niels Provos + * All rights reserved. + * + * Copyright (c) 2002 Matthieu Herrb + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - 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 COPYRIGHT HOLDERS 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 + * COPYRIGHT HOLDERS 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 +#include +#include + +#include +#include +#include +#include + +#include + +void +send_fd(int sock, int fd) +{ + struct msghdr msg; + char tmp[CMSG_SPACE(sizeof(int))]; + struct cmsghdr *cmsg; + struct iovec vec; + int result = 0; + ssize_t n; + + memset(&msg, 0, sizeof(msg)); + + if (fd >= 0) { + msg.msg_control = (caddr_t)tmp; + msg.msg_controllen = CMSG_LEN(sizeof(int)); + cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_len = CMSG_LEN(sizeof(int)); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + *(int *)CMSG_DATA(cmsg) = fd; + } else + result = errno; + + vec.iov_base = &result; + vec.iov_len = sizeof(int); + msg.msg_iov = &vec; + msg.msg_iovlen = 1; + + if ((n = sendmsg(sock, &msg, 0)) == -1) + warn("%s: sendmsg(%d)", __func__, sock); + if (n != sizeof(int)) + warnx("%s: sendmsg: expected sent 1 got %ld", + __func__, (long)n); +} + +int +receive_fd(int sock) +{ + struct msghdr msg; + char tmp[CMSG_SPACE(sizeof(int))]; + struct cmsghdr *cmsg; + struct iovec vec; + ssize_t n; + int result; + int fd; + + memset(&msg, 0, sizeof(msg)); + vec.iov_base = &result; + vec.iov_len = sizeof(int); + msg.msg_iov = &vec; + msg.msg_iovlen = 1; + msg.msg_control = tmp; + msg.msg_controllen = sizeof(tmp); + + if ((n = recvmsg(sock, &msg, 0)) == -1) + warn("%s: recvmsg", __func__); + if (n != sizeof(int)) + warnx("%s: recvmsg: expected received 1 got %ld", + __func__, (long)n); + if (result == 0) { + cmsg = CMSG_FIRSTHDR(&msg); + if (cmsg->cmsg_type != SCM_RIGHTS) + warnx("%s: expected type %d got %d", __func__, + SCM_RIGHTS, cmsg->cmsg_type); + fd = (*(int *)CMSG_DATA(cmsg)); + return (fd); + } else { + errno = result; + return (-1); + } +} diff --git a/usr.sbin/bind/lib/isc/unix/socket.c b/usr.sbin/bind/lib/isc/unix/socket.c index ec0bd2cd8f1..3cfc215e735 100644 --- a/usr.sbin/bind/lib/isc/unix/socket.c +++ b/usr.sbin/bind/lib/isc/unix/socket.c @@ -44,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -229,6 +230,8 @@ struct isc_socketmgr { static isc_socketmgr_t *socketmgr = NULL; #endif /* ISC_PLATFORM_USETHREADS */ +static int privsep = 0; + #define CLOSED 0 /* this one must be zero */ #define MANAGED 1 #define CLOSE_PENDING 2 @@ -2800,7 +2803,9 @@ isc_socket_bind(isc_socket_t *sock, isc_sockaddr_t *sockaddr) { ISC_MSG_FAILED, "failed")); /* Press on... */ } - if (bind(sock->fd, &sockaddr->type.sa, sockaddr->length) < 0) { + if ((privsep ? + isc_priv_bind(sock->fd, &sockaddr->type.sa, sockaddr->length) : + bind(sock->fd, &sockaddr->type.sa, sockaddr->length)) < 0) { UNLOCK(&sock->lock); switch (errno) { case EACCES: @@ -2827,6 +2832,12 @@ isc_socket_bind(isc_socket_t *sock, isc_sockaddr_t *sockaddr) { return (ISC_R_SUCCESS); } +isc_result_t +isc_socket_privsep(int flag) { + privsep = flag; + return (ISC_R_SUCCESS); +} + /* * Set up to listen on a given socket. We do this by creating an internal * event that will be dispatched when the socket has read activity. The -- cgit v1.2.3