Sisyphus repository
Last update: 11 december 2017 | SRPMs: 18127 | Visits: 10547878
en ru br
ALT Linux repos
S:4.1.20060426-alt10
5.0: 4.1.20060426-alt6
4.1: 4.1.20060426-alt4
4.0: 4.1.20060426-alt4
3.0: 4.1.20040916-alt2

Group :: System/Servers
RPM: vixie-cron

 Main   Changelog   Spec   Patches   Sources   Download   Gear   Bugs and FR  Repocop 

Patch: vixie-cron-4.1.20060426-alt-selinux.patch
Download


diff --git a/usr.bin/crontab/Makefile b/usr.bin/crontab/Makefile
index d6da601..c08d580 100644
--- a/usr.bin/crontab/Makefile
+++ b/usr.bin/crontab/Makefile
@@ -1,10 +1,11 @@
 #	$OpenBSD: Makefile,v 1.5 2005/12/19 19:12:17 millert Exp $
 
 PROG=	crontab
-SRCS=	crontab.c misc.c entry.c env.c  closeall.c  ../../lib/libc/gen/pw_dup.c
-CFLAGS+=-I${.CURDIR} -I${.CURDIR}/../../usr.sbin/cron -DDEBUGGING=0
+SRCS=	crontab.c misc.c entry.c env.c  closeall.c selinux.c ../../lib/libc/gen/pw_dup.c
+CFLAGS+=-I${.CURDIR} -I${.CURDIR}/../../usr.sbin/cron -DDEBUGGING=0 -DWITH_SELINUX=1
 BINGRP =crontab
 BINMODE=2555
+LDLIBS+=-lselinux
 MAN=	crontab.1 crontab.5
 
 #.PATH: ${.CURDIR}/../../usr.sbin/cron
diff --git a/usr.sbin/cron/Makefile b/usr.sbin/cron/Makefile
index 9f4d50e..ee7e7f2 100644
--- a/usr.sbin/cron/Makefile
+++ b/usr.sbin/cron/Makefile
@@ -2,9 +2,9 @@
 
 PROG=	crond
 SRCS=	cron.c database.c user.c entry.c job.c do_command.c \
-	misc.c env.c popen.c atrun.c closeall.c pam_auth.c ../../lib/libc/gen/pw_dup.c
-CFLAGS+=-I${.CURDIR} -DHAVE_SETPROCTITLE=1
-LDLIBS+=-lpam -lsetproctitle
+	misc.c env.c popen.c atrun.c closeall.c pam_auth.c selinux.c ../../lib/libc/gen/pw_dup.c
+CFLAGS+=-I${.CURDIR} -DHAVE_SETPROCTITLE=1 -DWITH_SELINUX=1
+LDLIBS+=-lpam -lsetproctitle -lselinux
 MAN=	cron.8
 
 #.include <bsd.prog.mk>
diff --git a/usr.sbin/cron/atrun.c b/usr.sbin/cron/atrun.c
index 328b729..8a53d71 100644
--- a/usr.sbin/cron/atrun.c
+++ b/usr.sbin/cron/atrun.c
@@ -529,6 +529,17 @@ run_job(atjob *job, char *atfile)
 		if (job->queue > 'b')
 			(void)setpriority(PRIO_PROCESS, 0, job->queue - 'b');
 
+#ifdef WITH_SELINUX
+		if (is_selinux_enabled() > 0) {
+			char *err_msg = NULL;
+			if (set_at_selinux_context(pw->pw_name, STDIN, &err_msg) < 0) {
+				fprintf(stderr, "at job %s: %s\n", atfile, err_msg);
+				free(err_msg);
+				if (security_getenforce() > 0)
+					_exit(ERROR_EXIT);
+			}
+		}
+#endif /* WITH_SELINUX */
 #if DEBUGGING
 		if (DebugFlags & DTEST) {
 			fprintf(stderr,
diff --git a/usr.sbin/cron/crontab.1 b/usr.sbin/cron/crontab.1
index 26f19c9..603f1ff 100644
--- a/usr.sbin/cron/crontab.1
+++ b/usr.sbin/cron/crontab.1
@@ -117,6 +117,11 @@ environment variables.
 After you exit from the editor, the modified
 .Xr crontab 5
 will be installed automatically.
+.It Fl s
+It will append the current SELinux security context string as an
+MLS_LEVEL setting to the crontab file before editing / replacement
+occurs - see the documentation of MLS_LEVEL in
+.Xr crontab 5 .
 .El
 .Sh FILES
 .Bl -tag -width "/etc/cron.allow" -compact
diff --git a/usr.sbin/cron/crontab.5 b/usr.sbin/cron/crontab.5
index 838a330..2cba75d 100644
--- a/usr.sbin/cron/crontab.5
+++ b/usr.sbin/cron/crontab.5
@@ -167,6 +167,24 @@ Otherwise mail is sent to the owner of the
 This option is useful for pseudo-users that lack an alias
 that would otherwise redirect the mail to a real person.
 .Pp
+The
+.Ev MLS_LEVEL
+environment variable provides support for multiple per-job
+SELinux security contexts in the same crontab.
+By default, cron jobs execute with the default SELinux security context of the
+user that created the crontab file.
+When using multiple security levels and roles, this may not be sufficient, because
+the same user may be running in a different role or at a different security level.
+For more about roles and SELinux MLS/MCS see
+.Xr selinux 8
+and undermentioned crontab example.
+You can set MLS_LEVEL to the SELinux security context string specifying
+the SELinux security context in which you want the job to run, and crond will set
+the execution context of the or jobs to which the setting applies to the specified
+context.
+See also the
+.Xr crontab 1 -s option.
+.Pp
 .Em cron Commands
 .Pp
 The format of a
@@ -368,6 +386,27 @@ MAILTO=paul
 23 0-23/2 * * * echo "run 23 minutes after midn, 2am, 4am ..., everyday"
 5 4 * * sun     echo "run at 5 after 4 every sunday"
 .Ed
+.Sh SELinux with multi level security (MLS)
+In crontab is important specified security level by \fIcrontab\ -s\fR or specifying
+the required level on the first line of the crontab. Each level is specified
+in \fI/etc/selinux/targeted/seusers\fR. For using crontab in MLS mode is really important:
+.br
+- check/change actual role,
+.br
+- set correct \fIrole for directory\fR, which is used for input/output.
+.Sh EXAMPLE FOR SELINUX MLS
+.nf
+# login as root
+newrole -r sysadm_r
+mkdir /tmp/SystemHigh
+chcon -l SystemHigh /tmp/SystemHigh
+crontab -e
+# write in crontab file
+MLS_LEVEL=SystemHigh
+0-59 * * * * id -Z > /tmp/SystemHigh/crontest
+When I log in as a normal user, it can't work, because \fI/tmp/SystemHigh\fR is
+higher than my level.
+.fi
 .Sh SEE ALSO
 .Xr crontab 1 ,
 .Xr cron 8
diff --git a/usr.sbin/cron/crontab.c b/usr.sbin/cron/crontab.c
index 31889af..4335ef4 100644
--- a/usr.sbin/cron/crontab.c
+++ b/usr.sbin/cron/crontab.c
@@ -35,13 +35,20 @@ static char const rcsid[] = "$OpenBSD: crontab.c,v 1.49 2005/11/29 20:43:31 mill
 #define NHEADER_LINES 3
 #define CRONTAB_TEMPLATE "/etc/crontab.template"
 
+#ifdef WITH_SELINUX
+static int set_mls_level;
+#define OPT_S "s"
+#else
+#define OPT_S
+#endif /* WITH_SELINUX */
+
 enum opt_t	{ opt_unknown, opt_list, opt_delete, opt_edit, opt_replace };
 
 #if DEBUGGING
 static char	*Options[] = { "???", "list", "delete", "edit", "replace" };
-static char	*getoptargs = "u:lerx:";
+static char	*getoptargs = "u:lerx:" OPT_S;
 #else
-static char	*getoptargs = "u:ler";
+static char	*getoptargs = "u:ler" OPT_S;
 #endif
 
 static	PID_T		Pid;
@@ -71,6 +78,9 @@ usage(const char *msg) {
 	fprintf(stderr, "\t-e\t(edit user's crontab)\n");
 	fprintf(stderr, "\t-l\t(list user's crontab)\n");
 	fprintf(stderr, "\t-r\t(delete user's crontab)\n");
+#ifdef WITH_SELINUX
+	fprintf(stderr, "\t-s\t(selinux context)\n");
+#endif /* WITH_SELINUX */
 	exit(ERROR_EXIT);
 }
 
@@ -154,6 +164,13 @@ parse_args(int argc, char *argv[]) {
 					"must be privileged to use -u\n");
 				exit(ERROR_EXIT);
 			}
+#ifdef WITH_SELINUX
+			if (selinux_crontab_access_denied()) {
+				fprintf(stderr,
+					"access denied by SELinux, must be privileged to use -u\n");
+				exit(ERROR_EXIT);
+			}
+#endif /* WITH_SELINUX */
 			if (!(pw = getpwnam(optarg))) {
 				fprintf(stderr, "%s:  user `%s' unknown\n",
 					ProgramName, optarg);
@@ -177,6 +194,12 @@ parse_args(int argc, char *argv[]) {
 				usage("only one operation permitted");
 			Option = opt_edit;
 			break;
+#ifdef WITH_SELINUX
+		case 's':
+			set_mls_level = 1;
+			break;
+#endif /* WITH_SELINUX */
+
 		default:
 			usage("unrecognized option");
 		}
@@ -363,6 +386,15 @@ edit_cmd(void) {
 	Set_LineNum(1)
 	copy_crontab(f, NewCrontab);
 	fclose(f);
+#ifdef WITH_SELINUX
+	if (set_mls_level) {
+		/* Do not write MLS_LEVEL again in replace_cmd() */
+		set_mls_level = 0;
+		if (write_crontab_mls_level(NewCrontab) < 0)
+			goto fatal;
+	}
+#endif /* WITH_SELINUX */
+
 	if (fflush(NewCrontab) < OK) {
 		perror(Filename);
 		exit(ERROR_EXIT);
@@ -592,6 +624,14 @@ replace_cmd(void) {
 	fprintf(tmp, "# (%s installed on %-24.24s)\n", Filename, ctime(&now));
 	fprintf(tmp, "# (Cron version %s -- %s)\n", CRON_VERSION, rcsid);
 
+#ifdef WITH_SELINUX
+	if (set_mls_level && write_crontab_mls_level(NewCrontab) < 0) {
+		fclose(tmp);
+		error = -2;
+		goto done;
+	}
+#endif /* WITH_SELINUX */
+
 	/* copy the crontab to the tmp
 	 */
 	rewind(NewCrontab);
diff --git a/usr.sbin/cron/database.c b/usr.sbin/cron/database.c
index 28b0dd6..d0cbf84 100644
--- a/usr.sbin/cron/database.c
+++ b/usr.sbin/cron/database.c
@@ -40,7 +40,7 @@ struct spooldir {
 
 static struct spooldir spools[] = {
 	{SPOOL_DIR, NULL, NULL},
-	{"/etc/cron.d", "root", "*system*"},
+	{"/etc/cron.d", "root", SYSUSERNAME},
 	{NULL, NULL, NULL}
 };
 
@@ -99,7 +99,7 @@ load_database(cron_db *old_db) {
 	new_db.head = new_db.tail = NULL;
 
 	if (syscron_stat.st_mtime) {
-		process_crontab(ROOT_USER, "*system*", SYSCRONTAB, &syscron_stat,
+		process_crontab(ROOT_USER, SYSUSERNAME, SYSCRONTAB, &syscron_stat,
 				&new_db, old_db);
 	}
 
@@ -210,7 +210,7 @@ process_crontab(const char *uname, const char *fname, const char *tabname,
 	struct stat lstatbuf;
 	user *u;
 
-	if (strcmp(fname, "*system*") && !(pw = getpwnam(uname))) {
+	if (strcmp(fname, SYSUSERNAME) && !(pw = getpwnam(uname))) {
 		/* file doesn't have a user in passwd file.
 		 * 		 */
 		log_it(fname, getpid(), "ORPHAN", "no passwd entry");
@@ -295,7 +295,8 @@ process_crontab(const char *uname, const char *fname, const char *tabname,
 		free_user(u);
 		log_it(fname, getpid(), "RELOAD", tabname);
 	}
-	u = load_user(crontab_fd, pw, fname);
+
+	u = load_user(crontab_fd, pw, fname, tabname);
 	if (u != NULL) {
 		u->mtime = statbuf->st_mtime;
 		link_user(new_db, u);
diff --git a/usr.sbin/cron/do_command.c b/usr.sbin/cron/do_command.c
index d84fc0d..ab4961a 100644
--- a/usr.sbin/cron/do_command.c
+++ b/usr.sbin/cron/do_command.c
@@ -313,6 +313,13 @@ child_process(entry *e, user *u) {
 				_exit(OK_EXIT);
 			}
 # endif /*DEBUGGING*/
+#ifdef WITH_SELINUX
+			if (is_selinux_enabled() > 0 &&
+			    set_job_selinux_context(u, envp) &&
+			    security_getenforce() > 0) {
+				_exit(ERROR_EXIT);
+			}
+#endif /* WITH_SELINUX */
 			execle(shell, shell, "-c", e->cmd, (char *)NULL, envp);
 			fprintf(stderr, "execle: couldn't exec `%s'\n", shell);
 			perror("execle");
diff --git a/usr.sbin/cron/externs.h b/usr.sbin/cron/externs.h
index 20b1dec..8af57eb 100644
--- a/usr.sbin/cron/externs.h
+++ b/usr.sbin/cron/externs.h
@@ -68,6 +68,10 @@
 # include <bsd_auth.h>
 #endif /*BSD_AUTH*/
 
+#ifdef WITH_SELINUX
+#include <selinux/selinux.h>
+#endif /* WITH_SELINUX */
+
 #define DIR_T	struct dirent
 #define WAIT_T	int
 #define SIG_T	sig_t
diff --git a/usr.sbin/cron/funcs.h b/usr.sbin/cron/funcs.h
index 49f9c4d..2460abe 100644
--- a/usr.sbin/cron/funcs.h
+++ b/usr.sbin/cron/funcs.h
@@ -69,7 +69,7 @@ char		*env_get(char *, char **),
 struct passwd	*pw_dup(const struct passwd *);
 void		mkprint(char *, unsigned char *, int);
 
-user		*load_user(int, struct passwd *, const char *),
+user		*load_user(int, struct passwd *, const char *, const char *),
 		*find_user(cron_db *, const char *);
 
 entry		*load_entry(FILE *,
@@ -89,3 +89,11 @@ extern void cron_pam_finish (void);
 extern void cron_pam_child_close (void);
 extern char **cron_pam_getenvlist (char **envp);
 #endif
+
+#ifdef WITH_SELINUX
+security_context_t get_selinux_context(const char *name, int fd, char **err_msg);
+int set_job_selinux_context(const user *u, char **envp);
+int set_at_selinux_context(const char *name, int fd, char **err_msg);
+int selinux_crontab_access_denied(void);
+int write_crontab_mls_level(FILE *crontab);
+#endif /* WITH_SELINUX */
diff --git a/usr.sbin/cron/macros.h b/usr.sbin/cron/macros.h
index e96c342..1b856ab 100644
--- a/usr.sbin/cron/macros.h
+++ b/usr.sbin/cron/macros.h
@@ -70,6 +70,8 @@
 #define MAXHOSTNAMELEN 64
 #endif
 
+#define SYSUSERNAME "*system*"
+
 #define	Skip_Blanks(c, f) \
 			while (c == '\t' || c == ' ') \
 				c = get_char(f);
diff --git a/usr.sbin/cron/selinux.c b/usr.sbin/cron/selinux.c
new file mode 100644
index 0000000..07dd291
--- /dev/null
+++ b/usr.sbin/cron/selinux.c
@@ -0,0 +1,236 @@
+#include "cron.h"
+#include <selinux/context.h>
+#include <selinux/get_context_list.h>
+#include <selinux/flask.h>
+#include <selinux/av_permissions.h>
+
+static int
+selinux_authorize(security_context_t scon, security_context_t tcon,
+		security_class_t tclass,  access_vector_t requested) {
+	struct av_decision avd;
+	int rc;
+
+	rc = security_compute_av(scon, tcon, tclass, requested, &avd);
+	if (rc || ((requested & avd.allowed) != requested))
+		return 0;
+	return 1;
+}
+
+static int
+selinux_authorize_context(security_context_t scontext,
+		security_context_t file_context) {
+	/* Since crontab files are not directly executed,
+	 * crond must ensure that the crontab file has
+	 * a context that is appropriate for the context of
+	 * the user cron job.  It performs an entrypoint
+	 * permission check for this purpose.
+	 */
+	return selinux_authorize(scontext, file_context, SECCLASS_FILE, FILE__ENTRYPOINT);
+}
+
+static int
+selinux_authorize_range(const security_context_t scontext,
+		const security_context_t ucontext) {
+	/* Since crontab files are not directly executed,
+	 * so crond must ensure that any user specified range
+	 * falls within the seusers-specified range for that Linux user.
+	 */
+	return selinux_authorize(scontext, ucontext, SECCLASS_CONTEXT, CONTEXT__CONTAINS);
+}
+
+static security_context_t
+get_job_context(const char *u_name, security_context_t u_scontext, const char *range)
+{
+	security_context_t scontext = NULL;
+	context_t ccon = NULL;
+
+	if (range) {
+		if (!(ccon = context_new(u_scontext))) {
+			log_it(u_name, getpid(),
+				"context_new FAILED for MLS_LEVEL", range);
+			return NULL;
+		}
+
+		if (context_range_set(ccon, range)) {
+			log_it(u_name, getpid(),
+				"context_range_set FAILED for MLS_LEVEL", range);
+			goto out;
+		}
+
+		if (!(scontext = context_str(ccon))) {
+			log_it(u_name, getpid(),
+				"context_str FAILED for MLS_LEVEL", range);
+			goto out;
+		}
+	}
+
+	if (!(scontext = strdup(scontext ? scontext : u_scontext))) {
+		log_it(u_name, getpid(), "strdup FAILED for MLS_LEVEL", range);
+	}
+
+out:
+	context_free(ccon);
+	return scontext;
+}
+
+security_context_t
+get_selinux_context(const char *name, int fd, char **err_msg) {
+	security_context_t user_context = NULL;
+	security_context_t file_context = NULL;
+	char *seuser = NULL;
+	char *level = NULL;
+
+	if (fgetfilecon(fd, &file_context) < OK) {
+		*err_msg = strdup("getfilecon FAILED");
+		return NULL;
+	}
+
+	if (name && getseuserbyname(name, &seuser, &level) < OK) {
+		*err_msg = strdup("NO SEUSER");
+		goto out;
+	}
+
+	if (get_default_context_with_level(name ? seuser : "system_u",
+					level, NULL, &user_context) < OK) {
+		*err_msg = strdup("No SELinux security context");
+		goto out;
+	}
+
+	if (!selinux_authorize_context(user_context, file_context)) {
+		freecon(user_context);
+		user_context = NULL;
+		*err_msg = strdup("Unauthorized SELinux context");
+	}
+
+out:
+	free(seuser);
+	free(level);
+	freecon(file_context);
+
+	return user_context;
+}
+
+int
+set_job_selinux_context(const user *u, char **envp)
+{
+	security_context_t scontext = NULL;
+	int rc = -1;
+
+	if (u->scontext == NULL) {
+		log_it(u->name, getpid(), "NULL security context for user", "");
+		return -1;
+	}
+
+	scontext = get_job_context(u->name, u->scontext, env_get("MLS_LEVEL", envp));
+	if (!scontext)
+		return -1;
+	if (strcmp(u->scontext, scontext) &&
+			!selinux_authorize_range(u->scontext, scontext)) {
+		char *msg = NULL;
+		if (asprintf(&msg, "Unauthorized range in %s for user range in %s",
+				(char *) scontext, (const char *)u->scontext) >= 0) {
+			log_it(u->name, getpid(), "ERROR", msg);
+			free(msg);
+		}
+		goto out;
+	}
+
+	if (setexeccon(scontext) < 0 || setkeycreatecon(scontext) < 0) {
+		char *msg = NULL;
+		if (asprintf(&msg, "Could not set exec or keycreate context to %s for user",
+				(char *) scontext) >= 0) {
+			log_it(u->name, getpid(), "ERROR", msg);
+			free(msg);
+		}
+		goto out;
+	}
+
+	rc = 0;
+out:
+	freecon(scontext);
+
+	return rc;
+}
+
+int
+set_at_selinux_context(const char *name, int fd, char **err_msg)
+{
+	security_context_t scontext = NULL;
+	int rc = 0;
+
+	scontext = get_selinux_context(name, fd, err_msg);
+	if (scontext == NULL)
+		return -1;
+
+	if (setexeccon(scontext) < 0 || setkeycreatecon(scontext) < 0) {
+		asprintf(err_msg, "Could not set exec or keycreate context to %s",
+					(char *) scontext);
+		rc = -1;
+	}
+
+	freecon(scontext);
+
+	return rc;
+}
+
+int
+selinux_crontab_access_denied(void) {
+	security_class_t passwd_class;
+	security_context_t scontext;
+	int rc = 1;
+
+	if (is_selinux_enabled() <= 0)
+		return 0;
+
+	if (getprevcon(&scontext)) {
+		perror("Cannot obtain SELinux process context");
+		return security_getenforce() > 0;
+	}
+
+	passwd_class = string_to_security_class("passwd");
+	if (passwd_class <= 0) {
+		fprintf(stderr, "Security class \"passwd\" is not defined in the SELinux policy.\n");
+		goto out;
+	}
+
+	rc = !selinux_authorize(scontext, scontext, passwd_class, PASSWD__CRONTAB);
+
+out:
+	freecon(scontext);
+
+	return rc ? (security_getenforce() > 0) : 0;
+}
+
+int
+write_crontab_mls_level(FILE *crontab) {
+	security_context_t scontext = NULL;
+	context_t ccon = NULL;
+	const char *level = NULL;
+	int rc = -1;
+
+	if (getprevcon(&scontext)) {
+		perror("Cannot obtain SELinux process context");
+		return -1;
+	}
+
+	if (!(ccon = context_new(scontext))) {
+		perror("context_new failed");
+		goto out;
+	}
+
+	if (!(level = context_range_get(ccon))) {
+		perror("context_range failed");
+		goto out;
+	}
+
+	rc = fprintf(crontab, "MLS_LEVEL=%s\n", level);
+	if (rc < 0)
+		perror("Cannot write MLS level");
+
+out:
+	context_free(ccon);
+	freecon(scontext);
+
+	return rc;
+}
+
diff --git a/usr.sbin/cron/structs.h b/usr.sbin/cron/structs.h
index 1b72d79..8ecc5e7 100644
--- a/usr.sbin/cron/structs.h
+++ b/usr.sbin/cron/structs.h
@@ -48,6 +48,9 @@ typedef	struct _user {
 	char		*name;
 	time_t		mtime;		/* last modtime of crontab */
 	entry		*crontab;	/* this person's crontab */
+#ifdef WITH_SELINUX
+	security_context_t  scontext;	/* SELinux security context */
+#endif /* WITH_SELINUX */
 } user;
 
 typedef	struct _cron_db {
diff --git a/usr.sbin/cron/user.c b/usr.sbin/cron/user.c
index facc0b1..efa7394 100644
--- a/usr.sbin/cron/user.c
+++ b/usr.sbin/cron/user.c
@@ -39,11 +39,15 @@ free_user(user *u) {
 		ne = e->next;
 		free_entry(e);
 	}
+#ifdef WITH_SELINUX
+	freecon(u->scontext);
+#endif /* WITH_SELINUX */
 	free(u);
 }
 
 user *
-load_user(int crontab_fd, struct passwd	*pw, const char *name) {
+load_user(int crontab_fd, struct passwd	*pw,
+          const char *name, const char *tabname) {
 	char envstr[MAX_ENVSTR];
 	FILE *file;
 	user *u;
@@ -80,6 +84,24 @@ load_user(int crontab_fd, struct passwd	*pw, const char *name) {
 		return (NULL);
 	}
 
+#ifdef WITH_SELINUX
+	u->scontext = NULL;
+	if (is_selinux_enabled() > 0) {
+		char *err_msg = NULL;
+		u->scontext = get_selinux_context(strcmp(name, SYSUSERNAME) ? name : NULL,
+						crontab_fd, &err_msg);
+		if (!u->scontext) {
+			log_it(name, getpid(), err_msg, tabname);
+			free(err_msg);
+			if(security_getenforce() > 0) {
+				free_user(u);
+				u = NULL;
+				goto done;
+			}
+		}
+	}
+#endif /* WITH_SELINUX */
+
 	/* load the crontab
 	 */
 	while ((status = load_env(envstr, file)) >= OK) {
 
design & coding: Vladimir Lettiev aka crux © 2004-2005, Andrew Avramenko aka liks © 2007-2008
current maintainer: Michael Shigorin