From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.1 (2015-04-28) on sa.local.altlinux.org X-Spam-Level: X-Spam-Status: No, score=-1.8 required=5.0 tests=BAYES_00,DKIM_SIGNED, T_DKIM_INVALID autolearn=no autolearn_force=no version=3.4.1 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=altlinux.org; s=dkim; h=Subject:Content-Transfer-Encoding:MIME-Version: References:In-Reply-To:Message-Id:Date:Cc:To:From:Sender:Reply-To: Content-Type:Content-ID:Content-Description:Resent-Date:Resent-From: Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID:List-Id:List-Help: List-Unsubscribe:List-Subscribe:List-Post:List-Owner:List-Archive; bh=UAVHWYogImRg7KD1DBILEEIi/KeLwXXA0gDVHj+813E=; b=dAMgPc4zJ/pCuF4onCz4qNDUud xzHmg+9HdB3+YshsWbKW+aAuP6nKfJ5xWMSbsjpCvnDof++YXReXK9TrK0YvrEw9pskSU7xMblI8y E14WpXm1skFhcMZK22cEKOfa1Nnydzmi1cig3/9IOBOz5S8yPLo3LzUM4isVy6uhT8XdBKFjYptWl 8gBOAqieIMlIskTdV38kYVhZF/qy+0hGUBIeGI5UJRyU4MOJmtjv7PrDTTmI/ST5m+0PBiiQEdkru l14EVfM8Syc9xxjHvU5pxiFe2ATUB2NHkehxQEFE4+hgRdW5gGWxnBaGj+6RpISwRHMz2M5qPIqOL jVxjOgMA==; From: Arseny Maslennikov To: devel@lists.altlinux.org Date: Tue, 24 Aug 2021 11:24:30 +0300 Message-Id: <20210824082436.1555890-2-arseny@altlinux.org> X-Mailer: git-send-email 2.32.0 In-Reply-To: <20210824082436.1555890-1-arseny@altlinux.org> References: <20210824082436.1555890-1-arseny@altlinux.org> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SA-Exim-Connect-IP: 10.15.13.28 X-SA-Exim-Mail-From: arseny@altlinux.org X-SA-Exim-Version: 4.2.1 X-SA-Exim-Scanned: No (on mail.cs.msu.ru); Unknown failure Cc: Arseny Maslennikov , Alexey Gladkov Subject: [devel] [PATCH hasher-priv v3 1/7] Turn hasher-priv into a daemon X-BeenThere: devel@lists.altlinux.org X-Mailman-Version: 2.1.12 Precedence: list Reply-To: ALT Linux Team development discussions List-Id: ALT Linux Team development discussions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Tue, 24 Aug 2021 08:25:06 -0000 Archived-At: List-Archive: List-Post: All the privileged operations are now performed by a daemonised system service. The daemon accepts IPC commands via a Unix domain socket, fulfills the request and sends back a response. The kernel provides a reliable way to obtain the sender's credentials, which are then verified. This allows to install hasher-privd without SUID bit to reduce attack surface and facilitate NO_NEW_PRIVS environments. When contacted by a client of user X, the daemon creates a separate session process that handles requests only from user X. This session process ends after a certain period of inactivity. Co-authored-by: Alexey Gladkov Signed-off-by: Arseny Maslennikov --- hasher-priv/.gitignore | 1 + hasher-priv/DESIGN | 281 ++++++++++++++-------- hasher-priv/Makefile | 28 ++- hasher-priv/caller.c | 81 ++++--- hasher-priv/caller_server.c | 350 ++++++++++++++++++++++++++++ hasher-priv/caller_task.c | 219 ++++++++++++++++++ hasher-priv/cmdline.c | 27 ++- hasher-priv/communication.c | 394 +++++++++++++++++++++++++++++++ hasher-priv/communication.h | 80 +++++++ hasher-priv/config.c | 147 +++++++++++- hasher-priv/daemon.conf | 13 ++ hasher-priv/epoll.c | 39 ++++ hasher-priv/epoll.h | 18 ++ hasher-priv/hasher-priv.c | 78 +++++++ hasher-priv/hasher-priv.spec | 3 + hasher-priv/hasher-privd.c | 437 +++++++++++++++++++++++++++++++++++ hasher-priv/io_log.c | 2 +- hasher-priv/io_x11.c | 2 +- hasher-priv/killuid.c | 2 +- hasher-priv/logging.c | 71 ++++++ hasher-priv/logging.h | 55 +++++ hasher-priv/main.c | 75 ------ hasher-priv/pass.c | 117 +++++++++- hasher-priv/pidfile.c | 129 +++++++++++ hasher-priv/pidfile.h | 44 ++++ hasher-priv/priv.h | 57 ++++- hasher-priv/sockets.c | 180 +++++++++++++++ hasher-priv/sockets.h | 32 +++ hasher-priv/x11.c | 1 + 29 files changed, 2715 insertions(+), 248 deletions(-) create mode 100644 hasher-priv/caller_server.c create mode 100644 hasher-priv/caller_task.c create mode 100644 hasher-priv/communication.c create mode 100644 hasher-priv/communication.h create mode 100644 hasher-priv/daemon.conf create mode 100644 hasher-priv/epoll.c create mode 100644 hasher-priv/epoll.h create mode 100644 hasher-priv/hasher-priv.c create mode 100644 hasher-priv/hasher-privd.c create mode 100644 hasher-priv/logging.c create mode 100644 hasher-priv/logging.h delete mode 100644 hasher-priv/main.c create mode 100644 hasher-priv/pidfile.c create mode 100644 hasher-priv/pidfile.h create mode 100644 hasher-priv/sockets.c create mode 100644 hasher-priv/sockets.h diff --git a/hasher-priv/.gitignore b/hasher-priv/.gitignore index 5ca24b0..8f0f658 100644 --- a/hasher-priv/.gitignore +++ b/hasher-priv/.gitignore @@ -4,6 +4,7 @@ getconf.sh getugid1.sh getugid2.sh hasher-priv +hasher-privd hasher-priv.8 hasher-priv.conf.5 hasher-useradd diff --git a/hasher-priv/DESIGN b/hasher-priv/DESIGN index 1470ce7..0fa6e1d 100644 --- a/hasher-priv/DESIGN +++ b/hasher-priv/DESIGN @@ -1,35 +1,63 @@ -Here is a hasher-priv (euid=root,egid=hashman,gid!=egid) control flow: +Here is the control flow of hasher-priv (euid=user,egid=hashman,gid!=egid): + sanitize file descriptors + set safe umask + ensure 0,1,2 are valid file descriptors + close all the rest + parse command line arguments - + check for non-zero argument list - + parse -h, --help and -number options - + caller_num initialized here - + parse arguments, abort if wrong - + chroot_path and chroot_argv are initialized here - + valid arguments are: - getconf - killuid - getugid1 - chrootuid1 [program args] - getugid2 - chrootuid2 [program args] ++ connect to $XDG_RUNTIME_DIR/hasher-priv socket + + wait for the creation of a session server + + close socket ++ connect to session server by $XDG_RUNTIME_DIR/hasher-priv-UID socket ++ send command to start new task + + receive a result code from the server ++ send current stdout and stderr to server + + receive a result code from the server ++ send task arguments + + receive a result code from the server ++ send environment variables + + receive a result code from the server ++ send command to run task + + receive a task result code from the server + +Here is the control flow of hasher-privd (euid=root,egid=hashman,gid==egid): ++ parse command line arguments + + parse -h and --help options ++ set safe umask ++ check pidfile, abort if server already running ++ if not a daemon already, daemonize + + change current working directory to "/" + + close all file descriptors ++ initialize logger ++ create pidfile ++ examine and change blocked signals ++ I/O event notification and add file descriptors + + create a file descriptor for accepting signals + + create and listen server socket ++ wait for incoming caller connections + + handle signal if signal is received + + close caller's session + + handle connection if the caller opened a new connection + + get connection credentials + + fork new process for caller if don't have any + + notify the client if the session server already running + + close caller connection ++ close all descriptors in notification poll ++ close and remove pidfile + +Here is control flow for session server: + initialize data related to caller - + caller_uid initialized here from getuid() + + caller_uid initialized here by uid received via the socket + caller_uid must be valid uid - + caller_gid initialized here from getgid() + + caller_gid initialized here by uid received via the socket + caller_gid must be valid gid - + caller_user initialized here from getpwnam(LOGNAME)->pw_name or - getpwuid(caller_uid)->pw_name + + caller_user initialized here from getpwuid(caller_uid)->pw_name + must be true: caller_uid == pw->pw_uid + must be true: caller_gid == pw->pw_gid + caller_home initialized here + caller_user's home directory must exist -+ read work limit hints from environment variables -+ drop all environment variables ++ set safe umask ++ open a caller-specific socket + load configuration + safe chdir to /etc/hasher-priv + safe load "system" file @@ -40,7 +68,7 @@ Here is a hasher-priv (euid=root,egid=hashman,gid!=egid) control flow: prefix umask nice - allowed_devices + allow_ttydev allowed_mountpoints rlimit_(hard|soft)_* wlimit_(time_elapsed|time_idle|bytes_written) @@ -53,85 +81,136 @@ Here is a hasher-priv (euid=root,egid=hashman,gid!=egid) control flow: + change_user1 and change_user2 should be initialized here + change_uid1 and change_gid1 initialized from change_user1 + change_uid2 and change_gid2 initialized from change_user2 -+ execute choosen task - + getconf: print config file /etc/hasher-priv/user.d/caller_user[:caller_num] - + getugid1: print change_uid1:change_gid1 pair - + getugid2: print change_uid2:change_gid2 pair - + killuid - + check for valid uids specified - + drop dumpable flag (just in case - not required) - + setuid to specified uid pair - + kill (-1, SIGKILL) - + purge all SYSV IPC objects belonging to specified uid pair - + chrootuid1/chrootuid2 - + check for valid uid specified - + setup mounts and devices - + unshare mount namespace - + safe chdir to chroot_path - + safe chdir to dev - + mount dev - + create devices available to all users: null, zero, full, random, urandom - + if makedev_console environment variable is true, - create devices available to root only: console, tty0, fb0 - + mount /dev/shm - + mount all mountpoints specified by requested_mountpoints environment variable - + if /dev/pts was mounted, create devices: tty, ptmx - + safe chdir to chroot_path - + sanitize file descriptors again - + if use_pty is disabled, create pipe to handle child's stdout and stderr - + if X11 forwarding is requested, create socketpair and - open /tmp/.X11-unix directory readonly for later use with fchdir() - + unless share_ipc is enabled, isolate System V IPC namespace - + unless share_uts is enabled, unshare UTS namespace - + if X11 forwarding to a tcp address was not requested, - unless share_network is enabled, unshare network - + clear supplementary group access list - + create pty: - + temporarily switch to called_uid:caller_gid - + open /dev/ptmx - + fetch pts number - + unlock pts pair - + open pts slave - + switch uid:gid back - + chroot to "." - + create another pty if possible: - + temporarily switch to called_uid:caller_gid - + safely chdir to "dev" - + if "pts/ptmx" is available with right permissions: - + replace "ptmx" with a symlink to "pts/ptmx" - + open "ptmx" - + fetch pts number - + unlock pts pair - + open pts slave - + chdir back to "/" - + switch uid:gid back - + if another pty was crated, close pts pair that was opened earlier - + set rlimits - + set close-on-exec flag on all non-standard descriptors - + fork - + in parent: - + setgid/setuid to caller user - + install CHLD signal handler - + unblock master pty and pipe descriptors - + if use_pty is enabled, initialize tty and install WINCH signal handler - + listen to "/dev/log" - + while work limits are not exceeded, handle child input/output - + close master pty descriptor, thus sending HUP to child session - + wait for child process termination - + remove CHLD signal handler - + return child proccess exit code - + in child: - + if X11 forwarding to a tcp address was requested, ++ I/O event notification and add file descriptors + + create a file descriptor for accepting signals + + create and listen the session socket ++ notify the client that the session server is ready ++ wait for incomming caller connections + + handle signal if signal is received + + handle connection if the caller opened a new connection + + task handler + + reset timeout timer + + finish the server if the task doesn't arrive from the caller + for more than a minute. + +Here is control flow for the task handler: ++ receive task header + + receive client's stdin, stdout and stderr + + check number of arguments ++ receive task arguments if we expect them ++ receive environment variables if client want to send them ++ fork process to handle task + + in parent: + + wait exit status + + in child: + + replace stdin, stdout and stderr with those that were received + + drop all environment variables + + apply the client's environment variables + + sanitize file descriptors + + set safe umask + + ensure 0,1,2 are valid file descriptors + + close all the rest + + parse task arguments + + check for non-zero argument list + + parse -h, --help and -number options + + caller_num initialized here + + parse arguments, abort if wrong + + chroot_path and chroot_argv are initialized here + + valid arguments are: + getconf + killuid + getugid1 + chrootuid1 [program args] + getugid2 + chrootuid2 [program args] + + caller_num initialized from task + + chroot_path and chroot_argv are initialized here + + read work limit hints from environment variables + + drop all environment variables + + execute choosen task + + getconf: print config file /etc/hasher-priv/user.d/caller_user[:caller_num] + + getugid1: print change_uid1:change_gid1 pair + + getugid2: print change_uid2:change_gid2 pair + + killuid + + check for valid uids specified + + drop dumpable flag (just in case - not required) + + setuid to specified uid pair + + kill (-1, SIGKILL) + + purge all SYSV IPC objects belonging to specified uid pair + + chrootuid1/chrootuid2 + + fork to kill the processes that were left from the previous chrootuid call + + in parent: + + wait exit status + + in child: + + killuid + + check for valid uid specified + + setup mounts and devices + + unshare mount namespace + + safe chdir to chroot_path + + safe chdir to dev + + mount dev + + create devices available to all users: null, zero, full, random, urandom + + if makedev_console environment variable is true, + create devices available to root only: console, tty0, fb0 + + mount /dev/shm + + mount all mountpoints specified by requested_mountpoints environment variable + + if /dev/pts was mounted, create devices: tty, ptmx + + safe chdir to chroot_path + + sanitize file descriptors again + + if use_pty is disabled, create pipe to handle child's stdout and stderr + + if X11 forwarding is requested, create socketpair and + open /tmp/.X11-unix directory readonly for later use with fchdir() + + unless share_ipc is enabled, isolate System V IPC namespace + + unless share_uts is enabled, unshare UTS namespace + + if X11 forwarding to a tcp address was not requested, unless share_network is enabled, unshare network - + setgid/setuid to specified user - + setsid - + change controlling terminal to pty - + redirect stdin if required, either to null or to pty - + redirect stdout and stderr either to pipe or to pty - + set nice - + if X11 forwarding is requested, - + add X11 auth entry using xauth utility - + create and bind unix socket for X11 forwarding - + send listening descriptor and fake auth data to the parent - + set umask - + execute specified program + + clear supplementary group access list + + create pty: + + temporarily switch to called_uid:caller_gid + + open /dev/ptmx + + fetch pts number + + unlock pts pair + + open pts slave + + switch uid:gid back + + chroot to "." + + create another pty if possible: + + temporarily switch to called_uid:caller_gid + + safely chdir to "dev" + + if "pts/ptmx" is available with right permissions: + + replace "ptmx" with a symlink to "pts/ptmx" + + open "ptmx" + + fetch pts number + + unlock pts pair + + open pts slave + + chdir back to "/" + + switch uid:gid back + + if another pty was crated, close pts pair that was opened earlier + + set rlimits + + set close-on-exec flag on all non-standard descriptors + + fork + + in parent: + + setgid/setuid to caller user + + install CHLD signal handler + + unblock master pty and pipe descriptors + + if use_pty is enabled, initialize tty and install WINCH signal handler + + listen to "/dev/log" + + while work limits are not exceeded, handle child input/output + + close master pty descriptor, thus sending HUP to child session + + wait for child process termination + + remove CHLD signal handler + + return child proccess exit code + + in child: + + if X11 forwarding to a tcp address was requested, + unless share_network is enabled, unshare network + + setgid/setuid to specified user + + setsid + + change controlling terminal to pty + + redirect stdin if required, either to null or to pty + + redirect stdout and stderr either to pipe or to pty + + set nice + + if X11 forwarding is requested, + + add X11 auth entry using xauth utility + + create and bind unix socket for X11 forwarding + + send listening descriptor and fake auth data to the parent + + set umask + + execute specified program diff --git a/hasher-priv/Makefile b/hasher-priv/Makefile index a029a63..8ccf99d 100644 --- a/hasher-priv/Makefile +++ b/hasher-priv/Makefile @@ -11,7 +11,7 @@ VERSION = $(shell sed '/^Version: */!d;s///;q' hasher-priv.spec) HELPERS = getconf.sh getugid1.sh chrootuid1.sh getugid2.sh chrootuid2.sh MAN5PAGES = $(PROJECT).conf.5 MAN8PAGES = $(PROJECT).8 hasher-useradd.8 -TARGETS = $(PROJECT) hasher-useradd $(HELPERS) $(MAN5PAGES) $(MAN8PAGES) +TARGETS = $(PROJECT) hasher-privd hasher-useradd $(HELPERS) $(MAN5PAGES) $(MAN8PAGES) have-cc-function = $(shell echo 'extern void $(1)(void); int main () { $(1)(); return 0; }' |$(CC) -o /dev/null -xc - > /dev/null 2>&1 && echo "-D$(2)") @@ -23,6 +23,7 @@ man5dir = $(mandir)/man5 man8dir = $(mandir)/man8 configdir = $(sysconfdir)/$(PROJECT) helperdir = $(libexecdir)/$(PROJECT) +socketdir = /run/hasher-priv DESTDIR = MKDIR_P = mkdir -p @@ -36,17 +37,25 @@ WARNINGS = -Wall -W -Wshadow -Wpointer-arith -Wwrite-strings \ -Wmissing-format-attribute -Wredundant-decls -Wdisabled-optimization CPPFLAGS = -std=gnu99 -D_GNU_SOURCE $(CHDIRUID_FLAGS) \ $(call have-cc-function,close_range,HAVE_CLOSE_RANGE) \ - $(LFS_CFLAGS) -DPROJECT_VERSION=\"$(VERSION)\" + $(LFS_CFLAGS) -DPROJECT_VERSION=\"$(VERSION)\" \ + -DSOCKETDIR=\"$(socketdir)\" -DPROJECT=\"$(PROJECT)\" CFLAGS = -pipe -O2 override CFLAGS += $(WARNINGS) LDLIBS = -SRC = caller.c chdir.c chdiruid.c chid.c child.c chrootuid.c cmdline.c \ +SRC = hasher-priv.c cmdline.c fds.c sockets.c logging.c communication.c xmalloc.c pass.c +OBJ = $(SRC:.c=.o) + +server_SRC = hasher-privd.c \ + communication.c epoll.c pidfile.c logging.c sockets.c \ + caller.c caller_server.c caller_task.c \ + chdir.c chdiruid.c chid.c child.c chrootuid.c cmdline.c \ config.c fds.c getconf.c getugid.c ipc.c killuid.c io_log.c io_x11.c \ - main.c makedev.c mount.c net.c parent.c pass.c pty.c signal.c tty.c \ + makedev.c mount.c net.c parent.c pass.c pty.c signal.c tty.c \ unshare.c xmalloc.c x11.c -OBJ = $(SRC:.c=.o) -DEP = $(SRC:.c=.d) +server_OBJ = $(server_SRC:.c=.o) + +DEP = $(SRC:.c=.d) $(server_SRC:.c=.d) .PHONY: all install clean indent @@ -55,14 +64,19 @@ all: $(TARGETS) $(PROJECT): $(OBJ) $(LINK.o) $^ $(LOADLIBES) $(LDLIBS) -o $@ +hasher-privd: $(server_OBJ) + $(LINK.o) $^ $(LOADLIBES) $(LDLIBS) -o $@ + install: all $(MKDIR_P) -m710 $(DESTDIR)$(configdir)/user.d $(INSTALL) -p -m640 fstab $(DESTDIR)$(configdir)/fstab $(INSTALL) -p -m640 system.conf $(DESTDIR)$(configdir)/system + $(INSTALL) -p -m640 daemon.conf $(DESTDIR)$(configdir)/daemon.conf $(MKDIR_P) -m750 $(DESTDIR)$(helperdir) $(INSTALL) -p -m700 $(PROJECT) $(DESTDIR)$(helperdir)/ $(INSTALL) -p -m755 $(HELPERS) $(DESTDIR)$(helperdir)/ $(MKDIR_P) -m755 $(DESTDIR)$(sbindir) + $(INSTALL) -p -m755 hasher-privd $(DESTDIR)$(sbindir)/ $(INSTALL) -p -m755 hasher-useradd $(DESTDIR)$(sbindir)/ $(MKDIR_P) -m755 $(DESTDIR)$(man5dir) $(INSTALL) -p -m644 $(MAN5PAGES) $(DESTDIR)$(man5dir)/ @@ -70,7 +84,7 @@ install: all $(INSTALL) -p -m644 $(MAN8PAGES) $(DESTDIR)$(man8dir)/ clean: - $(RM) $(TARGETS) $(DEP) $(OBJ) core *~ + $(RM) $(TARGETS) $(DEP) $(OBJ) $(server_OBJ) core *~ indent: indent *.h *.c diff --git a/hasher-priv/caller.c b/hasher-priv/caller.c index e83084a..29f141c 100644 --- a/hasher-priv/caller.c +++ b/hasher-priv/caller.c @@ -19,62 +19,67 @@ #include "priv.h" #include "xmalloc.h" +#include "logging.h" -const char *caller_user, *caller_home; -uid_t caller_uid; -gid_t caller_gid; +char *caller_user = NULL; +char *caller_home = NULL; +uid_t caller_uid; +gid_t caller_gid; /* * Initialize caller_user, caller_uid, caller_gid and caller_home. */ -void -init_caller_data(void) +int +init_caller_data(uid_t uid, gid_t gid) { - const char *logname; struct passwd *pw = 0; - caller_uid = getuid(); - if (caller_uid < MIN_CHANGE_UID) - error(EXIT_FAILURE, 0, "caller has invalid uid: %u", - caller_uid); - - caller_gid = getgid(); - if (caller_gid < MIN_CHANGE_GID) - error(EXIT_FAILURE, 0, "caller has invalid gid: %u", - caller_gid); - - if ((logname = getenv("LOGNAME"))) - if (!*logname || strchr(logname, ':')) - logname = 0; - - if (logname) - { - pw = getpwnam(logname); - if (caller_uid != pw->pw_uid || caller_gid != pw->pw_gid) - pw = 0; + caller_uid = uid; + if (caller_uid < MIN_CHANGE_UID) { + err("caller has invalid uid: %u", caller_uid); + return -1; + } + + caller_gid = gid; + if (caller_gid < MIN_CHANGE_GID) { + err("caller has invalid gid: %u", caller_gid); + return -1; } - if (!pw) - pw = getpwuid(caller_uid); + pw = getpwuid(caller_uid); - if (!pw || !pw->pw_name) - error(EXIT_FAILURE, 0, "caller lookup failure"); + if (!pw || !pw->pw_name) { + err("caller pwent lookup failure"); + return -1; + } caller_user = xstrdup(pw->pw_name); - if (caller_uid != pw->pw_uid) - error(EXIT_FAILURE, 0, "caller %s: uid mismatch", - caller_user); + if (caller_uid != pw->pw_uid) { + err("caller %s: uid does not match pwent", caller_user); + return -1; + } - if (caller_gid != pw->pw_gid) - error(EXIT_FAILURE, 0, "caller %s: gid mismatch", - caller_user); + if (caller_gid != pw->pw_gid) { + err("caller %s: gid does not match pwent", caller_user); + return -1; + } errno = 0; if (pw->pw_dir && *pw->pw_dir) caller_home = canonicalize_file_name(pw->pw_dir); - if (!caller_home || !*caller_home) - error(EXIT_FAILURE, errno, "caller %s: invalid home", - caller_user); + if (!caller_home || !*caller_home) { + err("caller %s has invalid home: %m", caller_user); + return -1; + } + + return 0; +} + +void +free_caller_data(void) +{ + free(caller_user); + free(caller_home); } diff --git a/hasher-priv/caller_server.c b/hasher-priv/caller_server.c new file mode 100644 index 0000000..2e9ef9c --- /dev/null +++ b/hasher-priv/caller_server.c @@ -0,0 +1,350 @@ + +/* + Copyright (C) 2019 Alexey Gladkov + + The per-calling-user server part of the hasher-privd program. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/* Code in this file may be executed with root privileges. */ + +#include + +#include /* SOCK_CLOEXEC */ +#include +#include +#include + +#include +#include +#include +#include + +#include "priv.h" +#include "xmalloc.h" +#include "sockets.h" +#include "logging.h" +#include "epoll.h" +#include "communication.h" + +static char socketpath[UNIX_PATH_MAX]; + +static int +validate_arguments(task_t task, char **argv) +{ + int required_args = 0, more_args = 0; + int rc = -1; + int argc = 0; + + while (argv && argv[argc]) + argc++; + + switch (task) { + case TASK_GETCONF: + case TASK_KILLUID: + case TASK_GETUGID1: + case TASK_GETUGID2: + required_args = 0; + break; + case TASK_CHROOTUID1: + case TASK_CHROOTUID2: + more_args = 1; + required_args = 2; + break; + default: + err("unknown task type: %u", task); + return rc; + } + + if (argc < 0) + err("number of arguments must be a positive but got %d", argc); + + else if (argc < required_args) + err("%s task requires at least %d arguments but got %d", + task2str(task), required_args, argc); + + else if (argc > required_args && !more_args) + err("%s task requires exactly %d arguments but got %d", + task2str(task), required_args, argc); + + else + rc = 0; + + return rc; +} + +static void +free_task(struct task *task) +{ + if (task->stdin > 0) + close(task->stdin); + if (task->stdout > 0) + close(task->stdout); + if (task->stderr > 0) + close(task->stderr); + + if (task->env) { + free(task->env[0]); + free(task->env); + } + + if (task->argv) { + free(task->argv[0]); + free(task->argv); + } +} + +static int +cancel_task(int conn, struct task *task) +{ + free_task(task); + send_command_response(conn, CMD_STATUS_FAILED, "command failed"); + return 0; +} + +static int +receive_task_request(struct hadaemon* d, int conn) +{ + uid_t uid; + gid_t gid; + + if (get_peercred(conn, NULL, &uid, &gid) < 0) + return -1; + + if (caller_uid != uid || caller_gid != gid) { + err("user (uid=%d) has no permission to send commands" + " to the session of another user (uid=%d)", + uid, caller_uid); + return -1; + } + + struct task task = { 0 }; + int fds[3]; + + while (1) { + task_t type = TASK_NONE; + struct cmd hdr = { 0 }; + + if (xrecvmsg(conn, &hdr, sizeof(hdr)) < 0) + return cancel_task(conn, &task); + + switch (hdr.type) { + case CMD_TASK_BEGIN: + if (hdr.datalen != sizeof(type)) + return cancel_task(conn, &task); + + if (xrecvmsg(conn, &type, hdr.datalen) < 0) + return cancel_task(conn, &task); + + task.type = type; + + break; + + case CMD_TASK_FDS: + if (hdr.datalen != sizeof(int) * 3) + return cancel_task(conn, &task); + + if (task.stdin) + close(task.stdin); + + if (task.stdout) + close(task.stdout); + + if (task.stderr) + close(task.stderr); + + if (fds_recv(conn, fds, 3) < 0) + return cancel_task(conn, &task); + + task.stdin = fds[0]; + task.stdout = fds[1]; + task.stderr = fds[2]; + + break; + + case CMD_TASK_ARGUMENTS: + if (task.argv) { + free(task.argv[0]); + free(task.argv); + } + + if (recv_list(conn, &task.argv, hdr.datalen) < 0) + return cancel_task(conn, &task); + + if (validate_arguments(task.type, task.argv) < 0) + return cancel_task(conn, &task); + + break; + + case CMD_TASK_ENVIRON: + if (task.env) { + free(task.env[0]); + free(task.env); + } + + if (recv_list(conn, &task.env, hdr.datalen) < 0) + return cancel_task(conn, &task); + + break; + + case CMD_TASK_RUN: + process_cmd_task_run(d, conn, &task); + free_task(&task); + /* process_caller_task() sends a response by itself. */ + return 0; + + default: + err("unsupported command: %d", hdr.type); + } + + send_command_response(conn, CMD_STATUS_DONE, NULL); + } + + return 0; +} + +int +caller_server_listener_init(struct hadaemon* sdae) +{ + int ret = 0; + sdae->fd_ep = -1; + sdae->fd_signal = -1; + sdae->fd_conn = -1; + + umask(077); + + snprintf(socketpath, sizeof(socketpath), + "%s/%d:%u", + SOCKETDIR, caller_uid, caller_num); + + if ((sdae->fd_conn = srv_listen(socketpath)) < 0) + return -1; + + if (chown(socketpath, caller_uid, caller_gid)) { + err("fchown: %s: %m", socketpath); + ret = -1; + goto fail; + } + + /* Load config according to caller information. */ + configure(); + + sigset_t mask; + + sigfillset(&mask); + sigprocmask(SIG_SETMASK, &mask, NULL); + + if ((sdae->fd_signal = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC)) < 0) { + err("signalfd: %m"); + ret = -1; + goto fail; + } + + if ((sdae->fd_ep = epoll_create1(EPOLL_CLOEXEC)) < 0) { + err("epoll_create1: %m"); + ret = -1; + goto fail; + } + + if (epollin_add(sdae->fd_ep, sdae->fd_signal) < 0 || epollin_add(sdae->fd_ep, sdae->fd_conn) < 0) { + err("epollin_add: failed"); + ret = -1; + goto fail; + } + + return ret; + +fail: + epollin_remove(sdae->fd_ep, sdae->fd_signal); + epollin_remove(sdae->fd_ep, sdae->fd_conn); + + if (sdae->fd_ep >= 0) + close(sdae->fd_ep); + if (sdae->fd_signal >= 0) + close(sdae->fd_signal); + + unlink(socketpath); + + return ret; +} + +int +caller_server(struct hadaemon* sdae) +{ + int ret = 0; + + unsigned long nsec = 0; + int finish_server = 0; + + while (!finish_server) { + struct epoll_event ev[42]; + int fdcount; + + errno = 0; + if ((fdcount = epoll_wait(sdae->fd_ep, ev, ARRAY_SIZE(ev), 1000)) < 0) { + if (errno == EINTR) + continue; + err("epoll_wait: %m"); + break; + } + + if (fdcount == 0) { + nsec++; + + if (nsec >= server_session_timeout) + break; + + } else for (int i = 0; i < fdcount; i++) { + if (!(ev[i].events & EPOLLIN)) { + continue; + + } else if (ev[i].data.fd == sdae->fd_signal) { + struct signalfd_siginfo fdsi; + ssize_t size; + + size = read_retry(sdae->fd_signal, &fdsi, sizeof(fdsi)); + + if (size != sizeof(fdsi)) { + err("unable to read signal info from signalfd"); + continue; + } + + int status; + + switch (fdsi.ssi_signo) { + case SIGINT: + case SIGTERM: + finish_server = 1; + break; + case SIGCHLD: + if (waitpid(-1, &status, 0) < 0) + err("waitpid: %m"); + break; + } + + } else if (ev[i].data.fd == sdae->fd_conn) { + int conn; + + if ((conn = accept4(sdae->fd_conn, NULL, 0, SOCK_CLOEXEC)) < 0) { + err("accept4: %m"); + continue; + } + + if (set_recv_timeout(conn, 3) < 0) { + close(conn); + continue; + } + + if (!receive_task_request(sdae, conn)) { + /* reset timer */ + nsec = 0; + } + + close(conn); + } + } + } + + return ret; +} diff --git a/hasher-priv/caller_task.c b/hasher-priv/caller_task.c new file mode 100644 index 0000000..7a9fc98 --- /dev/null +++ b/hasher-priv/caller_task.c @@ -0,0 +1,219 @@ + +/* + Copyright (C) 2019 Alexey Gladkov + + The task handling part of the hasher-privd program. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "communication.h" +#include "xmalloc.h" +#include "logging.h" +#include "sockets.h" +#include "priv.h" + +/* Code in this function may be executed with root privileges. */ +static int +killprevious(void) +{ + int rc = 0; + pid_t pid = fork(); + + if (pid < 0) { + err("fork: %m"); + return -1; + } + + if (!pid) + exit(do_killuid()); + + while (1) { + int wstatus; + + if (waitpid(pid, &wstatus, WUNTRACED | WCONTINUED) < 0) { + err("waitpid: %m"); + rc = -1; + break; + } + + if (WIFEXITED(wstatus)) { + rc = WEXITSTATUS(wstatus); + break; + } + } + + return rc; +} + +/* Code in this function may be executed with root privileges. */ +static int +reopen_fd(int oldfd, int newfd) +{ + if (oldfd < 0) + return 0; + + close(newfd); + + if (dup2(oldfd, newfd) < 0) { + err("dup2: %m"); + return -1; + } + + close(oldfd); + + return 0; +} + +/* Code in this function may be executed with root privileges. */ +static int +reopen_iostreams(int stdin, int stdout, int stderr) +{ + return (reopen_fd(stdin, STDIN_FILENO) < 0 || + reopen_fd(stdout, STDOUT_FILENO) < 0 || + reopen_fd(stderr, STDERR_FILENO) < 0) ? -1 : 0; +} + +/* Code in this function may be executed with root privileges. */ +static int +spawn_caller_task(struct task *task) +{ + int rc = EXIT_FAILURE; + int i = 0; + pid_t pid; + + pid = fork(); + if (pid < 0) { + err("fork: %m"); + return -1; + } + if (pid != 0) { + return pid; + } + + if ((rc = reopen_iostreams(task->stdin, task->stdout, task->stderr)) < 0) + exit(rc); + + /* clean up environment to avoid side effects. */ + if (clearenv() != 0) + fatal("clearenv: %m"); + + while (task->env && task->env[i]) { + if (putenv(task->env[i++]) != 0) + fatal("putenv: %m"); + } + + /* First, check and sanitize file descriptors. */ + sanitize_fds(); + + /* Second, parse task arguments. */ + parse_task_args(task->type, (const char **)task->argv); + + /* Third, ensure correctness of chroot path. */ + if (chroot_path && *chroot_path != '/') + fatal("%s: invalid chroot path", chroot_path); + + /* Fourth, parse environment for config options. */ + parse_env(); + + /* We won't need environment variables any longer. */ + if (clearenv() != 0) + fatal("clearenv: %m"); + + /* Finally, execute the requested task. */ + switch (task->type) { + case TASK_GETCONF: + rc = do_getconf(); + break; + case TASK_KILLUID: + rc = do_killuid(); + break; + case TASK_GETUGID1: + rc = do_getugid1(); + break; + case TASK_CHROOTUID1: + rc = !killprevious() + ? do_chrootuid1() + : EXIT_FAILURE; + break; + case TASK_GETUGID2: + rc = do_getugid2(); + break; + case TASK_CHROOTUID2: + rc = !killprevious() + ? do_chrootuid2() + : EXIT_FAILURE; + break; + default: + fatal("unknown task %d", task->type); + } + + /* Write out all the user-space-buffered data */ + fflush(stdout); + fflush(stderr); + + exit(rc); +} + +/* Code in this function may be executed with root privileges. */ +pid_t +process_cmd_task_run(struct hadaemon* d, int conn, struct task *task) +{ + int rc = EXIT_FAILURE; + pid_t pid, cpid; + + pid = fork(); + if (pid < 0) { + err("fork: %m"); + return -1; + } + if (pid > 0) { + return pid; + } + + if (d->fd_ep >= 0) + close(d->fd_ep); + if (d->fd_signal >= 0) + close(d->fd_signal); + if (d->fd_conn >= 0) + close(d->fd_conn); + + if ((cpid = spawn_caller_task(task)) > 0) { + while (1) { + pid_t w; + int wstatus; + + if ((w = waitpid(cpid, &wstatus, WUNTRACED | WCONTINUED)) < 0) { + err("waitpid: %m"); + break; + } + + if (WIFEXITED(wstatus)) { + rc = WEXITSTATUS(wstatus); + info("%s: process %d exited, status=%d", task2str(task->type), cpid, WEXITSTATUS(wstatus)); + break; + } + + if (WIFSIGNALED(wstatus)) { + info("%s: process %d killed by signal %d", task2str(task->type), cpid, WTERMSIG(wstatus)); + break; + } + } + } + + /* Notify the client about the results. */ + (rc == EXIT_FAILURE) + ? send_command_response(conn, CMD_STATUS_FAILED, "command failed") + : send_command_response(conn, CMD_STATUS_DONE, NULL); + + exit(rc); +} diff --git a/hasher-priv/cmdline.c b/hasher-priv/cmdline.c index 1cdfe23..aba802f 100644 --- a/hasher-priv/cmdline.c +++ b/hasher-priv/cmdline.c @@ -80,6 +80,8 @@ const char *chroot_path; const char **chroot_argv; unsigned caller_num; +const char **task_args; + static unsigned get_caller_num(const char *str) { @@ -126,40 +128,57 @@ parse_cmdline(int argc, const char *argv[]) if (ac < 1) show_usage("insufficient arguments"); + task_args = NULL; + if (!strcmp("getconf", av[0])) { if (ac != 1) show_usage("%s: invalid usage", av[0]); + task_args = av + 1; return TASK_GETCONF; } else if (!strcmp("killuid", av[0])) { if (ac != 1) show_usage("%s: invalid usage", av[0]); + task_args = av + 1; return TASK_KILLUID; } else if (!strcmp("getugid1", av[0])) { if (ac != 1) show_usage("%s: invalid usage", av[0]); + task_args = av + 1; return TASK_GETUGID1; } else if (!strcmp("chrootuid1", av[0])) { if (ac < 3) show_usage("%s: invalid usage", av[0]); - chroot_path = av[1]; - chroot_argv = av + 2; + task_args = av + 1; return TASK_CHROOTUID1; } else if (!strcmp("getugid2", av[0])) { if (ac != 1) show_usage("%s: invalid usage", av[0]); + task_args = av + 1; return TASK_GETUGID2; } else if (!strcmp("chrootuid2", av[0])) { if (ac < 3) show_usage("%s: invalid usage", av[0]); - chroot_path = av[1]; - chroot_argv = av + 2; + task_args = av + 1; return TASK_CHROOTUID2; } else show_usage("%s: invalid argument", av[0]); } + +void +parse_task_args(task_t task, const char *argv[]) +{ + switch (task) { + case TASK_CHROOTUID1: + case TASK_CHROOTUID2: + chroot_path = argv[0]; + chroot_argv = (const char **)argv + 1; + default: + break; + } +} diff --git a/hasher-priv/communication.c b/hasher-priv/communication.c new file mode 100644 index 0000000..982a527 --- /dev/null +++ b/hasher-priv/communication.c @@ -0,0 +1,394 @@ + +/* + Copyright (C) 2019 Alexey Gladkov + + This file offers some helper functions for client-server communication. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include + +#include +#include +#include +#include +#include + +#include "priv.h" +#include "logging.h" +#include "xmalloc.h" +#include "sockets.h" +#include "communication.h" + +struct cmd_resp { + cmd_status_t status; + size_t msglen; +}; + +#define taskbuflen 20 +static char taskbuf[taskbuflen]; + +static const struct cm { + const char *name; + task_t value; +} taskmap[] = { + { "none", TASK_NONE }, + { "getconf", TASK_GETCONF }, + { "killuid", TASK_KILLUID }, + { "getugid1", TASK_GETUGID1 }, + { "getugid2", TASK_GETUGID2 }, + { "chrootuid1", TASK_CHROOTUID1 }, + { "chrootuid2", TASK_CHROOTUID2 } +}; + +static const size_t taskmap_size = ARRAY_SIZE(taskmap); + +const char *main_socket_base_name = "daemon"; + +char * +task2str(task_t type) +{ + size_t i; + + for (i = 0; i < taskmap_size; i++) { + if (taskmap[i].value == type) + return strncpy(taskbuf, taskmap[i].name, taskbuflen - 1); + } + return NULL; +} + +task_t +str2task(char *s) +{ + size_t i; + + for (i = 0; i < taskmap_size; i++) { + if (!strcmp(taskmap[i].name, s)) + return taskmap[i].value; + } + return TASK_NONE; +} + +static int +send_list(int conn, cmd_t cmd, const char **argv) +{ + int i = 0; + struct cmd hdr = { .type = cmd }; + + while (argv && argv[i]) + hdr.datalen += strlen(argv[i++]) + 1; + + if (xsendmsg(conn, &hdr, sizeof(hdr)) < 0) + return -1; + + i = 0; + while (argv && argv[i]) { + if (xsendmsg(conn, (char *)argv[i], strlen(argv[i]) + 1) < 0) + return -1; + i++; + } + + return 0; +} + +int +recv_list(int conn, char ***argv, size_t datalen) +{ + char *args = calloc(1UL, datalen); + if (!args) { + err("calloc: %m"); + return -1; + } + + if (datalen > 0 && xrecvmsg(conn, args, datalen) < 0) { + free(args); + return -1; + } + + if (!argv) { + free(args); + return 0; + } + + size_t i = 0, n = 0; + + while (args && n < datalen) { + i++; + n += strnlen(args + n, datalen - n) + 1; + } + + char **av = malloc(sizeof(char *) * (i + 1)); + if (!av) { + err("malloc: %m"); + return -1; + } + av[i] = NULL; + + i = n = 0; + + while (args && n < datalen) { + av[i++] = args + n; + n += strnlen(args + n, datalen - n) + 1; + } + + *argv = av; + + return 0; +} + +long int +send_command_response(int conn, cmd_status_t retcode, const char *fmt, ...) +{ + va_list ap; + + struct cmd_resp rs = { 0 }; + struct iovec iov = { 0 }; + struct msghdr msg = { 0 }; + + rs.status = retcode; + rs.msglen = 0; + + if (fmt && *fmt) { + int len; + + va_start(ap, fmt); + len = vsnprintf(NULL, 0, fmt, ap); + va_end(ap); + + if (len < 0) { + err("unable to calculate message size"); + return -1; + } + + rs.msglen = (size_t)len + 1; + } + + iov.iov_base = &rs; + iov.iov_len = sizeof(rs); + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + errno = 0; + if (sendmsg_retry(conn, &msg, MSG_NOSIGNAL) < 0) { + /* The client left without waiting for an answer */ + if (errno == EPIPE) + return 0; + + err("sendmsg: %m"); + return -1; + } + + if (!rs.msglen) + return 0; + + msg.msg_iov[0].iov_base = malloc(rs.msglen); + + if (!msg.msg_iov[0].iov_base) { + err("malloc: %m"); + return -1; + } + + msg.msg_iov[0].iov_len = rs.msglen; + + va_start(ap, fmt); + vsnprintf(msg.msg_iov[0].iov_base, msg.msg_iov[0].iov_len, fmt, ap); + va_end(ap); + + long int rc = 0; + errno = 0; + if (sendmsg_retry(conn, &msg, MSG_NOSIGNAL) < 0 && errno != EPIPE) { + err("sendmsg: %m"); + rc = -1; + } + + free(msg.msg_iov[0].iov_base); + + return rc; +} + +static int +recv_command_response(int conn, cmd_status_t *retcode, char **m) +{ + struct cmd_resp rs = { 0 }; + + if (xrecvmsg(conn, &rs, sizeof(rs)) < 0) + return -1; + + if (retcode) + *retcode = rs.status; + + if (!m || !rs.msglen) + return 0; + + char *x = xcalloc(1UL, rs.msglen); + if (xrecvmsg(conn, x, rs.msglen) < 0) + return -1; + + *m = x; + return 0; +} + +int +server_command(int conn, cmd_t cmd, const char **args) +{ + cmd_status_t status; + char *msg = NULL; + + if (send_list(conn, cmd, args) < 0) + return -1; + + if (recv_command_response(conn, &status, &msg) < 0) { + free(msg); + return -1; + } + + if (msg && *msg) { + err("%s", msg); + free(msg); + } + + return status == CMD_STATUS_FAILED ? -1 : 0; +} + +int +server_open_session(const char *dir_name, const char *file_name, unsigned num) +{ + int conn = srv_connect(dir_name, file_name); + + if (conn < 0) + return -1; + + struct cmd hdr = { + .type = CMD_OPEN_SESSION, + .datalen = sizeof(num), + }; + + if (xsendmsg(conn, &hdr, sizeof(hdr)) < 0) + return -1; + + if (xsendmsg(conn, &num, sizeof(num)) < 0) + return -1; + + cmd_status_t status; + char *msg = NULL; + + if (recv_command_response(conn, &status, &msg) < 0) { + free(msg); + return -1; + } + + close(conn); + + if (msg && *msg) { + err("%s", msg); + free(msg); + } + + return status == CMD_STATUS_FAILED ? -1 : 0; +} + +int +server_close_session(const char *dir_name, const char *file_name, unsigned num) +{ + int conn = srv_connect(dir_name, file_name); + + if (conn < 0) + return -1; + + struct cmd hdr = { + .type = CMD_CLOSE_SESSION, + .datalen = sizeof(num), + }; + + if (xsendmsg(conn, &hdr, sizeof(hdr)) < 0) + return -1; + + if (xsendmsg(conn, &num, sizeof(num)) < 0) + return -1; + + cmd_status_t status; + char *msg = NULL; + + if (recv_command_response(conn, &status, &msg) < 0) { + free(msg); + return -1; + } + + close(conn); + + if (msg && *msg) { + err("%s", msg); + free(msg); + } + + return status == CMD_STATUS_FAILED ? -1 : 0; +} + +int +server_task(int conn, task_t type) +{ + struct cmd hdr = { + .type = CMD_TASK_BEGIN, + .datalen = sizeof(type), + }; + + if (xsendmsg(conn, &hdr, sizeof(hdr)) < 0) + return -1; + + if (xsendmsg(conn, &type, hdr.datalen) < 0) + return -1; + + cmd_status_t status; + char *msg = NULL; + + if (recv_command_response(conn, &status, &msg) < 0) { + free(msg); + return -1; + } + + if (msg && *msg) { + err("%s", msg); + free(msg); + } + + return status == CMD_STATUS_FAILED ? -1 : 0; +} + +int +server_task_fds(int conn) +{ + cmd_status_t status; + char *msg = NULL; + + int myfds[3] = { + STDIN_FILENO, + STDOUT_FILENO, + STDERR_FILENO, + }; + + struct cmd hdr = { + .type = CMD_TASK_FDS, + .datalen = sizeof(myfds), + }; + + if (xsendmsg(conn, &hdr, sizeof(hdr)) < 0) + return -1; + + if (fds_send(conn, myfds, 3) < 0) + return -1; + + if (recv_command_response(conn, &status, &msg) < 0) { + free(msg); + return -1; + } + + if (msg && *msg) { + err("%s", msg); + free(msg); + } + + return status == CMD_STATUS_FAILED ? -1 : 0; +} diff --git a/hasher-priv/communication.h b/hasher-priv/communication.h new file mode 100644 index 0000000..9670593 --- /dev/null +++ b/hasher-priv/communication.h @@ -0,0 +1,80 @@ + +/* + Copyright (C) 2019 Alexey Gladkov + + This file offers some helper functions for client-server communication. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef _COMMUNICATION_H_ +#define _COMMUNICATION_H_ + +#include +#include + +extern const char *main_socket_base_name; + +typedef enum { + CMD_NONE = 0, + + /* Master commands */ + CMD_OPEN_SESSION, + CMD_CLOSE_SESSION, + + /* Session commands */ + CMD_TASK_BEGIN, + CMD_TASK_FDS, + CMD_TASK_ARGUMENTS, + CMD_TASK_ENVIRON, + CMD_TASK_RUN, + +} cmd_t; + +typedef enum { + CMD_STATUS_DONE = 0, + CMD_STATUS_FAILED, +} cmd_status_t; + +struct cmd { + cmd_t type; + size_t datalen; +}; + +typedef enum { + TASK_NONE = 0, + TASK_GETCONF, + TASK_KILLUID, + TASK_GETUGID1, + TASK_CHROOTUID1, + TASK_GETUGID2, + TASK_CHROOTUID2 +} task_t; + +struct task { + task_t type; + unsigned num; + int stdin; + int stdout; + int stderr; + char **argv; + char **env; +}; + +char *task2str(task_t type); +task_t str2task(char *s) __attribute__((nonnull(1))); + +int recv_list(int conn, char ***argv, size_t datalen); + +long int send_command_response(int conn, cmd_status_t retcode, const char *fmt, ...) __attribute__((format(printf, 3, 4))); + +int server_command(int conn, cmd_t cmd, const char **args); +int server_open_session(const char *dir_name, const char *file_name, unsigned caller_num) __attribute__((nonnull(1, 2))); +int server_close_session(const char *dir_name, const char *file_name, unsigned caller_num) __attribute__((nonnull(1, 2))); + +int server_task(int conn, task_t task); +int server_task_fds(int conn); + +#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) + +#endif /* _COMMUNICATION_H_ */ diff --git a/hasher-priv/config.c b/hasher-priv/config.c index e3fedcd..4aa499d 100644 --- a/hasher-priv/config.c +++ b/hasher-priv/config.c @@ -1,8 +1,9 @@ /* Copyright (C) 2003-2019 Dmitry V. Levin + Copyright (C) 2019 Alexey Gladkov - Configuration support module for the hasher-priv program. + Configuration support module for the hasher-privd. SPDX-License-Identifier: GPL-2.0-or-later */ @@ -19,13 +20,17 @@ #include #include #include +#include #include "priv.h" #include "xmalloc.h" +#include "logging.h" const char *const *chroot_prefix_list; const char *chroot_prefix_path; const char *change_user1, *change_user2; +char *server_access_group = NULL; +char *server_pidfile = NULL; const char *term; const char *x11_display, *x11_key; str_list_t allowed_devices; @@ -33,6 +38,8 @@ str_list_t allowed_mountpoints; str_list_t requested_mountpoints; uid_t change_uid1, change_uid2; gid_t change_gid1, change_gid2; +gid_t server_gid; +unsigned long server_session_timeout = 0; mode_t change_umask = 022; int change_nice = 8; int makedev_console; @@ -42,6 +49,7 @@ int share_caller_network = 0; int share_ipc = -1; int share_network = -1; int share_uts = -1; +int server_log_priority = -1; change_rlimit_t change_rlimit[] = { /* Per-process CPU limit, in seconds. */ @@ -209,7 +217,7 @@ parse_rlim(const char *name, const char *value, const char *optname, } static unsigned long -str2wlim(const char *name, const char *value, const char *filename) +str2ul(const char *name, const char *value, const char *filename) { char *p = 0; unsigned long long n; @@ -229,7 +237,7 @@ static void modify_wlim(unsigned long *pval, const char *value, const char *optname, const char *filename, int is_system) { - unsigned long val = str2wlim(optname, value, filename); + unsigned long val = str2ul(optname, value, filename); if (is_system || *pval == 0 || (val > 0 && val < *pval)) *pval = val; @@ -283,7 +291,7 @@ parse_str_list(const char *value, str_list_t *s) if (s->len) qsort(s->list, s->len, sizeof(*s->list), strp_cmp); /* clear duplicate entries */ - for (size_t i = 1; i < s->len; ++i) + for (size_t i = 1; i < s->len; ++i) if (!strcmp(s->list[i - 1], s->list[i])) s->list[i - 1] = 0; } @@ -633,3 +641,134 @@ parse_env(void) if ((e = getenv("requested_mountpoints"))) parse_str_list(e, &requested_mountpoints); } + +static void +check_server_access_group(void) +{ + struct group *gr; + + if (!server_access_group || !*server_access_group) + error(EXIT_FAILURE, 0, "config: undefined option: access_group"); + + gr = getgrnam(server_access_group); + + if (!gr || !gr->gr_name) + error(EXIT_FAILURE, 0, "config: access_group: %s lookup failure", server_access_group); + + server_gid = gr->gr_gid; +} + +static void +set_server_config(const char *name, const char *value, const char *filename) +{ + if (!strcasecmp("priority", name)) { + server_log_priority = logging_level(value); + } else if (!strcasecmp("session_timeout", name)) { + server_session_timeout = str2ul(name, value, filename); + } else if (!strcasecmp("pidfile", name)) { + free(server_pidfile); + server_pidfile = xstrdup(value); + } else if (!strcasecmp("access_group", name)) { + free(server_access_group); + server_access_group = xstrdup(value); + } else { + bad_option_name(name, filename); + } +} + +static void +read_server_config(int fd, const char *name) +{ + FILE *fp = fdopen(fd, "r"); + char buf[BUFSIZ]; + unsigned line; + + if (!fp) + error(EXIT_FAILURE, errno, "fdopen: %s", name); + + for (line = 1; fgets(buf, BUFSIZ, fp); ++line) { + const char *start, *left; + char *eq, *right, *end; + + for (start = buf; *start && isspace(*start); ++start) + ; + + if (!*start || '#' == *start) + continue; + + if (!(eq = strchr(start, '='))) + error(EXIT_FAILURE, 0, "%s: syntax error at line %u", + name, line); + + left = start; + right = eq + 1; + + for (; eq > left; --eq) + if (!isspace(eq[-1])) + break; + + if (left == eq) + error(EXIT_FAILURE, 0, "%s: syntax error at line %u", + name, line); + + *eq = '\0'; + end = right + strlen(right); + + for (; right < end; ++right) + if (!isspace(*right)) + break; + + for (; end > right; --end) + if (!isspace(end[-1])) + break; + + *end = '\0'; + set_server_config(left, right, name); + } + + if (ferror(fp)) + error(EXIT_FAILURE, errno, "fgets: %s", name); + + if (fclose(fp)) + error(EXIT_FAILURE, errno, "fclose: %s", name); +} + +static void +load_server_config(const char *name) +{ + struct stat st; + int fd = open(name, O_RDONLY | O_NOFOLLOW | O_NOCTTY); + + if (fd < 0) + error(EXIT_FAILURE, errno, "open: %s", name); + + if (fstat(fd, &st) < 0) + error(EXIT_FAILURE, errno, "fstat: %s", name); + + stat_root_ok_validator(&st, name); + + if (!S_ISREG(st.st_mode)) + error(EXIT_FAILURE, 0, "%s: not a regular file", name); + + if (st.st_size > MAX_CONFIG_SIZE) + error(EXIT_FAILURE, 0, "%s: file too large: %lu", + name, (unsigned long) st.st_size); + + read_server_config(fd, name); +} + +void +configure_server(void) +{ + safe_chdir("/", stat_root_ok_validator); + safe_chdir("etc/hasher-priv", stat_root_ok_validator); + load_server_config("daemon.conf"); + check_server_access_group(); +} + +void +free_server_configuration(void) +{ + free(server_pidfile); + free(server_access_group); +} diff --git a/hasher-priv/daemon.conf b/hasher-priv/daemon.conf new file mode 100644 index 0000000..2d740a6 --- /dev/null +++ b/hasher-priv/daemon.conf @@ -0,0 +1,13 @@ +# Server configuration + +# Set the default logging priority. (can override with command line arguments) +priority=info + +# Write a pid file. (can override with command line arguments) +pidfile=/var/run/hasher-privd.pid + +# Stop user's session server after {session_timeout} seconds of inactivity. +session_timeout=3600 + +# Allow users of this group to interact with hasher-privd via the control socket. +access_group=hashman diff --git a/hasher-priv/epoll.c b/hasher-priv/epoll.c new file mode 100644 index 0000000..6eecd71 --- /dev/null +++ b/hasher-priv/epoll.c @@ -0,0 +1,39 @@ + +/* + Copyright (C) 2019 Alexey Gladkov + + The helpers for epoll API for the hasher-privd program. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include +#include + +#include "epoll.h" +#include "logging.h" + +int epollin_add(int fd_ep, int fd) +{ + struct epoll_event ev = { + .events = EPOLLIN, + .data.fd = fd, + }; + + if (epoll_ctl(fd_ep, EPOLL_CTL_ADD, fd, &ev) < 0) { + err("epoll_ctl: %m"); + return -1; + } + + return 0; +} + +void epollin_remove(int fd_ep, int fd) +{ + if (fd < 0) + return; + epoll_ctl(fd_ep, EPOLL_CTL_DEL, fd, NULL); + close(fd); +} diff --git a/hasher-priv/epoll.h b/hasher-priv/epoll.h new file mode 100644 index 0000000..dc1e567 --- /dev/null +++ b/hasher-priv/epoll.h @@ -0,0 +1,18 @@ + +/* + Copyright (C) 2019 Alexey Gladkov + + The helpers for epoll API for the hasher-privd program. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef _EPOLL_H_ +#define _EPOLL_H_ + +#include + +int epollin_add(int fd_ep, int fd); +void epollin_remove(int fd_ep, int fd); + +#endif /* _EPOLL_H_ */ diff --git a/hasher-priv/hasher-priv.c b/hasher-priv/hasher-priv.c new file mode 100644 index 0000000..f60cb8e --- /dev/null +++ b/hasher-priv/hasher-priv.c @@ -0,0 +1,78 @@ + +/* + Copyright (C) 2003-2019 Dmitry V. Levin + + The entry function for the hasher-priv program. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/* Code in this file may be executed with root privileges. */ + +#include + +#include +#include +#include +#include +#include +#include + +#include "priv.h" +#include "logging.h" +#include "sockets.h" +#include "communication.h" + +int log_fd = -1; + +static void +my_error_print_progname(void) +{ + fprintf(stderr, "%s: ", program_invocation_short_name); +} + +int +main(int ac, const char *av[], const char *ev[]) +{ + int conn; + task_t task; + char socketname[UNIX_PATH_MAX]; + + error_print_progname = my_error_print_progname; + + /* First, check and sanitize file descriptors. */ + sanitize_fds(); + + /* Second, parse command line arguments. */ + task = parse_cmdline(ac, av); + + /* Connect to remote server and open session. */ + if (server_open_session(SOCKETDIR, main_socket_base_name, caller_num) < 0) + error(EXIT_FAILURE, errno, "server_open_session(num=%d): %m", caller_num); + + /* Open user session */ + snprintf(socketname, sizeof(socketname), "%d:%u", geteuid(), caller_num); + + if ((conn = srv_connect(SOCKETDIR, socketname)) < 0) + error(EXIT_FAILURE, errno, "connect(num=%d): %m", caller_num); + + if (server_task(conn, task) < 0) + error(EXIT_FAILURE, errno, "task: %m"); + + if (server_task_fds(conn) < 0) + error(EXIT_FAILURE, errno, "task fds: %m"); + + if (server_command(conn, CMD_TASK_ARGUMENTS, task_args) < 0) + error(EXIT_FAILURE, errno, "cmd_task_arguments: %m"); + + if (server_command(conn, CMD_TASK_ENVIRON, ev) < 0) + error(EXIT_FAILURE, errno, "cmd_task_environ: %m"); + + if (server_command(conn, CMD_TASK_RUN, NULL) < 0) + error(EXIT_FAILURE, errno, "cmd_task_run: %m"); + + /* Close session socket. */ + close(conn); + + return EXIT_SUCCESS; +} diff --git a/hasher-priv/hasher-priv.spec b/hasher-priv/hasher-priv.spec index 5479f2f..8ec0013 100644 --- a/hasher-priv/hasher-priv.spec +++ b/hasher-priv/hasher-priv.spec @@ -51,10 +51,13 @@ groupadd -r -f hashman %attr(750,root,hashman) %dir %configdir/user.d %attr(640,root,hashman) %config(noreplace) %configdir/fstab %attr(640,root,hashman) %config(noreplace) %configdir/system +%attr(640,root,hashman) %config(noreplace) %configdir/daemon.conf # helpers %attr(750,root,hashman) %dir %helperdir %attr(6710,root,hashman) %helperdir/%name %attr(755,root,root) %helperdir/*.sh +# daemon +%_sbindir/hasher-privd %doc DESIGN diff --git a/hasher-priv/hasher-privd.c b/hasher-priv/hasher-privd.c new file mode 100644 index 0000000..06f2811 --- /dev/null +++ b/hasher-priv/hasher-privd.c @@ -0,0 +1,437 @@ + +/* + Copyright (C) 2019 Alexey Gladkov + + The entry function for the hasher-privd program. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/* Code in this file may be executed with root privileges. */ + +#include + +#include +#include +#include +#include /* umask */ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "epoll.h" +#include "logging.h" +#include "pidfile.h" +#include "sockets.h" +#include "communication.h" +#include "priv.h" +#include "xmalloc.h" + +struct hadaemon dn = { -1, -1, -1, }; + +static struct session *pool = NULL; + +static pid_t +spawn_caller_server(struct hadaemon* d, int cl_conn, struct session *a) +{ + int ret; + pid_t pid = fork(); + + if (pid < 0) { + err("fork: %m"); + return -1; + } + if (pid != 0) { + return pid; + } + + if (d->fd_ep >= 0) + close(d->fd_ep); + if (d->fd_ep >= 0) + close(d->fd_signal); + if (d->fd_conn >= 0) + close(d->fd_conn); + + caller_num = a->caller_num; + ret = init_caller_data(a->caller_uid, a->caller_gid); + if (ret < 0) { + send_command_response(cl_conn, CMD_STATUS_FAILED, NULL); + close(cl_conn); + goto out_send_failure; + } + + info("%s(%d) num=%u: starting session server", caller_user, caller_uid, caller_num); + struct hadaemon sh = {}; + + ret = caller_server_listener_init(&sh); + if (ret < 0) { + send_command_response(cl_conn, CMD_STATUS_FAILED, NULL); + close(cl_conn); + goto out_send_failure; + } + + info("%s(%d) num=%u: started session server", caller_user, caller_uid, caller_num); + /* Tell the client that caller server is ready */ + send_command_response(cl_conn, CMD_STATUS_DONE, NULL); + close(cl_conn); + + ret = caller_server(&sh); + info("%s(%d): finish session server", caller_user, caller_uid); + exit(ret < 0 ? EXIT_FAILURE : EXIT_SUCCESS); + +out_send_failure: + free_caller_data(); + + exit(ret < 0 ? EXIT_FAILURE : EXIT_SUCCESS); +} + +static int +start_session(struct hadaemon* d, int conn, unsigned num) +{ + uid_t uid; + gid_t gid; + pid_t server_pid; + struct session *a = NULL; + struct session **sl = &pool; + + if (get_peercred(conn, NULL, &uid, &gid) < 0) + return -1; + + while (sl && *sl) { + if ((*sl)->caller_uid == uid && (*sl)->caller_num == num) { + /* Session exists and will be reused. */ + send_command_response(conn, CMD_STATUS_DONE, NULL); + return 0; + } + sl = &(*sl)->next; + } + + a = calloc(1L, sizeof(struct session)); + if (!a) { + err("calloc: %m"); + return -1; + } + + a->caller_uid = uid; + a->caller_gid = gid; + a->caller_num = num; + + info("starting session for user %d:%u", uid, num); + + if ((server_pid = spawn_caller_server(d, conn, a)) < 0) + return -1; + + a->server_pid = server_pid; + + info("started session for user %d:%u", uid, num); + + *sl = a; + + return 0; +} + +static int +close_session(int conn, unsigned num) +{ + uid_t uid; + gid_t gid; + struct session *e = pool; + + if (get_peercred(conn, NULL, &uid, &gid) < 0) + return -1; + + while (e) { + if (e->caller_uid == uid && e->caller_num == num) { + info("close session for %d:%u user by request", uid, num); + if (kill(e->server_pid, SIGTERM) < 0) { + err("kill: %m"); + return -1; + } + break; + } + e = e->next; + } + + return 0; +} + +static int +process_request(struct hadaemon* d, int conn) +{ + int rc; + struct cmd hdr = { 0 }; + unsigned num = 0; + + if (xrecvmsg(conn, &hdr, sizeof(hdr)) < 0) + return -1; + + if (hdr.datalen != sizeof(num)) { + err("bad command"); + send_command_response(conn, CMD_STATUS_FAILED, "bad command"); + return -1; + } + + if (xrecvmsg(conn, &num, sizeof(num)) < 0) + return -1; + + switch (hdr.type) { + case CMD_OPEN_SESSION: + rc = start_session(d, conn, num); + /* The successful response is delayed until the child + * server is ready or an error happens. + */ + if (rc < 0) + send_command_response(conn, CMD_STATUS_FAILED, "command failed"); + break; + case CMD_CLOSE_SESSION: + rc = close_session(conn, num); + (rc < 0) + ? send_command_response(conn, CMD_STATUS_FAILED, "command failed") + : send_command_response(conn, CMD_STATUS_DONE, NULL); + break; + default: + err("unknown command"); + send_command_response(conn, CMD_STATUS_FAILED, "unknown command"); + rc = -1; + } + + return rc; +} + +static void +finish_sessions(int sig) +{ + struct session *e = pool; + + while (e) { + if (kill(e->server_pid, sig) < 0) + err("kill: %m"); + e = e->next; + } +} + +static void +clean_session(pid_t pid) +{ + struct session *x, **a = &pool; + + while (a && *a) { + if ((*a)->server_pid == pid) { + x = *a; + *a = (*a)->next; + free(x); + } + a = &(*a)->next; + } +} + +int main(int argc, char **argv) +{ + int i; + int loglevel = -1; + + const char *pidfile = NULL; + int daemonize = 1; + + char socketpath[UNIX_PATH_MAX]; + + struct option long_options[] = { + { "help", no_argument, 0, 'h' }, + { "version", no_argument, 0, 'V' }, + { "foreground", no_argument, 0, 'f' }, + { "loglevel", required_argument, 0, 'l' }, + { "pidfile", required_argument, 0, 'p' }, + { 0, 0, 0, 0 } + }; + + while ((i = getopt_long(argc, argv, "hVfl:p:", long_options, NULL)) != -1) { + switch (i) { + case 'p': + pidfile = optarg; + break; + case 'l': + loglevel = logging_level(optarg); + break; + case 'f': + daemonize = 0; + break; + case 'V': + printf("%s %s\n", program_invocation_short_name, PROJECT_VERSION); + return EXIT_SUCCESS; + default: + case 'h': + printf("Usage: %s [options]\n" + " -p, --pidfile=FILE pid file location;\n" + " -l, --loglevel=LVL set logging level;\n" + " -f, --foreground stay in the foreground;\n" + " -V, --version print program version and exit;\n" + " -h, --help show this text and exit.\n" + "\n", + program_invocation_short_name); + return EXIT_SUCCESS; + } + } + + configure_server(); + + if (daemonize && !pidfile && server_pidfile && *server_pidfile) + pidfile = server_pidfile; + + if (loglevel < 0) + loglevel = (server_log_priority >= 0) + ? server_log_priority + : logging_level("info"); + + umask(022); + + if (pidfile && check_pid(pidfile)) + error(EXIT_FAILURE, 0, "%s: already running", + program_invocation_short_name); + + if (daemonize && daemon(0, 0) < 0) + error(EXIT_FAILURE, errno, "daemon"); + + logging_init(loglevel, !daemonize); + + if (pidfile && write_pid(pidfile) == 0) + return EXIT_FAILURE; + + sigset_t mask; + + sigfillset(&mask); + sigprocmask(SIG_SETMASK, &mask, NULL); + + sigdelset(&mask, SIGABRT); + sigdelset(&mask, SIGSEGV); + + struct hadaemon *d = &dn; + + if ((d->fd_ep = epoll_create1(EPOLL_CLOEXEC)) < 0) + fatal("epoll_create1: %m"); + + if ((d->fd_signal = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC)) < 0) + fatal("signalfd: %m"); + + + mode_t m = umask(017); + + snprintf(socketpath, sizeof(socketpath), "%s/%s", SOCKETDIR, main_socket_base_name); + + if ((d->fd_conn = srv_listen(socketpath)) < 0) + fatal("srv_listen: %m"); + + umask(m); + + if (chown(socketpath, 0, server_gid)) + fatal("fchown: %s: %m", socketpath); + + if (epollin_add(d->fd_ep, d->fd_signal) < 0 || epollin_add(d->fd_ep, d->fd_conn) < 0) + return EXIT_FAILURE; + + int ep_timeout = -1; + int finish_server = 0; + + while (1) { + struct epoll_event ev[42]; + int fdcount; + + errno = 0; + if ((fdcount = epoll_wait(d->fd_ep, ev, ARRAY_SIZE(ev), ep_timeout)) < 0) { + if (errno == EINTR) + continue; + err("epoll_wait: %m"); + break; + } + + for (i = 0; i < fdcount; i++) { + if (!(ev[i].events & EPOLLIN)) { + continue; + + } else if (ev[i].data.fd == d->fd_signal) { + struct signalfd_siginfo fdsi; + ssize_t size; + + size = read_retry(d->fd_signal, &fdsi, sizeof(fdsi)); + + if (size != sizeof(fdsi)) { + err("unable to read signal info"); + continue; + } + + pid_t pid; + int status; + + switch (fdsi.ssi_signo) { + case SIGINT: + case SIGTERM: + finish_server = 1; + break; + case SIGCHLD: + if ((pid = waitpid(-1, &status, 0)) < 0) + err("waitpid: %m"); + + clean_session(pid); + break; + } + + } else if (ev[i].data.fd == d->fd_conn) { + int conn; + + if ((conn = accept4(d->fd_conn, NULL, 0, SOCK_CLOEXEC)) < 0) { + err("accept4: %m"); + continue; + } + + if (set_recv_timeout(conn, 3) < 0) { + close(conn); + continue; + } + + process_request(d, conn); + close(conn); + } + } + + if (finish_server) { + if (!pool) + break; + + if (d->fd_conn >= 0) { + epollin_remove(d->fd_ep, d->fd_conn); + d->fd_conn = -1; + ep_timeout = 3000; + } + + finish_sessions(SIGTERM); + } + } + + epollin_remove(d->fd_ep, d->fd_signal); + epollin_remove(d->fd_ep, d->fd_conn); + + if (d->fd_ep >= 0) + close(d->fd_ep); + if (d->fd_ep >= 0) + close(d->fd_signal); + if (d->fd_conn >= 0) + close(d->fd_conn); + + if (pidfile) + remove_pid(pidfile); + + logging_close(); + free_server_configuration(); + + return EXIT_SUCCESS; +} diff --git a/hasher-priv/io_log.c b/hasher-priv/io_log.c index f5aea59..2689ff0 100644 --- a/hasher-priv/io_log.c +++ b/hasher-priv/io_log.c @@ -2,7 +2,7 @@ /* Copyright (C) 2008-2019 Dmitry V. Levin - The chrootuid parent log I/O handler for the hasher-priv program. + The chrootuid parent log I/O handler for the hasher-privd program. SPDX-License-Identifier: GPL-2.0-or-later */ diff --git a/hasher-priv/io_x11.c b/hasher-priv/io_x11.c index 93e3add..f193c56 100644 --- a/hasher-priv/io_x11.c +++ b/hasher-priv/io_x11.c @@ -2,7 +2,7 @@ /* Copyright (C) 2005-2019 Dmitry V. Levin - The chrootuid parent X11 I/O handler for the hasher-priv program. + The chrootuid parent X11 I/O handler for the hasher-privd program. SPDX-License-Identifier: GPL-2.0-or-later */ diff --git a/hasher-priv/killuid.c b/hasher-priv/killuid.c index f0bee84..71e52e1 100644 --- a/hasher-priv/killuid.c +++ b/hasher-priv/killuid.c @@ -2,7 +2,7 @@ /* Copyright (C) 2003-2019 Dmitry V. Levin - The killuid actions for the hasher-priv program. + The killuid actions for the hasher-privd program. SPDX-License-Identifier: GPL-2.0-or-later */ diff --git a/hasher-priv/logging.c b/hasher-priv/logging.c new file mode 100644 index 0000000..cd693e5 --- /dev/null +++ b/hasher-priv/logging.c @@ -0,0 +1,71 @@ + +/* + Copyright (C) 2019 Alexey Gladkov + + Logging functions for the hasher-privd program. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include +#include +#include + +#include "logging.h" + +int log_priority = -1; +int log_to_stderr = 0; + +int logging_level(const char *name) +{ + if (!strcasecmp(name, "debug")) + return LOG_DEBUG; + + if (!strcasecmp(name, "info")) + return LOG_INFO; + + if (!strcasecmp(name, "warning")) + return LOG_WARNING; + + if (!strcasecmp(name, "error")) + return LOG_ERR; + + return 0; +} + +void logging_init(int loglevel, int is_foreground) +{ + int options = LOG_PID; + log_priority = loglevel; + log_to_stderr = !!is_foreground; + if (!is_foreground) + openlog(program_invocation_short_name, options, LOG_DAEMON); +} + +void logging_close(void) +{ + if (!log_to_stderr) + closelog(); +} + +void +message(int priority, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + if (priority <= log_priority) + { + if (log_to_stderr) + { + fprintf(stderr, "<%d>", priority); + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); + } + else + vsyslog(priority, fmt, ap); + } + va_end(ap); +} diff --git a/hasher-priv/logging.h b/hasher-priv/logging.h new file mode 100644 index 0000000..bc6d0da --- /dev/null +++ b/hasher-priv/logging.h @@ -0,0 +1,55 @@ + +/* + Copyright (C) 2019 Alexey Gladkov + + Logging functions for the hasher-privd program. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef _LOGGING_H_ +#define _LOGGING_H_ + +#include +#include + +void logging_init(int, int); +void logging_close(void); +int logging_level(const char *lvl) __attribute__((nonnull(1))); + +void message(int priority, const char *fmt, ...) __attribute__((format(printf, 2, 3))); + +#define fatal(format, arg...) \ + do { \ + message(LOG_CRIT, \ + "%s(%d): %s: " format, \ + __FILE__, __LINE__, __FUNCTION__, \ + ##arg); \ + exit(EXIT_FAILURE); \ + } while (0) + +#define err(format, arg...) \ + do { \ + message(LOG_ERR, \ + "%s(%d): %s: " format, \ + __FILE__, __LINE__, __FUNCTION__, \ + ##arg); \ + } while (0) + +#define info(format, arg...) \ + do { \ + message(LOG_INFO, \ + "%s(%d): %s: " format, \ + __FILE__, __LINE__, __FUNCTION__, \ + ##arg); \ + } while (0) + +#define dbg(format, arg...) \ + do { \ + message(LOG_DEBUG, \ + "%s(%d): %s: " format, \ + __FILE__, __LINE__, __FUNCTION__, \ + ##arg); \ + } while (0) + +#endif /* _LOGGING_H_ */ diff --git a/hasher-priv/main.c b/hasher-priv/main.c deleted file mode 100644 index 310fd5d..0000000 --- a/hasher-priv/main.c +++ /dev/null @@ -1,75 +0,0 @@ - -/* - Copyright (C) 2003-2019 Dmitry V. Levin - - The entry function for the hasher-priv program. - - SPDX-License-Identifier: GPL-2.0-or-later -*/ - -/* Code in this file may be executed with root privileges. */ - -#include -#include -#include -#include - -#include "priv.h" - -static void -my_error_print_progname(void) -{ - fprintf(stderr, "%s: ", program_invocation_short_name); -} - -int -main(int ac, const char *av[]) -{ - task_t task; - - error_print_progname = my_error_print_progname; - - /* First, check and sanitize file descriptors. */ - sanitize_fds(); - - /* Second, parse command line arguments. */ - task = parse_cmdline(ac, av); - - if (chroot_path && *chroot_path != '/') - error(EXIT_FAILURE, 0, "%s: invalid chroot path", - chroot_path); - - /* Third, initialize data related to caller. */ - init_caller_data(); - - /* 4th, parse environment for config options. */ - parse_env(); - - /* We don't need environment variables any longer. */ - if (clearenv() != 0) - error(EXIT_FAILURE, errno, "clearenv"); - - /* Load config according to caller information. */ - configure(); - - /* Finally, execute choosen task. */ - switch (task) - { - case TASK_GETCONF: - return do_getconf(); - case TASK_KILLUID: - return do_killuid(); - case TASK_GETUGID1: - return do_getugid1(); - case TASK_CHROOTUID1: - return do_chrootuid1(); - case TASK_GETUGID2: - return do_getugid2(); - case TASK_CHROOTUID2: - return do_chrootuid2(); - default: - error(EXIT_FAILURE, 0, "unknown task %d", task); - } - - return EXIT_FAILURE; -} diff --git a/hasher-priv/pass.c b/hasher-priv/pass.c index b52c87e..03a07ec 100644 --- a/hasher-priv/pass.c +++ b/hasher-priv/pass.c @@ -17,6 +17,8 @@ #include #include +#include "logging.h" +#include "sockets.h" #include "priv.h" union cmsg_data_u @@ -55,8 +57,7 @@ fd_send(int ctl, int pass, const char *data, size_t data_len) ssize_t rc; - if ((rc = TEMP_FAILURE_RETRY(sendmsg(ctl, &msg, 0))) != - (ssize_t) data_len) + if ((rc = sendmsg_retry(ctl, &msg, 0)) != (ssize_t) data_len) { if (rc < 0) { @@ -96,8 +97,7 @@ fd_recv(int ctl, char *data, size_t data_len) ssize_t rc; - if ((rc = TEMP_FAILURE_RETRY(recvmsg(ctl, &msg, 0))) != - (ssize_t) data_len) + if ((rc = recvmsg_retry(ctl, &msg, 0)) != (ssize_t) data_len) { if (rc < 0) { @@ -132,3 +132,112 @@ fd_recv(int ctl, char *data, size_t data_len) cmsg_data_p.c = CMSG_DATA(cmsg); return *cmsg_data_p.i; } + +int +fds_send(int conn, int *fds, size_t fds_len) +{ + ssize_t rc; + size_t len = sizeof(int) * fds_len; + + union { + char buf[CMSG_SPACE(len)]; + struct cmsghdr align; + } u; + + /* + * We must send at least 1 byte of real data in + * order to send ancillary data + */ + char dummy; + + struct iovec iov = { 0 }; + + iov.iov_base = &dummy; + iov.iov_len = sizeof(dummy); + + struct msghdr msg = { 0 }; + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = u.buf; + msg.msg_controllen = sizeof(u.buf); + + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(len); + + memcpy(CMSG_DATA(cmsg), fds, len); + + if ((rc = sendmsg_retry(conn, &msg, 0)) != sizeof(dummy)) { + if (rc < 0) + err("sendmsg: %m"); + else if (rc) + err("sendmsg: expected size %lu, got %lu", sizeof(dummy), rc); + else + err("sendmsg: unexpected EOF"); + return -1; + } + + return 0; +} + +int +fds_recv(int conn, int *fds, size_t n_fds) +{ + char dummy; + size_t datalen = sizeof(int) * n_fds; + + union { + struct cmsghdr cmh; + char control[CMSG_SPACE(datalen)]; + } u; + + u.cmh.cmsg_len = CMSG_LEN(datalen); + u.cmh.cmsg_level = SOL_SOCKET; + u.cmh.cmsg_type = SCM_RIGHTS; + + struct iovec iov = { 0 }; + + iov.iov_base = &dummy; + iov.iov_len = sizeof(dummy); + + struct msghdr msg = { 0 }; + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = u.control; + msg.msg_controllen = sizeof(u.control); + + ssize_t n; + if ((n = recvmsg_retry(conn, &msg, 0)) != sizeof(dummy)) { + if (n < 0) + err("recvmsg: %m"); + else if (n) + err("recvmsg: expected size %lu, got %lu", sizeof(dummy), n); + else + err("recvmsg: unexpected EOF"); + return -1; + } + + if (!msg.msg_controllen) { + err("ancillary data not specified"); + return -1; + } + + for (struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) { + if (cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS) + continue; + + if (cmsg->cmsg_len - CMSG_LEN(0) != datalen) { + err("expected fd payload"); + return -1; + } + + memcpy(fds, CMSG_DATA(cmsg), datalen); + break; + } + + return 0; +} diff --git a/hasher-priv/pidfile.c b/hasher-priv/pidfile.c new file mode 100644 index 0000000..3da63dd --- /dev/null +++ b/hasher-priv/pidfile.c @@ -0,0 +1,129 @@ + +/* + pidfile.c - interact with pidfiles + Copyright (c) 1995 Martin Schulze + + This file was originally part of the sysklogd package, a kernel and system + log daemon. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/* + * Sat Aug 19 13:24:33 MET DST 1995: Martin Schulze + * First version (v0.2) released + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "logging.h" +#include "pidfile.h" + +/* read_pid + * + * Reads the specified pidfile and returns the read pid. + * 0 is returned if either there's no pidfile, it's empty + * or no pid can be read. + */ +int read_pid(const char *pidfile) +{ + FILE *f; + int pid; + + if (!(f = fopen(pidfile, "r"))) + return 0; + if (fscanf(f, "%d", &pid) != 1) + pid = 0; + fclose(f); + return pid; +} + +/* check_pid + * + * Reads the pid using read_pid and looks up the pid in the process + * table (using /proc) to determine if the process already exists. If + * so 1 is returned, otherwise 0. + */ +int check_pid(const char *pidfile) +{ + int pid = read_pid(pidfile); + + /* Amazing ! _I_ am already holding the pid file... */ + if ((!pid) || (pid == getpid())) + return 0; + + /* + * The 'standard' method of doing this is to try and do a 'fake' kill + * of the process. If an ESRCH error is returned the process cannot + * be found -- GW + */ + /* But... errno is usually changed only on error.. */ + if (kill(pid, 0) && errno == ESRCH) + return (0); + + return pid; +} + +/* write_pid + * + * Writes the pid to the specified file. If that fails 0 is + * returned, otherwise the pid. + */ +int write_pid(const char *pidfile) +{ + FILE *f; + int fd; + int pid; + + if (((fd = open(pidfile, O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC, 0644)) == -1) || + ((f = fdopen(fd, "r+")) == NULL)) { + err("Can't open or create %s", pidfile); + return 0; + } + + if (flock(fd, LOCK_EX | LOCK_NB) == -1) { + if (fscanf(f, "%d", &pid) != 1) + pid = 0; + fclose(f); + err("Can't lock, lock is held by pid %d", pid); + return 0; + } + + pid = getpid(); + if (!fprintf(f, "%d\n", pid)) { + err("Can't write pid: %m"); + close(fd); + return 0; + } + fflush(f); + + if (flock(fd, LOCK_UN) == -1) { + err("flock: %s: %m", pidfile); + close(fd); + return 0; + } + close(fd); + fclose(f); + + return pid; +} + +/* remove_pid + * + * Remove the the specified file. The result from unlink(2) + * is returned + */ +int remove_pid(const char *pidfile) +{ + if (unlink(pidfile) == -1) { + err("unlink: %s: %m", pidfile); + return -1; + } + return 0; +} diff --git a/hasher-priv/pidfile.h b/hasher-priv/pidfile.h new file mode 100644 index 0000000..e152538 --- /dev/null +++ b/hasher-priv/pidfile.h @@ -0,0 +1,44 @@ + +/* + pidfile.h - interact with pidfiles + Copyright (c) 1995 Martin Schulze + + This file is part of the sysklogd package, a kernel and system log daemon. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef _PIDFILE_H_ +#define _PIDFILE_H_ + +/* read_pid + * + * Reads the specified pidfile and returns the read pid. + * 0 is returned if either there's no pidfile, it's empty + * or no pid can be read. + */ +int read_pid(const char *pidfile) __attribute__((nonnull(1))); + +/* check_pid + * + * Reads the pid using read_pid and looks up the pid in the process + * table (using /proc) to determine if the process already exists. If + * so 1 is returned, otherwise 0. + */ +int check_pid(const char *pidfile) __attribute__((nonnull(1))); + +/* write_pid + * + * Writes the pid to the specified file. If that fails 0 is + * returned, otherwise the pid. + */ +int write_pid(const char *pidfile) __attribute__((nonnull(1))); + +/* remove_pid + * + * Remove the the specified file. The result from unlink(2) + * is returned + */ +int remove_pid(const char *pidfile) __attribute__((nonnull(1))); + +#endif /* _PIDFILE_H_ */ diff --git a/hasher-priv/priv.h b/hasher-priv/priv.h index 87e72fb..1dc86dd 100644 --- a/hasher-priv/priv.h +++ b/hasher-priv/priv.h @@ -18,16 +18,30 @@ #define MIN_CHANGE_GID 34 #define MAX_CONFIG_SIZE 16384 -typedef enum -{ - TASK_NONE = 0, - TASK_GETCONF, - TASK_KILLUID, - TASK_GETUGID1, - TASK_CHROOTUID1, - TASK_GETUGID2, - TASK_CHROOTUID2, -} task_t; +#include "communication.h" + +struct hadaemon { + int fd_ep; + int fd_signal; + int fd_conn; +}; + +struct session { + struct session *next; + + uid_t caller_uid; + gid_t caller_gid; + + unsigned caller_num; + + pid_t server_pid; +}; + +struct caller { + uid_t uid; + gid_t gid; + int fd_conn; +}; typedef struct { @@ -68,9 +82,13 @@ void restore_tty(void); int tty_copy_winsize(int master_fd, int slave_fd); int open_pty(int *slave_fd, int chrooted, int verbose_error); task_t parse_cmdline(int ac, const char *av[]); -void init_caller_data(void); +void parse_task_args(task_t task, const char *argv[]); +int init_caller_data(uid_t uid, gid_t gid); +void free_caller_data(void); void parse_env(void); void configure(void); +void configure_server(void); +void free_server_configuration(void); void ch_uid(uid_t uid, uid_t *save); void ch_gid(gid_t gid, gid_t *save); void chdiruid(const char *path, VALIDATE_FPTR validator); @@ -85,6 +103,9 @@ void stat_root_ok_validator(struct stat *st, const char *name); void stat_any_ok_validator(struct stat *st, const char *name); void fd_send(int ctl, int pass, const char *data, size_t len); int fd_recv(int ctl, char *data, size_t data_len); +int fds_send(int conn, int *fds, size_t fds_len) __attribute__((nonnull(2))); +int fds_recv(int conn, int *fds, size_t datalen) __attribute__((nonnull(2))); + int unix_accept(int fd); int log_listen(void); void x11_drop_display(void); @@ -120,9 +141,15 @@ int do_chrootuid1(void); int do_getugid2(void); int do_chrootuid2(void); +int process_cmd_task_run(struct hadaemon*, int, struct task *); +int caller_server_listener_init(struct hadaemon*); +int caller_server(struct hadaemon*); + extern const char *chroot_path; extern const char **chroot_argv; +extern const char **task_args; + extern str_list_t allowed_devices; extern str_list_t allowed_mountpoints; extern str_list_t requested_mountpoints; @@ -143,7 +170,7 @@ extern int log_fd; extern const char *const *chroot_prefix_list; extern const char *chroot_prefix_path; -extern const char *caller_user, *caller_home; +extern char *caller_user, *caller_home; extern uid_t caller_uid; extern gid_t caller_gid; extern unsigned caller_num; @@ -156,4 +183,10 @@ extern int change_nice; extern change_rlimit_t change_rlimit[]; extern work_limit_t wlimit; +extern int server_log_priority; +extern unsigned long server_session_timeout; +extern char *server_access_group; +extern char *server_pidfile; +extern gid_t server_gid; + #endif /* PKG_BUILD_PRIV_H */ diff --git a/hasher-priv/sockets.c b/hasher-priv/sockets.c new file mode 100644 index 0000000..baad57e --- /dev/null +++ b/hasher-priv/sockets.c @@ -0,0 +1,180 @@ + +/* + Copyright (C) 2019 Alexey Gladkov + + A collection of helpers to simplify the use of sockets. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "logging.h" +#include "sockets.h" + +/* This function may be executed with caller or child privileges. */ +/* For simplicity, we assume the init scripts have already created all + * necessary path components. + */ +int srv_listen(const char *file_name) +{ + struct sockaddr_un sun = { .sun_family = AF_UNIX }; + + snprintf(sun.sun_path, sizeof(sun.sun_path), "%s", file_name); + + if (unlink(sun.sun_path) && errno != ENOENT) { + err("unlink: %s: %m", sun.sun_path); + return -1; + } + + int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + + if (fd < 0) { + err("socket AF_UNIX: %m"); + return -1; + } + + if (bind(fd, (struct sockaddr *)&sun, (socklen_t) sizeof(sun))) { + err("bind: %s: %m", sun.sun_path); + (void)close(fd); + return -1; + } + + if (listen(fd, 16) < 0) { + err("listen: %s: %m", sun.sun_path); + (void)close(fd); + return -1; + } + + return fd; +} + +int srv_connect(const char *dir_name, const char *file_name) +{ + int conn = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + + if (conn < 0) { + err("socket AF_UNIX: %m"); + return -1; + } + + struct sockaddr_un sun = { .sun_family = AF_UNIX }; + + snprintf(sun.sun_path, sizeof(sun.sun_path), "%s/%s", dir_name, file_name); + + if (connect(conn, (const struct sockaddr *)&sun, sizeof(sun)) < 0) { + err("connect: %s: %m", sun.sun_path); + return -1; + } + + return conn; +} + +int get_peercred(int fd, pid_t *pid, uid_t *uid, gid_t *gid) +{ + struct ucred uc; + socklen_t len = sizeof(struct ucred); + + if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &uc, &len) < 0) { + err("getsockopt(SO_PEERCRED): %m"); + return -1; + } + + if (pid) + *pid = uc.pid; + + if (uid) + *uid = uc.uid; + + if (gid) + *gid = uc.gid; + + return 0; +} + +int +set_recv_timeout(int fd, int secs) +{ + struct timeval tv = { .tv_sec = secs }; + + if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) { + err("setsockopt(SO_RCVTIMEO): %m"); + return -1; + } + return 0; +} + +ssize_t +sendmsg_retry(int sockfd, const struct msghdr *msg, int flags) +{ + return TEMP_FAILURE_RETRY(sendmsg(sockfd, msg, flags)); +} + +ssize_t +recvmsg_retry(int sockfd, struct msghdr *msg, int flags) +{ + return TEMP_FAILURE_RETRY(recvmsg(sockfd, msg, flags)); +} + +int +xsendmsg(int conn, void *data, size_t len) +{ + struct iovec iov = { 0 }; + struct msghdr msg = { 0 }; + + iov.iov_base = data; + iov.iov_len = len; + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + ssize_t n = sendmsg_retry(conn, &msg, 0); + + if (n != (ssize_t) len) { + if (n < 0) + err("recvmsg: %m"); + else if (n) + err("recvmsg: expected size %lu, got %lu", len, n); + else + err("recvmsg: unexpected EOF"); + return -1; + } + + return 0; +} + +int +xrecvmsg(int conn, void *data, size_t len) +{ + struct iovec iov = { 0 }; + struct msghdr msg = { 0 }; + + iov.iov_base = data; + iov.iov_len = len; + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + ssize_t n = recvmsg_retry(conn, &msg, MSG_WAITALL); + + if (n != (ssize_t) len) { + if (n < 0) + err("recvmsg: %m"); + else if (n) + err("recvmsg: expected size %lu, got %lu", len, n); + else + err("recvmsg: unexpected EOF"); + return -1; + } + + return 0; +} diff --git a/hasher-priv/sockets.h b/hasher-priv/sockets.h new file mode 100644 index 0000000..0b65eb9 --- /dev/null +++ b/hasher-priv/sockets.h @@ -0,0 +1,32 @@ + +/* + Copyright (C) 2019 Alexey Gladkov + + A collection of helpers to simplify the use of sockets. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef _SOCKETS_H_ +#define _SOCKETS_H_ + +#include +#include + +ssize_t sendmsg_retry(int sockfd, const struct msghdr *msg, int flags) __attribute__((nonnull(2))); +ssize_t recvmsg_retry(int sockfd, struct msghdr *msg, int flags) __attribute__((nonnull(2))); + +#include + +int srv_listen(const char *) __attribute__((nonnull(1))); +int srv_connect(const char *, const char *) __attribute__((nonnull(1, 2))); + +int get_peercred(int, pid_t *, uid_t *, gid_t *); +int set_recv_timeout(int fd, int secs); + +#include + +int xsendmsg(int conn, void *data, size_t len) __attribute__((nonnull(2))); +int xrecvmsg(int conn, void *data, size_t len) __attribute__((nonnull(2))); + +#endif /* _SOCKETS_H_ */ diff --git a/hasher-priv/x11.c b/hasher-priv/x11.c index 854c453..0a6b4fb 100644 --- a/hasher-priv/x11.c +++ b/hasher-priv/x11.c @@ -22,6 +22,7 @@ #include "priv.h" #include "xmalloc.h" +#include "sockets.h" #define X11_UNIX_DIR "/tmp/.X11-unix" -- 2.32.0