/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include <sys/resource.h>
#include <sys/time.h>
#include <unistd.h>

#include "alloc-util.h"
#include "bpf-devices.h"
#include "bpf-program.h"
#include "cgroup-setup.h"
#include "errno-list.h"
#include "fd-util.h"
#include "fs-util.h"
#include "path-util.h"
#include "tests.h"

static void test_policy_closed(const char *cgroup_path, BPFProgram **installed_prog) {
        _cleanup_(bpf_program_unrefp) BPFProgram *prog = NULL;
        unsigned wrong = 0;
        int r;

        log_info("/* %s */", __func__);

        r = bpf_devices_cgroup_init(&prog, CGROUP_DEVICE_POLICY_CLOSED, true);
        assert_se(r >= 0);

        r = bpf_devices_allow_list_static(prog, cgroup_path);
        assert_se(r >= 0);

        r = bpf_devices_apply_policy(prog, CGROUP_DEVICE_POLICY_CLOSED, true, cgroup_path, installed_prog);
        assert_se(r >= 0);

        const char *s;
        FOREACH_STRING(s, "/dev/null",
                          "/dev/zero",
                          "/dev/full",
                          "/dev/random",
                          "/dev/urandom",
                          "/dev/tty",
                          "/dev/ptmx") {
                _cleanup_close_ int fd, fd2;

                fd = open(s, O_CLOEXEC|O_RDONLY|O_NOCTTY);
                log_debug("open(%s, \"r\") = %d/%s", s, fd, fd < 0 ? errno_to_name(errno) : "-");
                wrong += fd < 0 && errno == EPERM;
                /* We ignore errors other than EPERM, e.g. ENOENT or ENXIO */

                fd2 = open(s, O_CLOEXEC|O_WRONLY|O_NOCTTY);
                log_debug("open(%s, \"w\") = %d/%s", s, fd2, fd2 < 0 ? errno_to_name(errno) : "-");
                wrong += fd2 < 0 && errno == EPERM;
        }
        assert_se(wrong == 0);
}

static void test_policy_strict(const char *cgroup_path, BPFProgram **installed_prog) {
        _cleanup_(bpf_program_unrefp) BPFProgram *prog = NULL;
        unsigned wrong = 0;
        int r;

        log_info("/* %s */", __func__);

        r = bpf_devices_cgroup_init(&prog, CGROUP_DEVICE_POLICY_STRICT, true);
        assert_se(r >= 0);

        r = bpf_devices_allow_list_device(prog, cgroup_path, "/dev/null", "rw");
        assert_se(r >= 0);

        r = bpf_devices_allow_list_device(prog, cgroup_path, "/dev/random", "r");
        assert_se(r >= 0);

        r = bpf_devices_allow_list_device(prog, cgroup_path, "/dev/zero", "w");
        assert_se(r >= 0);

        r = bpf_devices_apply_policy(prog, CGROUP_DEVICE_POLICY_STRICT, true, cgroup_path, installed_prog);
        assert_se(r >= 0);

        {
                _cleanup_close_ int fd, fd2;
                const char *s = "/dev/null";

                fd = open(s, O_CLOEXEC|O_RDONLY|O_NOCTTY);
                log_debug("open(%s, \"r\") = %d/%s", s, fd, fd < 0 ? errno_to_name(errno) : "-");
                wrong += fd < 0;

                fd2 = open(s, O_CLOEXEC|O_WRONLY|O_NOCTTY);
                log_debug("open(%s, \"w\") = %d/%s", s, fd2, fd2 < 0 ? errno_to_name(errno) : "-");
                wrong += fd2 < 0;
        }

        {
                _cleanup_close_ int fd, fd2;
                const char *s = "/dev/random";

                fd = open(s, O_CLOEXEC|O_RDONLY|O_NOCTTY);
                log_debug("open(%s, \"r\") = %d/%s", s, fd, fd < 0 ? errno_to_name(errno) : "-");
                wrong += fd < 0;

                fd2 = open(s, O_CLOEXEC|O_WRONLY|O_NOCTTY);
                log_debug("open(%s, \"w\") = %d/%s", s, fd2, fd2 < 0 ? errno_to_name(errno) : "-");
                wrong += fd2 >= 0;
        }

        {
                _cleanup_close_ int fd, fd2;
                const char *s = "/dev/zero";

                fd = open(s, O_CLOEXEC|O_RDONLY|O_NOCTTY);
                log_debug("open(%s, \"r\") = %d/%s", s, fd, fd < 0 ? errno_to_name(errno) : "-");
                wrong += fd >= 0;

                fd2 = open(s, O_CLOEXEC|O_WRONLY|O_NOCTTY);
                log_debug("open(%s, \"w\") = %d/%s", s, fd2, fd2 < 0 ? errno_to_name(errno) : "-");
                wrong += fd2 < 0;
        }

        {
                _cleanup_close_ int fd, fd2;
                const char *s = "/dev/full";

                fd = open(s, O_CLOEXEC|O_RDONLY|O_NOCTTY);
                log_debug("open(%s, \"r\") = %d/%s", s, fd, fd < 0 ? errno_to_name(errno) : "-");
                wrong += fd >= 0;

                fd2 = open(s, O_CLOEXEC|O_WRONLY|O_NOCTTY);
                log_debug("open(%s, \"w\") = %d/%s", s, fd2, fd2 < 0 ? errno_to_name(errno) : "-");
                wrong += fd2 >= 0;
        }

        assert_se(wrong == 0);
}

static void test_policy_allow_list_major(const char *pattern, const char *cgroup_path, BPFProgram **installed_prog) {
        _cleanup_(bpf_program_unrefp) BPFProgram *prog = NULL;
        unsigned wrong = 0;
        int r;

        log_info("/* %s(%s) */", __func__, pattern);

        r = bpf_devices_cgroup_init(&prog, CGROUP_DEVICE_POLICY_STRICT, true);
        assert_se(r >= 0);

        r = bpf_devices_allow_list_major(prog, cgroup_path, pattern, 'c', "rw");
        assert_se(r >= 0);

        r = bpf_devices_apply_policy(prog, CGROUP_DEVICE_POLICY_STRICT, true, cgroup_path, installed_prog);
        assert_se(r >= 0);

        /* /dev/null, /dev/full have major==1, /dev/tty has major==5 */
        {
                _cleanup_close_ int fd, fd2;
                const char *s = "/dev/null";

                fd = open(s, O_CLOEXEC|O_RDONLY|O_NOCTTY);
                log_debug("open(%s, \"r\") = %d/%s", s, fd, fd < 0 ? errno_to_name(errno) : "-");
                wrong += fd < 0;

                fd2 = open(s, O_CLOEXEC|O_WRONLY|O_NOCTTY);
                log_debug("open(%s, \"w\") = %d/%s", s, fd2, fd2 < 0 ? errno_to_name(errno) : "-");
                wrong += fd2 < 0;
        }

        {
                _cleanup_close_ int fd, fd2;
                const char *s = "/dev/full";

                fd = open(s, O_CLOEXEC|O_RDONLY|O_NOCTTY);
                log_debug("open(%s, \"r\") = %d/%s", s, fd, fd < 0 ? errno_to_name(errno) : "-");
                wrong += fd < 0;

                fd2 = open(s, O_CLOEXEC|O_WRONLY|O_NOCTTY);
                log_debug("open(%s, \"w\") = %d/%s", s, fd2, fd2 < 0 ? errno_to_name(errno) : "-");
                wrong += fd2 < 0;
        }

        {
                _cleanup_close_ int fd, fd2;
                const char *s = "/dev/tty";

                fd = open(s, O_CLOEXEC|O_RDONLY|O_NOCTTY);
                log_debug("open(%s, \"r\") = %d/%s", s, fd, fd < 0 ? errno_to_name(errno) : "-");
                wrong += fd >= 0;

                fd2 = open(s, O_CLOEXEC|O_WRONLY|O_NOCTTY);
                log_debug("open(%s, \"w\") = %d/%s", s, fd2, fd2 < 0 ? errno_to_name(errno) : "-");
                wrong += fd2 >= 0;
        }

        assert_se(wrong == 0);
}

static void test_policy_allow_list_major_star(char type, const char *cgroup_path, BPFProgram **installed_prog) {
        _cleanup_(bpf_program_unrefp) BPFProgram *prog = NULL;
        unsigned wrong = 0;
        int r;

        log_info("/* %s(type=%c) */", __func__, type);

        r = bpf_devices_cgroup_init(&prog, CGROUP_DEVICE_POLICY_STRICT, true);
        assert_se(r >= 0);

        r = bpf_devices_allow_list_major(prog, cgroup_path, "*", type, "rw");
        assert_se(r >= 0);

        r = bpf_devices_apply_policy(prog, CGROUP_DEVICE_POLICY_STRICT, true, cgroup_path, installed_prog);
        assert_se(r >= 0);

        {
                _cleanup_close_ int fd;
                const char *s = "/dev/null";

                fd = open(s, O_CLOEXEC|O_RDWR|O_NOCTTY);
                log_debug("open(%s, \"r\") = %d/%s", s, fd, fd < 0 ? errno_to_name(errno) : "-");
                if (type == 'c')
                        wrong += fd < 0;
                else
                        wrong += fd >= 0;
        }

        assert_se(wrong == 0);
}

static void test_policy_empty(bool add_mismatched, const char *cgroup_path, BPFProgram **installed_prog) {
        _cleanup_(bpf_program_unrefp) BPFProgram *prog = NULL;
        unsigned wrong = 0;
        int r;

        log_info("/* %s(add_mismatched=%s) */", __func__, yes_no(add_mismatched));

        r = bpf_devices_cgroup_init(&prog, CGROUP_DEVICE_POLICY_STRICT, add_mismatched);
        assert_se(r >= 0);

        if (add_mismatched) {
                r = bpf_devices_allow_list_major(prog, cgroup_path, "foobarxxx", 'c', "rw");
                assert_se(r < 0);
        }

        r = bpf_devices_apply_policy(prog, CGROUP_DEVICE_POLICY_STRICT, false, cgroup_path, installed_prog);
        assert_se(r >= 0);

        {
                _cleanup_close_ int fd;
                const char *s = "/dev/null";

                fd = open(s, O_CLOEXEC|O_RDWR|O_NOCTTY);
                log_debug("open(%s, \"r\") = %d/%s", s, fd, fd < 0 ? errno_to_name(errno) : "-");
                wrong += fd >= 0;
        }

        assert_se(wrong == 0);
}


int main(int argc, char *argv[]) {
        _cleanup_free_ char *cgroup = NULL, *parent = NULL;
        _cleanup_(rmdir_and_freep) char *controller_path = NULL;
        CGroupMask supported;
        struct rlimit rl;
        int r;

        test_setup_logging(LOG_DEBUG);

        assert_se(getrlimit(RLIMIT_MEMLOCK, &rl) >= 0);
        rl.rlim_cur = rl.rlim_max = MAX(rl.rlim_max, CAN_MEMLOCK_SIZE);
        (void) setrlimit(RLIMIT_MEMLOCK, &rl);

        r = cg_all_unified();
        if (r <= 0)
                return log_tests_skipped("We don't seem to be running with unified cgroup hierarchy");

        if (!can_memlock())
                return log_tests_skipped("Can't use mlock()");

        r = enter_cgroup_subroot(&cgroup);
        if (r == -ENOMEDIUM)
                return log_tests_skipped("cgroupfs not available");

        r = bpf_devices_supported();
        if (!r)
                return log_tests_skipped("BPF device filter not supported");
        assert_se(r == 1);

        r = cg_get_path(SYSTEMD_CGROUP_CONTROLLER, cgroup, NULL, &controller_path);
        assert_se(r >= 0);

        _cleanup_(bpf_program_unrefp) BPFProgram *prog = NULL;

        test_policy_closed(cgroup, &prog);
        test_policy_strict(cgroup, &prog);

        test_policy_allow_list_major("mem", cgroup, &prog);
        test_policy_allow_list_major("1", cgroup, &prog);

        test_policy_allow_list_major_star('c', cgroup, &prog);
        test_policy_allow_list_major_star('b', cgroup, &prog);

        test_policy_empty(false, cgroup, &prog);
        test_policy_empty(true, cgroup, &prog);

        assert_se(parent = dirname_malloc(cgroup));

        assert_se(cg_mask_supported(&supported) >= 0);
        r = cg_attach_everywhere(supported, parent, 0, NULL, NULL);
        assert_se(r >= 0);

        return 0;
}
