#include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef SUPPORT_SECCOMP #include #include #endif #ifndef landlock_create_ruleset static inline int landlock_create_ruleset (const struct landlock_ruleset_attr *const attr, const size_t size, const uint32_t flags) { return syscall (__NR_landlock_create_ruleset, attr, size, flags); } #endif #ifndef landlock_create_ruleset static inline int landlock_add_rule (const int ruleset_fd, const enum landlock_rule_type rule_type, const void *const rule_attr, const uint32_t flags) { return syscall (__NR_landlock_add_rule, ruleset_fd, rule_type, rule_attr, flags); } #endif #ifndef landlock_create_ruleset static inline int landlock_restrict_self (const int ruleset_fd, const uint32_t flags) { return syscall (__NR_landlock_restrict_self, ruleset_fd, flags); } #endif enum class SyscallFilterMode : uint8_t { None = 0, Whitelist, Blacklist }; struct Options { std::vector fs_ro; std::vector fs_rw; std::vector tcp_bind; std::vector tcp_connect; std::vector scopes; #ifdef SUPPORT_SECCOMP std::vector syscalls; SyscallFilterMode syscall_filter_mode = SyscallFilterMode::None; #endif /* Includes program arguments. */ std::vector command; }; static bool init_seccomp ( #ifndef SUPPORT_SECCOMP [[maybe_unused]] #endif const Options &opts) { #ifdef SUPPORT_SECCOMP using std::cerr; scmp_filter_ctx ctx; #define SECCOMP_INIT_FINAL_RULESET(VIOLATE_ACTION, RULESET_ACTION) \ do \ { \ ctx = seccomp_init (VIOLATE_ACTION); \ if (ctx == nullptr) \ { \ cerr << "Failed to initialize seccomp context, bailing.\n"; \ return false; \ } \ for (const auto &syscall : opts.syscalls) \ { \ const int resolved_syscall = \ seccomp_syscall_resolve_name(syscall.c_str ()); \ if (resolved_syscall == __NR_SCMP_ERROR) \ { \ cerr << "Failed to resolve syscall '" << syscall << "', bailing.\n"; \ seccomp_release (ctx); \ return false; \ } \ if (seccomp_rule_add_exact (ctx, RULESET_ACTION, resolved_syscall, 0) > 0) \ { \ cerr << "Failed to add seccomp rule " #RULESET_ACTION " for syscall '" \ << syscall << "', bailing.\n"; \ seccomp_release (ctx); \ return false; \ } \ } \ } while (0); switch (opts.syscall_filter_mode) { case SyscallFilterMode::Whitelist: SECCOMP_INIT_FINAL_RULESET (SCMP_ACT_KILL, SCMP_ACT_ALLOW) break; case SyscallFilterMode::Blacklist: SECCOMP_INIT_FINAL_RULESET (SCMP_ACT_ALLOW, SCMP_ACT_KILL) break; case SyscallFilterMode::None: return true; default: /* Unreachable. */ __builtin_unreachable (); } #undef SECCOMP_INIT_FINAL_RULESET if (seccomp_load (ctx) < 0) { cerr << "Failed to load seccomp ruleset, bailing.\n"; seccomp_release (ctx); return false; } seccomp_release (ctx); /* No new privs for the calling thread, preserved across execve. */ if (prctl (PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) { cerr << "Failed to restrict calling thread to no new privileges: " << strerror (errno) << '\n'; return false; } #endif return true; } int main (int argc, char **argv) { using std::cout, std::cerr; /* Argument parsing. */ std::string_view progname = argv[0]; CLI::App app{"Initalise and launch a restricted program environment"}; Options opts; app.set_help_flag ("--help", "Display this help and exit"); app.add_option ("--fs-ro", opts.fs_ro, "Add PATH to read-only allowed paths") ->type_name ("PATH"); app.add_option ("--fs-rw", opts.fs_rw, "Add PATH to read-write allowed paths") ->type_name ("PATH"); app.add_option ("--tcp-bind", opts.tcp_bind, "Allow binding to PORT") ->type_name ("PORT"); app.add_option ("--tcp-connect", opts.tcp_connect, "Allow connecting to PORT") ->type_name ("PORT"); app.add_option ("--scope", opts.scopes) ->type_name ("ACTION") ->check (CLI::IsMember ({"abstract-socket", "send-signal", "a", "s"})) ->transform (CLI::CheckedTransformer ({ {"a", "abstract-socket"}, {"s", "send-signal"}})) ->description (R"(Deny ACTION outside of the landlock domain - abstract-socket: Restrict opening abstract unix sockets - send-signal: Restrict sending signals)"); #ifdef SUPPORT_SECCOMP auto syscall_whitelist = app.add_option ("--syscall-whitelist", opts.syscalls, "Whitelist syscall") ->type_name ("SYSCALL"); auto syscall_blacklist = app.add_option ("--syscall-blacklist", opts.syscalls, "Blacklist syscall") ->type_name ("SYSCALL"); syscall_whitelist->excludes (syscall_blacklist); syscall_blacklist->excludes (syscall_whitelist); #endif app.add_option ("COMMAND", opts.command, "Command to run (with arguments)") ->required () ->allow_extra_args (); CLI11_PARSE (app, argc, argv); /* Initalise ruleset. */ int abi = landlock_create_ruleset (nullptr, 0, LANDLOCK_CREATE_RULESET_VERSION); if (abi < 0) { switch (errno) { case ENOSYS: /* Landlock not supported. */ cerr << "The running kernel does not support Landlock, bailing.\n"; break; case EOPNOTSUPP: /* Landlock disabled. */ cerr << "The running kernel has Landlock supported disabled, bailing.\n"; break; } return EXIT_FAILURE; } #ifdef SUPPORT_SECCOMP if (syscall_whitelist->count () > 0) opts.syscall_filter_mode = SyscallFilterMode::Whitelist; else if (syscall_blacklist->count () > 0) opts.syscall_filter_mode = SyscallFilterMode::Blacklist; #endif if (!init_seccomp(opts)) return EXIT_FAILURE; std::vector args; for (const auto &arg : opts.command) args.push_back (const_cast(arg.c_str ())); args.push_back (nullptr); execvp(args.at (0), args.data ()); /* If the command launched succesfully, this entire section should become unreachable. */ cerr << opts.command.at (0) << ": " << strerror (errno) << '\n'; return EXIT_FAILURE; }