gmid/sandbox.c
Omar Polo 3499ce5a9a landlock the server process
Trying to implement some landlock policies (rules?) where possible.
The server process is, of course, the most dangerous process so start
with that.

The following should be equivalent to the unveil(2) call on OpenBSD:
allows only to read files and directories inside the vhost roots.

I'm assuming seccomp is enabled so I'm not trying to disallow actions
such as LANDLOCK_ACCESS_FS_EXECUTE or LANDLOCK_ACCESS_FS_REMOVE_FILE
which require syscalls that are already disallowed.  I'm only trying
to limit the damage that the currently allowed system calls can do.
e.g. since write(2) is allowed, gmid could modify *any* file it has
access to; this is now forbidden by landlock.

There are still too many #ifdefs for my tastes, but it's still better
than the seccomp code.
2021-09-19 17:08:12 +00:00

659 lines
15 KiB
C

/*
* Copyright (c) 2021 Omar Polo <op@omarpolo.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include "gmid.h"
#if DISABLE_SANDBOX
#warning "Sandbox disabled! Please report issues upstream instead of disabling the sandbox."
void
sandbox_server_process(void)
{
return;
}
void
sandbox_executor_process(void)
{
log_notice(NULL, "Sandbox disabled! "
"Please report issues upstream instead of disabling the sandbox.");
}
void
sandbox_logger_process(void)
{
return;
}
#elif defined(__FreeBSD__)
#include <sys/capsicum.h>
void
sandbox_server_process(void)
{
if (cap_enter() == -1)
fatal("cap_enter");
}
void
sandbox_executor_process(void)
{
/*
* We cannot capsicum the executor process because it needs to
* fork(2)+execve(2) cgi scripts
*/
return;
}
void
sandbox_logger_process(void)
{
if (cap_enter() == -1)
fatal("cap_enter");
}
#elif defined(__linux__)
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <linux/audit.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <errno.h>
#include <fcntl.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#if HAVE_LANDLOCK
# include "landlock_shim.h"
#endif
/* uncomment to enable debugging. ONLY FOR DEVELOPMENT */
/* #define SC_DEBUG */
#ifdef SC_DEBUG
# define SC_FAIL SECCOMP_RET_TRAP
#else
# define SC_FAIL SECCOMP_RET_KILL
#endif
#if (BYTE_ORDER == LITTLE_ENDIAN)
# define SC_ARG_LO 0
# define SC_ARG_HI sizeof(uint32_t)
#elif (BYTE_ORDER == BIG_ENDIAN)
# define SC_ARG_LO sizeof(uint32_t)
# define SC_ARG_HI 0
#else
# error "Uknown endian"
#endif
/* make the filter more readable */
#define SC_ALLOW(nr) \
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_##nr, 0, 1), \
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)
/*
* SC_ALLOW_ARG and the SECCOMP_AUDIT_ARCH below are courtesy of
* https://roy.marples.name/git/dhcpcd/blob/HEAD:/src/privsep-linux.c
*/
#define SC_ALLOW_ARG(_nr, _arg, _val) \
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, (_nr), 0, 6), \
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, \
offsetof(struct seccomp_data, args[(_arg)]) + SC_ARG_LO), \
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, \
((_val) & 0xffffffff), 0, 3), \
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, \
offsetof(struct seccomp_data, args[(_arg)]) + SC_ARG_HI), \
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, \
(((uint32_t)((uint64_t)(_val) >> 32)) & 0xffffffff), 0, 1), \
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW), \
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, \
offsetof(struct seccomp_data, nr))
/*
* I personally find this quite nutty. Why can a system header not
* define a default for this?
*/
#if defined(__i386__)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_I386
#elif defined(__x86_64__)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_X86_64
#elif defined(__arc__)
# if defined(__A7__)
# if (BYTE_ORDER == LITTLE_ENDIAN)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_ARCOMPACT
# else
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_ARCOMPACTBE
# endif
# elif defined(__HS__)
# if (BYTE_ORDER == LITTLE_ENDIAN)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_ARCV2
# else
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_ARCV2BE
# endif
# else
# error "Platform does not support seccomp filter yet"
# endif
#elif defined(__arm__)
# ifndef EM_ARM
# define EM_ARM 40
# endif
# if (BYTE_ORDER == LITTLE_ENDIAN)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_ARM
# else
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_ARMEB
# endif
#elif defined(__aarch64__)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_AARCH64
#elif defined(__alpha__)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_ALPHA
#elif defined(__hppa__)
# if defined(__LP64__)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_PARISC64
# else
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_PARISC
# endif
#elif defined(__ia64__)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_IA64
#elif defined(__microblaze__)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_MICROBLAZE
#elif defined(__m68k__)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_M68K
#elif defined(__mips__)
# if defined(__MIPSEL__)
# if defined(__LP64__)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_MIPSEL64
# else
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_MIPSEL
# endif
# elif defined(__LP64__)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_MIPS64
# else
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_MIPS
# endif
#elif defined(__nds32__)
# if (BYTE_ORDER == LITTLE_ENDIAN)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_NDS32
#else
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_NDS32BE
#endif
#elif defined(__nios2__)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_NIOS2
#elif defined(__or1k__)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_OPENRISC
#elif defined(__powerpc64__)
# if (BYTE_ORDER == LITTLE_ENDIAN)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_PPC64LE
# else
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_PPC64
# endif
#elif defined(__powerpc__)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_PPC
#elif defined(__riscv)
# if defined(__LP64__)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_RISCV64
# else
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_RISCV32
# endif
#elif defined(__s390x__)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_S390X
#elif defined(__s390__)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_S390
#elif defined(__sh__)
# if defined(__LP64__)
# if (BYTE_ORDER == LITTLE_ENDIAN)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_SHEL64
# else
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_SH64
# endif
# else
# if (BYTE_ORDER == LITTLE_ENDIAN)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_SHEL
# else
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_SH
# endif
# endif
#elif defined(__sparc__)
# if defined(__arch64__)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_SPARC64
# else
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_SPARC
# endif
#elif defined(__xtensa__)
# define SECCOMP_AUDIT_ARCH AUDIT_ARCH_XTENSA
#else
# error "Platform does not support seccomp filter yet"
#endif
static struct sock_filter filter[] = {
/* load the *current* architecture */
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
(offsetof(struct seccomp_data, arch))),
/* ensure it's the same that we've been compiled on */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,
SECCOMP_AUDIT_ARCH, 1, 0),
/* if not, kill the program */
BPF_STMT(BPF_RET | BPF_K, SC_FAIL),
/* load the syscall number */
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
(offsetof(struct seccomp_data, nr))),
#ifdef __NR_accept
SC_ALLOW(accept),
#endif
#ifdef __NR_accept4
SC_ALLOW(accept4),
#endif
#ifdef __NR_brk
SC_ALLOW(brk),
#endif
#ifdef __NR_clock_gettime
SC_ALLOW(clock_gettime),
#endif
#if defined(__x86_64__) && defined(__ILP32__) && defined(__X32_SYSCALL_BIT)
SECCOMP_ALLOW(__NR_clock_gettime & ~__X32_SYSCALL_BIT),
#endif
#ifdef __NR_clock_gettime64
SC_ALLOW(clock_gettime64),
#endif
#ifdef __NR_close
SC_ALLOW(close),
#endif
#ifdef __NR_epoll_ctl
SC_ALLOW(epoll_ctl),
#endif
#ifdef __NR_epoll_pwait
SC_ALLOW(epoll_pwait),
#endif
#ifdef __NR_epoll_wait
SC_ALLOW(epoll_wait),
#endif
#ifdef __NR_exit
SC_ALLOW(exit),
#endif
#ifdef __NR_exit_group
SC_ALLOW(exit_group),
#endif
#ifdef __NR_fcntl
SC_ALLOW(fcntl),
#endif
#ifdef __NR_fcntl64
SC_ALLOW(fcntl64),
#endif
#ifdef __NR_fstat
SC_ALLOW(fstat),
#endif
#ifdef __NR_fstat64
SC_ALLOW(fstat64),
#endif
#ifdef __NR_getdents64
SC_ALLOW(getdents64),
#endif
#ifdef __NR_getpid
SC_ALLOW(getpid),
#endif
#ifdef __NR_getrandom
SC_ALLOW(getrandom),
#endif
#ifdef __NR_gettimeofday
SC_ALLOW(gettimeofday),
#endif
#ifdef __NR_ioctl
/* allow ioctl only on fd 1, glibc doing stuff? */
SC_ALLOW_ARG(__NR_ioctl, 0, 1),
#endif
#ifdef __NR_lseek
SC_ALLOW(lseek),
#endif
#ifdef __NR_madvise
SC_ALLOW(madvise),
#endif
#ifdef __NR_mmap
SC_ALLOW(mmap),
#endif
#ifdef __NR_mmap2
SC_ALLOW(mmap2),
#endif
#ifdef __NR_munmap
SC_ALLOW(munmap),
#endif
#ifdef __NR_newfstatat
SC_ALLOW(newfstatat),
#endif
#ifdef __NR_oldfstat
SC_ALLOW(oldfstat),
#endif
#ifdef __NR_openat
SC_ALLOW(openat),
#endif
#ifdef __NR_prlimit64
SC_ALLOW(prlimit64),
#endif
#ifdef __NR_read
SC_ALLOW(read),
#endif
#ifdef __NR_recvmsg
SC_ALLOW(recvmsg),
#endif
#ifdef __NR_readv
SC_ALLOW(readv),
#endif
#ifdef __NR_rt_sigaction
SC_ALLOW(rt_sigaction),
#endif
#ifdef __NR_rt_sigreturn
SC_ALLOW(rt_sigreturn),
#endif
#ifdef __NR_sendmsg
SC_ALLOW(sendmsg),
#endif
#ifdef __NR_statx
SC_ALLOW(statx),
#endif
#ifdef __NR_write
SC_ALLOW(write),
#endif
#ifdef __NR_writev
SC_ALLOW(writev),
#endif
/* disallow everything else */
BPF_STMT(BPF_RET | BPF_K, SC_FAIL),
};
#ifdef SC_DEBUG
#include <signal.h>
#include <unistd.h>
static void
sandbox_seccomp_violation(int signum, siginfo_t *info, void *ctx)
{
(void)signum;
(void)ctx;
fprintf(stderr, "%s: unexpected system call (arch:0x%x,syscall:%d @ %p)\n",
__func__, info->si_arch, info->si_syscall, info->si_call_addr);
_exit(1);
}
static void
sandbox_seccomp_catch_sigsys(void)
{
struct sigaction act;
sigset_t mask;
memset(&act, 0, sizeof(act));
sigemptyset(&mask);
sigaddset(&mask, SIGSYS);
act.sa_sigaction = &sandbox_seccomp_violation;
act.sa_flags = SA_SIGINFO;
if (sigaction(SIGSYS, &act, NULL) == -1)
fatal("%s: sigaction(SIGSYS): %s",
__func__, strerror(errno));
if (sigprocmask(SIG_UNBLOCK, &mask, NULL) == -1)
fatal("%s: sigprocmask(SIGSYS): %s\n",
__func__, strerror(errno));
}
#endif /* SC_DEBUG */
#if HAVE_LANDLOCK
static int
server_landlock(void)
{
int fd, err;
struct vhost *h;
struct location *l;
/*
* These are all the actions that we want to either allow or
* disallow. Things like LANDLOCK_ACCESS_FS_EXECUTE are
* omitted because are already handled by seccomp.
*/
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_WRITE_FILE |
LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR |
LANDLOCK_ACCESS_FS_MAKE_CHAR |
LANDLOCK_ACCESS_FS_MAKE_DIR |
LANDLOCK_ACCESS_FS_MAKE_REG |
LANDLOCK_ACCESS_FS_MAKE_SOCK |
LANDLOCK_ACCESS_FS_MAKE_FIFO |
LANDLOCK_ACCESS_FS_MAKE_BLOCK |
LANDLOCK_ACCESS_FS_MAKE_SYM,
};
/*
* These are all the actions allowed for the root directories
* of the vhosts. All the other rules mentioned in
* ruleset_attr and omitted here are implicitly disallowed.
*/
struct landlock_path_beneath_attr path_beneath = {
.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE |
LANDLOCK_ACCESS_FS_READ_DIR,
};
fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
if (fd == -1) {
switch (errno) {
case ENOSYS:
fatal("%s: failed to create ruleset. "
"Landlock doesn't seem to be supported by the "
"current kernel.", __func__);
case EOPNOTSUPP:
log_warn(NULL, "%s: failed to create ruleset. "
"Landlock seems to be currently disabled; "
"continuing without it.", __func__);
return -1;
default:
fatal("%s: failed to create ruleset: %s",
__func__, strerror(errno));
}
}
TAILQ_FOREACH(h, &hosts, vhosts) {
TAILQ_FOREACH(l, &h->locations, locations) {
if (l->dir == NULL)
continue;
path_beneath.parent_fd = open(l->dir, O_PATH);
if (path_beneath.parent_fd == -1)
fatal("%s: can't open %s for landlock: %s",
__func__, l->dir, strerror(errno));
err = landlock_add_rule(fd, LANDLOCK_RULE_PATH_BENEATH,
&path_beneath, 0);
if (err)
fatal("%s: landlock_add_rule(%s) failed: %s",
__func__, l->dir, strerror(errno));
close(path_beneath.parent_fd);
}
}
return fd;
}
#endif
void
sandbox_server_process(void)
{
int fd;
struct sock_fprog prog = {
.len = (unsigned short) (sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};
#ifdef SC_DEBUG
sandbox_seccomp_catch_sigsys();
#endif
#if HAVE_LANDLOCK
log_warn(NULL, "loading landlock...");
fd = server_landlock();
#else
(void)fd; /* avoid unused var warning */
#endif
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1)
fatal("%s: prctl(PR_SET_NO_NEW_PRIVS): %s",
__func__, strerror(errno));
#if HAVE_LANDLOCK
if (fd != -1) {
if (landlock_restrict_self(fd, 0))
fatal("%s: landlock_restrict_self: %s",
__func__, strerror(errno));
close(fd);
}
#endif
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1)
fatal("%s: prctl(PR_SET_SECCOMP): %s\n",
__func__, strerror(errno));
}
void
sandbox_executor_process(void)
{
/*
* We cannot use seccomp for the executor process because we
* don't know what the child will do. Also, our filter will
* be inherited so the child cannot set its own seccomp
* policy.
*/
return;
}
void
sandbox_logger_process(void)
{
/*
* To be honest, here we could use a seccomp policy to only
* allow writev(2) and memory allocations.
*/
return;
}
#elif defined(__OpenBSD__)
#include <unistd.h>
void
sandbox_server_process(void)
{
struct vhost *h;
struct location *l;
TAILQ_FOREACH(h, &hosts, vhosts) {
TAILQ_FOREACH(l, &h->locations, locations) {
if (l->dir == NULL)
continue;
if (unveil(l->dir, "r") == -1)
fatal("unveil %s for domain %s",
l->dir,
h->domain);
}
}
if (pledge("stdio recvfd rpath inet", NULL) == -1)
fatal("pledge");
}
void
sandbox_executor_process(void)
{
struct vhost *h;
struct location *l;
struct fcgi *f;
size_t i;
TAILQ_FOREACH(h, &hosts, vhosts) {
TAILQ_FOREACH(l, &h->locations, locations) {
if (l->dir == NULL)
continue;
/* r so we can chdir into the directory */
if (unveil(l->dir, "rx") == -1)
fatal("unveil %s for domain %s",
l->dir, h->domain);
}
}
for (i = 0; i < FCGI_MAX; i++) {
f = &fcgi[i];
if (f->path != NULL) {
if (unveil(f->path, "rw") == -1)
fatal("unveil %s", f->path);
}
if (f->prog != NULL) {
if (unveil(f->prog, "rx") == -1)
fatal("unveil %s", f->prog);
}
}
/*
* rpath: to chdir into the correct directory
* proc exec: CGI
* dns inet unix: FastCGI
*/
if (pledge("stdio rpath sendfd proc exec dns inet unix", NULL))
err(1, "pledge");
}
void
sandbox_logger_process(void)
{
if (pledge("stdio recvfd", NULL) == -1)
err(1, "pledge");
}
#else
#warning "No sandbox method known for this OS"
void
sandbox_server_process(void)
{
return;
}
void
sandbox_executor_process(void)
{
log_notice(NULL, "no sandbox method known for this OS");
}
void
sandbox_logger_process(void)
{
return;
}
#endif