Re: [PATCH v6 0/6] landlock: Restrict whiteout object creation
Mickaël Salaün <[email protected]>
| Newsgroups | gmane.linux.kernel.lsm |
|---|---|
| Message-ID | <[email protected]> |
Thanks! It's merged (with some extra explanation in commit message). On Thu, Aug 13, 2026 at 11:31:51AM +0200, Günther Noack wrote: > Hello! > > As discussed in [1], the renameat2() syscall's RENAME_WHITEOUT flag allows > the creation of chardev directory entries with major=minor=0 as "whiteout > objects" in the location of the rename source file [2]. > > This functionality is available even without having any OverlayFS mounted > and can be invoked with the regular renameat2(2) syscall [3]. > > In V1 [5], it was discussed that whiteout objects are not the same as character > devices, and should therefore be treated differently. After considering a new > access right in V2 and V3, we eventually settled on guarding it with the > existing LANDLOCK_ACCESS_FS_MAKE_REG access right and introducing a new erratum > [6]. > > V6 addresses the remaining review feedback: the V4 feedback on the selftests > in patches 3/5 and 4/5, and the V5 remarks on the get_dentry_access() helper. > > Motivation > ========== > > The RENAME_WHITEOUT flag side-steps all of the existing Landlock access > rights, which are designed to restrict the creation of directory entries. > It is desirable to restrict that. > > This patch set fixes that by adding a check in Landlock's path_rename and > path_mknod hooks. Where we previously treated whiteout creation like the > creation of a character device, we now guard it with > LANDLOCK_ACCESS_FS_MAKE_REG and add an erratum to probe for that. > > > [1] https://lore.kernel.org/all/[email protected]/ > [2] https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories > [3] https://man7.org/linux/man-pages/man2/renameat2.2.html#DESCRIPTION > [4] https://codesearch.debian.net/search?q=rename.*RENAME_WHITEOUT&literal=0 > [5] https://lore.kernel.org/all/[email protected]/ > [6] https://lore.kernel.org/all/[email protected]/ > > Changelog > ========= > > v6: > - Smaller code changes: > - get_dentry_access(): Drop the incorrect __attribute_const__ and use a > local inode variable > - Fix a LANDLOCK_ACCESS_FS_MAKE_REG typo in the commit message of patch 2/5 > - Tests, addressing the remaining v4 feedback on patch 3/5: > - Add rename_whiteout_allowed: RENAME_WHITEOUT works when MAKE_REG is > granted, and the whiteout object shows up in the source location > - Add rename_whiteout_reparenting: the MAKE_REG right for the created > whiteout object is required in the source directory of the rename > - Add rename_whiteout_exchange: RENAME_EXCHANGE of an existing whiteout > object requires MAKE_REG in the directory which the whiteout moves into > - Add audit test make_whiteout: denied whiteout creation is logged with > blockers=fs.make_reg instead of fs.make_char > - Tests, addressing the v4 feedback on patch 4/5: > - Rework the OverlayFS rename test to rename a FIFO which originates > from the lower layer, instead of preparing it in the upper layer with > unlink() and mknod(). The unlink() had already created the whiteout > in the upper layer before Landlock was enforced > - Check that the whiteout object in the upper layer did not exist before > the rename > - Verified that the new tests fail on a kernel which lacks the fix from > patch 2/5 > > v5: > - Erratum: Clarify that this is about mknod(2) only > - Add Cc [email protected] and Depends-on lines > - Smaller code changes > - Use dev_t where appropriate > - Introduce get_dentry_access() helper > - Use get_mode_access(S_IFCHR | WHITEOUT_MODE, WHITEOUT_DEV) > to calculate the required access right for the created whiteout object > - Tests: > - rename_whiteout_denied: move a FIFO > - otherwise, the test succeeded without the change > - make_whiteout: drop set_cap(CAP_MKNOD) (not needed) > - This is an intermediary state: It incorporates feedback to 2/5 and some 3/5 > feedback from v4, but not the 4/5 feedback yet. > - https://lore.kernel.org/all/[email protected]/ > > v4: > - Guard it with LANDLOCK_ACCESS_FS_MAKE_REG as discussed. > - Selftests and documentation. > - https://lore.kernel.org/all/[email protected]/ > > v3: > - Do LANDLOCK_ACCESS_FS_MAKE_WHITEOUT check as part of > current_check_refer_path(). > - https://lore.kernel.org/all/[email protected]/ > > v2: > - Introduce LANDLOCK_ACCESS_FS_MAKE_WHITEOUT access right > and guard it with that. > - Bump ABI version > - https://lore.kernel.org/all/[email protected]/ > > v1: > - initial version > https://lore.kernel.org/all/[email protected]/ > > > Günther Noack (6): > selftests/landlock: Use an actual chardev for MAKE_CHAR audit test > landlock: Require LANDLOCK_ACCESS_FS_MAKE_REG for whiteout creation > selftests/landlock: Add tests for whiteout object creation > selftests/landlock: Add audit test for whiteout object creation > selftests/landlock: Test whiteout object behaviour in OverlayFS > renames > landlock: Link the erratum documentation for whiteout objects > > Documentation/userspace-api/landlock.rst | 3 + > include/uapi/linux/landlock.h | 1 + > security/landlock/errata/abi-1.h | 23 ++ > security/landlock/fs.c | 41 +++- > tools/testing/selftests/landlock/fs_test.c | 253 ++++++++++++++++++++- > 5 files changed, 309 insertions(+), 12 deletions(-) > > Range-diff against v5: No need for such range-diff. > 1: d1a8a84d92e0 = 1: 49a6fa0831e8 selftests/landlock: Use an actual chardev for MAKE_CHAR audit test > 2: 9fe2edf17413 ! 2: a29c7919a412 landlock: Require LANDLOCK_ACCESS_FS_MAKE_REG for whiteout creation > @@ Commit message > For the mknod(2) case, introduce a Landlock erratum. The creation of > whiteout objects through mknod(2) was previously guarded using > LANDLOCK_ACCESS_FS_MAKE_CHAR, and it is now guarded using > - LANDLOCK_ACCESS_MAKE_REG. > + LANDLOCK_ACCESS_FS_MAKE_REG. > > For the renameat2(2) case, fix a bug: Before this commit, renameat2(2) > with RENAME_WHITEOUT would create a directory entry even when all > @@ security/landlock/fs.c: static __attribute_const__ access_mask_t get_mode_access > } > } > > -+static __attribute_const__ access_mask_t > -+get_dentry_access(const struct dentry *const dentry) > ++static access_mask_t get_dentry_access(const struct dentry *const dentry) > +{ > -+ return get_mode_access(d_backing_inode(dentry)->i_mode, > -+ d_backing_inode(dentry)->i_rdev); > ++ const struct inode *const inode = d_backing_inode(dentry); > ++ > ++ return get_mode_access(inode->i_mode, inode->i_rdev); > +} > + > static access_mask_t maybe_remove(const struct dentry *const dentry) > 3: d23ea62b3325 ! 3: 0711d9e709fb selftests/landlock: Add tests for whiteout object creation > @@ tools/testing/selftests/landlock/fs_test.c: TEST_F_FORK(layout1, rename_file) > + TMP_DIR "/s3d1/s3d2/s3d3/f2", RENAME_WHITEOUT)); > + EXPECT_EQ(EACCES, errno); > +} > ++ > ++static bool is_whiteout(const char *const path) > ++{ > ++ struct stat st; > ++ > ++ if (stat(path, &st) == -1) > ++ return false; > ++ > ++ return S_ISCHR(st.st_mode) && st.st_rdev == makedev(0, 0); > ++} > ++ > ++static bool is_fifo(const char *const path) > ++{ > ++ struct stat st; > ++ > ++ return stat(path, &st) == 0 && S_ISFIFO(st.st_mode); > ++} > ++ > ++TEST_F_FORK(layout1, rename_whiteout_allowed) > ++{ > ++ const struct rule rules[] = { > ++ { > ++ .path = dir_s3d3, > ++ .access = LANDLOCK_ACCESS_FS_MAKE_REG, > ++ }, > ++ {}, > ++ }; > ++ > ++ /* The affected file is a FIFO. */ > ++ ASSERT_EQ(0, unlink(file1_s3d3)); > ++ ASSERT_EQ(0, mknod(file1_s3d3, S_IFIFO | 0600, 0)); > ++ > ++ /* Allow MAKE_REG below dir_s3d3. */ > ++ enforce_fs(_metadata, LANDLOCK_ACCESS_FS_MAKE_REG, rules); > ++ > ++ /* > ++ * Rename a file with RENAME_WHITEOUT within the same directory. > ++ * Allowed, because MAKE_REG is granted for the whiteout object which > ++ * gets created in the source location. > ++ */ > ++ EXPECT_EQ(0, renameat2(AT_FDCWD, file1_s3d3, AT_FDCWD, > ++ TMP_DIR "/s3d1/s3d2/s3d3/f2", RENAME_WHITEOUT)); > ++ > ++ /* A whiteout object took the place of the moved FIFO. */ > ++ EXPECT_TRUE(is_whiteout(file1_s3d3)); > ++ EXPECT_TRUE(is_fifo(TMP_DIR "/s3d1/s3d2/s3d3/f2")); > ++} > ++ > ++TEST_F_FORK(layout1, rename_whiteout_reparenting) > ++{ > ++ const struct rule rules[] = { > ++ { > ++ .path = dir_s3d2, > ++ .access = LANDLOCK_ACCESS_FS_REFER, > ++ }, > ++ { > ++ .path = dir_s3d3, > ++ .access = LANDLOCK_ACCESS_FS_MAKE_REG, > ++ }, > ++ {}, > ++ }; > ++ > ++ /* The moved files are FIFOs. */ > ++ ASSERT_EQ(0, unlink(file1_s3d3)); > ++ ASSERT_EQ(0, mknod(file1_s3d3, S_IFIFO | 0600, 0)); > ++ ASSERT_EQ(0, unlink(file1_s3d4)); > ++ ASSERT_EQ(0, mknod(file1_s3d4, S_IFIFO | 0600, 0)); > ++ > ++ /* Allow REFER below dir_s3d2, but MAKE_REG only below dir_s3d3. */ > ++ enforce_fs(_metadata, > ++ LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER, > ++ rules); > ++ > ++ /* > ++ * The whiteout object is created in the source directory: Moving the > ++ * FIFO out of dir_s3d4 is denied because MAKE_REG is not granted > ++ * there, even though it is granted in the destination directory > ++ * dir_s3d3. > ++ */ > ++ EXPECT_EQ(-1, renameat2(AT_FDCWD, file1_s3d4, AT_FDCWD, > ++ TMP_DIR "/s3d1/s3d2/s3d3/f2", RENAME_WHITEOUT)); > ++ EXPECT_EQ(EACCES, errno); > ++ > ++ /* > ++ * Moving the FIFO out of dir_s3d3 is allowed, because MAKE_REG is > ++ * granted there for the created whiteout object. > ++ */ > ++ EXPECT_EQ(0, renameat2(AT_FDCWD, file1_s3d3, AT_FDCWD, > ++ TMP_DIR "/s3d1/s3d2/s3d4/f2", RENAME_WHITEOUT)); > ++ > ++ /* A whiteout object took the place of the moved FIFO. */ > ++ EXPECT_TRUE(is_whiteout(file1_s3d3)); > ++ EXPECT_TRUE(is_fifo(TMP_DIR "/s3d1/s3d2/s3d4/f2")); > ++} > ++ > ++TEST_F_FORK(layout1, rename_whiteout_exchange) > ++{ > ++ const char *const whiteout_s3d3 = TMP_DIR "/s3d1/s3d2/s3d3/f2"; > ++ const struct rule rules[] = { > ++ { > ++ .path = dir_s3d2, > ++ .access = LANDLOCK_ACCESS_FS_REFER, > ++ }, > ++ { > ++ .path = dir_s3d3, > ++ .access = LANDLOCK_ACCESS_FS_MAKE_REG, > ++ }, > ++ {}, > ++ }; > ++ > ++ /* The exchanged files are FIFOs and an existing whiteout object. */ > ++ ASSERT_EQ(0, unlink(file1_s3d3)); > ++ ASSERT_EQ(0, mknod(file1_s3d3, S_IFIFO | 0600, 0)); > ++ ASSERT_EQ(0, mknod(whiteout_s3d3, S_IFCHR | 0600, makedev(0, 0))); > ++ ASSERT_EQ(0, unlink(file1_s3d4)); > ++ ASSERT_EQ(0, mknod(file1_s3d4, S_IFIFO | 0600, 0)); > ++ > ++ /* Allow REFER below dir_s3d2, but MAKE_REG only below dir_s3d3. */ > ++ enforce_fs(_metadata, > ++ LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER, > ++ rules); > ++ > ++ /* > ++ * With RENAME_EXCHANGE, the whiteout object moves into the source > ++ * directory of the rename: Exchanging the FIFO in dir_s3d4 with the > ++ * whiteout object is denied because MAKE_REG is not granted in > ++ * dir_s3d4, even though it is granted in the whiteout object's own > ++ * directory dir_s3d3. > ++ */ > ++ EXPECT_EQ(-1, renameat2(AT_FDCWD, file1_s3d4, AT_FDCWD, whiteout_s3d3, > ++ RENAME_EXCHANGE)); > ++ EXPECT_EQ(EACCES, errno); > ++ > ++ /* > ++ * Exchanging the FIFO in dir_s3d3 with the whiteout object is > ++ * allowed, because MAKE_REG is granted in the directory into which > ++ * the whiteout object moves. > ++ */ > ++ EXPECT_EQ(0, renameat2(AT_FDCWD, file1_s3d3, AT_FDCWD, whiteout_s3d3, > ++ RENAME_EXCHANGE)); > ++ > ++ /* The FIFO and the whiteout object swapped places. */ > ++ EXPECT_TRUE(is_whiteout(file1_s3d3)); > ++ EXPECT_TRUE(is_fifo(whiteout_s3d3)); > ++} > + > TEST_F_FORK(layout1, rename_dir) > { > @@ tools/testing/selftests/landlock/fs_test.c: TEST_F_FORK(layout1, make_char) > > +TEST_F_FORK(layout1, make_whiteout) > +{ > -+ /* Creates a whiteout object (creation guarded by MAKE_REG). */ > ++ /* > ++ * Creates a whiteout object (creation guarded by MAKE_REG). > ++ * > ++ * Contrary to the other character devices, this does not require > ++ * CAP_MKNOD, cf. vfs_mknod(). > ++ */ > + test_make_file(_metadata, LANDLOCK_ACCESS_FS_MAKE_REG, S_IFCHR, > + makedev(0, 0)); > +} > -: ------------ > 4: f61b6ddb635e selftests/landlock: Add audit test for whiteout object creation > 4: 10ed1d74636d ! 5: 4862aead03f2 selftests/landlock: Test whiteout object behaviour in OverlayFS renames > @@ Metadata > ## Commit message ## > selftests/landlock: Test whiteout object behaviour in OverlayFS renames > > - Even though OverlayFS uses vfs_rename() with RENAME_WHITEOUT, and even > - though RENAME_WHITEOUT requires LANDLOCK_ACCESS_FS_MAKE_REG, a process > - that renames non-regular files in an OverlayFS can do so without > - having the LANDLOCK_ACCESS_FS_MAKE_REG right in that location. > + Even though OverlayFS uses vfs_rename() with RENAME_WHITEOUT on its backing > + directories, and even though RENAME_WHITEOUT requires > + LANDLOCK_ACCESS_FS_MAKE_REG, a process that renames non-regular files in an > + OverlayFS can do so without having the LANDLOCK_ACCESS_FS_MAKE_REG right in > + that location. > > - This works, and is supposed to work, because OverlayFS uses the > - credentials determined at mount time for the internal vfs_rename() > - operation. The rename happens with the credentials of the user who > - mounted the OverlayFS. > + This works, and is supposed to work, because the changes to the backing > + directories are done by OverlayFS, not by the originator task that did the > + original rename() on the OverlayFS mount. Therefore, the changes done to > + backing directories are not subject to the originator task's credentials. > > Signed-off-by: Günther Noack <[email protected]> > > ## tools/testing/selftests/landlock/fs_test.c ## > +@@ tools/testing/selftests/landlock/fs_test.c: static bool is_fifo(const char *const path) > + return stat(path, &st) == 0 && S_ISFIFO(st.st_mode); > + } > + > ++static bool is_missing(const char *const path) > ++{ > ++ struct stat st; > ++ > ++ return stat(path, &st) == -1 && errno == ENOENT; > ++} > ++ > + TEST_F_FORK(layout1, rename_whiteout_allowed) > + { > + const struct rule rules[] = { > +@@ tools/testing/selftests/landlock/fs_test.c: static const char lower_fo1[] = LOWER_DATA "/fo1"; > + static const char lower_do1[] = LOWER_DATA "/do1"; > + static const char lower_do1_fo2[] = LOWER_DATA "/do1/fo2"; > + static const char lower_do1_fl3[] = LOWER_DATA "/do1/fl3"; > ++/* lower_pl1 is a FIFO and is deliberately not in the lists below. */ > ++static const char lower_pl1[] = LOWER_DATA "/pl1"; > + > + static const char (*lower_base_files[])[] = { > + &lower_fl1, > +@@ tools/testing/selftests/landlock/fs_test.c: static const char (*upper_sub_files[])[] = { > + #define MERGE_BASE TMP_DIR "/merge" > + #define MERGE_DATA MERGE_BASE "/data" > + static const char merge_fl1[] = MERGE_DATA "/fl1"; > ++/* merge_pl1 is a FIFO and is deliberately not in the lists below. */ > ++static const char merge_pl1[] = MERGE_DATA "/pl1"; > + static const char merge_dl1[] = MERGE_DATA "/dl1"; > + static const char merge_dl1_fl2[] = MERGE_DATA "/dl1/fl2"; > + static const char merge_fu1[] = MERGE_DATA "/fu1"; > +@@ tools/testing/selftests/landlock/fs_test.c: static const char (*merge_sub_files[])[] = { > + * │ │ ├── fl3 > + * │ │ └── fo2 > + * │ ├── fl1 > +- * │ └── fo1 > ++ * │ ├── fo1 > ++ * │ └── pl1 [FIFO] > + * ├── merge > + * │ └── data > + * │ ├── dl1 > +@@ tools/testing/selftests/landlock/fs_test.c: static const char (*merge_sub_files[])[] = { > + * │ │ └── fu2 > + * │ ├── fl1 > + * │ ├── fo1 > +- * │ └── fu1 > ++ * │ ├── fu1 > ++ * │ └── pl1 [FIFO] > + * └── upper > + * ├── data > + * │ ├── do1 > +@@ tools/testing/selftests/landlock/fs_test.c: FIXTURE_SETUP(layout2_overlay) > + create_file(_metadata, lower_fo1); > + create_file(_metadata, lower_do1_fo2); > + create_file(_metadata, lower_do1_fl3); > ++ ASSERT_EQ(0, mknod(lower_pl1, S_IFIFO | 0600, 0)); > + > + create_directory(_metadata, UPPER_BASE); > + set_cap(_metadata, CAP_SYS_ADMIN); > +@@ tools/testing/selftests/landlock/fs_test.c: FIXTURE_TEARDOWN_PARENT(layout2_overlay) > + EXPECT_EQ(0, remove_path(lower_fl1)); > + EXPECT_EQ(0, remove_path(lower_do1_fo2)); > + EXPECT_EQ(0, remove_path(lower_fo1)); > ++ EXPECT_EQ(0, remove_path(lower_pl1)); > + > + /* umount(LOWER_BASE)) is handled by namespace lifetime. */ > + EXPECT_EQ(0, remove_path(LOWER_BASE)); > @@ tools/testing/selftests/landlock/fs_test.c: TEST_F_FORK(layout2_overlay, same_content_different_file) > } > } > > +TEST_F_FORK(layout2_overlay, rename_in_overlay_without_make_reg) > +{ > -+ struct stat st; > -+ const char *merge_fl1_renamed = MERGE_DATA "/fl1_renamed"; > ++ const char *const merge_pl1_renamed = MERGE_DATA "/pl1_renamed"; > + > + if (self->skip_test) > + SKIP(return, "overlayfs is not supported (test)"); > + > + /* > -+ * In this test, merge_fl1 is a FIFO file. MAKE_REG is restricted, but > -+ * MAKE_FIFO is allowed. Despite MAKE_REG being restricted, the rename > -+ * on the OverlayFS works and creates a whiteout file in the underlying > -+ * upper file system. > ++ * merge_pl1 is a FIFO which only exists in the lower layer. Before > ++ * the rename, the upper layer has no entry under this name. > + */ > -+ ASSERT_EQ(0, unlink(merge_fl1)); > -+ ASSERT_EQ(0, mknod(merge_fl1, S_IFIFO, 0)); > ++ ASSERT_TRUE(is_fifo(merge_pl1)); > ++ ASSERT_TRUE(is_missing(UPPER_DATA "/pl1")); > ++ > ++ /* MAKE_REG is restricted, but MAKE_FIFO is not. */ > + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_MAKE_REG, NULL); > + > + /* > -+ * Execute a regular file rename within OverlayFS. > -+ * merge_fl1 originates from lower layer, so this triggers a copy-up > -+ * and creation of a whiteout in the upper layer. > ++ * Rename the FIFO through OverlayFS. merge_pl1 originates from the > ++ * lower layer, so this triggers a copy-up and creates the whiteout in > ++ * the upper layer to hide the lower layer FIFO file. Even though > ++ * MAKE_REG is restricted, the rename on the OverlayFS works. > + */ > -+ EXPECT_EQ(0, rename(merge_fl1, merge_fl1_renamed)); > ++ EXPECT_EQ(0, rename(merge_pl1, merge_pl1_renamed)); > + > + /* Check that the rename worked. */ > -+ EXPECT_EQ(0, stat(merge_fl1_renamed, &st)); > -+ EXPECT_EQ(-1, stat(merge_fl1, &st)); > -+ EXPECT_EQ(ENOENT, errno); > ++ EXPECT_TRUE(is_fifo(merge_pl1_renamed)); > ++ EXPECT_TRUE(is_missing(merge_pl1)); > + > + /* > -+ * Check that the whiteout object on the underlying "upper" filesystem > -+ * exists after the rename. This is OK because it was done with the > -+ * credentials of the OverlayFS. > ++ * Check that the whiteout object was created on the underlying "upper" > ++ * filesystem during the rename. This is OK because the whiteout object > ++ * was created by OverlayFS, not by the calling task. > + */ > -+ EXPECT_EQ(0, stat(UPPER_DATA "/fl1", &st)); > -+ EXPECT_TRUE(S_ISCHR(st.st_mode)); > -+ EXPECT_EQ(0, st.st_rdev); > ++ EXPECT_TRUE(is_whiteout(UPPER_DATA "/pl1")); > +} > + > FIXTURE(layout3_fs) > 5: 35651219395c = 6: 4a53acb99752 landlock: Link the erratum documentation for whiteout objects > -- > 2.55.0.699.gb54405d56f-goog > >