/* SPDX-License-Identifier: BSD-2-Clause */

/* Best-effort Capsicum helper implementation using Landlock, prctl, etc. */

#ifndef CAPSICUM_HELPERS_H
#define CAPSICUM_HELPERS_H

#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

#include <sys/fcntl.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <unistd.h>

#include <linux/landlock.h>
#include <linux/prctl.h>

/* Capsicum helper capability flags */
#define CAPH_READ		0x0001
#define CAPH_WRITE		0x0002
#define CAPH_IGNORE_EBADF	0x0004

/* System call wrappers */

static int
caph_landlock_create_ruleset(const struct landlock_ruleset_attr *attr,
			size_t size, uint32_t flags)
{
    return syscall(SYS_landlock_create_ruleset, attr, size, flags);
}

static int caph_landlock_restrict_self(int ruleset_fd, uint32_t flags)
{
    return syscall(SYS_landlock_restrict_self, ruleset_fd, flags);
}

static int caph_landlock_abi = -1;
static int caph_landlock_fd = -1;

static int caph_landlock_init(void)
{
    struct landlock_ruleset_attr attr;

    if (caph_landlock_abi < 0)
	caph_landlock_abi = caph_landlock_create_ruleset(
	    NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
    if (caph_landlock_abi <= 0) {
	/*
	 * Landlock is not supported.  We can't treat this as an error
	 * because it's an optional kernel feature.
	 */
	caph_landlock_abi = 0;
	return 0;
    }

    if (caph_landlock_fd < 0) {
	memset(&attr, 0, sizeof(attr));

	/*
	 * Restrict almost everything we can, given the supported ABI
	 * version.  As an exception LANDLOCK_ACCESS_FS_IOCTL_DEV is
	 * not restricted.
	 *
	 * This still doesn't cover everything that the real Capsicum
	 * does.
	 */
	switch (caph_landlock_abi) {
	default:
	case 6:
	    attr.scoped |=
		LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
		LANDLOCK_SCOPE_SIGNAL;
	    /* fall through */
	case 5:
	case 4:
	    attr.handled_access_net |=
		LANDLOCK_ACCESS_NET_BIND_TCP |
		LANDLOCK_ACCESS_NET_CONNECT_TCP;
	    /* fall through */
	case 3:
	    attr.handled_access_fs |=
		LANDLOCK_ACCESS_FS_TRUNCATE;
	    /* fall through */
	case 2:
	    attr.handled_access_fs |=
		LANDLOCK_ACCESS_FS_REFER;
	    /* fall through */
	case 1:
	    attr.handled_access_fs |=
		LANDLOCK_ACCESS_FS_EXECUTE |
		LANDLOCK_ACCESS_FS_WRITE_FILE |
		LANDLOCK_ACCESS_FS_READ_FILE |
		LANDLOCK_ACCESS_FS_READ_DIR |
		LANDLOCK_ACCESS_FS_REMOVE_DIR |
		LANDLOCK_ACCESS_FS_REMOVE_FILE |
		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;
	    break;
	}

	caph_landlock_fd = caph_landlock_create_ruleset(&attr, sizeof(attr), 0);
    }

    return caph_landlock_fd;
}

static int caph_limit_stream(int fd, int flags)
{
    static const int flags_open_mode[] = {
	[0] =				__O_PATH,
	[CAPH_READ] =			O_RDONLY,
	[CAPH_WRITE] =			O_WRONLY,
	[CAPH_READ | CAPH_WRITE] =	O_RDWR,
    };
    char fd_name[30];
    int new_fd;
    int ret;
    int saved_errno;

    if (flags & ~(CAPH_READ | CAPH_WRITE | CAPH_IGNORE_EBADF)) {
	errno = EINVAL;
	return -1;
    }

    if ((flags & CAPH_IGNORE_EBADF) &&
	fcntl(fd, F_GETFD, 0) < 0 && errno == EBADF)
	return 0;

    /*
     * Landlock doesn't appear to allow restricting read/write on
     * an already open file descriptor, and we can't change the
     * mode any other way, so reopen the file via procfs and use
     * dup2() to replace the original file descriptor.
     */
    snprintf(fd_name, sizeof(fd_name), "/proc/self/fd/%d", fd);
    new_fd = open(fd_name, flags_open_mode[flags & (CAPH_READ | CAPH_WRITE)]);
    if (new_fd < 0)
	return -1;
    ret = dup2(new_fd, fd);
    saved_errno = errno;
    close(new_fd);
    errno = saved_errno;
    return ret;
}

static int caph_limit_stdio(void)
{
    if (caph_limit_stream(0, CAPH_READ | CAPH_IGNORE_EBADF) < 0 ||
	caph_limit_stream(1, CAPH_WRITE | CAPH_IGNORE_EBADF) < 0 ||
	caph_limit_stream(2, CAPH_WRITE | CAPH_IGNORE_EBADF) < 0)
	return -1;
    return 0;
}

static int caph_enter(void)
{
    int ret;

    ret = caph_landlock_init();
    if (ret < 0 || caph_landlock_abi == 0)
	return ret;

    ret = prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
    if (ret < 0)
	return -1;

    ret = caph_landlock_restrict_self(caph_landlock_fd, 0);
    if (ret < 0)
	return -1;

    close(caph_landlock_fd);
    caph_landlock_fd = -1;
    return 0;
}

#endif /* CAPSICUM_HELPERS_H */
