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.9 required=5.0 tests=BAYES_00 autolearn=unavailable autolearn_force=no version=3.4.1 From: Alex Gladkov To: ldv@altlinux.org Date: Fri, 13 Dec 2019 12:42:03 +0100 Message-Id: <9bca7626b593f896de4283cba2d6290ec99eb4f2.1576183643.git.legion@altlinux.org> X-Mailer: git-send-email 2.24.0 In-Reply-To: References: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Mailman-Approved-At: Fri, 13 Dec 2019 14:47:50 +0300 Cc: devel@lists.altlinux.org Subject: [devel] [PATCH hasher-priv v1 1/3] Make a daemon from the hasher-priv 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: Fri, 13 Dec 2019 11:42:22 -0000 Archived-At: List-Archive: List-Post: From: Alexey Gladkov All privileged operations moved to the daemon. Commands to the server are sent through the unix domain socket. The credentials which the sender specifies are checked by the kernel. The hasher-priv no longer SUID. For each user server creates a separate session process that executes commands only from the user who created it. The session process ends after a certain period of inactivity. Signed-off-by: Alexey Gladkov --- hasher-priv/.gitignore | 1 + hasher-priv/DESIGN | 281 ++++++++++++++++---------- hasher-priv/Makefile | 28 ++- hasher-priv/caller.c | 81 ++++---- hasher-priv/caller_server.c | 373 ++++++++++++++++++++++++++++++++++ hasher-priv/caller_task.c | 214 ++++++++++++++++++++ hasher-priv/cmdline.c | 27 ++- hasher-priv/communication.c | 392 ++++++++++++++++++++++++++++++++++++ hasher-priv/communication.h | 77 +++++++ hasher-priv/config.c | 143 ++++++++++++- hasher-priv/epoll.c | 39 ++++ hasher-priv/epoll.h | 18 ++ hasher-priv/hasher-priv.c | 78 +++++++ hasher-priv/hasher-privd.c | 375 ++++++++++++++++++++++++++++++++++ hasher-priv/io_log.c | 2 +- hasher-priv/io_x11.c | 2 +- hasher-priv/killuid.c | 2 +- hasher-priv/logging.c | 64 ++++++ hasher-priv/logging.h | 55 +++++ hasher-priv/main.c | 75 ------- hasher-priv/pass.c | 117 ++++++++++- hasher-priv/pidfile.c | 128 ++++++++++++ hasher-priv/pidfile.h | 44 ++++ hasher-priv/priv.h | 33 +-- hasher-priv/server.conf | 13 ++ hasher-priv/sockets.c | 183 +++++++++++++++++ hasher-priv/sockets.h | 32 +++ hasher-priv/x11.c | 1 + 28 files changed, 2632 insertions(+), 246 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/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/server.conf 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..310667d 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 a hasher-priv (euid=user,egid=hashman,gid!=egid) control flow: + 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 /var/run/hasher-priv socket + + wait for the creation of a session server + + close socket ++ connect to session server by /var/run/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 a hasher-privd (euid=root,egid=hashman,gid==egid) control flow: ++ parse command line arguments + + parse -h and --help options ++ set safe umask ++ check pidfile, abort if server already running ++ 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 incomming 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 a815e9e..82aa385 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) sysconfdir = /etc libexecdir = /usr/lib @@ -21,6 +21,7 @@ man5dir = $(mandir)/man5 man8dir = $(mandir)/man8 configdir = $(sysconfdir)/$(PROJECT) helperdir = $(libexecdir)/$(PROJECT) +socketdir = /var/run DESTDIR = MKDIR_P = mkdir -p @@ -33,17 +34,25 @@ WARNINGS = -Wall -W -Wshadow -Wpointer-arith -Wwrite-strings \ -Wmissing-prototypes -Wmissing-declarations -Wmissing-noreturn \ -Wmissing-format-attribute -Wredundant-decls -Wdisabled-optimization CPPFLAGS = -std=gnu99 -D_GNU_SOURCE $(CHDIRUID_FLAGS) \ - $(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 @@ -52,14 +61,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 server.conf $(DESTDIR)$(configdir)/server $(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)/ @@ -67,7 +81,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..031ddef 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 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 mismatch", 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 mismatch", 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: 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..8182c69 --- /dev/null +++ b/hasher-priv/caller_server.c @@ -0,0 +1,373 @@ + +/* + Copyright (C) 2019 Alexey Gladkov + + The part of the hasher-privd program. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#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->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 +process_task(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 permissions 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_caller_task(conn, &task); + free_task(&task); + return 0; + + default: + err("unsupported command: %d", hdr.type); + } + + send_command_response(conn, CMD_STATUS_DONE, NULL); + } + + return 0; +} + +static int +caller_server(int cl_conn) +{ + int ret = 0; + + umask(077); + + snprintf(socketpath, sizeof(socketpath), + "hasher-priv-%d-%u", + caller_uid, caller_num); + + int fd_conn = -1; + int fd_signal = -1; + int fd_ep = -1; + + if ((fd_conn = srv_listen(SOCKETDIR, socketpath)) < 0) + return -1; + + snprintf(socketpath, sizeof(socketpath), + "%s/hasher-priv-%d-%u", + SOCKETDIR, caller_uid, caller_num); + + 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 ((fd_signal = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC)) < 0) { + err("signalfd: %m"); + ret = -1; + goto fail; + } + + if ((fd_ep = epoll_create1(EPOLL_CLOEXEC)) < 0) { + err("epoll_create1: %m"); + ret = -1; + goto fail; + } + + if (epollin_add(fd_ep, fd_signal) < 0 || epollin_add(fd_ep, fd_conn) < 0) { + err("epollin_add: failed"); + ret = -1; + goto fail; + } + + /* Tell client that caller server is ready */ + send_command_response(cl_conn, CMD_STATUS_DONE, NULL); + close(cl_conn); + + unsigned long nsec = 0; + int finish_server = 0; + + while (!finish_server) { + struct epoll_event ev[42]; + int fdcount; + + errno = 0; + if ((fdcount = epoll_wait(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 == fd_signal) { + struct signalfd_siginfo fdsi; + ssize_t size; + + size = read_retry(fd_signal, &fdsi, sizeof(fdsi)); + + if (size != sizeof(fdsi)) { + err("unable to read signal info"); + 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 == fd_conn) { + int conn; + + if ((conn = accept4(fd_conn, NULL, 0, SOCK_CLOEXEC)) < 0) { + err("accept4: %m"); + continue; + } + + if (set_recv_timeout(conn, 3) < 0) { + close(conn); + continue; + } + + if (!process_task(conn)) { + /* reset timer */ + nsec = 0; + } + + close(conn); + } + } + } +fail: + epollin_remove(fd_ep, fd_signal); + epollin_remove(fd_ep, fd_conn); + + if (fd_ep >= 0) + close(fd_ep); + + unlink(socketpath); + + return ret; +} + +pid_t +fork_server(int cl_conn, uid_t uid, gid_t gid, unsigned num) +{ + pid_t pid = fork(); + + if (pid != 0) { + if (pid < 0) { + err("fork: %m"); + return -1; + } + return pid; + } + + caller_num = num; + + int ret = init_caller_data(uid, gid); + + if (ret < 0) { + info("%s(%d) num=%u: start session server", caller_user, caller_uid, caller_num); + ret = caller_server(cl_conn); + info("%s(%d): finish session server", caller_user, caller_uid); + } + + if (ret < 0) { + send_command_response(cl_conn, CMD_STATUS_FAILED, NULL); + ret = EXIT_FAILURE; + } else { + ret = EXIT_SUCCESS; + } + + free_caller_data(); + close(cl_conn); + + exit(ret); +} diff --git a/hasher-priv/caller_task.c b/hasher-priv/caller_task.c new file mode 100644 index 0000000..d8f2dd5 --- /dev/null +++ b/hasher-priv/caller_task.c @@ -0,0 +1,214 @@ + +/* + Copyright (C) 2019 Alexey Gladkov + + The 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" + +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; +} + +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; +} + +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; +} + +static int +caller_task(struct task *task) +{ + int rc = EXIT_FAILURE; + int i = 0; + pid_t pid; + + if ((pid = fork()) != 0) { + if (pid < 0) { + err("fork: %m"); + return -1; + } + return pid; + } + + if ((rc = reopen_iostreams(task->stdin, task->stdout, task->stderr)) < 0) + exit(rc); + + /* cleanup 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); + + if (chroot_path && *chroot_path != '/') + fatal("%s: invalid chroot path", chroot_path); + + /* Fourth, parse environment for config options. */ + parse_env(); + + /* We don't need environment variables any longer. */ + if (clearenv() != 0) + fatal("clearenv: %m"); + + /* Finally, execute choosen 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 of all user-space buffered data */ + fflush(stdout); + fflush(stderr); + + exit(rc); +} + +int +process_caller_task(int conn, struct task *task) +{ + int rc = EXIT_FAILURE; + pid_t pid, cpid; + + if ((pid = fork()) != 0) { + if (pid < 0) { + err("fork: %m"); + return -1; + } + return 0; + } + + if ((cpid = 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; + } + } + } + + if (task->env) { + free(task->env[0]); + free(task->env); + } + + if (task->argv) { + free(task->argv[0]); + free(task->argv); + } + + /* Notify client about result */ + (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..77f93e1 --- /dev/null +++ b/hasher-priv/communication.c @@ -0,0 +1,392 @@ + +/* + Copyright (C) 2019 Alexey Gladkov + + Functions for communication between the hasher-priv and the hasher-privd. + + 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); + +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..e6f1530 --- /dev/null +++ b/hasher-priv/communication.h @@ -0,0 +1,77 @@ + +/* + Copyright (C) 2019 Alexey Gladkov + + Functions for communication between the hasher-priv and the hasher-privd. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef _COMMUNICATION_H_ +#define _COMMUNICATION_H_ + +#include + +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 { + int 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..6b6bdb1 100644 --- a/hasher-priv/config.c +++ b/hasher-priv/config.c @@ -1,6 +1,7 @@ /* Copyright (C) 2003-2019 Dmitry V. Levin + Copyright (C) 2019 Alexey Gladkov Configuration support module for the hasher-priv program. @@ -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_control_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; @@ -633,3 +641,134 @@ parse_env(void) if ((e = getenv("requested_mountpoints"))) parse_str_list(e, &requested_mountpoints); } + +static void +check_server_control_group(void) +{ + struct group *gr; + + if (!server_control_group || !*server_control_group) + error(EXIT_FAILURE, 0, "config: undefined: control_group"); + + gr = getgrnam(server_control_group); + + if (!gr || !gr->gr_name) + error(EXIT_FAILURE, 0, "config: control_group: %s lookup failure", server_control_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("control_group", name)) { + free(server_control_group); + server_control_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("server"); + check_server_control_group(); +} + +void +free_server_configuration(void) +{ + free(server_pidfile); + free(server_control_group); +} 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..9eb598c --- /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, PROJECT, caller_num) < 0) + return EXIT_FAILURE; + + /* Open user session */ + snprintf(socketname, sizeof(socketname), "hasher-priv-%d-%u", geteuid(), caller_num); + + if ((conn = srv_connect(SOCKETDIR, socketname)) < 0) + return EXIT_FAILURE; + + if (server_task(conn, task) < 0) + return EXIT_FAILURE; + + if (server_task_fds(conn) < 0) + return EXIT_FAILURE; + + if (server_command(conn, CMD_TASK_ARGUMENTS, task_args) < 0) + return EXIT_FAILURE; + + if (server_command(conn, CMD_TASK_ENVIRON, ev) < 0) + return EXIT_FAILURE; + + if (server_command(conn, CMD_TASK_RUN, NULL) < 0) + return EXIT_FAILURE; + + /* Close session socket. */ + close(conn); + + return EXIT_SUCCESS; +} diff --git a/hasher-priv/hasher-privd.c b/hasher-priv/hasher-privd.c new file mode 100644 index 0000000..5c56033 --- /dev/null +++ b/hasher-priv/hasher-privd.c @@ -0,0 +1,375 @@ + +/* + Copyright (C) 2019 Alexey Gladkov + + The entry function for the hasher-privd program. + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include +#include +#include +#include /* umask */ +#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" + +struct session { + struct session *next; + + uid_t caller_uid; + gid_t caller_gid; + + unsigned caller_num; + + pid_t server_pid; +}; + +static struct session *pool = NULL; +unsigned caller_num; + +static int +start_session(int conn, unsigned num) +{ + uid_t uid; + gid_t gid; + pid_t server_pid; + struct session **a = &pool; + + if (get_peercred(conn, NULL, &uid, &gid) < 0) + return -1; + + while (a && *a) { + if ((*a)->caller_uid == uid && (*a)->caller_num == num) { + send_command_response(conn, CMD_STATUS_DONE, NULL); + return 0; + } + a = &(*a)->next; + } + + info("start session for %d:%u user", uid, num); + + if ((server_pid = fork_server(conn, uid, gid, num)) < 0) + return -1; + + *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; + (*a)->server_pid = server_pid; + + 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(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(conn, num); + 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 (!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); + + int fd_ep = -1; + + if ((fd_ep = epoll_create1(EPOLL_CLOEXEC)) < 0) + fatal("epoll_create1: %m"); + + int fd_signal = -1; + + if ((fd_signal = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC)) < 0) + fatal("signalfd: %m"); + + mode_t m = umask(017); + + int fd_conn = -1; + + if ((fd_conn = srv_listen(SOCKETDIR, PROJECT)) < 0) + return EXIT_FAILURE; + + umask(m); + + snprintf(socketpath, sizeof(socketpath), "%s/%s", SOCKETDIR, PROJECT); + + if (chown(socketpath, 0, server_gid)) + fatal("fchown: %s: %m", socketpath); + + if (epollin_add(fd_ep, fd_signal) < 0 || epollin_add(fd_ep, 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(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 == fd_signal) { + struct signalfd_siginfo fdsi; + ssize_t size; + + size = read_retry(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 == fd_conn) { + int conn; + + if ((conn = accept4(fd_conn, NULL, 0, SOCK_CLOEXEC)) < 0) { + err("accept4: %m"); + continue; + } + + if (set_recv_timeout(conn, 3) < 0) { + close(conn); + continue; + } + + process_request(conn); + close(conn); + } + } + + if (finish_server) { + if (!pool) + break; + + if (fd_conn >= 0) { + epollin_remove(fd_ep, fd_conn); + fd_conn = -1; + ep_timeout = 3000; + } + + finish_sessions(SIGTERM); + } + } + + epollin_remove(fd_ep, fd_signal); + epollin_remove(fd_ep, fd_conn); + + if (fd_ep >= 0) + close(fd_ep); + + 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..9adac47 --- /dev/null +++ b/hasher-priv/logging.c @@ -0,0 +1,64 @@ + +/* + Copyright (C) 2019 Alexey Gladkov + + A 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 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 stderr) +{ + int options = LOG_PID; + if (stderr) + options |= LOG_PERROR; + log_priority = loglevel; + openlog(program_invocation_short_name, options, LOG_DAEMON); +} + +void logging_close(void) +{ + closelog(); +} + +void +message(int priority, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + if (priority <= log_priority) + vsyslog(priority, fmt, ap); + else if (log_priority < 0) { + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); + } + va_end(ap); +} diff --git a/hasher-priv/logging.h b/hasher-priv/logging.h new file mode 100644 index 0000000..9d28fc8 --- /dev/null +++ b/hasher-priv/logging.h @@ -0,0 +1,55 @@ + +/* + Copyright (C) 2019 Alexey Gladkov + + A 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..9e23e74 --- /dev/null +++ b/hasher-priv/pidfile.c @@ -0,0 +1,128 @@ + +/* + pidfile.c - 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 +*/ + +/* + * 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..f0eb9f9 100644 --- a/hasher-priv/priv.h +++ b/hasher-priv/priv.h @@ -18,16 +18,7 @@ #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" typedef struct { @@ -68,9 +59,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 +80,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 +118,14 @@ int do_chrootuid1(void); int do_getugid2(void); int do_chrootuid2(void); +int process_caller_task(int, struct task *); +pid_t fork_server(int, uid_t, gid_t, unsigned); + 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 +146,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 +159,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_control_group; +extern char *server_pidfile; +extern gid_t server_gid; + #endif /* PKG_BUILD_PRIV_H */ diff --git a/hasher-priv/server.conf b/hasher-priv/server.conf new file mode 100644 index 0000000..53ea5c3 --- /dev/null +++ b/hasher-priv/server.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. +control_group=hashman diff --git a/hasher-priv/sockets.c b/hasher-priv/sockets.c new file mode 100644 index 0000000..42618a7 --- /dev/null +++ b/hasher-priv/sockets.c @@ -0,0 +1,183 @@ + +/* + Copyright (C) 2019 Alexey Gladkov + + A сollection of helpers to simplify work with 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. */ + +int srv_listen(const char *dir_name, const char *file_name) +{ + struct sockaddr_un sun = { .sun_family = AF_UNIX }; + + snprintf(sun.sun_path, sizeof(sun.sun_path), "%s/%s", dir_name, file_name); + + if (unlink(sun.sun_path) && errno != ENOENT) { + err("unlink: %s: %m", sun.sun_path); + return -1; + } + + if (mkdir(dir_name, 0700) && errno != EEXIST) { + err("mkdir: %s: %m", dir_name); + 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..5acdb49 --- /dev/null +++ b/hasher-priv/sockets.h @@ -0,0 +1,32 @@ + +/* + Copyright (C) 2019 Alexey Gladkov + + A collection of helpers to simplify work with 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 *, const char *) __attribute__((nonnull(1, 2))); +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.24.0