selftests/landlock: Test IOCTL support
authorGünther Noack <gnoack@google.com>
Fri, 19 Apr 2024 16:11:13 +0000 (16:11 +0000)
committerMickaël Salaün <mic@digikod.net>
Mon, 13 May 2024 04:58:30 +0000 (06:58 +0200)
Exercises Landlock's IOCTL feature in different combinations of
handling and permitting the LANDLOCK_ACCESS_FS_IOCTL_DEV right, and in
different combinations of using files and directories.

Signed-off-by: Günther Noack <gnoack@google.com>
Link: https://lore.kernel.org/r/20240419161122.2023765-3-gnoack@google.com
Signed-off-by: Mickaël Salaün <mic@digikod.net>
tools/testing/selftests/landlock/fs_test.c

index 17b00e6c778d3b37742bd005fc69f88f97428cd6..fd7793b413d1618ea38bb47c5bce318759e1f251 100644 (file)
@@ -8,6 +8,7 @@
  */
 
 #define _GNU_SOURCE
+#include <asm/termbits.h>
 #include <fcntl.h>
 #include <libgen.h>
 #include <linux/landlock.h>
@@ -16,6 +17,7 @@
 #include <stdio.h>
 #include <string.h>
 #include <sys/capability.h>
+#include <sys/ioctl.h>
 #include <sys/mount.h>
 #include <sys/prctl.h>
 #include <sys/sendfile.h>
 #include <sys/vfs.h>
 #include <unistd.h>
 
+/*
+ * Intentionally included last to work around header conflict.
+ * See https://sourceware.org/glibc/wiki/Synchronizing_Headers.
+ */
+#include <linux/fs.h>
+
 #include "common.h"
 
 #ifndef renameat2
@@ -744,6 +752,9 @@ static int create_ruleset(struct __test_metadata *const _metadata,
        }
 
        for (i = 0; rules[i].path; i++) {
+               if (!rules[i].access)
+                       continue;
+
                add_path_beneath(_metadata, ruleset_fd, rules[i].access,
                                 rules[i].path);
        }
@@ -3452,7 +3463,7 @@ TEST_F_FORK(layout1, truncate_unhandled)
                              LANDLOCK_ACCESS_FS_WRITE_FILE;
        int ruleset_fd;
 
-       /* Enable Landlock. */
+       /* Enables Landlock. */
        ruleset_fd = create_ruleset(_metadata, handled, rules);
 
        ASSERT_LE(0, ruleset_fd);
@@ -3535,7 +3546,7 @@ TEST_F_FORK(layout1, truncate)
                              LANDLOCK_ACCESS_FS_TRUNCATE;
        int ruleset_fd;
 
-       /* Enable Landlock. */
+       /* Enables Landlock. */
        ruleset_fd = create_ruleset(_metadata, handled, rules);
 
        ASSERT_LE(0, ruleset_fd);
@@ -3761,7 +3772,7 @@ TEST_F_FORK(ftruncate, open_and_ftruncate)
        };
        int fd, ruleset_fd;
 
-       /* Enable Landlock. */
+       /* Enables Landlock. */
        ruleset_fd = create_ruleset(_metadata, variant->handled, rules);
        ASSERT_LE(0, ruleset_fd);
        enforce_ruleset(_metadata, ruleset_fd);
@@ -3854,6 +3865,181 @@ TEST(memfd_ftruncate)
        ASSERT_EQ(0, close(fd));
 }
 
+static int test_fionread_ioctl(int fd)
+{
+       size_t sz = 0;
+
+       if (ioctl(fd, FIONREAD, &sz) < 0 && errno == EACCES)
+               return errno;
+       return 0;
+}
+
+/* clang-format off */
+FIXTURE(ioctl) {};
+
+FIXTURE_SETUP(ioctl) {};
+
+FIXTURE_TEARDOWN(ioctl) {};
+/* clang-format on */
+
+FIXTURE_VARIANT(ioctl)
+{
+       const __u64 handled;
+       const __u64 allowed;
+       const mode_t open_mode;
+       /*
+        * FIONREAD is used as a characteristic device-specific IOCTL command.
+        * It is implemented in fs/ioctl.c for regular files,
+        * but we do not blanket-permit it for devices.
+        */
+       const int expected_fionread_result;
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ioctl, handled_i_allowed_none) {
+       /* clang-format on */
+       .handled = LANDLOCK_ACCESS_FS_IOCTL_DEV,
+       .allowed = 0,
+       .open_mode = O_RDWR,
+       .expected_fionread_result = EACCES,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ioctl, handled_i_allowed_i) {
+       /* clang-format on */
+       .handled = LANDLOCK_ACCESS_FS_IOCTL_DEV,
+       .allowed = LANDLOCK_ACCESS_FS_IOCTL_DEV,
+       .open_mode = O_RDWR,
+       .expected_fionread_result = 0,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ioctl, unhandled) {
+       /* clang-format on */
+       .handled = LANDLOCK_ACCESS_FS_EXECUTE,
+       .allowed = LANDLOCK_ACCESS_FS_EXECUTE,
+       .open_mode = O_RDWR,
+       .expected_fionread_result = 0,
+};
+
+TEST_F_FORK(ioctl, handle_dir_access_file)
+{
+       const int flag = 0;
+       const struct rule rules[] = {
+               {
+                       .path = "/dev",
+                       .access = variant->allowed,
+               },
+               {},
+       };
+       int file_fd, ruleset_fd;
+
+       /* Enables Landlock. */
+       ruleset_fd = create_ruleset(_metadata, variant->handled, rules);
+       ASSERT_LE(0, ruleset_fd);
+       enforce_ruleset(_metadata, ruleset_fd);
+       ASSERT_EQ(0, close(ruleset_fd));
+
+       file_fd = open("/dev/zero", variant->open_mode);
+       ASSERT_LE(0, file_fd);
+
+       /* Checks that IOCTL commands return the expected errors. */
+       EXPECT_EQ(variant->expected_fionread_result,
+                 test_fionread_ioctl(file_fd));
+
+       /* Checks that unrestrictable commands are unrestricted. */
+       EXPECT_EQ(0, ioctl(file_fd, FIOCLEX));
+       EXPECT_EQ(0, ioctl(file_fd, FIONCLEX));
+       EXPECT_EQ(0, ioctl(file_fd, FIONBIO, &flag));
+       EXPECT_EQ(0, ioctl(file_fd, FIOASYNC, &flag));
+       EXPECT_EQ(0, ioctl(file_fd, FIGETBSZ, &flag));
+
+       ASSERT_EQ(0, close(file_fd));
+}
+
+TEST_F_FORK(ioctl, handle_dir_access_dir)
+{
+       const int flag = 0;
+       const struct rule rules[] = {
+               {
+                       .path = "/dev",
+                       .access = variant->allowed,
+               },
+               {},
+       };
+       int dir_fd, ruleset_fd;
+
+       /* Enables Landlock. */
+       ruleset_fd = create_ruleset(_metadata, variant->handled, rules);
+       ASSERT_LE(0, ruleset_fd);
+       enforce_ruleset(_metadata, ruleset_fd);
+       ASSERT_EQ(0, close(ruleset_fd));
+
+       /*
+        * Ignore variant->open_mode for this test, as we intend to open a
+        * directory.  If the directory can not be opened, the variant is
+        * infeasible to test with an opened directory.
+        */
+       dir_fd = open("/dev", O_RDONLY);
+       if (dir_fd < 0)
+               return;
+
+       /*
+        * Checks that IOCTL commands return the expected errors.
+        * We do not use the expected values from the fixture here.
+        *
+        * When using IOCTL on a directory, no Landlock restrictions apply.
+        */
+       EXPECT_EQ(0, test_fionread_ioctl(dir_fd));
+
+       /* Checks that unrestrictable commands are unrestricted. */
+       EXPECT_EQ(0, ioctl(dir_fd, FIOCLEX));
+       EXPECT_EQ(0, ioctl(dir_fd, FIONCLEX));
+       EXPECT_EQ(0, ioctl(dir_fd, FIONBIO, &flag));
+       EXPECT_EQ(0, ioctl(dir_fd, FIOASYNC, &flag));
+       EXPECT_EQ(0, ioctl(dir_fd, FIGETBSZ, &flag));
+
+       ASSERT_EQ(0, close(dir_fd));
+}
+
+TEST_F_FORK(ioctl, handle_file_access_file)
+{
+       const int flag = 0;
+       const struct rule rules[] = {
+               {
+                       .path = "/dev/zero",
+                       .access = variant->allowed,
+               },
+               {},
+       };
+       int file_fd, ruleset_fd;
+
+       /* Enables Landlock. */
+       ruleset_fd = create_ruleset(_metadata, variant->handled, rules);
+       ASSERT_LE(0, ruleset_fd);
+       enforce_ruleset(_metadata, ruleset_fd);
+       ASSERT_EQ(0, close(ruleset_fd));
+
+       file_fd = open("/dev/zero", variant->open_mode);
+       ASSERT_LE(0, file_fd)
+       {
+               TH_LOG("Failed to open /dev/zero: %s", strerror(errno));
+       }
+
+       /* Checks that IOCTL commands return the expected errors. */
+       EXPECT_EQ(variant->expected_fionread_result,
+                 test_fionread_ioctl(file_fd));
+
+       /* Checks that unrestrictable commands are unrestricted. */
+       EXPECT_EQ(0, ioctl(file_fd, FIOCLEX));
+       EXPECT_EQ(0, ioctl(file_fd, FIONCLEX));
+       EXPECT_EQ(0, ioctl(file_fd, FIONBIO, &flag));
+       EXPECT_EQ(0, ioctl(file_fd, FIOASYNC, &flag));
+       EXPECT_EQ(0, ioctl(file_fd, FIGETBSZ, &flag));
+
+       ASSERT_EQ(0, close(file_fd));
+}
+
 /* clang-format off */
 FIXTURE(layout1_bind) {};
 /* clang-format on */