selftests/landlock: Add tests for whiteout object creation

Add tests to check that whiteout object creation is guarded by
LANDLOCK_ACCESS_FS_MAKE_REG, in the cases where these are created from
userspace:

* Conventional creation with mknod()
* Linking or renaming an existing whiteout object
* renameat2() with RENAME_WHITEOUT,
  which creates a new whiteout object in the source location
* renameat2() with RENAME_EXCHANGE,
  with one of the renamed objects being a whiteout object

Signed-off-by: Günther Noack <gnoack@google.com>
Link: https://patch.msgid.link/20260813093157.1436894-4-gnoack@google.com
[mic: Update commit message as requested]
Signed-off-by: Mickaël Salaün <mic@digikod.net>
This commit is contained in:
Günther Noack
2026-08-13 11:31:54 +02:00
committed by Mickaël Salaün
parent 672fa082d4
commit ee890889b3

View File

@@ -2247,6 +2247,170 @@ TEST_F_FORK(layout1, rename_file)
RENAME_EXCHANGE));
}
TEST_F_FORK(layout1, rename_whiteout_denied)
{
/* The affected file is a FIFO. */
ASSERT_EQ(0, unlink(file1_s3d3));
ASSERT_EQ(0, mknod(file1_s3d3, S_IFIFO | 0600, 0));
/* Deny MAKE_REG, but allow MAKE_FIFO. */
enforce_fs(_metadata, LANDLOCK_ACCESS_FS_MAKE_REG, NULL);
/*
* Try to rename a file with RENAME_WHITEOUT.
* file1_s3d3 is in dir_s3d2 (tmpfs), so it supports RENAME_WHITEOUT.
* Denied, because whiteout creation is guarded with MAKE_REG.
*/
EXPECT_EQ(-1, renameat2(AT_FDCWD, file1_s3d3, AT_FDCWD,
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)
{
const struct rule rules[] = {
@@ -3270,6 +3434,18 @@ TEST_F_FORK(layout1, make_char)
makedev(1, 3));
}
TEST_F_FORK(layout1, make_whiteout)
{
/*
* 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));
}
TEST_F_FORK(layout1, make_block)
{
/* Creates a /dev/loop0 device. */