ALT Linux Team development discussions
 help / color / mirror / Atom feed
From: Arseny Maslennikov <arseny@altlinux.org>
To: devel@lists.altlinux.org
Cc: Arseny Maslennikov <arseny@altlinux.org>,
	Alexey Gladkov <legion@altlinux.org>
Subject: [devel] [PATCH hasher-priv v3 1/7] Turn hasher-priv into a daemon
Date: Tue, 24 Aug 2021 11:24:30 +0300
Message-ID: <20210824082436.1555890-2-arseny@altlinux.org> (raw)
In-Reply-To: <20210824082436.1555890-1-arseny@altlinux.org>

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 <legion@altlinux.org>
Signed-off-by: Arseny Maslennikov <arseny@altlinux.org>
---
 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 <chroot path> <program> [program args]
-      getugid2
-      chrootuid2 <chroot path> <program> [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 <chroot path> <program> [program args]
+        getugid2
+        chrootuid2 <chroot path> <program> [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 <legion@altlinux.org>
+
+  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 <linux/un.h>
+
+#include <sys/socket.h> /* SOCK_CLOEXEC */
+#include <sys/prctl.h>
+#include <sys/signalfd.h>
+#include <sys/wait.h>
+
+#include <stdio.h>
+#include <errno.h>
+#include <unistd.h>
+#include <grp.h>
+
+#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 <legion@altlinux.org>
+
+  The task handling part of the hasher-privd program.
+
+  SPDX-License-Identifier: GPL-2.0-or-later
+*/
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <sys/param.h>
+#include <sys/wait.h>
+
+#include <unistd.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdint.h>
+
+#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 <legion@altlinux.org>
+
+  This file offers some helper functions for client-server communication.
+
+  SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <string.h>
+#include <unistd.h>
+
+#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 <legion@altlinux.org>
+
+  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 <stddef.h>
+#include <stdint.h>
+
+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 <ldv@altlinux.org>
+  Copyright (C) 2019  Alexey Gladkov <legion@altlinux.org>
 
-  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 <unistd.h>
 #include <limits.h>
 #include <pwd.h>
+#include <grp.h>
 
 #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 <legion@altlinux.org>
+
+  The helpers for epoll API for the hasher-privd program.
+
+  SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include <sys/epoll.h>
+
+#include <string.h>
+#include <unistd.h>
+
+#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 <legion@altlinux.org>
+
+  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 <sys/epoll.h>
+
+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 <ldv@altlinux.org>
+
+  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 <linux/un.h>
+
+#include <errno.h>
+#include <error.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+
+#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 <legion@altlinux.org>
+
+  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 <linux/un.h>
+
+#include <sys/types.h>
+#include <sys/epoll.h>
+#include <sys/signalfd.h>
+#include <sys/stat.h> /* umask */
+#include <sys/wait.h>
+#include <sys/socket.h>
+
+#include <errno.h>
+#include <error.h>
+#include <getopt.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <setproctitle.h>
+
+#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 <ldv@altlinux.org>
 
-  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 <ldv@altlinux.org>
 
-  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 <ldv@altlinux.org>
 
-  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 <legion@altlinux.org>
+
+  Logging functions for the hasher-privd program.
+
+  SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include <stdio.h>
+#include <stdarg.h>
+#include <strings.h>
+#include <errno.h>
+#include <syslog.h>
+
+#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 <legion@altlinux.org>
+
+  Logging functions for the hasher-privd program.
+
+  SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#ifndef _LOGGING_H_
+#define _LOGGING_H_
+
+#include <syslog.h>
+#include <stdlib.h>
+
+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 <ldv@altlinux.org>
-
-  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 <errno.h>
-#include <error.h>
-#include <stdio.h>
-#include <stdlib.h>
-
-#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 <unistd.h>
 #include <sys/socket.h>
 
+#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 <Martin.Schulze@Linux.DE>
+
+    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 <errno.h>
+#include <signal.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/file.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#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 <Martin.Schulze@Linux.DE>
+
+    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 <legion@altlinux.org>
+
+  A collection of helpers to simplify the use of sockets.
+
+  SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/un.h>
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#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 <legion@altlinux.org>
+
+  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 <sys/types.h>
+#include <sys/socket.h>
+
+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 <unistd.h>
+
+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 <stdint.h>
+
+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



  reply	other threads:[~2021-08-24  8:24 UTC|newest]

Thread overview: 11+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-08-24  8:24 [devel] [PATCH hasher-priv v3 0/7] hasher-privd Arseny Maslennikov
2021-08-24  8:24 ` Arseny Maslennikov [this message]
2021-08-24  8:24 ` [devel] [PATCH hasher-priv v3 2/7] sockets: xsendmsg: get rid of SIGPIPE on socket writes Arseny Maslennikov
2021-08-24  8:24 ` [devel] [PATCH hasher-priv v3 3/7] chrootuid: explicitly reset signal mask before forking off payload Arseny Maslennikov
2021-12-01 19:23   ` Dmitry V. Levin
2021-12-03 15:03     ` Arseny Maslennikov
2021-12-03 16:06       ` Dmitry V. Levin
2021-08-24  8:24 ` [devel] [PATCH hasher-priv v3 4/7] Link with libsetproctitle by Dmitry V. Levin Arseny Maslennikov
2021-08-24  8:24 ` [devel] [PATCH hasher-priv v3 5/7] daemon: set titles for subprocesses Arseny Maslennikov
2021-08-24  8:24 ` [devel] [PATCH hasher-priv v3 6/7] Add systemd and sysvinit service files Arseny Maslennikov
2021-08-24  8:24 ` [devel] [PATCH hasher-priv v3 7/7] Install hasher-priv without set ugids Arseny Maslennikov

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20210824082436.1555890-2-arseny@altlinux.org \
    --to=arseny@altlinux.org \
    --cc=devel@lists.altlinux.org \
    --cc=legion@altlinux.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link

ALT Linux Team development discussions

This inbox may be cloned and mirrored by anyone:

	git clone --mirror http://lore.altlinux.org/devel/0 devel/git/0.git

	# If you have public-inbox 1.1+ installed, you may
	# initialize and index your mirror using the following commands:
	public-inbox-init -V2 devel devel/ http://lore.altlinux.org/devel \
		devel@altlinux.org devel@altlinux.ru devel@lists.altlinux.org devel@lists.altlinux.ru devel@linux.iplabs.ru mandrake-russian@linuxteam.iplabs.ru sisyphus@linuxteam.iplabs.ru
	public-inbox-index devel

Example config snippet for mirrors.
Newsgroup available over NNTP:
	nntp://lore.altlinux.org/org.altlinux.lists.devel


AGPL code for this site: git clone https://public-inbox.org/public-inbox.git