#include <err.h>
#include <errno.h>
#include <signal.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <unistd.h>

#include <linux/filter.h>
#include <linux/seccomp.h>

#define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0]))

/* POC for seccomp making all system calls interruptible, even with
 * SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV.
 *
 * The parent process sets up a seccomp unotify filter trying to emulate getuid
 * in userspace.
 *
 * The child sets itself up to be repeatedly interrupted by signals while it
 * calls getuid in a loop. If the parent is emulating getuid the call can
 * unexpectedly fail with -EINTR.
 */

static void
sigalrm_handler(int sig)
{}

static int
child(void)
{
	struct sigaction sig_act = {
		.sa_handler = sigalrm_handler,
		.sa_flags = 0, /* no SA_RESTART */
	};
	struct itimerval timer = {
		.it_value = { .tv_usec = 10000 },
		.it_interval = { .tv_usec = 10000 },
	};
	unsigned long long it = 0;

	if (sigaction(SIGALRM, &sig_act, NULL) < 0)
		err(111, "sigaction(SIGALRM, ...)");

	/* kill with SIGALRM repeatedly, to race for -EINTR on the caught syscall */
	if (setitimer(ITIMER_REAL, &timer, NULL) < 0)
		err(111, "setitimer()");

	for (;;) {
		uid_t uid = getuid();
		it++;

		if (uid != 0)
			errx(111, "uid() at iteration %llu: got %d", it, uid);

		if (!(it % 1000))
			warnx("finished %llu iterations", it);
	}

	return 0;
}

static pid_t pid;

static int
seccomp(unsigned int op, unsigned int flags, void *args)
{
	return syscall(SYS_seccomp, op, flags, args);
}

static sig_atomic_t finished;

static void
sig_finish(int sig)
{
	finished = 1;
}

static int
parent(int fd)
{
	struct seccomp_notif_sizes sizes;
	struct seccomp_notif *req;
	struct seccomp_notif_resp *resp;

	int status;

	if (seccomp(SECCOMP_GET_NOTIF_SIZES, 0, &sizes) < 0)
		err(111, "seccomp(SECCOMP_GET_NOTIF_SIZES, ...)");

	if (sizeof(struct seccomp_notif) > sizes.seccomp_notif)
		sizes.seccomp_notif = sizeof(struct seccomp_notif);
	if (sizeof(struct seccomp_notif_resp) > sizes.seccomp_notif_resp)
		sizes.seccomp_notif_resp = sizeof(struct seccomp_notif_resp);

	req = malloc(sizes.seccomp_notif);
	if (!req)
		err(111, "malloc()");

	resp = malloc(sizes.seccomp_notif_resp);
	if (!resp)
		err(111, "malloc()");

	while (!finished) {
		memset(req, 0, sizes.seccomp_notif);

		if (ioctl(fd, SECCOMP_IOCTL_NOTIF_RECV, req) < 0) {
			warn("ioctl(fd, SECCOMP_IOCTL_NOTIF_RECV, ...)");
			if (errno == EINTR || errno == ENOENT)
				continue;
			else
				break;
		}

		memset(resp, 0, sizes.seccomp_notif_resp);
		resp->id = req->id;
		resp->val = 0;
		resp->error = 0;
		resp->flags = 0;

		if (ioctl(fd, SECCOMP_IOCTL_NOTIF_SEND, resp) < 0) {
			/* ENOENT should not happen late due to SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV */
			warn("ioctl(fd, SECCOMP_IOCTL_NOTIF_SEND, ...)");
			break;
		}
	}

	kill(pid, SIGKILL);
	waitpid(pid, &status, 0);
	exit(111);
}

int
main(void)
{
	static char stderr_buf[BUFSIZ];
	struct sigaction sa;
	int fd;

	struct sock_filter filter[] = {
		BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
			 offsetof(struct seccomp_data, nr)),
		BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,
			 SYS_getuid, 0, 1),
		BPF_STMT(BPF_RET | BPF_K,
			 SECCOMP_RET_USER_NOTIF),
		BPF_STMT(BPF_RET | BPF_K,
			 SECCOMP_RET_ALLOW),
	};

	struct sock_fprog prog = {
		.len = ARRAY_SIZE(filter),
		.filter = filter,
	};

	/* best effort attempt to prevent parent and child messages from interleaving */
	setvbuf(stderr, stderr_buf, _IOLBF, sizeof(stderr_buf));

	if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
		err(111, "prctl(PR_SET_NO_NEW_PRIVS, ...)");

	fd = seccomp(SECCOMP_SET_MODE_FILTER,
		     SECCOMP_FILTER_FLAG_NEW_LISTENER | SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV,
		     &prog);
	if (fd < 0)
		err(111, "seccomp(SECCOMP_SET_MODE_FILTER, ...)");

	sa.sa_handler = sig_finish;
	sa.sa_flags = 0,
	sigemptyset(&sa.sa_mask);
	if (sigaction(SIGCHLD, &sa, NULL) < 0)
		err(111, "sigaction(SIGCHLD, ...)");

	pid = fork();
	if (pid == (pid_t)-1)
		err(111, "fork()");
	else if (!pid)
		return child();
	else
		return parent(fd);
}
