/** CAVE: Rulesets support only till ABI v7 ! Source /usr/include/linux/landlock.h Guide https://docs.kernel.org/userspace-api/landlock.html Example in C https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/samples/landlock/sandboxer.c Very short example rules := landlock_all_rules(); ok, ll_fd := landlock_create_ruleset(*rules); assert(ok, "Create Ruleset"); ok = landlock_lock_privileges(); assert(ok, "Lock Privileges"); ok = landlock_restrict_self(ll_fd); assert(ok, "Restrict Self"); */ LL_Ruleset_Create :: enum_flags u64 { NULL :: 0; VERSION :: 1; // CAVE: https://docs.kernel.org/userspace-api/landlock.html#landlock-errata ERRATA; } LL_Restrict_Self :: enum_flags u64 { LOG_SAME_EXEC_OFF :: 1; LOG_NEW_EXEC_ON; LOG_SUBDOMAINS_OFF; } landlock_rule_type :: enum #specified { PATH_BENEATH :: 1; NET_PORT :: 2; } /** Supports till ABI v7 */ LL_Filesystem :: enum_flags u64 { EXECUTE :: 1; WRITE_FILE; READ_FILE; READ_DIR; REMOVE_DIR; REMOVE_FILE; MAKE_CHAR; MAKE_DIR; MAKE_REG; MAKE_SOCK; MAKE_FIFO; MAKE_BLOCK; MAKE_SYM; REFER; /** ABI v2+ */ TRUNCATE; /** ABI v3+ */ IOCTL_DEV; /** ABI v5+ */ } LL_Network :: enum_flags u64 { BIND_TCP :: 1; CONNECT_TCP; } LL_Scoped :: enum_flags u64 { ABSTRACT_UNIX_SOCKET :: 1; SIGNAL; } landlock_ruleset_attr :: struct { handled_access_fs: LL_Filesystem; handled_access_net: LL_Network; scoped: LL_Scoped; } landlock_path_beneath_attr :: struct { allowed_access: LL_Filesystem; parent_fd: s32; } #no_padding landlock_net_port_attr :: struct { allowed_access: LL_Network; port: u64; } /** https://man7.org/linux/man-pages/man2/landlock_create_ruleset.2.html CAVE: Flags must be 0 if attr is used. Otherwise flags can be set to: .VERSION Quote from above man page: If attr is NULL and size is 0, then the returned value is the highest supported Landlock ABI version (starting at 1). This version can be used for a best-effort security approach, which is encouraged when user space is not pinned to a specific kernel version. [...] */ landlock_create_ruleset :: ( ruleset: *landlock_ruleset_attr, flags: LL_Ruleset_Create = .NULL, loc := #caller_location ) -> (ok: bool, fd: int) { assert(CPU == .X64 && OS == .LINUX, "Sorry, only support for x64 Linux!"); size := ifx ruleset == null then 0 else size_of(landlock_ruleset_attr); fd := syscall( SYS_LANDLOCK_CREATE_RULESET, ruleset, size, cast(u64, flags) ); if fd < 0 { print_last_error(ERR_LL_CREATE_RULESET, loc); return false, -1; } return true, fd; } /** https://man7.org/linux/man-pages/man2/landlock_restrict_self.2.html */ landlock_restrict_self :: (ruleset_fd: int) -> ok: bool { /** According to above man page, flags MUST be ZERO */ ok := syscall(SYS_LANDLOCK_RESTRICT_SELF, ruleset_fd, 0); if ok < 0 { print_last_error(ERR_LL_RESTRICT); return false; } return true; } landlock_add_rule :: inline ( ruleset_fd: int, rule_attr: *landlock_net_port_attr ) -> ok: bool { return add_rule(ruleset_fd, .NET_PORT, rule_attr); } landlock_add_rule :: inline ( ruleset_fd: int, rule_attr: *landlock_path_beneath_attr ) -> ok: bool { return add_rule(ruleset_fd, .PATH_BENEATH, rule_attr); } /** * The following procedures are non standard and added by me */ /** According to the man pages you must lock privileges before applying any landlock rules. https://man7.org/linux/man-pages/man7/landlock.7.html 'man prctl' on the return value: On success, a nonnegative value is returned. On error, -1 is returned, and errno is set to indicate the error. */ landlock_lock_privileges :: () -> ok: bool { ok := prctl(PR_SET_NO_NEW_PRIVS, 1); if ok < 0 { print_last_error(ERR_PRCTL); return false; } return true; } landlock_is_version_equal_or_higher :: (pinned_version: int, $print_version := false) -> bool { ok, system_version := landlock_create_ruleset(null, .VERSION); if !ok { log_error("Could not determine version"); return false; } #if print_version then log("Landlock System Version: %", system_version); return system_version >= pinned_version; } /** Returns _all_ available rules, essentially locking everything. This might be useful if you want more a whitelist, rather than a blacklist. Remove flags like this: my_ruleset.handled_access_fs &= ~TRUNCATE; Or if you want to remove items based on the supported ABI version. */ landlock_all_rules :: () -> landlock_ruleset_attr { #insert -> string { ll_fs := type_info(LL_Filesystem); ll_net := type_info(LL_Network); ll_scoped := type_info(LL_Scoped); temp: [..]string; sb: String_Builder; append(*sb, "return .{ handled_access_fs = "); for ll_fs.names array_add(*temp, tprint(".%", it)); fs_values := join(..temp, " | "); append(*sb, fs_values); append(*sb, ", handled_access_net = "); array_reset(*temp); for ll_net.names array_add(*temp, tprint(".%", it)); net_values := join(..temp, " | "); append(*sb, net_values); append(*sb, ", scoped = "); array_reset(*temp); for ll_scoped.names array_add(*temp, tprint(".%", it)); scoped_values := join(..temp, " | "); append(*sb, scoped_values); append(*sb, ", };"); code := builder_to_string(*sb); return code; }; } #scope_file #import "POSIX"; libc :: #library,system "libc"; /** This assertion is important, since syscall numbers might shift when using a different arch! */ #assert(CPU == .X64 && OS == .LINUX) "Only x64 Linux support!"; /** 2026-04-23 Glibc didn't implented landlock functions yet, so we have to wrap the syscalls ourselves. Jai's Linux-faced stdlib does not have the syscall numbers for the landlock functions, so we stole them from the Android lib. Which are identical with x64 Linux anyway. */ SYS_LANDLOCK_CREATE_RULESET :: 444; SYS_LANDLOCK_ADD_RULE :: 445; SYS_LANDLOCK_RESTRICT_SELF :: 446; /** /usr/include/linux/prctl.h */ PR_SET_NO_NEW_PRIVS :: 38; Error_Kind :: enum { LANDLOCK; PRCTL; } ERR_LL_CREATE_RULESET :: #code { if err == { case EOPNOTSUPP; log_error("Landlock is supported by the kernel but disabled at boot time."); case EINVAL; log_error("Unknown flags, or unknown access, or too small size."); case E2BIG; log_error("size is too big."); case EFAULT; log_error("attr was not a valid address."); case ENOMSG; log_error("Empty accesses (i.e., attr did not specify any access rights to restrict)."); case; log_error("Uncovered error: %", err); } } ERR_LL_ADD_RULE :: #code { if err == { case EAFNOSUPPORT; log_error("rule_type is set correctly, but TCP is not supported by the kernel."); case EOPNOTSUPP; log_error("Landlock is supported, but this feature is disabled at boot time by the kernel."); case EINVAL; log_error("Flags is not 0 OR inalivid port number supplied."); case ENOMSG; log_error("Empty accesses (i.e., rule_attr.allowed_access is 0)."); case EBADF; log_error("No valid file descriptor supplied in `ruleset_fd` or `rule_attr`."); case EBADFD; log_error("No valid file descriptor in `ruleset_fd`."); case EPERM; log_error("`ruleset_fd` has no write access to the underlying ruleset."); case EFAULT; log_error("`rule_attr` was not a valid address."); } } ERR_LL_RESTRICT :: #code { if err == { case EOPNOTSUPP; log_error("Landlock is supported by the kernel but disabled at boot time."); case EINVAL; log_error("flags is not 0."); case EBADF; log_error("ruleset_fd is not a file descriptor for the current thread."); case EBADFD; log_error("ruleset_fd is not a ruleset file descriptor."); case EPERM; log_error("ruleset_fd has no read access to the underlying ruleset.\nOr the calling thread is not running with no_new_privs,\nor it doesn't have the CAP_SYS_ADMIN in its user namespace."); case E2BIG; log_error("The maximum number of composed rulesets is reached for the calling thread. This limit is currently 64."); } } ERR_PRCTL :: #code { if err == { case EINVAL; log_error("OP value is not recognized/supported"); } } prctl :: (op: int, a2 := 0, a3 := 0, a4 := 0, a5 := 0) -> int #foreign libc; /** https://man7.org/linux/man-pages/man2/landlock_add_rule.2.html */ add_rule :: ( ruleset_fd: int, rule_type: landlock_rule_type, rule_attr: *void ) -> ok: bool { /** According to above man page, flags MUST be ZERO */ ok := syscall(SYS_LANDLOCK_ADD_RULE, ruleset_fd, rule_type, rule_attr, 0); if ok < 0 { print_last_error(ERR_LL_ADD_RULE); return false; } return true; } print_last_error :: ($code: Code, loc := #caller_location) { err := errno(); if err == 0 return; log_error("-- Error --------------------"); log_error("%", loc); #insert,scope() code; log_error("------------------------------"); }