/* Uses x86 specifics! Uses x86 stack direction! */
/* Backend tool */
/* Программа нужна как минимум потому, что один раз не сработал systemd-nspawn, а asjail-max сработал (один раз так было из-за того, что там было что-то смонтировано, reported here: https://github.com/systemd/systemd/issues/3695 ) */
/* Существует программа unshare, так что эта программа deprecated */
/* Самописная альтернатива systemd-nspawn нужна снова и снова. 2020-12-30 мне снова понадобился asjail, чтобы отдебажить проблему с statx: https://bugzilla.redhat.com/show_bug.cgi?id=1760300 */
/* поддерживает только архитектуры, где стек растёт вниз (т. е. почти все) */
/* Мы не подменяем текущий процесс (как это делает chroot), то есть PID другой */
/* Новые ядра поддерживают всё больше и больше опций */
/* CLONE_NEWNET позволяет слушать занятый порт и делает интернет недоступным внутри asjail */
/*
* Список опций, упомянутых в man page от 2010-09-10 (те из них, которые пригодны для создания контейнеров, отмечены; те, что непригодны - не отмечены; точно):
* CLONE_CHILD_CLEARTID
* CLONE_CHILD_SETTID
* CLONE_FILES
* CLONE_FS
* CLONE_IO
* CLONE_NEWIPC [*]
* CLONE_NEWNET [*]
* CLONE_NEWNS [*]
* CLONE_NEWPID [*]
* CLONE_NEWUTS [*]
* CLONE_PARENT
* CLONE_PARENT_SETTID
* CLONE_PID
* CLONE_PTRACE
* CLONE_SETTLS
* CLONE_SIGHAND
* CLONE_STOPPED
* CLONE_SYSVSEM
* CLONE_THREAD
* CLONE_UNTRACES
* CLONE_VFORK
* CLONE_VM
* Все отмеченные опции требуют прав рута (а точнее, определённых CAP_*)
*/
/* TO DO: сделать понятные сообщения об ошибках */
/* TO DO: ядро может быть без поддержки некоторых опций (при конфигурировании не заданы) */
/* TO DO: с сигналами более-менее разобрался, но всё же: когда я нажимаю Ctrl-C, сигнал отсылается всей группе? А если не с клавиатуры, то не всей? А можно вообще послать не всей? */
/* TO DO: на самом деле надо было сделать cgroup, чтобы процесс не смог от нас отфоркнуться (хотя можно просто всегда использовать CLONE_NEWPID) */
/* TO DO: почему-то CLONE_NEWUSER не работает на ядре 3.9-rc5 с допкоммитами на Sid-2013-Apr в Qemu */
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sched.h>
/* Yes, this is (probably) bad function */
#define dbg(...) (fprintf(stderr, "asjail: debug: " __VA_ARGS__), fputc('\n', stderr), (void)0)
#define my_assert(...) do { if (!(__VA_ARGS__)) { fprintf (stderr, "assertion failed\n"); exit (EXIT_FAILURE); } } while (0)
sigset_t all_but_chld_and_term;
static int fn(void *argv)
{
dbg("child: started; setting procmask");
if (sigprocmask(SIG_UNBLOCK, &all_but_chld_and_term, NULL) != 0)
{
abort();
}
dbg("child: execvp");
char *const *cargv = (char **)argv;
execvp(cargv[0], cargv);
perror(cargv[0]);
exit (EXIT_FAILURE);
}
pid_t child = -1;
static void kill_child(int signum /* unused */)
{
if (child == -1)
{
dbg("основной процесс убит до того, как успел создать дочерний процесс");
exit (EXIT_FAILURE); /* Нас убили до того, как мы успели запустить процесс */
}
dbg("got signal, убиваем дочерний процесс");
kill(child, SIGKILL); /* Не проверяем код возврата, так как процесс может уже завершиться (а мы ещё не успели убрать обработчик) */
}
int main(int argc, char *argv[])
{
int flags = SIGCHLD;
int opt;
while ((opt = getopt(argc, argv, "inmpuU")) != -1) {
switch (opt) {
case 'i':
flags |= CLONE_NEWIPC;
break;
case 'n':
flags |= CLONE_NEWNET;
break;
case 'm':
flags |= CLONE_NEWNS; /* Mount namespace */
break;
case 'p':
flags |= CLONE_NEWPID;
break;
case 'u':
flags |= CLONE_NEWUTS;
break;
case 'U':
flags |= CLONE_NEWUSER;
break;
default:
fprintf (stderr, "Usage: %s [OPTION]... PROGRAM [ARG]...\n", argv[0]);
exit (EXIT_FAILURE);
}
}
if (optind >= argc)
{
fprintf (stderr, "no program\n");
exit (EXIT_FAILURE);
}
if (sigfillset(&all_but_chld_and_term) == -1)
{
fprintf (stderr, "cannot initialize signal set; this should never happen\n");
exit (EXIT_FAILURE);
}
sigdelset(&all_but_chld_and_term, SIGCHLD);
sigdelset(&all_but_chld_and_term, SIGTERM);
dbg("sigprocmask");
my_assert(sigprocmask(SIG_BLOCK, &all_but_chld_and_term, NULL) == 0);
dbg("signal");
signal(SIGTERM, &kill_child); /* TO DO: use sigaction */
const int stack_size = 65536; /* TO DO: why this size? */
void *child_stack = malloc(stack_size);
my_assert (child_stack != NULL);
/* Внутри child не нужно перемонтировать /proc (если используется CLONE_NEWNET). Он и так содержит новую информацию в /proc/net. А вот при использовании CLONE_NEWPID /proc надо перемонтировать */
/* Более того, если в child с NEWPID смонтировать /proc, то он будет выглядеть снаружи довольно странно */
dbg("clone");
child = clone(&fn, (char *)child_stack + stack_size, flags, argv + optind);
if (child == -1)
{
fprintf(stderr, "clone failed (are you superuser?)\n");
exit (EXIT_FAILURE);
}
int status;
dbg("waitpid");
my_assert (waitpid(child, &status, 0) != -1);
dbg("waited, restore sigprocmask and signal");
my_assert (sigprocmask(SIG_UNBLOCK, &all_but_chld_and_term, NULL) == 0);
signal(SIGTERM, SIG_DFL);
if (WIFEXITED(status))
{
exit (WEXITSTATUS(status));
}
else if (WIFSIGNALED(status))
{
signal(WTERMSIG(status), SIG_DFL); /* We don't check return value, because it may be SIGKILL */
/* TO DO: если он сгеренил core dump, то мне это делать уже не нужно */
if (kill(getpid(), WTERMSIG(status)) == -1)
{
fprintf(stderr, "I cannot kill myself; this should never happen\n");
exit (EXIT_FAILURE);
}
pause();
}
else
{
fprintf(stderr, "bug: child didn't exit and isn't signaled\n"); /* TO DO: is this possible? is WIFCONTINUED possible? */
exit (EXIT_FAILURE);
}
/* NOTREACHED */
}