[devel] [PATCH v2 1/6] Turn hasher-priv into a daemon

Arseny Maslennikov arseny на altlinux.org
Чт Окт 22 14:43:38 MSK 2020


From: Alexey Gladkov <legion на altlinux.org>

From: Alexey Gladkov <legion на 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.

Signed-off-by: Alexey Gladkov <legion на altlinux.org>
Co-authored-by: Arseny Maslennikov <arseny на 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 | 382 ++++++++++++++++++++++++++++++++++
 hasher-priv/caller_task.c   | 217 ++++++++++++++++++++
 hasher-priv/cmdline.c       |  27 ++-
 hasher-priv/communication.c | 394 ++++++++++++++++++++++++++++++++++++
 hasher-priv/communication.h |  79 ++++++++
 hasher-priv/config.c        | 145 ++++++++++++-
 hasher-priv/epoll.c         |  39 ++++
 hasher-priv/epoll.h         |  18 ++
 hasher-priv/hasher-priv.c   |  78 +++++++
 hasher-priv/hasher-privd.c  | 381 ++++++++++++++++++++++++++++++++++
 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          |  33 +--
 hasher-priv/server.conf     |  13 ++
 hasher-priv/sockets.c       | 183 +++++++++++++++++
 hasher-priv/sockets.h       |  32 +++
 hasher-priv/x11.c           |   1 +
 28 files changed, 2663 insertions(+), 247 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..8a57d2d 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 /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 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
++ 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 a815e9e..ce55274 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 = /run/hasher-priv
 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..abc2746
--- /dev/null
+++ b/hasher-priv/caller_server.c
@@ -0,0 +1,382 @@
+
+/*
+  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
+*/
+
+#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
+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 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_caller_task() sends a response by itself. */
+				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),
+		"%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/%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 the 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 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 == 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;
+	}
+
+	/* From now on the child sends its own failure responses. */
+	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..8965196
--- /dev/null
+++ b/hasher-priv/caller_task.c
@@ -0,0 +1,217 @@
+
+/*
+  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"
+
+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;
+
+	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);
+}
+
+int
+process_caller_task(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 ((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 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..2e3d442
--- /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..48a4e2d
--- /dev/null
+++ b/hasher-priv/communication.h
@@ -0,0 +1,79 @@
+
+/*
+  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 <stdint.h>
+
+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 {
+	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..ce83e15 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;
@@ -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("server");
+	check_server_access_group();
+}
+
+void
+free_server_configuration(void)
+{
+	free(server_pidfile);
+	free(server_access_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 <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..f0c8fa8
--- /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)
+		return EXIT_FAILURE;
+
+	/* Open user session */
+	snprintf(socketname, sizeof(socketname), "%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..270804e
--- /dev/null
+++ b/hasher-priv/hasher-privd.c
@@ -0,0 +1,381 @@
+
+/*
+  Copyright (C) 2019  Alexey Gladkov <legion на altlinux.org>
+
+  The entry function for the hasher-privd program.
+
+  SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#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 "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) {
+			/* Session exists and will be reused. */
+			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);
+			/* 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);
+
+	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, main_socket_base_name)) < 0)
+		return EXIT_FAILURE;
+
+	umask(m);
+
+	snprintf(socketpath, sizeof(socketpath), "%s/%s", SOCKETDIR, main_socket_base_name);
+
+	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 <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..7d5a5a5 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_access_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..2d740a6
--- /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.
+access_group=hashman
diff --git a/hasher-priv/sockets.c b/hasher-priv/sockets.c
new file mode 100644
index 0000000..5ba3511
--- /dev/null
+++ b/hasher-priv/sockets.c
@@ -0,0 +1,183 @@
+
+/*
+  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. */
+
+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..1ee37a2
--- /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 *, 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 <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.25.4



Подробная информация о списке рассылки Devel