[PATCH 5.15.y 1/2] landlock: Require LANDLOCK_ACCESS_FS_MAKE_REG for whiteout creation

From: Mickaël Salaün

Date: Thu Sep 17 2026 - 11:29:24 EST


From: Günther Noack <gnoack@xxxxxxxxxx>

[ Upstream commit 672fa082d48b21e1fb62cdb184fee41513e53421 ]

Whiteout objects are used in the upper layer of an OverlayFS to
indicate that the file with this name does not exist in the unified
view, even if it is present in one of the lower layer file systems.

For the userspace implementations of OverlayFS (fuse-overlayfs),
whiteout objects can be created from userspace as well:

* mknod(2) with S_IFCHR and makedev(0, 0)
* renameat2(2) with RENAME_WHITEOUT,
creating the whiteout in the old place of the moved file.

This commit guards whiteout creation in both of these cases with
LANDLOCK_ACCESS_FS_MAKE_REG. Whiteout objects are *not* considered
character devices and are not bound to a driver.

LANDLOCK_ACCESS_FS_MAKE_REG describes the same permission class as a
whiteout object: creating one is the only S_IFCHR creation that the VFS
exempts from CAP_MKNOD, so it is as unprivileged as creating a regular
file, while LANDLOCK_ACCESS_FS_MAKE_CHAR and
LANDLOCK_ACCESS_FS_MAKE_BLOCK keep meaning the creation of devices that
expose a kernel interface [1].

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_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
LANDLOCK_ACCESS_FS_MAKE_* rights were denied.

This does not affect normal renames within layered OverlayFS mounts:
When doing a regular rename() on a mounted fuse-overlayfs, it is the
fuse-overlayfs daemon that exercises renameat2() with RENAME_WHITEOUT,
and only the Landlock domain of that daemon is checked there.

Suggested-by: Christian Brauner <brauner@xxxxxxxxxx>
Suggested-by: Mickaël Salaün <mic@xxxxxxxxxxx>
Cc: stable@xxxxxxxxxxxxxxx
Fixes: cb2c7d1a1776 ("landlock: Support filesystem access-control")
Signed-off-by: Günther Noack <gnoack@xxxxxxxxxx>
Link: https://patch.msgid.link/20260720.chow9ohYie5b@xxxxxxxxxxx [1]
Link: https://patch.msgid.link/20260813093157.1436894-3-gnoack@xxxxxxxxxx
[mic: Record why LANDLOCK_ACCESS_FS_MAKE_REG is the matching right, and
add link(2) to the user doc]
Signed-off-by: Mickaël Salaün <mic@xxxxxxxxxxx>
[mic: Backport: partially backport 100f59d96405 ("LSM: Remove double
path_rename hook calls for RENAME_EXCHANGE") to propagate rename flags,
adapt to ABI 1, and omit the unavailable REFER and context-only
documentation dependencies]
Cc: James Morris <jmorris@xxxxxxxxx>
Cc: Kentaro Takeda <takedakn@xxxxxxxxxxxxx>
Cc: Serge E. Hallyn <serge@xxxxxxxxxx>
Cc: John Johansen <john.johansen@xxxxxxxxxxxxx>
Cc: Tetsuo Handa <penguin-kernel@xxxxxxxxxxxxxxxxxxx>
Cc: Paul Moore <paul@xxxxxxxxxxxxxx>
Link: https://lore.kernel.org/r/20220506161102.525323-7-mic@xxxxxxxxxxx
Signed-off-by: Mickaël Salaün <mic@xxxxxxxxxxx>
---
include/linux/lsm_hook_defs.h | 2 +-
include/linux/lsm_hooks.h | 1 +
include/uapi/linux/landlock.h | 1 +
security/apparmor/lsm.c | 7 ++++--
security/landlock/errata/abi-1.h | 24 +++++++++++++++++++
security/landlock/fs.c | 40 ++++++++++++++++++++++++--------
security/security.c | 4 ++--
security/tomoyo/tomoyo.c | 4 +++-
8 files changed, 67 insertions(+), 16 deletions(-)
create mode 100644 security/landlock/errata/abi-1.h

diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h
index 2ef89b872716..da79e7ef1ca6 100644
--- a/include/linux/lsm_hook_defs.h
+++ b/include/linux/lsm_hook_defs.h
@@ -101,7 +101,7 @@ LSM_HOOK(int, 0, path_link, struct dentry *old_dentry,
const struct path *new_dir, struct dentry *new_dentry)
LSM_HOOK(int, 0, path_rename, const struct path *old_dir,
struct dentry *old_dentry, const struct path *new_dir,
- struct dentry *new_dentry)
+ struct dentry *new_dentry, unsigned int flags)
LSM_HOOK(int, 0, path_chmod, const struct path *path, umode_t mode)
LSM_HOOK(int, 0, path_chown, const struct path *path, kuid_t uid, kgid_t gid)
LSM_HOOK(int, 0, path_chroot, const struct path *path)
diff --git a/include/linux/lsm_hooks.h b/include/linux/lsm_hooks.h
index 9ccfc8fbbe0b..8eec3bdaee32 100644
--- a/include/linux/lsm_hooks.h
+++ b/include/linux/lsm_hooks.h
@@ -357,6 +357,7 @@
* @old_dentry contains the dentry structure of the old link.
* @new_dir contains the path structure for parent of the new link.
* @new_dentry contains the dentry structure of the new link.
+ * @flags may contain rename options such as RENAME_EXCHANGE.
* Return 0 if permission is granted.
* @path_chmod:
* Check for permission to change a mode of the file @path. The new
diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index d00621d4d52d..0fb097510fcf 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h
@@ -109,6 +109,7 @@ struct landlock_path_beneath_attr {
* device.
* - %LANDLOCK_ACCESS_FS_MAKE_DIR: Create (or rename) a directory.
* - %LANDLOCK_ACCESS_FS_MAKE_REG: Create (or rename or link) a regular file.
+ * This also guards the creation of whiteout objects as used in OverlayFS.
* - %LANDLOCK_ACCESS_FS_MAKE_SOCK: Create (or rename or link) a UNIX domain
* socket.
* - %LANDLOCK_ACCESS_FS_MAKE_FIFO: Create (or rename or link) a named pipe.
diff --git a/security/apparmor/lsm.c b/security/apparmor/lsm.c
index afa19da69436..f63db47d57e7 100644
--- a/security/apparmor/lsm.c
+++ b/security/apparmor/lsm.c
@@ -353,8 +353,11 @@ static int apparmor_path_link(struct dentry *old_dentry, const struct path *new_
return error;
}

-static int apparmor_path_rename(const struct path *old_dir, struct dentry *old_dentry,
- const struct path *new_dir, struct dentry *new_dentry)
+static int apparmor_path_rename(const struct path *old_dir,
+ struct dentry *old_dentry,
+ const struct path *new_dir,
+ struct dentry *new_dentry,
+ const unsigned int flags)
{
struct aa_label *label;
int error = 0;
diff --git a/security/landlock/errata/abi-1.h b/security/landlock/errata/abi-1.h
new file mode 100644
index 000000000000..53c96c2057fa
--- /dev/null
+++ b/security/landlock/errata/abi-1.h
@@ -0,0 +1,24 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+
+/**
+ * DOC: erratum_4
+ *
+ * Erratum 4: Creation of whiteout objects
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * This fix changes the access rights required for the creation of whiteout
+ * objects through :manpage:`mknod(2)`, :manpage:`renameat2(2)`, or
+ * :manpage:`link(2)`. Creating whiteout objects is now guarded by
+ * ``LANDLOCK_ACCESS_FS_MAKE_REG`` instead of ``LANDLOCK_ACCESS_FS_MAKE_CHAR``.
+ *
+ * Whiteout objects are used in OverlayFS to mark the absence of a file in an
+ * upper file system. Despite being created with ``S_IFCHR``, whiteout objects
+ * do not count as character devices.
+ *
+ * Impact:
+ *
+ * Sandboxed programs that create OverlayFS whiteouts (such as fuse-overlayfs)
+ * now require ``LANDLOCK_ACCESS_FS_MAKE_REG`` instead of
+ * ``LANDLOCK_ACCESS_FS_MAKE_CHAR``.
+ */
+LANDLOCK_ERRATUM(4)
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index a3d99bba5f1e..51d095f07890 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -14,6 +14,7 @@
#include <linux/err.h>
#include <linux/fs.h>
#include <linux/init.h>
+#include <linux/kdev_t.h>
#include <linux/kernel.h>
#include <linux/limits.h>
#include <linux/list.h>
@@ -365,7 +366,7 @@ static inline int current_check_access_path(const struct path *const path,
return check_access_path(dom, path, access_request);
}

-static inline access_mask_t get_mode_access(const umode_t mode)
+static inline access_mask_t get_mode_access(const umode_t mode, const dev_t dev)
{
switch (mode & S_IFMT) {
case S_IFLNK:
@@ -373,6 +374,9 @@ static inline access_mask_t get_mode_access(const umode_t mode)
case S_IFDIR:
return LANDLOCK_ACCESS_FS_MAKE_DIR;
case S_IFCHR:
+ /* Whiteout objects are guarded with MAKE_REG. */
+ if (dev == WHITEOUT_DEV)
+ return LANDLOCK_ACCESS_FS_MAKE_REG;
return LANDLOCK_ACCESS_FS_MAKE_CHAR;
case S_IFBLK:
return LANDLOCK_ACCESS_FS_MAKE_BLOCK;
@@ -389,6 +393,13 @@ static inline access_mask_t get_mode_access(const umode_t mode)
}
}

+static inline access_mask_t get_dentry_access(const struct dentry *const dentry)
+{
+ const struct inode *const inode = d_backing_inode(dentry);
+
+ return get_mode_access(inode->i_mode, inode->i_rdev);
+}
+
static inline access_mask_t maybe_remove(const struct dentry *const dentry)
{
if (d_is_negative(dentry))
@@ -613,18 +624,18 @@ static int hook_path_link(struct dentry *const old_dentry,
return -EXDEV;
if (unlikely(d_is_negative(old_dentry)))
return -ENOENT;
- return check_access_path(
- dom, new_dir,
- get_mode_access(d_backing_inode(old_dentry)->i_mode));
+ return check_access_path(dom, new_dir, get_dentry_access(old_dentry));
}

static int hook_path_rename(const struct path *const old_dir,
struct dentry *const old_dentry,
const struct path *const new_dir,
- struct dentry *const new_dentry)
+ struct dentry *const new_dentry,
+ const unsigned int flags)
{
const struct landlock_ruleset *const dom =
landlock_get_current_domain();
+ access_mask_t access_request;

if (!dom)
return 0;
@@ -635,10 +646,18 @@ static int hook_path_rename(const struct path *const old_dir,
if (unlikely(d_is_negative(old_dentry)))
return -ENOENT;
/* RENAME_EXCHANGE is handled because directories are the same. */
- return check_access_path(
- dom, old_dir,
- maybe_remove(old_dentry) | maybe_remove(new_dentry) |
- get_mode_access(d_backing_inode(old_dentry)->i_mode));
+ access_request = maybe_remove(old_dentry) | maybe_remove(new_dentry) |
+ get_dentry_access(old_dentry);
+ /*
+ * In case of renameat2(2) with RENAME_WHITEOUT, a whiteout object is
+ * created in the source location, so we require an additional access
+ * right there.
+ */
+ if (flags & RENAME_WHITEOUT)
+ access_request |=
+ get_mode_access(S_IFCHR | WHITEOUT_MODE, WHITEOUT_DEV);
+
+ return check_access_path(dom, old_dir, access_request);
}

static int hook_path_mkdir(const struct path *const dir,
@@ -656,7 +675,8 @@ static int hook_path_mknod(const struct path *const dir,

if (!dom)
return 0;
- return check_access_path(dom, dir, get_mode_access(mode));
+ return check_access_path(dom, dir,
+ get_mode_access(mode, new_decode_dev(dev)));
}

static int hook_path_symlink(const struct path *const dir,
diff --git a/security/security.c b/security/security.c
index 1daea0a94626..e8bd6701a415 100644
--- a/security/security.c
+++ b/security/security.c
@@ -1247,13 +1247,13 @@ int security_path_rename(const struct path *old_dir, struct dentry *old_dentry,

if (flags & RENAME_EXCHANGE) {
int err = call_int_hook(path_rename, new_dir, new_dentry,
- old_dir, old_dentry);
+ old_dir, old_dentry, flags);
if (err)
return err;
}

return call_int_hook(path_rename, old_dir, old_dentry, new_dir,
- new_dentry);
+ new_dentry, flags);
}
EXPORT_SYMBOL(security_path_rename);

diff --git a/security/tomoyo/tomoyo.c b/security/tomoyo/tomoyo.c
index 1be96e2ce9f1..f2103b0c4c7a 100644
--- a/security/tomoyo/tomoyo.c
+++ b/security/tomoyo/tomoyo.c
@@ -264,13 +264,15 @@ static int tomoyo_path_link(struct dentry *old_dentry, const struct path *new_di
* @old_dentry: Pointer to "struct dentry".
* @new_parent: Pointer to "struct path".
* @new_dentry: Pointer to "struct dentry".
+ * @flags: Rename options.
*
* Returns 0 on success, negative value otherwise.
*/
static int tomoyo_path_rename(const struct path *old_parent,
struct dentry *old_dentry,
const struct path *new_parent,
- struct dentry *new_dentry)
+ struct dentry *new_dentry,
+ const unsigned int flags)
{
struct path path1 = { .mnt = old_parent->mnt, .dentry = old_dentry };
struct path path2 = { .mnt = new_parent->mnt, .dentry = new_dentry };