]> git.ipfire.org Git - thirdparty/util-linux.git/commitdiff
setpriv: add landlock support
authorThomas Weißschuh <thomas@t-8ch.de>
Wed, 6 Dec 2023 17:39:51 +0000 (18:39 +0100)
committerThomas Weißschuh <thomas@t-8ch.de>
Mon, 11 Dec 2023 19:52:20 +0000 (20:52 +0100)
landlock [0] is a Linux stackable LSM that can be used by unprivileged
processes to build a sandbox around them.

With support for landlock in setpriv users can easily construct a
sandbox on-the-fly when executing programs.

[0] https://landlock.io/

Signed-off-by: Thomas Weißschuh <thomas@t-8ch.de>
13 files changed:
bash-completion/setpriv
configure.ac
meson.build
sys-utils/Makemodule.am
sys-utils/meson.build
sys-utils/setpriv-landlock.c [new file with mode: 0644]
sys-utils/setpriv-landlock.h [new file with mode: 0644]
sys-utils/setpriv.1.adoc
sys-utils/setpriv.c
tests/commands.sh
tests/expected/setpriv/landlock-nothing-allowed [new file with mode: 0644]
tests/expected/setpriv/landlock-partial-access-fail [new file with mode: 0644]
tests/ts/setpriv/landlock [new file with mode: 0755]

index 69df34b39bc7963d3c3b36c8e31c181ef4487732..766bbcb79bcbfdd9c3c984d2241f319feda61ce0 100644 (file)
@@ -87,6 +87,16 @@ _setpriv_module()
                        COMPREPLY=( $(compgen -W "profile" -- $cur) )
                        return 0
                        ;;
+               '--landlock-access')
+                       # FIXME: how to list landlock accesses?
+                       COMPREPLY=( $(compgen -W "access" -- $cur) )
+                       return 0
+                       ;;
+               '--landlock-rule')
+                       # FIXME: how to list landlock rules?
+                       COMPREPLY=( $(compgen -W "rule" -- $cur) )
+                       return 0
+                       ;;
                '-h'|'--help'|'-V'|'--version')
                        return 0
                        ;;
@@ -112,6 +122,8 @@ _setpriv_module()
                                --reset-env
                                --selinux-label
                                --apparmor-profile
+                               --landlock-access
+                               --landlock-rule
                                --help
                                --version"
                        COMPREPLY=( $(compgen -W "${OPTS[*]}" -- $cur) )
index 913e50449f1fe521e447fbcdb140a479c08af4b0..6ed1f7822803d7e55942534446019454cc67d6e3 100644 (file)
@@ -325,6 +325,7 @@ AC_CHECK_HEADERS([ \
        linux/falloc.h \
        linux/fd.h \
        linux/fiemap.h \
+       linux/landlock.h \
        linux/kcmp.h \
        linux/net_namespace.h \
        linux/nsfs.h \
@@ -588,6 +589,9 @@ AC_CHECK_FUNCS([ \
        getttynam \
        inotify_init \
        jrand48 \
+       landlock_create_ruleset \
+       landlock_add_rule \
+       landlock_restrict_self \
        lchown \
        lgetxattr \
        llistxattr \
@@ -646,6 +650,7 @@ AC_CHECK_FUNCS([reboot], [have_reboot=yes],[have_reboot=no])
 AC_CHECK_FUNCS([updwtmpx updwtmpx], [have_gnu_utmpx=yes], [have_gnu_utmpx=no])
 
 AM_CONDITIONAL([HAVE_OPENAT], [test "x$have_openat" = xyes])
+AM_CONDITIONAL([HAVE_LINUX_LANDLOCK_H], [test "x$ac_cv_header_linux_landlock_h" = xyes])
 
 have_setns_syscall="yes"
 UL_CHECK_SYSCALL([setns])
index 331d7582987b32dff25450f3c2b349357a6b0990..2d0f6e14f1485184e6c1335f5eaaed25e590da97 100644 (file)
@@ -176,6 +176,7 @@ headers = '''
        linux/fiemap.h
        linux/gsmmux.h
         linux/if_alg.h
+        linux/landlock.h
         linux/kcmp.h
         linux/net_namespace.h
         linux/nsfs.h
@@ -539,6 +540,9 @@ funcs = '''
         getsgnam
         inotify_init
         jrand48
+        landlock_create_ruleset
+        landlock_add_rule
+        landlock_restrict_self
         lchown
         lgetxattr
         llistxattr
index 4d2728c191f3ff93983805c30bc1fa01e1328b1c..209b656b009cfa3c54f8e1c438dd73b3f2ad0b43 100644 (file)
@@ -581,5 +581,9 @@ MANPAGES += sys-utils/setpriv.1
 dist_noinst_DATA += sys-utils/setpriv.1.adoc
 setpriv_SOURCES = sys-utils/setpriv.c \
                  lib/caputils.c
+dist_noinst_HEADERS += sys-utils/setpriv-landlock.h
+if HAVE_LINUX_LANDLOCK_H
+setpriv_SOURCES += sys-utils/setpriv-landlock.c
+endif
 setpriv_LDADD = $(LDADD) -lcap-ng libcommon.la
 endif
index 0e9857349e640a4672596309b1795706bbe91dbb..e683253287b634c909e4ce7a214c44b79416e793 100644 (file)
@@ -185,6 +185,9 @@ nsenter_sources = files(
 setpriv_sources = files(
   'setpriv.c',
 )
+if LINUX and conf.get('HAVE_LINUX_LANDLOCK_H').to_string() == '1'
+  setpriv_sources += files('setpriv-landlock.c')
+endif
 
 flock_sources = files(
   'flock.c',
diff --git a/sys-utils/setpriv-landlock.c b/sys-utils/setpriv-landlock.c
new file mode 100644 (file)
index 0000000..153e748
--- /dev/null
@@ -0,0 +1,211 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Copyright (C) 2023 Thomas Weißschuh <thomas@t-8ch.de>
+ */
+
+#include <sys/prctl.h>
+#include <sys/syscall.h>
+#include <linux/landlock.h>
+
+#include "setpriv-landlock.h"
+
+#include "strutils.h"
+#include "xalloc.h"
+#include "nls.h"
+#include "c.h"
+
+#ifndef HAVE_LANDLOCK_CREATE_RULESET
+static inline int landlock_create_ruleset(
+               const struct landlock_ruleset_attr *attr,
+               size_t size, uint32_t flags)
+{
+       return syscall(__NR_landlock_create_ruleset, attr, size, flags);
+}
+#endif
+
+#ifndef HAVE_LANDLOCK_ADD_RULE
+static inline int landlock_add_rule(
+               int ruleset_fd, enum landlock_rule_type rule_type,
+               const void *rule_attr, uint32_t flags)
+{
+       return syscall(__NR_landlock_add_rule, ruleset_fd, rule_type,
+                      rule_attr, flags);
+}
+#endif
+
+#ifndef HAVE_LANDLOCK_RESTRICT_SELF
+static inline int landlock_restrict_self(int ruleset_fd, uint32_t flags)
+{
+       return syscall(__NR_landlock_restrict_self, ruleset_fd, flags);
+}
+#endif
+
+#define SETPRIV_EXIT_PRIVERR 127       /* how we exit when we fail to set privs */
+
+struct landlock_rule_entry {
+       struct list_head head;
+       enum landlock_rule_type rule_type;
+       union {
+               struct landlock_path_beneath_attr path_beneath_attr;
+       };
+};
+
+static const struct {
+       unsigned long long value;
+       const char *type;
+} landlock_access_fs[] = {
+       { LANDLOCK_ACCESS_FS_EXECUTE,     "execute"     },
+       { LANDLOCK_ACCESS_FS_WRITE_FILE,  "write-file"  },
+       { LANDLOCK_ACCESS_FS_READ_FILE,   "read-file"   },
+       { LANDLOCK_ACCESS_FS_READ_DIR,    "read-dir"    },
+       { LANDLOCK_ACCESS_FS_REMOVE_DIR,  "remove-dir"  },
+       { LANDLOCK_ACCESS_FS_REMOVE_FILE, "remove-file" },
+       { LANDLOCK_ACCESS_FS_MAKE_CHAR,   "make-char"   },
+       { LANDLOCK_ACCESS_FS_MAKE_DIR,    "make-dir"    },
+       { LANDLOCK_ACCESS_FS_MAKE_REG,    "make-reg"    },
+       { LANDLOCK_ACCESS_FS_MAKE_SOCK,   "make-sock"   },
+       { LANDLOCK_ACCESS_FS_MAKE_FIFO,   "make-fifo"   },
+       { LANDLOCK_ACCESS_FS_MAKE_BLOCK,  "make-block"  },
+       { LANDLOCK_ACCESS_FS_MAKE_SYM,    "make-sym"    },
+#ifdef LANDLOCK_ACCESS_FS_REFER
+       { LANDLOCK_ACCESS_FS_REFER,       "refer"       },
+#endif
+#ifdef LANDLOCK_ACCESS_FS_TRUNCATE
+       { LANDLOCK_ACCESS_FS_TRUNCATE,    "truncate"    },
+#endif
+};
+
+static long landlock_access_to_mask(const char *str, size_t len)
+{
+       size_t i;
+
+       for (i = 0; i < ARRAY_SIZE(landlock_access_fs); i++)
+               if (strncmp(landlock_access_fs[i].type, str, len) == 0)
+                       return landlock_access_fs[i].value;
+       return -1;
+}
+
+static uint64_t parse_landlock_fs_access(const char *list)
+{
+       unsigned long r = 0;
+       size_t i;
+
+       /* without argument, match all */
+       if (list[0] == '\0') {
+               for (i = 0; i < ARRAY_SIZE(landlock_access_fs); i++)
+                       r |= landlock_access_fs[i].value;
+       } else {
+               if (string_to_bitmask(list, &r, landlock_access_to_mask))
+                       errx(EXIT_FAILURE,
+                            _("could not parse landlock fs access: %s"), list);
+       }
+
+       return r;
+}
+
+void parse_landlock_access(struct setpriv_landlock_opts *opts, const char *str)
+{
+       const char *type;
+       size_t i;
+
+       if (strcmp(str, "fs") == 0) {
+               for (i = 0; i < ARRAY_SIZE(landlock_access_fs); i++)
+                       opts->access_fs |= landlock_access_fs[i].value;
+               return;
+       }
+
+       type = startswith(str, "fs:");
+       if (type)
+               opts->access_fs |= parse_landlock_fs_access(type);
+}
+
+void parse_landlock_rule(struct setpriv_landlock_opts *opts, const char *str)
+{
+       struct landlock_rule_entry *rule = xmalloc(sizeof(*rule));
+       const char *accesses, *path;
+       char *accesses_part;
+       int parent_fd;
+
+       accesses = startswith(str, "path-beneath:");
+       if (!accesses)
+               errx(EXIT_FAILURE, _("invalid landlock rule: %s"), str);
+       path = strchr(accesses, ':');
+       if (!path)
+               errx(EXIT_FAILURE, _("invalid landlock rule: %s"), str);
+       rule->rule_type = LANDLOCK_RULE_PATH_BENEATH;
+
+       accesses_part = xstrndup(accesses, path - accesses);
+       rule->path_beneath_attr.allowed_access = parse_landlock_fs_access(accesses_part);
+       free(accesses_part);
+
+       path++;
+
+       parent_fd = open(path, O_RDONLY | O_PATH | O_CLOEXEC);
+       if (parent_fd == -1)
+               err(EXIT_FAILURE, _("could not open file for landlock: %s"), path);
+
+       rule->path_beneath_attr.parent_fd = parent_fd;
+
+       list_add(&rule->head, &opts->rules);
+}
+
+void init_landlock_opts(struct setpriv_landlock_opts *opts)
+{
+       INIT_LIST_HEAD(&opts->rules);
+}
+
+void do_landlock(const struct setpriv_landlock_opts *opts)
+{
+       struct landlock_rule_entry *rule;
+       struct list_head *entry;
+       int fd, ret;
+
+       const struct landlock_ruleset_attr ruleset_attr = {
+               .handled_access_fs = opts->access_fs,
+       };
+
+       fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+       if (fd == -1)
+               err(SETPRIV_EXIT_PRIVERR, _("landlock_create_ruleset failed"));
+
+       list_for_each(entry, &opts->rules) {
+               rule = list_entry(entry, struct landlock_rule_entry, head);
+
+               assert(rule->rule_type == LANDLOCK_RULE_PATH_BENEATH);
+
+               ret = landlock_add_rule(fd, rule->rule_type, &rule->path_beneath_attr, 0);
+               if (ret == -1)
+                       err(SETPRIV_EXIT_PRIVERR, _("adding landlock rule failed"));
+       }
+
+       if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1)
+               err(SETPRIV_EXIT_PRIVERR, _("disallow granting new privileges for landlock failed"));
+
+       if (landlock_restrict_self(fd, 0) == -1)
+               err(SETPRIV_EXIT_PRIVERR, _("landlock_restrict_self faild"));
+}
+
+void usage_setpriv(FILE *out)
+{
+       size_t i;
+
+       fprintf(out, "\n");
+       fprintf(out, _("Landlock accesses:\n"));
+       fprintf(out, " Access: fs\n");
+       fprintf(out, " Rule types: path-beneath\n");
+
+       fprintf(out, " Rules: ");
+       for (i = 0; i < ARRAY_SIZE(landlock_access_fs); i++) {
+               fprintf(out, "%s", landlock_access_fs[i].type);
+               if (i == ARRAY_SIZE(landlock_access_fs) - 1)
+                       fprintf(out, "\n");
+               else
+                       fprintf(out, ",");
+       }
+}
diff --git a/sys-utils/setpriv-landlock.h b/sys-utils/setpriv-landlock.h
new file mode 100644 (file)
index 0000000..d66f86d
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Copyright (C) 2023 Thomas Weißschuh <thomas@t-8ch.de>
+ */
+
+#ifndef UTIL_LINUX_SETPRIV_LANDLOCK
+#define UTIL_LINUX_SETPRIV_LANDLOCK
+
+#ifdef HAVE_LINUX_LANDLOCK_H
+
+#include <stdint.h>
+
+#include "list.h"
+
+struct setpriv_landlock_opts {
+       uint64_t access_fs;
+       struct list_head rules;
+};
+
+void do_landlock(const struct setpriv_landlock_opts *opts);
+void parse_landlock_access(struct setpriv_landlock_opts *opts, const char *str);
+void parse_landlock_rule(struct setpriv_landlock_opts *opts, const char *str);
+void init_landlock_opts(struct setpriv_landlock_opts *opts);
+void usage_setpriv(FILE *out);
+
+#else
+
+#include "c.h"
+#include "nls.h"
+
+struct setpriv_landlock_opts {};
+
+static inline void do_landlock(const void *opts __attribute__((unused))) {}
+static inline void parse_landlock_access(
+               void *opts __attribute__((unused)),
+               const char *str __attribute__((unused)))
+{
+       errx(EXIT_FAILURE, _("no support for landlock"));
+}
+#define parse_landlock_rule parse_landlock_access
+static inline void init_landlock_opts(void *opts __attribute__((unused))) {}
+static inline void usage_setpriv(FILE *out __attribute__((unused))) {}
+
+#endif /* HAVE_LINUX_LANDLOCK_H */
+
+#endif
index a0ad6f8dfa5a809551877eff1d06569e0d0ec0c3..902934622597451cd9b876caf84fc732ce001c61 100644 (file)
@@ -84,6 +84,32 @@ Request a particular SELinux transition (using a transition on exec, not dyntran
 *--apparmor-profile* _profile_::
 Request a particular AppArmor profile (using a transition on exec). This will fail and cause *setpriv* to abort if AppArmor is not in use, and the transition may be ignored or cause *execve*(2) to fail at AppArmor's whim.
 
+*--landlock-access* _access_::
+Enable landlock restrictions for a specific set of system accesses.
+To allow specific subgroups of accesses use *--landlock-rule*.
++
+Block all filesystem access:
++
+*setpriv --landlock-access fs*
++
+Block all file deletions and directory creations:
++
+*setpriv --landlock-access fs:remove-file,make-dir*
++
+For a complete set of supported access categories use *setpriv --help*.
+
+*--landlock-rule* _rule_::
+
+Allow one specific access from the categories blocked by *--landlock-access*.
++
+The syntax is as follows:
++
+*--landlock-rule $ruletype:$access:$rulearg*
++
+For example grant file read access to everything under */boot*:
++
+*--landlock-rule path-beneath:read-file:/boot*
+
 *--reset-env*::
 Clears all the environment variables except *TERM*; initializes the environment variables *HOME*, *SHELL*, *USER*, *LOGNAME* according to the user's passwd entry; sets *PATH* to _/usr/local/bin:/bin:/usr/bin_ for a regular user and to _/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin_ for root.
 +
@@ -117,6 +143,7 @@ mailto:luto@amacapital.net[Andy Lutomirski]
 *su*(1),
 *prctl*(2),
 *capabilities*(7)
+*landlock*(7)
 
 include::man-common/bugreports.adoc[]
 
index 4099355106913270d2e811d32f5471509f932744..74d3fbe7893b75c99e542ed6d0d31b3d5498219e 100644 (file)
@@ -41,6 +41,7 @@
 #include "pathnames.h"
 #include "signames.h"
 #include "env.h"
+#include "setpriv-landlock.h"
 
 #ifndef PR_SET_NO_NEW_PRIVS
 # define PR_SET_NO_NEW_PRIVS 38
@@ -110,6 +111,7 @@ struct privctx {
        /* LSMs */
        const char *selinux_label;
        const char *apparmor_profile;
+       struct setpriv_landlock_opts landlock;
 };
 
 static void __attribute__((__noreturn__)) usage(void)
@@ -143,6 +145,8 @@ static void __attribute__((__noreturn__)) usage(void)
                "                             set or clear parent death signal\n"), out);
        fputs(_(" --selinux-label <label>     set SELinux label\n"), out);
        fputs(_(" --apparmor-profile <pr>     set AppArmor profile\n"), out);
+       fputs(_(" --landlock-access <access>  add Landlock access\n"), out);
+       fputs(_(" --landlock-rule <rule>      add Landlock rule\n"), out);
        fputs(_(" --reset-env                 clear all environment and initialize\n"
                "                               HOME, SHELL, USER, LOGNAME and PATH\n"), out);
 
@@ -152,6 +156,8 @@ static void __attribute__((__noreturn__)) usage(void)
        fputs(_(" This tool can be dangerous.  Read the manpage, and be careful.\n"), out);
        fprintf(out, USAGE_MAN_TAIL("setpriv(1)"));
 
+       usage_setpriv(out);
+
        exit(EXIT_SUCCESS);
 }
 
@@ -651,7 +657,6 @@ static void do_apparmor_profile(const char *label)
                    _("write failed: %s"), _PATH_PROC_ATTR_EXEC);
 }
 
-
 static void do_reset_environ(struct passwd *pw)
 {
        char *term = getenv("TERM");
@@ -754,6 +759,8 @@ int main(int argc, char **argv)
                PDEATHSIG,
                SELINUX_LABEL,
                APPARMOR_PROFILE,
+               LANDLOCK_ACCESS,
+               LANDLOCK_RULE,
                RESET_ENV
        };
 
@@ -779,6 +786,8 @@ int main(int argc, char **argv)
                { "pdeathsig",        required_argument, NULL, PDEATHSIG,       },
                { "selinux-label",    required_argument, NULL, SELINUX_LABEL    },
                { "apparmor-profile", required_argument, NULL, APPARMOR_PROFILE },
+               { "landlock-access",  required_argument, NULL, LANDLOCK_ACCESS  },
+               { "landlock-rule",    required_argument, NULL, LANDLOCK_RULE    },
                { "help",             no_argument,       NULL, 'h'              },
                { "reset-env",        no_argument,       NULL, RESET_ENV,       },
                { "version",          no_argument,       NULL, 'V'              },
@@ -805,6 +814,7 @@ int main(int argc, char **argv)
        close_stdout_atexit();
 
        memset(&opts, 0, sizeof(opts));
+       init_landlock_opts(&opts.landlock);
 
        while ((c = getopt_long(argc, argv, "+dhV", longopts, NULL)) != -1) {
                err_exclusive_options(c, longopts, excl, excl_st);
@@ -933,6 +943,12 @@ int main(int argc, char **argv)
                                     _("duplicate --apparmor-profile option"));
                        opts.apparmor_profile = optarg;
                        break;
+               case LANDLOCK_ACCESS:
+                       parse_landlock_access(&opts.landlock, optarg);
+                       break;
+               case LANDLOCK_RULE:
+                       parse_landlock_rule(&opts.landlock, optarg);
+                       break;
                case RESET_ENV:
                        opts.reset_env = 1;
                        break;
@@ -1056,6 +1072,8 @@ int main(int argc, char **argv)
        if (opts.pdeathsig && prctl(PR_SET_PDEATHSIG, opts.pdeathsig < 0 ? 0 : opts.pdeathsig) != 0)
                err(SETPRIV_EXIT_PRIVERR, _("set parent death signal failed"));
 
+       do_landlock(&opts.landlock);
+
        execvp(argv[optind], argv + optind);
        errexec(argv[optind]);
 }
index a7714bdf3cdbb35ebe23eed8a76957aeee385b5a..318292ac58d08b096d08d15975da34bdd085dee8 100644 (file)
@@ -111,6 +111,7 @@ TS_CMD_SCRIPTREPLAY=${TS_CMD_SCRIPTREPLAY-"${ts_commandsdir}scriptreplay"}
 TS_CMD_SCRIPTLIVE=${TS_CMD_SCRIPTLIVE-"${ts_commandsdir}scriptlive"}
 TS_CMD_SETARCH=${TS_CMD_SETARCH-"${ts_commandsdir}setarch"}
 TS_CMD_SETPGID=${TS_CMD_SETPGID-"${ts_commandsdir}setpgid"}
+TS_CMD_SETPRIV=${TS_CMD_SETPRIV-"${ts_commandsdir}setpriv"}
 TS_CMD_SETSID=${TS_CMD_SETSID-"${ts_commandsdir}setsid"}
 TS_CMD_SWAPLABEL=${TS_CMD_SWAPLABEL:-"${ts_commandsdir}swaplabel"}
 TS_CMD_SWAPOFF=${TS_CMD_SWAPOFF:-"${ts_commandsdir}swapoff"}
diff --git a/tests/expected/setpriv/landlock-nothing-allowed b/tests/expected/setpriv/landlock-nothing-allowed
new file mode 100644 (file)
index 0000000..6a63dbc
--- /dev/null
@@ -0,0 +1 @@
+setpriv: failed to execute true: Permission denied
diff --git a/tests/expected/setpriv/landlock-partial-access-fail b/tests/expected/setpriv/landlock-partial-access-fail
new file mode 100644 (file)
index 0000000..0a9a503
--- /dev/null
@@ -0,0 +1 @@
+cp: cannot create regular file '/dev/zero': Permission denied
diff --git a/tests/ts/setpriv/landlock b/tests/ts/setpriv/landlock
new file mode 100755 (executable)
index 0000000..d4ce5d1
--- /dev/null
@@ -0,0 +1,64 @@
+#!/bin/bash
+
+# Copyright (C) 2023 Thomas Weißschuh <thomas@t-8ch.de>
+#
+# This file is part of util-linux.
+#
+# This file is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This file is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+TS_TOPDIR="${0%/*}/../.."
+TS_DESC="setpriv landlock"
+
+. "$TS_TOPDIR"/functions.sh
+ts_init "$*"
+
+[[ "$COVERAGE" = yes ]] && ts_skip "does not work with coverage"
+
+ts_check_test_command "$TS_CMD_SETPRIV"
+
+"$TS_CMD_SETPRIV" --landlock-access fs \
+       --landlock-rule path-beneath:execute:/ \
+       --landlock-rule path-beneath:read-file:/ \
+       true \
+       || ts_skip "no landlock support in setpriv"
+
+ts_init_subtest "nothing-allowed"
+"$TS_CMD_SETPRIV" --landlock-access fs true &> "$TS_OUTPUT"
+ts_finalize_subtest
+
+ts_init_subtest "partial-access-fail"
+"$TS_CMD_SETPRIV" --landlock-access \
+       fs:write cp /dev/null /dev/zero \
+       &> "$TS_OUTPUT"
+ts_finalize_subtest
+
+ts_init_subtest "partial-access-success"
+"$TS_CMD_SETPRIV" \
+       --landlock-access fs:write --landlock-rule path-beneath:write:/dev/zero \
+       cp /dev/null /dev/zero \
+       &> "$TS_OUTPUT"
+ts_finalize_subtest
+
+ts_init_subtest "combined-access"
+"$TS_CMD_SETPRIV" --landlock-access fs:execute,read-file \
+       --landlock-rule path-beneath:execute,read-file:/ \
+       true \
+       &> "$TS_OUTPUT"
+ts_finalize_subtest
+
+ts_init_subtest "wildcard-access"
+"$TS_CMD_SETPRIV" --landlock-access fs \
+       --landlock-rule path-beneath::/ \
+       true \
+       &> "$TS_OUTPUT"
+ts_finalize_subtest
+
+ts_finalize