lists.openwall.net   lists  /  announce  owl-users  owl-dev  john-users  john-dev  passwdqc-users  yescrypt  popa3d-users  /  oss-security  kernel-hardening  musl  sabotage  tlsify  passwords  /  crypt-dev  xvendor  /  Bugtraq  Full-Disclosure  linux-kernel  linux-netdev  linux-ext4  linux-hardening  PHC 
Open Source and information security mailing list archives
 
Hash Suite: Windows password security audit tool. GUI, reports in PDF.
[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Date:   Sun, 15 Nov 2020 11:37:18 +0100
From:   Christian Brauner <christian.brauner@...ntu.com>
To:     Alexander Viro <viro@...iv.linux.org.uk>,
        Christoph Hellwig <hch@...radead.org>,
        linux-fsdevel@...r.kernel.org
Cc:     John Johansen <john.johansen@...onical.com>,
        James Morris <jmorris@...ei.org>,
        Mimi Zohar <zohar@...ux.ibm.com>,
        Dmitry Kasatkin <dmitry.kasatkin@...il.com>,
        Stephen Smalley <stephen.smalley.work@...il.com>,
        Casey Schaufler <casey@...aufler-ca.com>,
        Arnd Bergmann <arnd@...db.de>,
        Andreas Dilger <adilger.kernel@...ger.ca>,
        OGAWA Hirofumi <hirofumi@...l.parknet.co.jp>,
        Geoffrey Thomas <geofft@...reload.com>,
        Mrunal Patel <mpatel@...hat.com>,
        Josh Triplett <josh@...htriplett.org>,
        Andy Lutomirski <luto@...nel.org>,
        Theodore Tso <tytso@....edu>, Alban Crequy <alban@...volk.io>,
        Tycho Andersen <tycho@...ho.ws>,
        David Howells <dhowells@...hat.com>,
        James Bottomley <James.Bottomley@...senpartnership.com>,
        Jann Horn <jannh@...gle.com>,
        Seth Forshee <seth.forshee@...onical.com>,
        St├ęphane Graber <stgraber@...ntu.com>,
        Aleksa Sarai <cyphar@...har.com>,
        Lennart Poettering <lennart@...ttering.net>,
        "Eric W. Biederman" <ebiederm@...ssion.com>, smbarber@...omium.org,
        Phil Estes <estesp@...il.com>, Serge Hallyn <serge@...lyn.com>,
        Kees Cook <keescook@...omium.org>,
        Todd Kjos <tkjos@...gle.com>, Jonathan Corbet <corbet@....net>,
        containers@...ts.linux-foundation.org,
        linux-security-module@...r.kernel.org, linux-api@...r.kernel.org,
        linux-ext4@...r.kernel.org, linux-audit@...hat.com,
        linux-integrity@...r.kernel.org, selinux@...r.kernel.org,
        Christian Brauner <christian.brauner@...ntu.com>,
        Christoph Hellwig <hch@....de>
Subject: [PATCH v2 39/39] tests: add vfs/idmapped mounts test suite

This adds a whole test suite for idmapped mounts but in order to ensure that
there are no regression for the vfs itself it also includes tests for correct
functionality on non-idmapped mounts. The following tests are currently
available with more to come in the future:

01. create_delete_rename: test that basic file interactions work for idmapped mounts
02. create_delete_rename_userns: test that basic file interactions work for
    idmapped mounts from within user namespaces
03. hardlinks: verify that hardlinks work correctly
04. rename: verity tat rename works correctly
05. create_userns: verify that file creation in user namespaces works from idmapped mounts
06. create_userns_device_node: verify that device node creation fails inside user
    namespace from idmapped mounts.
07. expected_uid_gid: verify that file ownership works correctly on idmapped mounts
08. expected_uid_gid_userns: verify that file ownership works correctly on
    idmapped mounts inside user namespaces
09. expected_fscaps_userns: verify that filesystem capabilities work correctly on
    idmapped mounts and inside user namespaces
10. expected_fscaps_reverse: verify that filesystem capabilities work correctly
    on idmapped mounts and inside user namespaces where we map from unprivileged
    ids to privileged ids
11. setid_binaries: verify that suid and sgid binaries work correctly on idmapped mounts
12. setid_binaries_reverse: verify that suid and sgid binaries work correctly on
    idmapped mounts where we map from unprivileged ids to privileged ids
13. setid_binaries_userns: verify that suid and sgid binaries work correctly on
    idmapped mounts inside user namespaces
14. idmap_mount_tree: verify that idmapping a whole mount tree works correctly
15. idmap_mount_tree_invalid: verify that idmapping a mount tree with a mount of
    a filesystem that doesn't support being idmapped yet fails
16. sticky_bit_unlink: verify that unlinking in sticky directories works correctly
17. sticky_bit_unlink_idmapped: verify that unlinking in sticky directories works
    correctly on idmapped mounts
18. sticky_bit_unlink_idmapped_userns: verify that unlinking in sticky directories works
    correctly on idmapped mounts inside user namespaces
19. sticky_bit_rename_idmapped: verify that renaminging in sticky directories works
    correctly on idmapped mounts
20. sticky_bit_rename_idmapped_userns: verify that renaming in sticky directories works
    correctly on idmapped mounts inside user namespaces
21. follow_symlinks: test that following protected symlinks works correctly
22. follow_symlinks_idmapped: test that following symlinks works correctly on idmapped mounts
23. follow_symlinks_idmapped_userns: test that following symlinks works correctly
    on idmapped mounts inside user namespaces
24. invalid_fd_negative: test that negative fds are rejected when idmapping mounts
25. invalid_fd_large: test that excessively large fds are rejected when idmappings mounts
26. invalid_fd_closed: test that closed fds are rejected when idmapping mounts
27. invalid_fd_initial_userns: test that fds referencing the initial user namespace are rejected
28. attached_mount_inside_current_mount_namespace: test that attached mounts can be idmapped
29. attached_mount_outside_current_mount_namespace: test that attached mounts
    can't be idmapped if we are in a different user namespace
30. detached_mount_inside_current_mount_namespace: test that detached mounts can be idmapped
31. detached_mount_outside_current_mount_namespace: test that detached mounts can
    be idmapped outside of our current user namespace
32. change_idmapping: test that idmapped mounts can't be changed

Output:
 TAP version 13
 1..33
 # Starting 33 tests from 2 test cases.
 #  RUN           core.invalid_fd_negative ...
 #            OK  core.invalid_fd_negative
 ok 1 core.invalid_fd_negative
 #  RUN           core.invalid_fd_large ...
 #            OK  core.invalid_fd_large
 ok 2 core.invalid_fd_large
 #  RUN           core.invalid_fd_closed ...
 #            OK  core.invalid_fd_closed
 ok 3 core.invalid_fd_closed
 #  RUN           core.invalid_fd_initial_userns ...
 #            OK  core.invalid_fd_initial_userns
 ok 4 core.invalid_fd_initial_userns
 #  RUN           core.attached_mount_inside_current_mount_namespace ...
 #            OK  core.attached_mount_inside_current_mount_namespace
 ok 5 core.attached_mount_inside_current_mount_namespace
 #  RUN           core.attached_mount_outside_current_mount_namespace ...
 #            OK  core.attached_mount_outside_current_mount_namespace
 ok 6 core.attached_mount_outside_current_mount_namespace
 #  RUN           core.detached_mount_inside_current_mount_namespace ...
 #            OK  core.detached_mount_inside_current_mount_namespace
 ok 7 core.detached_mount_inside_current_mount_namespace
 #  RUN           core.detached_mount_outside_current_mount_namespace ...
 #            OK  core.detached_mount_outside_current_mount_namespace
 ok 8 core.detached_mount_outside_current_mount_namespace
 #  RUN           core.change_idmapping ...
 #            OK  core.change_idmapping
 ok 9 core.change_idmapping
 #  RUN           core.create_delete_rename ...
 #            OK  core.create_delete_rename
 ok 10 core.create_delete_rename
 #  RUN           core.create_delete_rename_userns ...
 #            OK  core.create_delete_rename_userns
 ok 11 core.create_delete_rename_userns
 #  RUN           core.hardlinks ...
 #            OK  core.hardlinks
 ok 12 core.hardlinks
 #  RUN           core.rename ...
 #            OK  core.rename
 ok 13 core.rename
 #  RUN           core.create_userns ...
 #            OK  core.create_userns
 ok 14 core.create_userns
 #  RUN           core.create_userns_device_node ...
 #            OK  core.create_userns_device_node
 ok 15 core.create_userns_device_node
 #  RUN           core.expected_uid_gid ...
 #            OK  core.expected_uid_gid
 ok 16 core.expected_uid_gid
 #  RUN           core.expected_uid_gid_userns ...
 #            OK  core.expected_uid_gid_userns
 ok 17 core.expected_uid_gid_userns
 #  RUN           core.expected_fscaps_userns ...
 #            OK  core.expected_fscaps_userns
 ok 18 core.expected_fscaps_userns
 #  RUN           core.expected_fscaps_reverse ...
 #            OK  core.expected_fscaps_reverse
 ok 19 core.expected_fscaps_reverse
 #  RUN           core.setid_binaries ...
 #            OK  core.setid_binaries
 ok 20 core.setid_binaries
 #  RUN           core.setid_binaries_reverse ...
 #            OK  core.setid_binaries_reverse
 ok 21 core.setid_binaries_reverse
 #  RUN           core.setid_binaries_userns ...
 #            OK  core.setid_binaries_userns
 ok 22 core.setid_binaries_userns
 #  RUN           core.idmap_mount_tree ...
 #            OK  core.idmap_mount_tree
 ok 23 core.idmap_mount_tree
 #  RUN           core.idmap_mount_tree_invalid ...
 #            OK  core.idmap_mount_tree_invalid
 ok 24 core.idmap_mount_tree_invalid
 #  RUN           core.sticky_bit_unlink ...
 #            OK  core.sticky_bit_unlink
 ok 25 core.sticky_bit_unlink
 #  RUN           core.sticky_bit_unlink_idmapped ...
 #            OK  core.sticky_bit_unlink_idmapped
 ok 26 core.sticky_bit_unlink_idmapped
 #  RUN           core.sticky_bit_unlink_idmapped_userns ...
 #            OK  core.sticky_bit_unlink_idmapped_userns
 ok 27 core.sticky_bit_unlink_idmapped_userns
 #  RUN           core.sticky_bit_rename ...
 #            OK  core.sticky_bit_rename
 ok 28 core.sticky_bit_rename
 #  RUN           core.sticky_bit_rename_idmapped ...
 #            OK  core.sticky_bit_rename_idmapped
 ok 29 core.sticky_bit_rename_idmapped
 #  RUN           core.sticky_bit_rename_idmapped_userns ...
 #            OK  core.sticky_bit_rename_idmapped_userns
 ok 30 core.sticky_bit_rename_idmapped_userns
 #  RUN           core.follow_symlinks ...
 #            OK  core.follow_symlinks
 ok 31 core.follow_symlinks
 #  RUN           core.follow_symlinks_idmapped ...
 #            OK  core.follow_symlinks_idmapped
 ok 32 core.follow_symlinks_idmapped
 #  RUN           core.follow_symlinks_idmapped_userns ...
 #            OK  core.follow_symlinks_idmapped_userns
 ok 33 core.follow_symlinks_idmapped_userns
 # PASSED: 33 / 33 tests passed.
 # Totals: pass:33 fail:0 xfail:0 xpass:0 skip:0 error:0

Cc: Christoph Hellwig <hch@....de>
Cc: David Howells <dhowells@...hat.com>
Cc: Al Viro <viro@...iv.linux.org.uk>
Cc: linux-fsdevel@...r.kernel.org
Signed-off-by: Christian Brauner <christian.brauner@...ntu.com>
---
/* v2 */
patch introduced
---
 .../testing/selftests/idmap_mounts/.gitignore |    1 +
 tools/testing/selftests/idmap_mounts/Makefile |    7 +-
 tools/testing/selftests/idmap_mounts/core.c   | 3476 +++++++++++++++++
 .../testing/selftests/idmap_mounts/internal.h |   33 +-
 tools/testing/selftests/idmap_mounts/utils.c  |  136 +
 tools/testing/selftests/idmap_mounts/utils.h  |   17 +
 tools/testing/selftests/idmap_mounts/xattr.c  |  126 +-
 7 files changed, 3664 insertions(+), 132 deletions(-)
 create mode 100644 tools/testing/selftests/idmap_mounts/core.c
 create mode 100644 tools/testing/selftests/idmap_mounts/utils.c
 create mode 100644 tools/testing/selftests/idmap_mounts/utils.h

diff --git a/tools/testing/selftests/idmap_mounts/.gitignore b/tools/testing/selftests/idmap_mounts/.gitignore
index 18c5e90522ad..03c7198482d2 100644
--- a/tools/testing/selftests/idmap_mounts/.gitignore
+++ b/tools/testing/selftests/idmap_mounts/.gitignore
@@ -1 +1,2 @@
+core
 xattr
diff --git a/tools/testing/selftests/idmap_mounts/Makefile b/tools/testing/selftests/idmap_mounts/Makefile
index 1d495c99d924..67697b788353 100644
--- a/tools/testing/selftests/idmap_mounts/Makefile
+++ b/tools/testing/selftests/idmap_mounts/Makefile
@@ -1,9 +1,12 @@
 # SPDX-License-Identifier: GPL-2.0
 # Makefile for mount selftests.
 CFLAGS = -g -I../../../../usr/include/ -Wall -O2 -pthread
+LDLIBS += -lcap
 
-TEST_GEN_FILES += xattr
+TEST_GEN_FILES = xattr
+TEST_GEN_FILES += core
 
 include ../lib.mk
 
-$(OUTPUT)/xattr: xattr.c internal.h
+$(OUTPUT)/xattr: xattr.c internal.h utils.c utils.h
+$(OUTPUT)/core: core.c internal.h utils.c utils.h
diff --git a/tools/testing/selftests/idmap_mounts/core.c b/tools/testing/selftests/idmap_mounts/core.c
new file mode 100644
index 000000000000..23909a55beb8
--- /dev/null
+++ b/tools/testing/selftests/idmap_mounts/core.c
@@ -0,0 +1,3476 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <grp.h>
+#include <limits.h>
+#include <linux/capability.h>
+#include <sys/capability.h>
+#include <linux/limits.h>
+#include <sched.h>
+#include <stdbool.h>
+#include <sys/fsuid.h>
+#include <sys/sysmacros.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <sys/acl.h>
+#include <sys/xattr.h>
+
+#include "internal.h"
+#include "utils.h"
+#include "../kselftest_harness.h"
+
+#define IMAGE_FILE1 "ext4_1.img"
+#define IMAGE_FILE2 "ext4_2.img"
+#define IMAGE_ROOT_MNT1 "mnt_root_1"
+#define IMAGE_ROOT_MNT2_RELATIVE "mnt_root_2"
+#define IMAGE_ROOT_MNT2 IMAGE_ROOT_MNT1 "/" IMAGE_ROOT_MNT2_RELATIVE
+#define FILESYSTEM_MOUNT1 IMAGE_ROOT_MNT2 "/fs_root_1"
+#define MNT_TARGET1 "mnt_target1"
+#define MNT_TARGET2 "mnt_target2"
+#define FILE1 "file1"
+#define FILE1_RENAME "file1_rename"
+#define FILE2 "file2"
+#define FILE2_RENAME "file2_rename"
+#define DIR1 "dir1"
+#define DIR1_RENAME "dir1_rename"
+#define HARDLINK1 "hardlink1"
+#define SYMLINK1 "symlink1"
+#define SYMLINK_USER1 "symlink_user1"
+#define SYMLINK_USER2 "symlink_user2"
+#define SYMLINK_USER3 "symlink_user3"
+#define CHRDEV1 "chrdev1"
+
+/* Attempt to de-conflict with the selftests tree. */
+#ifndef SKIP
+#define SKIP(s, ...)	XFAIL(s, ##__VA_ARGS__)
+#endif
+
+static bool symlinks_protected(void)
+{
+	int fd;
+	ssize_t ret;
+	char buf[256];
+
+	fd = open("/proc/sys/fs/protected_symlinks", O_RDONLY | O_CLOEXEC);
+	if (fd < 0)
+		return false;
+
+	ret = read(fd, buf, sizeof(buf));
+	close(fd);
+	if (ret < sizeof(int))
+		return false;
+
+	return atoi(buf) >= 1;
+}
+
+/**
+ * caps_down - lower all effective caps
+ */
+static bool caps_down(void)
+{
+	cap_t caps = NULL;
+	int ret = -1;
+	bool fret = false;
+
+	caps = cap_get_proc();
+	if (!caps)
+		goto out;
+
+	ret = cap_clear_flag(caps, CAP_EFFECTIVE);
+	if (ret)
+		goto out;
+
+	ret = cap_set_proc(caps);
+	if (ret)
+		goto out;
+
+	fret = true;
+
+out:
+	cap_free(caps);
+	return fret;
+}
+
+/**
+ * expected_uid_gid - check whether file is owned by the provided uid and gid
+ */
+static bool expected_uid_gid(int dfd, const char *path, int flags,
+			     uid_t expected_uid, gid_t expected_gid)
+{
+	int ret;
+	struct stat st;
+
+	ret = fstatat(dfd, path, &st, flags);
+	if (ret < 0)
+		return false;
+
+	return st.st_uid == expected_uid && st.st_gid == expected_gid;
+}
+
+/**
+ * is_setid - check whether file is S_ISUID and S_ISGID
+ */
+static bool is_setid(int dfd, const char *path, int flags)
+{
+	int ret;
+	struct stat st;
+
+	ret = fstatat(dfd, path, &st, flags);
+	if (ret < 0)
+		return false;
+
+	return (st.st_mode & S_ISUID) || (st.st_mode & S_ISGID);
+}
+
+/**
+ * is_sticky - check whether file is S_ISUID and S_ISGID
+ */
+static bool is_sticky(int dfd, const char *path, int flags)
+{
+	int ret;
+	struct stat st;
+
+	ret = fstatat(dfd, path, &st, flags);
+	if (ret < 0)
+		return false;
+
+	return (st.st_mode & S_ISVTX) > 0;
+}
+
+/**
+ * rm_r - recursively remove all files
+ */
+static int rm_r(const char *dirname)
+{
+	DIR *dir;
+	int ret;
+	struct dirent *direntp;
+
+	dir = opendir(dirname);
+	if (!dir)
+		return -1;
+
+	while ((direntp = readdir(dir))) {
+		char buf[PATH_MAX];
+		struct stat st;
+
+		if (!strcmp(direntp->d_name, ".") ||
+		    !strcmp(direntp->d_name, ".."))
+			continue;
+
+		snprintf(buf, sizeof(buf), "%s/%s", dirname, direntp->d_name);
+		ret = lstat(buf, &st);
+		if (ret < 0 && errno != ENOENT)
+			break;
+
+		if (S_ISDIR(st.st_mode))
+			ret = rm_r(buf);
+		else
+			ret = unlink(buf);
+		if (ret < 0 && errno != ENOENT)
+			break;
+	}
+
+	ret = rmdir(dirname);
+	closedir(dir);
+	return ret;
+}
+
+/**
+ * umount_r - recursively umount all mounts
+ */
+static void umount_r(const char *path)
+{
+	DIR *dir;
+	int ret;
+	struct dirent *direntp;
+
+	dir = opendir(path);
+	if (!dir)
+		return;
+
+	while ((direntp = readdir(dir))) {
+		char buf[PATH_MAX];
+		struct stat st;
+
+		if (!strcmp(direntp->d_name, ".") ||
+		    !strcmp(direntp->d_name, ".."))
+			continue;
+
+		snprintf(buf, sizeof(buf), "%s/%s", path, direntp->d_name);
+		umount2(buf, MNT_DETACH);
+
+		ret = lstat(buf, &st);
+		if (ret < 0 && errno != ENOENT)
+			break;
+
+		if (!S_ISDIR(st.st_mode))
+			continue;
+
+		umount_r(buf);
+	}
+
+	umount2(path, MNT_DETACH);
+	closedir(dir);
+}
+
+/**
+ * chown_r - recursively change ownership of all files
+ */
+static int chown_r(const char *dirname, uid_t uid, gid_t gid)
+{
+	DIR *dir;
+	int ret;
+	struct dirent *direntp;
+
+	dir = opendir(dirname);
+	if (!dir)
+		return -1;
+
+	while ((direntp = readdir(dir))) {
+		char buf[PATH_MAX];
+		struct stat st;
+
+		if (!strcmp(direntp->d_name, ".") ||
+		    !strcmp(direntp->d_name, ".."))
+			continue;
+
+		snprintf(buf, sizeof(buf), "%s/%s", dirname, direntp->d_name);
+		ret = lstat(buf, &st);
+		if (ret < 0 && errno != ENOENT)
+			break;
+
+		if (S_ISDIR(st.st_mode))
+			ret = chown_r(buf, uid, gid);
+		else
+			ret = chown(buf, uid, gid);
+		if (ret < 0 && errno != ENOENT)
+			break;
+	}
+
+	ret = chown(dirname, uid, gid);
+	closedir(dir);
+	return ret;
+}
+
+/**
+ * fd_to_fd - transfer data from one fd to another
+ */
+static int fd_to_fd(int from, int to)
+{
+	for (;;) {
+		uint8_t buf[PATH_MAX];
+		uint8_t *p = buf;
+		ssize_t bytes_to_write;
+		ssize_t bytes_read;
+
+		bytes_read = read_nointr(from, buf, sizeof buf);
+		if (bytes_read < 0)
+			return -1;
+		if (bytes_read == 0)
+			break;
+
+		bytes_to_write = (size_t)bytes_read;
+		do {
+			ssize_t bytes_written;
+
+			bytes_written = write_nointr(to, p, bytes_to_write);
+			if (bytes_written < 0)
+				return -1;
+
+			bytes_to_write -= bytes_written;
+			p += bytes_written;
+		} while (bytes_to_write > 0);
+	}
+
+	return 0;
+}
+
+static int sys_execveat(int fd, const char *path, char **argv, char **envp,
+			int flags)
+{
+#ifdef __NR_execveat
+	return syscall(__NR_execveat, fd, path, argv, envp, flags);
+#else
+	errno = ENOSYS;
+	return -1;
+#endif
+}
+
+#ifndef VFS_CAP_U32_3
+#define VFS_CAP_U32_3 2
+#endif
+
+#ifndef VFS_CAP_U32
+#define VFS_CAP_U32 VFS_CAP_U32_3
+#endif
+
+#ifndef VFS_CAP_REVISION_1
+#define VFS_CAP_REVISION_1 0x01000000
+#endif
+
+#ifndef VFS_CAP_REVISION_2
+#define VFS_CAP_REVISION_2 0x02000000
+#endif
+
+#ifndef VFS_CAP_REVISION_3
+#define VFS_CAP_REVISION_3 0x03000000
+struct vfs_ns_cap_data {
+	__le32 magic_etc;
+	struct {
+		__le32 permitted;
+		__le32 inheritable;
+	} data[VFS_CAP_U32];
+	__le32 rootid;
+};
+#endif
+
+#if __BYTE_ORDER == __BIG_ENDIAN
+#define cpu_to_le16(w16) le16_to_cpu(w16)
+#define le16_to_cpu(w16) ((u_int16_t)((u_int16_t)(w16) >> 8) | (u_int16_t)((u_int16_t)(w16) << 8))
+#define cpu_to_le32(w32) le32_to_cpu(w32)
+#define le32_to_cpu(w32)                                                                       \
+	((u_int32_t)((u_int32_t)(w32) >> 24) | (u_int32_t)(((u_int32_t)(w32) >> 8) & 0xFF00) | \
+	 (u_int32_t)(((u_int32_t)(w32) << 8) & 0xFF0000) | (u_int32_t)((u_int32_t)(w32) << 24))
+#elif __BYTE_ORDER == __LITTLE_ENDIAN
+#define cpu_to_le16(w16) ((u_int16_t)(w16))
+#define le16_to_cpu(w16) ((u_int16_t)(w16))
+#define cpu_to_le32(w32) ((u_int32_t)(w32))
+#define le32_to_cpu(w32) ((u_int32_t)(w32))
+#else
+#error Expected endianess macro to be set
+#endif
+
+/**
+ * expected_dummy_vfs_caps_uid - check vfs caps are stored with the provided uid
+ */
+static bool expected_dummy_vfs_caps_uid(int fd, uid_t expected_uid)
+{
+#define __cap_raised_permitted(x, ns_cap_data)                                 \
+	((ns_cap_data.data[(x) >> 5].permitted) & (1 << ((x)&31)))
+	struct vfs_ns_cap_data ns_xattr = {};
+	ssize_t ret;
+
+	ret = fgetxattr(fd, "security.capability", &ns_xattr, sizeof(ns_xattr));
+	if (ret < 0 || ret == 0)
+		return false;
+
+	if (ns_xattr.magic_etc & VFS_CAP_REVISION_3)
+		return (le32_to_cpu(ns_xattr.rootid) == expected_uid) && (__cap_raised_permitted(CAP_NET_RAW, ns_xattr) > 0);
+
+	return false;
+}
+
+/**
+ * set_dummy_vfs_caps - set dummy vfs caps for the provided uid
+ */
+static int set_dummy_vfs_caps(int fd, int flags, int rootuid)
+{
+#define __raise_cap_permitted(x, ns_cap_data)                                  \
+	ns_cap_data.data[(x) >> 5].permitted |= (1 << ((x)&31))
+
+	struct vfs_ns_cap_data ns_xattr;
+
+	memset(&ns_xattr, 0, sizeof(ns_xattr));
+	__raise_cap_permitted(CAP_NET_RAW, ns_xattr);
+	ns_xattr.magic_etc |= VFS_CAP_REVISION_3 | VFS_CAP_FLAGS_EFFECTIVE;
+	ns_xattr.rootid = cpu_to_le32(rootuid);
+
+	return fsetxattr(fd, "security.capability",
+			 &ns_xattr, sizeof(ns_xattr), flags);
+}
+
+FIXTURE(core)
+{
+	/* fd for the main test directory */
+	int test_dir_fd;
+	/* the absolute path to the main test directory */
+	char test_dir_path[PATH_MAX];
+
+	/* fd for test test filesystem image */
+	int img_fd;
+	/* fd for the mountpoint of the test fs image */
+	int img_mnt_fd;
+
+	/* temporary buffer */
+	char cmdline[3 * PATH_MAX];
+
+	/* open_tree fd for the mountpoint of the test fs image */
+	int target1_mnt_fd_attached;
+
+	/* detached open_tree fd for the mountpoint of the test fs image */
+	int target1_mnt_fd_detached;
+};
+
+FIXTURE_SETUP(core)
+{
+	struct mount_attr attr = {
+		.attr_set	= 0,
+		.attr_clr	= 0,
+		.propagation	= MAKE_PROPAGATION_PRIVATE,
+	};
+
+	self->img_fd = -EBADF;
+	self->img_mnt_fd = -EBADF;
+
+	/* create separate mount namespace with mount propagation turned off */
+	ASSERT_EQ(unshare(CLONE_NEWNS), 0);
+	ASSERT_EQ(sys_mount_setattr(-1, "/", AT_RECURSIVE, &attr, sizeof(attr)), 0);
+
+	/* create unique test directory */
+	snprintf(self->test_dir_path, sizeof(self->test_dir_path),
+		 "/idmap_mount_core_XXXXXX");
+	ASSERT_NE(mkdtemp(self->test_dir_path), NULL);
+	self->test_dir_fd = open(self->test_dir_path, O_CLOEXEC | O_DIRECTORY);
+	ASSERT_GE(self->test_dir_fd, 0);
+	ASSERT_EQ(fchmod(self->test_dir_fd, 0777), 0);
+
+	/* create filesystem image */
+	self->img_fd = openat(self->test_dir_fd, IMAGE_FILE1, O_CREAT | O_WRONLY, 0600);
+	ASSERT_GE(self->img_fd, 0);
+	ASSERT_EQ(ftruncate(self->img_fd, 1024 * 2048), 0);
+	snprintf(self->cmdline, sizeof(self->cmdline),
+		 "mkfs.ext4 -q %s/" IMAGE_FILE1, self->test_dir_path);
+	ASSERT_EQ(system(self->cmdline), 0);
+
+	/* create mountpoint for image */
+	ASSERT_EQ(mkdirat(self->test_dir_fd, IMAGE_ROOT_MNT1, 0777), 0);
+
+	/* mount image */
+	snprintf(self->cmdline, sizeof(self->cmdline),
+		 "mount -o loop -t ext4 %s/" IMAGE_FILE1 " %s/" IMAGE_ROOT_MNT1,
+		 self->test_dir_path, self->test_dir_path);
+	ASSERT_EQ(system(self->cmdline), 0);
+
+	/* stash fd for the image mountpoint */
+	self->img_mnt_fd = openat(self->test_dir_fd, IMAGE_ROOT_MNT1, O_DIRECTORY | O_CLOEXEC, 0);
+	ASSERT_GE(self->img_mnt_fd, 0);
+
+	/* create mountpoint for bind-mount */
+	ASSERT_EQ(mkdirat(self->test_dir_fd, MNT_TARGET1, 0777), 0);
+
+	/* create new detached mount from image mountpoint */
+	self->target1_mnt_fd_detached = sys_open_tree(self->test_dir_fd,
+						      IMAGE_ROOT_MNT1,
+						      AT_NO_AUTOMOUNT |
+						      AT_SYMLINK_NOFOLLOW |
+						      OPEN_TREE_CLOEXEC |
+						      OPEN_TREE_CLONE);
+	ASSERT_GE(self->target1_mnt_fd_detached, 0);
+
+	/* create attached mount from image mountpoint */
+	self->target1_mnt_fd_attached = sys_open_tree(self->test_dir_fd,
+						      IMAGE_ROOT_MNT1,
+						      AT_NO_AUTOMOUNT |
+						      AT_SYMLINK_NOFOLLOW |
+						      OPEN_TREE_CLOEXEC);
+	ASSERT_GE(self->target1_mnt_fd_attached, 0);
+}
+
+FIXTURE_TEARDOWN(core)
+{
+	EXPECT_EQ(close(self->test_dir_fd), 0);
+	EXPECT_EQ(close(self->img_fd), 0);
+	EXPECT_EQ(close(self->img_mnt_fd), 0);
+	EXPECT_EQ(close(self->target1_mnt_fd_attached), 0);
+	EXPECT_EQ(close(self->target1_mnt_fd_detached), 0);
+	umount_r(self->test_dir_path);
+	rm_r(self->test_dir_path);
+}
+
+/**
+ * Validate that negative fd values are rejected.
+ */
+TEST_F(core, invalid_fd_negative)
+{
+	struct mount_attr attr = {
+		.attr_set	= MOUNT_ATTR_IDMAP,
+		.userns_fd	= -EBADF,
+	};
+
+	ASSERT_NE(sys_mount_setattr(-1, "/", 0, &attr, sizeof(attr)), 0) {
+		TH_LOG("failure: created idmapped mount with negative fd");
+	}
+}
+
+/**
+ * Validate that excessively large fd values are rejected.
+ */
+TEST_F(core, invalid_fd_large)
+{
+	struct mount_attr attr = {
+		.attr_set	= MOUNT_ATTR_IDMAP,
+		.userns_fd	= INT64_MAX,
+	};
+
+	ASSERT_NE(sys_mount_setattr(-1, "/", 0, &attr, sizeof(attr)), 0) {
+		TH_LOG("failure: created idmapped mount with too large fd value");
+	}
+}
+
+/**
+ * Validate that closed fd values are rejected.
+ */
+TEST_F(core, invalid_fd_closed)
+{
+	int fd;
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	fd = open("/dev/null", O_RDONLY | O_CLOEXEC);
+	ASSERT_GE(fd, 0);
+	ASSERT_GE(close(fd), 0);
+
+	attr.userns_fd = fd;
+	ASSERT_NE(sys_mount_setattr(-1, "/", 0, &attr, sizeof(attr)), 0) {
+		TH_LOG("failure: created idmapped mount with closed fd");
+	}
+}
+
+/**
+ * Validate that the initial user namespace is rejected.
+ */
+TEST_F(core, invalid_fd_initial_userns)
+{
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	attr.userns_fd = open("/proc/1/ns/user", O_RDONLY | O_CLOEXEC);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_NE(sys_mount_setattr(-1, "/", 0, &attr, sizeof(attr)), 0) {
+		TH_LOG("failure: created idmapped mount with initial user namespace");
+	}
+	ASSERT_EQ(errno, EPERM);
+	ASSERT_EQ(close(attr.userns_fd), 0);
+}
+
+/**
+ * Validate that an attached mount in our mount namespace can be idmapped.
+ * (The kernel enforces that the mount's mount namespace and the caller's mount
+ *  namespace match.)
+ */
+TEST_F(core, attached_mount_inside_current_mount_namespace)
+{
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	attr.userns_fd	= get_userns_fd(0, 10000, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(self->target1_mnt_fd_attached, "",
+				    AT_EMPTY_PATH, &attr, sizeof(attr)), 0) {
+		TH_LOG("%m - failure: failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       self->target1_mnt_fd_attached, self->test_dir_path);
+	}
+	ASSERT_EQ(close(attr.userns_fd), 0);
+}
+
+/**
+ * Validate that idmapping a mount is rejected if the mount's mount namespace
+ * and our mount namespace don't match.
+ * (The kernel enforces that the mount's mount namespace and the caller's mount
+ *  namespace match.)
+ */
+TEST_F(core, attached_mount_outside_current_mount_namespace)
+{
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	ASSERT_EQ(unshare(CLONE_NEWNS), 0);
+
+	attr.userns_fd	= get_userns_fd(0, 10000, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_NE(sys_mount_setattr(self->target1_mnt_fd_attached, "",
+				    AT_EMPTY_PATH, &attr, sizeof(attr)), 0) {
+		TH_LOG("%m - failure: managed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       self->target1_mnt_fd_attached, self->test_dir_path);
+	}
+	ASSERT_EQ(close(attr.userns_fd), 0);
+}
+
+/**
+ * Validate that an attached mount in our mount namespace can be idmapped.
+ */
+TEST_F(core, detached_mount_inside_current_mount_namespace)
+{
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	/* Changing mount properties on a detached mount. */
+	attr.userns_fd	= get_userns_fd(0, 10000, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(self->target1_mnt_fd_detached, "",
+				    AT_EMPTY_PATH, &attr, sizeof(attr)), 0) {
+		TH_LOG("%m - failure: failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       self->target1_mnt_fd_detached, self->test_dir_path);
+	}
+	ASSERT_EQ(close(attr.userns_fd), 0);
+}
+
+/**
+ * Validate that a detached mount not in our mount namespace can be idmapped.
+ */
+TEST_F(core, detached_mount_outside_current_mount_namespace)
+{
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	ASSERT_EQ(unshare(CLONE_NEWNS), 0);
+	attr.userns_fd	= get_userns_fd(0, 10000, 10000);
+	ASSERT_EQ(sys_mount_setattr(self->target1_mnt_fd_detached, "",
+				    AT_EMPTY_PATH, &attr, sizeof(attr)), 0) {
+		TH_LOG("%m - failure: failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       self->target1_mnt_fd_detached, self->test_dir_path);
+	}
+	ASSERT_EQ(close(attr.userns_fd), 0);
+}
+
+/**
+ * Validate that currently changing the idmapping of an idmapped mount fails.
+ */
+TEST_F(core, change_idmapping)
+{
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	attr.userns_fd	= get_userns_fd(0, 10000, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(self->target1_mnt_fd_detached, "",
+				    AT_EMPTY_PATH, &attr, sizeof(attr)), 0) {
+		TH_LOG("%m - failure: failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       self->target1_mnt_fd_detached, self->test_dir_path);
+	}
+	ASSERT_EQ(close(attr.userns_fd), 0);
+
+	/* Change idmapping on a detached mount that is already idmapped. */
+	attr.userns_fd	= get_userns_fd(0, 20000, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_NE(sys_mount_setattr(self->target1_mnt_fd_detached, "",
+				    AT_EMPTY_PATH, &attr, sizeof(attr)), 0) {
+		TH_LOG("failure: managed to change idmapping of already idmapped mount %d(%s/" MNT_TARGET1 ")",
+		       self->target1_mnt_fd_detached, self->test_dir_path);
+	}
+	ASSERT_EQ(close(attr.userns_fd), 0);
+}
+
+/**
+ * Validate that basic file operations on idmapped mounts.
+ */
+TEST_F(core, create_delete_rename)
+{
+	int file1_fd = -EBADF, hardlink_target_fd = -EBADF;
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	/* create hardlink target */
+	hardlink_target_fd = openat(self->img_mnt_fd, FILE1, O_CREAT | O_EXCL | O_CLOEXEC, 0644);
+	ASSERT_GE(hardlink_target_fd, 0);
+	ASSERT_EQ(close(hardlink_target_fd), 0);
+
+	/* create directory for rename test */
+	ASSERT_EQ(mkdirat(self->img_mnt_fd, DIR1, 0700), 0);
+
+	/* change ownership of all files to uid 0 */
+	snprintf(self->cmdline, sizeof(self->cmdline), "%s/" IMAGE_ROOT_MNT1,
+		 self->test_dir_path);
+	ASSERT_EQ(chown_r(self->cmdline, 0, 0), 0);
+
+	/* Changing mount properties on a detached mount. */
+	attr.userns_fd	= get_userns_fd(0, 10000, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(self->target1_mnt_fd_detached, "",
+				    AT_EMPTY_PATH, &attr, sizeof(attr)), 0) {
+		TH_LOG("%m - failure: failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       self->target1_mnt_fd_detached, self->test_dir_path);
+	}
+	ASSERT_EQ(close(attr.userns_fd), 0);
+
+	/*
+	 * The caller's fsids don't have a mappings in the idmapped mount so
+	 * any file creation must fail.
+	 */
+
+	/* create hardlink */
+	ASSERT_NE(linkat(self->target1_mnt_fd_detached, FILE1, self->target1_mnt_fd_detached, HARDLINK1, 0), 0);
+	ASSERT_EQ(errno, EOVERFLOW);
+
+	/* try to rename a file */
+	ASSERT_NE(renameat2(self->target1_mnt_fd_detached, FILE1,
+			    self->target1_mnt_fd_detached, FILE1_RENAME, 0), 0);
+	ASSERT_EQ(errno, EOVERFLOW);
+
+	/* try to rename a directory */
+	ASSERT_NE(renameat2(self->target1_mnt_fd_detached, DIR1,
+			    self->target1_mnt_fd_detached, DIR1_RENAME, 0), 0);
+	ASSERT_EQ(errno, EOVERFLOW);
+
+	/*
+	 * The caller is privileged over the inode so file deletion must work.
+	 */
+
+	/* remove file */
+	ASSERT_EQ(unlinkat(self->target1_mnt_fd_detached, FILE1, 0), 0);
+
+	/* remove directory */
+	ASSERT_EQ(unlinkat(self->target1_mnt_fd_detached, DIR1, AT_REMOVEDIR), 0);
+
+	/*
+	 * The caller's fsids don't have a mappings in the idmapped mount so
+	 * any file creation must fail.
+	 */
+
+	/* create regular file via open() */
+	file1_fd = openat(self->target1_mnt_fd_detached, FILE1, O_CREAT | O_EXCL | O_CLOEXEC, 0644);
+	ASSERT_LT(file1_fd, 0);
+	ASSERT_EQ(errno, EOVERFLOW);
+
+	/* create regular file via mknod */
+	ASSERT_NE(mknodat(self->target1_mnt_fd_detached, FILE2, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(errno, EOVERFLOW);
+
+	/* create character device */
+	ASSERT_NE(mknodat(self->target1_mnt_fd_detached, CHRDEV1, S_IFCHR | 0644,
+			  makedev(5, 1)), 0);
+	ASSERT_EQ(errno, EOVERFLOW);
+
+	/* create symlink */
+	ASSERT_NE(symlinkat(FILE2, self->target1_mnt_fd_detached, SYMLINK1), 0);
+	ASSERT_EQ(errno, EOVERFLOW);
+
+	/* create directory */
+	ASSERT_NE(mkdirat(self->target1_mnt_fd_detached, DIR1, 0700), 0);
+	ASSERT_EQ(errno, EOVERFLOW);
+}
+
+/**
+ * Validate that basic file operations on idmapped mounts from a user
+ * namespace.
+ */
+TEST_F(core, create_delete_rename_userns)
+{
+	int file1_fd = -EBADF;
+	pid_t pid;
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	/* change ownership of all files to uid 0 */
+	snprintf(self->cmdline, sizeof(self->cmdline), "%s/" IMAGE_ROOT_MNT1,
+		 self->test_dir_path);
+	ASSERT_EQ(chown_r(self->cmdline, 0, 0), 0);
+
+	/* Changing mount properties on a detached mount. */
+	attr.userns_fd	= get_userns_fd(0, 10000, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(self->target1_mnt_fd_detached, "",
+				    AT_EMPTY_PATH, &attr, sizeof(attr)), 0) {
+		TH_LOG("%m - failure: failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       self->target1_mnt_fd_detached, self->test_dir_path);
+	}
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		/* Switch to  user namespace where uid 10000 maps to 0. */
+		ASSERT_EQ(setns(attr.userns_fd, CLONE_NEWUSER), 0);
+		ASSERT_EQ(setresgid(0, 0, 0), 0);
+		ASSERT_EQ(setresuid(0, 0, 0), 0);
+
+		/* create regular file via open() */
+		file1_fd = openat(self->target1_mnt_fd_detached,
+				  FILE1, O_CREAT | O_EXCL | O_CLOEXEC, 0644);
+		ASSERT_GT(file1_fd, 0);
+		ASSERT_EQ(close(file1_fd), 0);
+
+		/* create regular file via mknod */
+		ASSERT_EQ(mknodat(self->target1_mnt_fd_detached,
+				  FILE2, S_IFREG | 0000, 0), 0);
+
+		/* create symlink */
+		ASSERT_EQ(symlinkat(FILE2, self->target1_mnt_fd_detached,
+				    SYMLINK1), 0);
+
+		/* create directory */
+		ASSERT_EQ(mkdirat(self->target1_mnt_fd_detached,
+				  DIR1, 0700), 0);
+
+		/* try to rename a file */
+		ASSERT_EQ(renameat2(self->target1_mnt_fd_detached, FILE1,
+				    self->target1_mnt_fd_detached, FILE1_RENAME,
+				    0), 0);
+
+		ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached,
+					   FILE1_RENAME, 0, 0, 0), true);
+
+		/* try to rename a file */
+		ASSERT_EQ(renameat2(self->target1_mnt_fd_detached, DIR1,
+				    self->target1_mnt_fd_detached, DIR1_RENAME,
+				    0), 0);
+
+		ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached,
+					   DIR1_RENAME, 0, 0, 0), true);
+
+		/* remove file */
+		ASSERT_EQ(unlinkat(self->target1_mnt_fd_detached,
+				   FILE1_RENAME, 0), 0);
+
+		/* remove directory */
+		ASSERT_EQ(unlinkat(self->target1_mnt_fd_detached,
+				   DIR1_RENAME, AT_REMOVEDIR), 0);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	ASSERT_EQ(close(attr.userns_fd), 0);
+}
+
+TEST_F(core, hardlinks)
+{
+	int file1_fd = -EBADF, open_tree_fd = -EBADF;
+	struct mount_attr attr1 = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	struct mount_attr attr2 = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	file1_fd = openat(self->img_mnt_fd, FILE1, O_CREAT | O_EXCL | O_CLOEXEC, 0644);
+	ASSERT_GE(file1_fd, 0);
+	ASSERT_EQ(close(file1_fd), 0);
+
+	/* Chown all files to an unprivileged user. */
+	snprintf(self->cmdline, sizeof(self->cmdline), "%s/" IMAGE_ROOT_MNT1,
+		 self->test_dir_path);
+	ASSERT_EQ(chown_r(self->cmdline, 10000, 10000), 0);
+
+	/* Changing mount properties on a detached mount. */
+	attr1.userns_fd	= get_userns_fd(10000, 0, 10000);
+	ASSERT_GE(attr1.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(self->target1_mnt_fd_detached, "",
+				    AT_EMPTY_PATH, &attr1, sizeof(attr1)), 0) {
+		TH_LOG("%m - failure: failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       self->target1_mnt_fd_detached, self->test_dir_path);
+	}
+
+	/* Create detached mount. */
+	open_tree_fd = sys_open_tree(self->test_dir_fd, IMAGE_ROOT_MNT1,
+				     AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW |
+				     OPEN_TREE_CLOEXEC | OPEN_TREE_CLONE |
+				     AT_RECURSIVE);
+	ASSERT_GE(open_tree_fd, 0);
+
+	/* Changing mount properties on a detached mount. */
+	attr2.userns_fd	= get_userns_fd(10000, 0, 10000);
+	ASSERT_GE(attr2.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(open_tree_fd, "",
+				    AT_EMPTY_PATH, &attr2, sizeof(attr2)), 0) {
+		TH_LOG("%m - failure: failed to idmap mount %d(%s/" MNT_TARGET2 ")",
+		       open_tree_fd, self->test_dir_path);
+	}
+
+	/* we're crossing a mountpoint so this must fail
+	 *
+	 * Note that this must also fail for non-idmapped mounts but here we're
+	 * interested in making sure we're not introducing an accidental way to
+	 * violate that restriction or that suddenly this becomes possible.
+	 */
+	ASSERT_NE(linkat(self->target1_mnt_fd_detached, FILE1, open_tree_fd, HARDLINK1, 0), 0);
+	ASSERT_EQ(errno, EXDEV);
+
+	ASSERT_EQ(close(attr1.userns_fd), 0);
+	ASSERT_EQ(close(attr2.userns_fd), 0);
+}
+
+TEST_F(core, rename)
+{
+	int file1_fd = -EBADF, open_tree_fd = -EBADF;
+	struct mount_attr attr1 = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	struct mount_attr attr2 = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	file1_fd = openat(self->img_mnt_fd, FILE1, O_CREAT | O_EXCL | O_CLOEXEC, 0644);
+	ASSERT_GE(file1_fd, 0);
+	ASSERT_EQ(close(file1_fd), 0);
+
+	/* Chown all files to an unprivileged user. */
+	snprintf(self->cmdline, sizeof(self->cmdline), "%s/" IMAGE_ROOT_MNT1,
+		 self->test_dir_path);
+	ASSERT_EQ(chown_r(self->cmdline, 10000, 10000), 0);
+
+	/* Changing mount properties on a detached mount. */
+	attr1.userns_fd	= get_userns_fd(10000, 0, 10000);
+	ASSERT_GE(attr1.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(self->target1_mnt_fd_detached, "",
+				    AT_EMPTY_PATH, &attr1, sizeof(attr1)), 0) {
+		TH_LOG("%m - failure: failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       self->target1_mnt_fd_detached, self->test_dir_path);
+	}
+
+	/* Create detached mount. */
+	open_tree_fd = sys_open_tree(self->test_dir_fd, IMAGE_ROOT_MNT1,
+				     AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW |
+				     OPEN_TREE_CLOEXEC | OPEN_TREE_CLONE |
+				     AT_RECURSIVE);
+	ASSERT_GE(open_tree_fd, 0);
+
+	/* Changing mount properties on a detached mount. */
+	attr2.userns_fd	= get_userns_fd(10000, 0, 10000);
+	ASSERT_GE(attr2.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(open_tree_fd, "",
+				    AT_EMPTY_PATH, &attr2, sizeof(attr2)), 0) {
+		TH_LOG("%m - failure: failed to idmap mount %d(%s/" MNT_TARGET2 ")",
+		       open_tree_fd, self->test_dir_path);
+	}
+
+	/* we're crossing a mountpoint so this must fail
+	 *
+	 * Note that this must also fail for non-idmapped mounts but here we're
+	 * interested in making sure we're not introducing an accidental way to
+	 * violate that restriction or that suddenly this becomes possible.
+	 */
+	ASSERT_NE(renameat2(self->target1_mnt_fd_detached, FILE1,
+			    open_tree_fd, FILE1_RENAME, 0), 0);
+
+	ASSERT_EQ(close(attr1.userns_fd), 0);
+	ASSERT_EQ(close(attr2.userns_fd), 0);
+}
+
+/**
+ * Validate that a caller whose fsids map into the idmapped mount within it's
+ * user namespace can create files.
+ */
+TEST_F(core, create_userns)
+{
+	int file1_fd = -EBADF;
+	pid_t pid;
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	/* Changing mount properties on a detached mount. */
+	attr.userns_fd	= get_userns_fd(0, 10000, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(self->target1_mnt_fd_detached, "",
+				    AT_EMPTY_PATH, &attr, sizeof(attr)), 0) {
+		TH_LOG("%m - failure: failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       self->target1_mnt_fd_detached, self->test_dir_path);
+	}
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		/* Switch to user namespace where uid 10000 maps to 0. */
+		ASSERT_EQ(setns(attr.userns_fd, CLONE_NEWUSER), 0);
+		ASSERT_EQ(setresgid(0, 0, 0), 0);
+		ASSERT_EQ(setresuid(0, 0, 0), 0);
+
+		/* create regular file via open() */
+		file1_fd = openat(self->target1_mnt_fd_detached,
+				  FILE1, O_CREAT | O_EXCL | O_CLOEXEC, 0644);
+		ASSERT_GE(file1_fd, 0);
+		ASSERT_EQ(close(file1_fd), 0);
+
+		/* create regular file via mknod */
+		ASSERT_EQ(mknodat(self->target1_mnt_fd_detached,
+				  FILE2, S_IFREG | 0000, 0), 0);
+
+		/* create hardlink */
+		ASSERT_EQ(linkat(self->target1_mnt_fd_detached,
+				 FILE1,
+				 self->target1_mnt_fd_detached,
+				 HARDLINK1, 0), 0);
+
+		/* create symlink */
+		ASSERT_EQ(symlinkat(FILE2, self->target1_mnt_fd_detached,
+				    SYMLINK1), 0);
+
+		/* create directory */
+		ASSERT_EQ(mkdirat(self->target1_mnt_fd_detached,
+				  DIR1, 0700), 0);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+	ASSERT_EQ(close(attr.userns_fd), 0);
+}
+
+/**
+ * Validate that a caller whose fsids map into the idmapped mount within it's
+ * user namespace cannot create any device nodes.
+ */
+TEST_F(core, create_userns_device_node)
+{
+	pid_t pid;
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	/* Changing mount properties on a detached mount. */
+	attr.userns_fd	= get_userns_fd(0, 10000, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(self->target1_mnt_fd_detached, "",
+				    AT_EMPTY_PATH, &attr, sizeof(attr)), 0) {
+		TH_LOG("%m - failure: failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       self->target1_mnt_fd_detached, self->test_dir_path);
+	}
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		/* Switch to user namespace where uid 10000 maps to 0. */
+		ASSERT_EQ(setns(attr.userns_fd, CLONE_NEWUSER), 0);
+		ASSERT_EQ(setresgid(0, 0, 0), 0);
+		ASSERT_EQ(setresuid(0, 0, 0), 0);
+
+		/* create character device */
+		ASSERT_NE(mknodat(self->target1_mnt_fd_detached,
+				  CHRDEV1, S_IFCHR | 0644, makedev(5, 1)), 0);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	ASSERT_EQ(close(attr.userns_fd), 0);
+}
+
+/**
+ * Validate that changing file ownership works correctly on idmapped mounts.
+ */
+TEST_F(core, expected_uid_gid)
+{
+	int file1_fd = -EBADF;
+	uid_t fsuid;
+	gid_t fsgid;
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	/* create regular file via open() */
+	file1_fd = openat(self->img_mnt_fd, FILE1, O_CREAT | O_EXCL | O_CLOEXEC, 0644);
+	ASSERT_GE(file1_fd, 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(self->img_mnt_fd, FILE2, S_IFREG | 0000, 0), 0);
+
+	/* create character device */
+	ASSERT_EQ(mknodat(self->img_mnt_fd, CHRDEV1, S_IFCHR | 0644,
+			  makedev(5, 1)), 0);
+
+	/* create hardlink */
+	ASSERT_EQ(linkat(self->img_mnt_fd, FILE1, self->img_mnt_fd, HARDLINK1, 0), 0);
+
+	/* create symlink */
+	ASSERT_EQ(symlinkat(FILE2, self->img_mnt_fd, SYMLINK1), 0);
+
+	/* create directory */
+	ASSERT_EQ(mkdirat(self->img_mnt_fd, DIR1, 0700), 0);
+
+	/* retrieve fsids */
+	fsuid = setfsuid(-1);
+	fsgid = setfsgid(-1);
+
+	/* Changing mount properties on a detached mount. */
+	attr.userns_fd	= get_userns_fd(0, 10000, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(self->target1_mnt_fd_detached, "",
+				    AT_EMPTY_PATH, &attr, sizeof(attr)), 0) {
+		TH_LOG("%m - failure: failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       self->target1_mnt_fd_detached, self->test_dir_path);
+	}
+	ASSERT_EQ(close(attr.userns_fd), 0);
+
+	/*
+	 * Validate that all files created through the image mountpoint are
+	 * owned by the callers fsuid and fsgid.
+	 */
+	ASSERT_EQ(expected_uid_gid(self->img_mnt_fd, FILE1,
+		  0, fsuid, fsgid), true);
+
+	ASSERT_EQ(expected_uid_gid(self->img_mnt_fd, FILE2,
+		  0, fsuid, fsgid), true);
+
+	ASSERT_EQ(expected_uid_gid(self->img_mnt_fd, HARDLINK1,
+		  0, fsuid, fsgid), true);
+
+	ASSERT_EQ(expected_uid_gid(self->img_mnt_fd, CHRDEV1,
+		  0, fsuid, fsgid), true);
+
+	ASSERT_EQ(expected_uid_gid(self->img_mnt_fd, SYMLINK1,
+		  0, fsuid, fsgid), true);
+
+	ASSERT_EQ(expected_uid_gid(self->img_mnt_fd, DIR1,
+		  0, fsuid, fsgid), true);
+
+	/*
+	 * Validate that all files are owned by the uid and gid specified in
+	 * the idmapping of the mount they are accessed from.
+	 */
+	ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached, FILE1,
+		  0, 10000, 10000), true);
+
+	ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached, FILE2,
+		  0, 10000, 10000), true);
+
+	ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached, HARDLINK1,
+		  0, 10000, 10000), true);
+
+	ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached, CHRDEV1,
+		  0, 10000, 10000), true);
+
+	ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached, SYMLINK1,
+		  0, 10000, 10000), true);
+
+	ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached, DIR1,
+		  0, 10000, 10000), true);
+
+	/* Change ownership throught original image mountpoint. */
+	ASSERT_EQ(fchownat(self->img_mnt_fd, FILE1, 1000, 1000, 0), 0);
+
+	/* Verify correct ownership through original image mountpoint. */
+	ASSERT_EQ(expected_uid_gid(self->img_mnt_fd, FILE1,
+		  0, 1000, 1000), true);
+
+	ASSERT_EQ(expected_uid_gid(self->img_mnt_fd, HARDLINK1,
+		  0, 1000, 1000), true);
+
+	/* Verify correct ownership through idmapped mountpoint. */
+	ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached, FILE1,
+		  0, 11000, 11000), true);
+
+	ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached, HARDLINK1,
+		  0, 11000, 11000), true);
+
+	/* Change ownership throught idmapped mountpoint. */
+	ASSERT_EQ(fchownat(self->target1_mnt_fd_detached, DIR1,
+			   11000, 11000, 0), 0);
+
+	/* Verify correct ownership through original image mountpoint. */
+	ASSERT_EQ(expected_uid_gid(self->img_mnt_fd, DIR1,
+		  0, 1000, 1000), true);
+
+	/* Verify correct ownership through idmapped mountpoint. */
+	ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached, DIR1,
+		  0, 11000, 11000), true);
+
+	/* Change ownership throught idmapped mountpoint. */
+	ASSERT_EQ(fchownat(self->target1_mnt_fd_detached, FILE1,
+			   11000, 11000, 0), 0);
+	ASSERT_EQ(fchownat(self->target1_mnt_fd_detached, FILE2,
+			   11000, 11000, 0), 0);
+	ASSERT_EQ(fchownat(self->target1_mnt_fd_detached, CHRDEV1,
+			   11000, 11000, 0), 0);
+	ASSERT_EQ(fchownat(self->target1_mnt_fd_detached, SYMLINK1,
+			   11000, 11000, 0), 0);
+	ASSERT_EQ(fchownat(self->target1_mnt_fd_detached, DIR1,
+			   11000, 11000, 0), 0);
+
+	/*
+	 * Validate that all files are owned by the uid and gid specified in
+	 * the idmapping of the mount they are accessed from.
+	 */
+	ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached, FILE1,
+		  0, 11000, 11000), true);
+
+	ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached, FILE2,
+		  0, 11000, 11000), true);
+
+	ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached, HARDLINK1,
+		  0, 11000, 11000), true);
+
+	ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached, CHRDEV1,
+		  0, 11000, 11000), true);
+
+	ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached, SYMLINK1,
+		  0, 11000, 11000), true);
+
+	ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached, DIR1,
+		  0, 11000, 11000), true);
+
+	/*
+	 * Try to change to an id for which we do not have an idmapping from
+	 * the idmapped mountpoint. This must fail.
+	 */
+	ASSERT_NE(fchownat(self->target1_mnt_fd_detached, FILE1,
+			   30000, 30000, 0), 0);
+	ASSERT_NE(fchownat(self->target1_mnt_fd_detached, FILE2,
+			   30000, 30000, 0), 0);
+	ASSERT_NE(fchownat(self->target1_mnt_fd_detached, CHRDEV1,
+			   30000, 30000, 0), 0);
+	ASSERT_NE(fchownat(self->target1_mnt_fd_detached, SYMLINK1,
+			   30000, 30000, 0), 0);
+	ASSERT_NE(fchownat(self->target1_mnt_fd_detached, DIR1,
+			   30000, 30000, 0), 0);
+
+	ASSERT_EQ(close(file1_fd), 0);
+}
+
+/**
+ * Validate that changing file ownership in a user namespace with a matching
+ * idmapping works correctly on idmapped mounts.
+ */
+TEST_F(core, expected_uid_gid_userns)
+{
+	int file1_fd = -EBADF;
+	pid_t pid;
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	/* create regular file via open() */
+	file1_fd = openat(self->img_mnt_fd, FILE1, O_CREAT | O_EXCL | O_CLOEXEC, 0644);
+	ASSERT_GE(file1_fd, 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(self->img_mnt_fd, FILE2, S_IFREG | 0000, 0), 0);
+
+	/* create character device */
+	ASSERT_EQ(mknodat(self->img_mnt_fd, CHRDEV1, S_IFCHR | 0644,
+			  makedev(5, 1)), 0);
+
+	/* create hardlink */
+	ASSERT_EQ(linkat(self->img_mnt_fd, FILE1, self->img_mnt_fd, HARDLINK1, 0), 0);
+
+	/* create symlink */
+	ASSERT_EQ(symlinkat(FILE2, self->img_mnt_fd, SYMLINK1), 0);
+
+	/* create directory */
+	ASSERT_EQ(mkdirat(self->img_mnt_fd, DIR1, 0700), 0);
+
+	/* change ownership of all files to uid 0 */
+	snprintf(self->cmdline, sizeof(self->cmdline), "%s/" IMAGE_ROOT_MNT1,
+		 self->test_dir_path);
+	ASSERT_EQ(chown_r(self->cmdline, 0, 0), 0);
+
+	/* Changing mount properties on a detached mount. */
+	attr.userns_fd	= get_userns_fd(0, 10000, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(self->target1_mnt_fd_detached, "",
+				    AT_EMPTY_PATH, &attr, sizeof(attr)), 0) {
+		TH_LOG("%m - failure: failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       self->target1_mnt_fd_detached, self->test_dir_path);
+	}
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		/* Switch to  user namespace where uid 10000 maps to 0. */
+		ASSERT_EQ(setns(attr.userns_fd, CLONE_NEWUSER), 0);
+		ASSERT_EQ(setresgid(0, 0, 0), 0);
+		ASSERT_EQ(setresuid(0, 0, 0), 0);
+
+		/*
+		 * All files are now owned by uid 0 if accessed through the
+		 * idmapped mountpoint.
+		 */
+		ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached,
+					   FILE1, 0, 0, 0), true);
+
+		ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached,
+					   FILE2, 0, 0, 0), true);
+
+		ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached,
+					   HARDLINK1, 0, 0, 0), true);
+
+		ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached,
+					   CHRDEV1, 0, 0, 0), true);
+
+		ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached,
+					   SYMLINK1, 0, 0, 0), true);
+
+		ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached,
+					   DIR1, 0, 0, 0), true);
+
+		/* Change ownership throught idmapped mountpoint. */
+		ASSERT_EQ(fchownat(self->target1_mnt_fd_detached,
+				   FILE1, 1000, 1000, 0), 0);
+		ASSERT_EQ(fchownat(self->target1_mnt_fd_detached,
+				   FILE2, 1000, 1000, 0), 0);
+		ASSERT_EQ(fchownat(self->target1_mnt_fd_detached,
+				   CHRDEV1, 1000, 1000, 0), 0);
+		ASSERT_EQ(fchownat(self->target1_mnt_fd_detached,
+				   SYMLINK1, 1000, 1000, 0), 0);
+		ASSERT_EQ(fchownat(self->target1_mnt_fd_detached,
+				   DIR1, 1000, 1000, 0), 0);
+
+		/*
+		 * All files are now owned by uid 1000 if accessed through the
+		 * idmapped mountpoint.
+		 */
+		ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached,
+					   FILE1, 0, 1000, 1000), true);
+
+		ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached,
+					   FILE2, 0, 1000, 1000), true);
+
+		ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached,
+					   HARDLINK1, 0, 1000, 1000), true);
+
+		ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached,
+					   CHRDEV1, 0, 1000, 1000), true);
+
+		ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached,
+					   SYMLINK1, 0, 1000, 1000), true);
+
+		ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached,
+					   DIR1, 0, 1000, 1000), true);
+
+		/*
+		 * Try to change to an id for which we do not have an idmapping
+		 * from the idmapped mountpoint. This must fail.
+		 */
+		ASSERT_NE(fchownat(self->target1_mnt_fd_detached,
+				   FILE1, 30000, 30000, 0), 0);
+		ASSERT_NE(fchownat(self->target1_mnt_fd_detached,
+				   FILE2, 30000, 30000, 0), 0);
+		ASSERT_NE(fchownat(self->target1_mnt_fd_detached,
+				   CHRDEV1, 30000, 30000, 0), 0);
+		ASSERT_NE(fchownat(self->target1_mnt_fd_detached,
+				   SYMLINK1, 30000, 30000, 0), 0);
+		ASSERT_NE(fchownat(self->target1_mnt_fd_detached,
+				   DIR1, 30000, 30000, 0), 0);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	/*
+	 * All files are owned by uid 10000 if accessed through the image
+	 * mountpoint.
+	 */
+	ASSERT_EQ(expected_uid_gid(self->img_mnt_fd,
+				  FILE1, 0, 1000, 1000), true);
+
+	ASSERT_EQ(expected_uid_gid(self->img_mnt_fd,
+				   FILE2, 0, 1000, 1000), true);
+
+	ASSERT_EQ(expected_uid_gid(self->img_mnt_fd,
+				   HARDLINK1, 0, 1000, 1000), true);
+
+	ASSERT_EQ(expected_uid_gid(self->img_mnt_fd,
+				   CHRDEV1, 0, 1000, 1000), true);
+
+	ASSERT_EQ(expected_uid_gid(self->img_mnt_fd,
+				   SYMLINK1, 0, 1000, 1000), true);
+
+	ASSERT_EQ(expected_uid_gid(self->img_mnt_fd,
+				   DIR1, 0, 1000, 1000), true);
+
+	ASSERT_EQ(close(file1_fd), 0);
+	ASSERT_EQ(close(attr.userns_fd), 0);
+}
+
+/**
+ * Validate that setting namespaced filesystem capabilities works correctly on
+ * idmapped mounts.
+ */
+TEST_F(core, expected_fscaps_userns)
+{
+	pid_t pid;
+	int file1_fd = -EBADF, file1_fd2 = -EBADF;
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	file1_fd = openat(self->img_mnt_fd, FILE1, O_CREAT | O_EXCL | O_CLOEXEC, 0644);
+	ASSERT_GE(file1_fd, 0);
+	ASSERT_EQ(close(file1_fd), 0);
+
+	/* Changing mount properties on a detached mount. */
+	attr.userns_fd	= get_userns_fd(0, 10000, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(self->target1_mnt_fd_detached, "",
+				    AT_EMPTY_PATH, &attr, sizeof(attr)), 0) {
+		TH_LOG("%m - failure: failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       self->target1_mnt_fd_detached, self->test_dir_path);
+	}
+
+	file1_fd = openat(self->target1_mnt_fd_detached, FILE1, O_RDWR | O_CLOEXEC, 0);
+	ASSERT_GE(file1_fd, 0);
+
+	/*
+	 * uid 10000 maps to 0 in the mount's user namespace and uid 0 has a
+	 * mapping in our current user namespace and in the superblock's
+	 * namespace so this must succeed.
+	 */
+	ASSERT_EQ(set_dummy_vfs_caps(file1_fd, 0, 10000), 0);
+	ASSERT_EQ(expected_dummy_vfs_caps_uid(file1_fd, 10000), true);
+
+	/*
+	 * uid 0 maps to 10000 in the mount's user namespace but uid 10000
+	 * doesn't have a mapping in our current user namespace so this must
+	 * fails.
+	 */
+	ASSERT_NE(set_dummy_vfs_caps(file1_fd, 0, 0), 0);
+
+	file1_fd2 = openat(self->img_mnt_fd, FILE1, O_RDWR | O_CLOEXEC, 0);
+	ASSERT_GE(file1_fd2, 0);
+	ASSERT_EQ(expected_dummy_vfs_caps_uid(file1_fd2, 0), true);
+
+	ASSERT_EQ(fremovexattr(file1_fd, "security.capability"), 0);
+	ASSERT_EQ(expected_dummy_vfs_caps_uid(file1_fd2, 0), false);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		/* Switch to a user namespace where uid 10000 maps to 0. */
+		ASSERT_EQ(setns(attr.userns_fd, CLONE_NEWUSER), 0);
+		ASSERT_EQ(setresgid(0, 0, 0), 0);
+		ASSERT_EQ(setresuid(0, 0, 0), 0);
+
+		/*
+		 * uid 0 maps to 10000 in the mount's user namespace and uid 10000
+		 * has a mapping in our current user namespace and in the superblock's
+		 * user namespace so this must succeed.
+		 */
+		ASSERT_EQ(set_dummy_vfs_caps(file1_fd, 0, 0), 0);
+		ASSERT_EQ(expected_dummy_vfs_caps_uid(file1_fd, 0), true);
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	ASSERT_EQ(expected_dummy_vfs_caps_uid(file1_fd2, 0), true);
+
+	ASSERT_EQ(close(file1_fd), 0);
+	ASSERT_EQ(close(file1_fd2), 0);
+	ASSERT_EQ(close(attr.userns_fd), 0);
+}
+
+/**
+ * Validate that setting filesystem capabilities works correctly on idmapped
+ * mounts where all files on disk are owned by uid and gid 10000.
+ */
+TEST_F(core, expected_fscaps_reverse)
+{
+	int file1_fd = -EBADF, file1_fd2 = -EBADF;
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	file1_fd = openat(self->img_mnt_fd, FILE1, O_CREAT | O_EXCL | O_CLOEXEC, 0644);
+	ASSERT_GE(file1_fd, 0);
+	ASSERT_EQ(close(file1_fd), 0);
+
+	/* Chown all files to an unprivileged user. */
+	snprintf(self->cmdline, sizeof(self->cmdline), "%s/" IMAGE_ROOT_MNT1,
+		 self->test_dir_path);
+	ASSERT_EQ(chown_r(self->cmdline, 10000, 10000), 0);
+
+	/* Changing mount properties on a detached mount. */
+	attr.userns_fd	= get_userns_fd(10000, 0, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(self->target1_mnt_fd_detached, "",
+				    AT_EMPTY_PATH, &attr, sizeof(attr)), 0) {
+		TH_LOG("%m - Failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       self->target1_mnt_fd_detached, self->test_dir_path);
+	}
+	ASSERT_EQ(close(attr.userns_fd), 0);
+
+	ASSERT_EQ(sys_move_mount(self->target1_mnt_fd_detached, "",
+				 self->test_dir_fd, MNT_TARGET1,
+				 MOVE_MOUNT_F_EMPTY_PATH), 0) {
+		TH_LOG("%m - Failed to attached detached mount %d(%s/" IMAGE_FILE1 ") to %s/" MNT_TARGET1,
+		       self->target1_mnt_fd_detached, self->test_dir_path,
+		       self->test_dir_path);
+	}
+
+	snprintf(self->cmdline, sizeof(self->cmdline),
+		 "setcap cap_dac_override,cap_sys_tty_config+ep %s/" MNT_TARGET1 "/" FILE1,
+		 self->test_dir_path);
+	ASSERT_EQ(system(self->cmdline), 0);
+
+	file1_fd = openat(self->target1_mnt_fd_detached, FILE1, O_RDWR | O_CLOEXEC, 0);
+	ASSERT_GE(file1_fd, 0);
+	ASSERT_EQ(set_dummy_vfs_caps(file1_fd, 0, 0), 0);
+	ASSERT_EQ(expected_dummy_vfs_caps_uid(file1_fd, 0), true);
+
+	ASSERT_NE(set_dummy_vfs_caps(file1_fd, 0, 10000), 0);
+
+	file1_fd2 = openat(self->img_mnt_fd, FILE1, O_RDWR | O_CLOEXEC, 0);
+	ASSERT_GE(file1_fd2, 0);
+	ASSERT_EQ(expected_dummy_vfs_caps_uid(file1_fd2, 10000), true);
+
+	ASSERT_EQ(fremovexattr(file1_fd, "security.capability"), 0);
+	ASSERT_EQ(expected_dummy_vfs_caps_uid(file1_fd2, 0), false);
+
+	ASSERT_EQ(close(file1_fd), 0);
+	ASSERT_EQ(close(file1_fd2), 0);
+}
+
+/**
+ * Validate that when the IDMAP_MOUNT_TEST_RUN_SETID environment variable is
+ * set to 1 that we are executed with setid privileges and if set to 0 we are
+ * not. If the env variable isn't set the tests are not run.
+ */
+static void __attribute__((constructor)) setuid_rexec(void)
+{
+	const char *expected_euid_str, *expected_egid_str, *rexec;
+
+	rexec = getenv("IDMAP_MOUNT_TEST_RUN_SETID");
+	/* This is a regular test-suite run. */
+	if (!rexec)
+		return;
+
+	expected_euid_str = getenv("EXPECTED_EUID");
+	expected_egid_str = getenv("EXPECTED_EGID");
+
+	if (expected_euid_str && expected_egid_str) {
+		uid_t expected_euid;
+		gid_t expected_egid;
+
+		expected_euid = atoi(expected_euid_str);
+		expected_egid = atoi(expected_egid_str);
+
+		if (strcmp(rexec, "1") == 0) {
+			/* we're expecting to run setid */
+			if ((getuid() != geteuid()) &&
+			    (expected_euid == geteuid()) &&
+			    (getgid() != getegid()) &&
+			    (expected_egid == getegid()))
+				exit(EXIT_SUCCESS);
+		} else if (strcmp(rexec, "0") == 0) {
+			/* we're expecting to not run setid */
+			if ((getuid() == geteuid()) &&
+			    (expected_euid == geteuid()) &&
+			    (getgid() == getegid()) &&
+			    (expected_egid == getegid()))
+				exit(EXIT_SUCCESS);
+		}
+	}
+
+	exit(EXIT_FAILURE);
+}
+
+/**
+ * Validate that setid transitions are handled correctly on idmapped mounts.
+ */
+TEST_F(core, setid_binaries)
+{
+	int file1_fd = -EBADF, exec_fd = -EBADF;
+	pid_t pid;
+
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	/* create a file to be used as setuid binary */
+	file1_fd = openat(self->img_mnt_fd, FILE1, O_CREAT | O_EXCL | O_RDWR | O_CLOEXEC, 0644);
+	ASSERT_GE(file1_fd, 0);
+
+	/* open our own executable */
+	exec_fd = openat(-EBADF, "/proc/self/exe", O_RDONLY | O_CLOEXEC, 0000);
+	ASSERT_GE(exec_fd, 0);
+
+	/* copy our own executable into the file we created */
+	ASSERT_EQ(fd_to_fd(exec_fd, file1_fd), 0);
+
+	/* Chown all files to an unprivileged user. */
+	snprintf(self->cmdline, sizeof(self->cmdline), "%s/" IMAGE_ROOT_MNT1,
+		 self->test_dir_path);
+	ASSERT_EQ(chown_r(self->cmdline, 10000, 10000), 0);
+
+	/* chown the file to the uid and gid we want to assume */
+	ASSERT_EQ(fchown(file1_fd, 5000, 5000), 0);
+
+	/* set the setid bits and grant execute permissions to the group */
+	ASSERT_EQ(fchmod(file1_fd, S_IXGRP | S_IEXEC | S_ISUID | S_ISGID), 0);
+
+	/* Verify that the sid bits got raised. */
+	ASSERT_EQ(is_setid(self->img_mnt_fd, FILE1, 0), true);
+
+	ASSERT_EQ(close(exec_fd), 0);
+	ASSERT_EQ(close(file1_fd), 0);
+
+	/* Changing mount properties on a detached mount. */
+	attr.userns_fd	= get_userns_fd(0, 10000, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(self->target1_mnt_fd_detached, "",
+				    AT_EMPTY_PATH, &attr, sizeof(attr)), 0) {
+		TH_LOG("%m - failure: failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       self->target1_mnt_fd_detached, self->test_dir_path);
+	}
+	ASSERT_EQ(close(attr.userns_fd), 0);
+
+	/* Verify we run setid binary as uid and gid 5000 from original image mount. */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		static char *envp[] = {
+			"IDMAP_MOUNT_TEST_RUN_SETID=1",
+			"EXPECTED_EUID=5000",
+			"EXPECTED_EGID=5000",
+			NULL,
+		};
+		static char *argv[] = {
+			NULL,
+		};
+
+		ASSERT_EQ(expected_uid_gid(self->img_mnt_fd, FILE1, 0, 5000, 5000), true);
+		ASSERT_EQ(sys_execveat(self->img_mnt_fd, FILE1, argv, envp, 0), 0) {
+			TH_LOG("%m - failure: failed to execute setuid binary");
+		}
+
+		exit(EXIT_FAILURE);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	/*
+	 * A detached mount will have an anonymous mount namespace attached to
+	 * it. This means that we can't execute setid binaries on a detached
+	 * mount because the mnt_may_suid() helper will fail the check_mount()
+	 * part of its check which compares the caller's mount namespace to the
+	 * detached mount's mount namespace. Since by definition an anonymous
+	 * mount namespace is not equale to any mount namespace currently in
+	 * use this can't work. So attach the mount to the filesystem first
+	 * before performing this check.
+	 */
+	ASSERT_EQ(sys_move_mount(self->target1_mnt_fd_detached, "",
+				 self->test_dir_fd, MNT_TARGET1,
+				 MOVE_MOUNT_F_EMPTY_PATH), 0) {
+		TH_LOG("%m - Failed to attached detached mount %d(%s/" IMAGE_FILE1 ") to %s/" MNT_TARGET1,
+		       self->target1_mnt_fd_detached, self->test_dir_path,
+		       self->test_dir_path);
+	}
+
+	/* Verify we run setid binary as uid and gid 10000 from idmapped mount mount. */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		static char *envp[] = {
+			"IDMAP_MOUNT_TEST_RUN_SETID=1",
+			"EXPECTED_EUID=15000",
+			"EXPECTED_EGID=15000",
+			NULL,
+		};
+		static char *argv[] = {
+			NULL,
+		};
+
+		ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached, FILE1, 0, 15000, 15000), true);
+		ASSERT_EQ(sys_execveat(self->target1_mnt_fd_detached, FILE1, argv, envp, 0), 0) {
+			TH_LOG("%m - Failed to execute setuid binary");
+		}
+
+		exit(EXIT_FAILURE);
+	}
+
+	ASSERT_GE(wait_for_pid(pid), 0);
+}
+
+/**
+ * Validate that setid transitions are handled correctly on idmapped
+ * mounts where all files on disk are owned by uid and gid 10000.
+ */
+TEST_F(core, setid_binaries_reverse)
+{
+	int file1_fd = -EBADF, exec_fd = -EBADF;
+	pid_t pid;
+
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	/* create a file to be used as setuid binary */
+	file1_fd = openat(self->img_mnt_fd, FILE1,
+			  O_CREAT | O_EXCL | O_RDWR | O_CLOEXEC, 0644);
+	ASSERT_GE(file1_fd, 0);
+
+	/* open our own executable */
+	exec_fd = openat(-EBADF, "/proc/self/exe", O_RDONLY | O_CLOEXEC, 0000);
+	ASSERT_GE(exec_fd, 0);
+
+	ASSERT_EQ(fd_to_fd(exec_fd, file1_fd), 0);
+	ASSERT_EQ(close(exec_fd), 0);
+	ASSERT_EQ(close(file1_fd), 0);
+
+	/* chown all files to uid and gid 15000 */
+	snprintf(self->cmdline, sizeof(self->cmdline), "%s/" IMAGE_ROOT_MNT1,
+		 self->test_dir_path);
+	ASSERT_EQ(chown_r(self->cmdline, 15000, 15000), 0);
+
+	/* Set setid bits on the newly chowned binary. */
+	ASSERT_EQ(fchmodat(self->img_mnt_fd, FILE1,
+			   S_IXGRP | S_IEXEC | S_ISUID | S_ISGID, 0), 0);
+
+	/* Verify that the sid bits got raised. */
+	ASSERT_EQ(is_setid(self->img_mnt_fd, FILE1, 0), true);
+
+	/* Changing mount properties on a detached mount. */
+	attr.userns_fd	= get_userns_fd(10000, 0, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(self->target1_mnt_fd_detached, "",
+				    AT_EMPTY_PATH, &attr, sizeof(attr)), 0) {
+		TH_LOG("%m - failure: failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       self->target1_mnt_fd_detached, self->test_dir_path);
+	}
+	ASSERT_EQ(close(attr.userns_fd), 0);
+
+	/* Attach the mount to the filesystem. */
+	ASSERT_EQ(sys_move_mount(self->target1_mnt_fd_detached, "",
+				 self->test_dir_fd, MNT_TARGET1,
+				 MOVE_MOUNT_F_EMPTY_PATH), 0) {
+		TH_LOG("%m - failure: failed to attached detached mount %d(%s/" IMAGE_FILE1 ") to %s/" MNT_TARGET1,
+		       self->target1_mnt_fd_detached, self->test_dir_path,
+		       self->test_dir_path);
+	}
+
+	/* Verify we run setid binary as uid and gid 5000 from idmapped mount mount. */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		static char *envp[] = {
+			"IDMAP_MOUNT_TEST_RUN_SETID=1",
+			"EXPECTED_EUID=5000",
+			"EXPECTED_EGID=5000",
+			NULL,
+		};
+		static char *argv[] = {
+			NULL,
+		};
+
+		ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached, FILE1, 0, 5000, 5000), true);
+		ASSERT_EQ(sys_execveat(self->target1_mnt_fd_detached, FILE1, argv, envp, 0), 0) {
+			TH_LOG("%m - Failed to execute setuid binary");
+		}
+
+		exit(EXIT_FAILURE);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	/* Verify we run setid binary as uid and gid 15000 from original image mount. */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		static char *envp[] = {
+			"IDMAP_MOUNT_TEST_RUN_SETID=1",
+			"EXPECTED_EUID=15000",
+			"EXPECTED_EGID=15000",
+			NULL,
+		};
+		static char *argv[] = {
+			NULL,
+		};
+
+		ASSERT_EQ(expected_uid_gid(self->img_mnt_fd, FILE1, 0, 15000, 15000), true);
+		ASSERT_EQ(sys_execveat(self->img_mnt_fd, FILE1, argv, envp, 0), 0) {
+			TH_LOG("%m - Failed to execute setuid binary");
+		}
+
+		exit(EXIT_FAILURE);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+}
+
+/**
+ * Validate that setid transitions are handled correctly on idmapped mounts
+ * running in a user namespace where the uid and gid of the setid binary have
+ * no mapping.
+ */
+TEST_F(core, setid_binaries_userns)
+{
+	int file1_fd = -EBADF, exec_fd = -EBADF;
+	pid_t pid;
+
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	/* create a file to be used as setuid binary */
+	file1_fd = openat(self->img_mnt_fd, FILE1, O_CREAT | O_EXCL | O_RDWR | O_CLOEXEC, 0644);
+	ASSERT_GE(file1_fd, 0);
+
+	/* open our own executable */
+	exec_fd = openat(-EBADF, "/proc/self/exe", O_RDONLY | O_CLOEXEC, 0000);
+	ASSERT_GE(exec_fd, 0);
+
+	/* copy our own executable into the file we created */
+	ASSERT_EQ(fd_to_fd(exec_fd, file1_fd), 0);
+
+	/* chown the file to the uid and gid we want to assume */
+	ASSERT_EQ(fchown(file1_fd, 5000, 5000), 0);
+
+	/* set the setid bits and grant execute permissions to the group */
+	ASSERT_EQ(fchmod(file1_fd, S_IXGRP | S_IEXEC | S_ISUID | S_ISGID), 0);
+
+	/* Verify that the sid bits got raised. */
+	ASSERT_EQ(is_setid(self->img_mnt_fd, FILE1, 0), true);
+
+	ASSERT_EQ(close(exec_fd), 0);
+	ASSERT_EQ(close(file1_fd), 0);
+
+	/* Changing mount properties on a detached mount. */
+	attr.userns_fd	= get_userns_fd(0, 10000, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(self->target1_mnt_fd_detached, "",
+				    AT_EMPTY_PATH, &attr, sizeof(attr)), 0) {
+		TH_LOG("%m - failure: failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       self->target1_mnt_fd_detached, self->test_dir_path);
+	}
+
+	/*
+	 * A detached mount will have an anonymous mount namespace attached to
+	 * it. This means that we can't execute setid binaries on a detached
+	 * mount because the mnt_may_suid() helper will fail the check_mount()
+	 * part of its check which compares the caller's mount namespace to the
+	 * detached mount's mount namespace. Since by definition an anonymous
+	 * mount namespace is not equale to any mount namespace currently in
+	 * use this can't work. So attach the mount to the filesystem first
+	 * before performing this check.
+	 */
+	ASSERT_EQ(sys_move_mount(self->target1_mnt_fd_detached, "",
+				 self->test_dir_fd, MNT_TARGET1,
+				 MOVE_MOUNT_F_EMPTY_PATH), 0) {
+		TH_LOG("%m - Failed to attached detached mount %d(%s/" IMAGE_FILE1 ") to %s/" MNT_TARGET1,
+		       self->target1_mnt_fd_detached, self->test_dir_path,
+		       self->test_dir_path);
+	}
+
+	/* Verify we run setid binary as uid and gid 5000 from original image mount. */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		static char *envp[] = {
+			"IDMAP_MOUNT_TEST_RUN_SETID=1",
+			"EXPECTED_EUID=5000",
+			"EXPECTED_EGID=5000",
+			NULL,
+		};
+		static char *argv[] = {
+			NULL,
+		};
+
+		/* Switch to user namespace where uid 10000 maps to 0. */
+		ASSERT_EQ(setns(attr.userns_fd, CLONE_NEWUSER), 0);
+		ASSERT_EQ(setresgid(0, 0, 0), 0);
+		ASSERT_EQ(setresuid(0, 0, 0), 0);
+
+		ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached, FILE1, 0, 5000, 5000), true);
+		ASSERT_EQ(sys_execveat(self->target1_mnt_fd_detached, FILE1, argv, envp, 0), 0) {
+			TH_LOG("%m - failure: failed to execute setuid binary");
+		}
+
+		exit(EXIT_FAILURE);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	file1_fd = openat(self->img_mnt_fd, FILE1, O_RDWR | O_CLOEXEC, 0644);
+	ASSERT_GE(file1_fd, 0);
+
+	/* chown the file to the uid and gid we want to assume */
+	ASSERT_EQ(fchown(file1_fd, 30000, 30000), 0);
+
+	/* set the setid bits and grant execute permissions to other users */
+	ASSERT_EQ(fchmod(file1_fd, S_IXOTH | S_IEXEC | S_ISUID | S_ISGID), 0);
+
+	ASSERT_EQ(close(file1_fd), 0);
+
+	/*
+	 * Verify that we can't assume a uid and gid of a setid binary for
+	 * which we have no mapping in our user namespace.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		char expected_euid[100];
+		char expected_egid[100];
+		static char *envp[4] = {
+			NULL,
+			NULL,
+			NULL,
+			NULL,
+		};
+		static char *argv[] = {
+			NULL,
+		};
+
+		/* Switch to user namespace where uid 10000 maps to 0. */
+		ASSERT_EQ(setns(attr.userns_fd, CLONE_NEWUSER), 0);
+		ASSERT_EQ(setresgid(0, 0, 0), 0);
+		ASSERT_EQ(setresuid(0, 0, 0), 0);
+
+		envp[0] = "IDMAP_MOUNT_TEST_RUN_SETID=0";
+		snprintf(expected_euid, sizeof(expected_euid), "EXPECTED_EUID=%d", geteuid());
+		envp[1] = expected_euid;
+		snprintf(expected_egid, sizeof(expected_egid), "EXPECTED_egid=%d", getegid());
+		envp[2] = expected_egid;
+		ASSERT_EQ(expected_uid_gid(self->target1_mnt_fd_detached, FILE1, 0, 65534, 65534), true);
+		ASSERT_EQ(sys_execveat(self->target1_mnt_fd_detached, FILE1, argv, envp, 0), 0) {
+			TH_LOG("%m - Failed to execute setuid binary");
+		}
+
+		exit(EXIT_FAILURE);
+	}
+	ASSERT_GE(wait_for_pid(pid), 0);
+
+	ASSERT_EQ(close(attr.userns_fd), 0);
+}
+
+/**
+ * Validate that idmapping a whole mount tree works correctly.
+ */
+TEST_F(core, idmap_mount_tree)
+{
+	int img_fd2 = -EBADF, img_mnt_fd2 = -EBADF, file1_fd = -EBADF,
+	    open_tree_fd = -EBADF;
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	/* create filesystem image */
+	img_fd2 = openat(self->test_dir_fd, IMAGE_FILE2,
+			 O_CREAT | O_WRONLY, 0600);
+	ASSERT_GE(img_fd2, 0);
+	ASSERT_EQ(ftruncate(img_fd2, 1024 * 2048), 0);
+	ASSERT_EQ(close(img_fd2), 0);
+	snprintf(self->cmdline, sizeof(self->cmdline),
+		 "mkfs.ext4 -q %s/" IMAGE_FILE2, self->test_dir_path);
+	ASSERT_EQ(system(self->cmdline), 0);
+
+	/* create mountpoint for image */
+	ASSERT_EQ(mkdirat(self->test_dir_fd, IMAGE_ROOT_MNT2, 0777), 0);
+	snprintf(self->cmdline, sizeof(self->cmdline),
+		 "mount -o loop -t ext4 %s/" IMAGE_FILE2 " %s/" IMAGE_ROOT_MNT2,
+		 self->test_dir_path, self->test_dir_path);
+	ASSERT_EQ(system(self->cmdline), 0);
+	img_mnt_fd2 = openat(self->test_dir_fd, IMAGE_ROOT_MNT2,
+			     O_DIRECTORY | O_CLOEXEC, 0);
+	ASSERT_GE(img_mnt_fd2, 0);
+
+	/* Create files in first filesystem. */
+	file1_fd = openat(self->img_mnt_fd, FILE1, O_CREAT | O_EXCL | O_CLOEXEC, 0644);
+	ASSERT_GE(file1_fd, 0);
+	ASSERT_EQ(close(file1_fd), 0);
+
+	ASSERT_EQ(mknodat(self->img_mnt_fd, FILE2, S_IFREG | 0000, 0), 0);
+
+	ASSERT_EQ(mknodat(self->img_mnt_fd, CHRDEV1, S_IFCHR | 0644,
+			  makedev(5, 1)), 0);
+
+	ASSERT_EQ(linkat(self->img_mnt_fd, FILE1, self->img_mnt_fd, HARDLINK1, 0), 0);
+
+	ASSERT_EQ(symlinkat(FILE2, self->img_mnt_fd, SYMLINK1), 0);
+
+	ASSERT_EQ(mkdirat(self->img_mnt_fd, DIR1, 0700), 0);
+
+	/* Chown all files to 1000. */
+	snprintf(self->cmdline, sizeof(self->cmdline), "%s/" IMAGE_ROOT_MNT1,
+		 self->test_dir_path);
+	ASSERT_EQ(chown_r(self->cmdline, 1000, 1000), 0);
+
+	/* Create files in second filesystem. */
+	file1_fd = openat(img_mnt_fd2, FILE1, O_CREAT | O_EXCL | O_CLOEXEC, 0644);
+	ASSERT_GE(file1_fd, 0);
+	ASSERT_EQ(close(file1_fd), 0);
+
+	ASSERT_EQ(mknodat(img_mnt_fd2, FILE2, S_IFREG | 0000, 0), 0);
+
+	ASSERT_EQ(mknodat(img_mnt_fd2, CHRDEV1, S_IFCHR | 0644,
+			  makedev(5, 1)), 0);
+
+	ASSERT_EQ(linkat(img_mnt_fd2, FILE1, img_mnt_fd2, HARDLINK1, 0), 0);
+
+	ASSERT_EQ(symlinkat(FILE2, img_mnt_fd2, SYMLINK1), 0);
+
+	ASSERT_EQ(mkdirat(img_mnt_fd2, DIR1, 0700), 0);
+	ASSERT_EQ(close(img_mnt_fd2), 0);
+
+	/* Chown all files to 1000. */
+	snprintf(self->cmdline, sizeof(self->cmdline), "%s/" IMAGE_ROOT_MNT2,
+		 self->test_dir_path);
+	ASSERT_EQ(chown_r(self->cmdline, 1000, 1000), 0);
+
+	/* Create detached mount. */
+	open_tree_fd = sys_open_tree(self->test_dir_fd, IMAGE_ROOT_MNT1,
+				     AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW |
+				     OPEN_TREE_CLOEXEC | OPEN_TREE_CLONE |
+				     AT_RECURSIVE);
+	ASSERT_GE(open_tree_fd, 0);
+
+	/* Changing mount properties on a detached mount. */
+	attr.userns_fd	= get_userns_fd(0, 10000, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(open_tree_fd, "",
+				    AT_EMPTY_PATH | AT_RECURSIVE, &attr,
+				    sizeof(attr)), 0) {
+		TH_LOG("%m - Failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       open_tree_fd, self->test_dir_path);
+	}
+	ASSERT_EQ(close(attr.userns_fd), 0);
+
+	/*
+	 * All files created through the original image mountpoint  are owned
+	 * by uid 0.
+	 */
+	ASSERT_EQ(expected_uid_gid(open_tree_fd, FILE1, 0, 11000, 11000), true);
+
+	ASSERT_EQ(expected_uid_gid(open_tree_fd, FILE2, 0, 11000, 11000), true);
+
+	ASSERT_EQ(expected_uid_gid(open_tree_fd, HARDLINK1, 0, 11000, 11000), true);
+
+	ASSERT_EQ(expected_uid_gid(open_tree_fd, CHRDEV1, 0, 11000, 11000), true);
+
+	ASSERT_EQ(expected_uid_gid(open_tree_fd, SYMLINK1, 0, 11000, 11000), true);
+
+	ASSERT_EQ(expected_uid_gid(open_tree_fd, DIR1, 0, 11000, 11000), true);
+
+	/*
+	 * All files created through the original image mountpoint  are owned
+	 * by uid 0.
+	 */
+	ASSERT_EQ(expected_uid_gid(open_tree_fd,
+				   IMAGE_ROOT_MNT2_RELATIVE "/" FILE1, 0, 11000,
+				   11000), true);
+
+	ASSERT_EQ(expected_uid_gid(open_tree_fd,
+				   IMAGE_ROOT_MNT2_RELATIVE "/" FILE2, 0, 11000,
+				   11000), true);
+
+	ASSERT_EQ(expected_uid_gid(open_tree_fd,
+				   IMAGE_ROOT_MNT2_RELATIVE "/" HARDLINK1, 0,
+				   11000, 11000), true);
+
+	ASSERT_EQ(expected_uid_gid(open_tree_fd,
+				   IMAGE_ROOT_MNT2_RELATIVE "/" CHRDEV1, 0,
+				   11000, 11000), true);
+
+	ASSERT_EQ(expected_uid_gid(open_tree_fd,
+				   IMAGE_ROOT_MNT2_RELATIVE "/" SYMLINK1, 0,
+				   11000, 11000), true);
+
+	ASSERT_EQ(expected_uid_gid(open_tree_fd,
+				   IMAGE_ROOT_MNT2_RELATIVE "/" DIR1, 0, 11000,
+				   11000), true);
+
+	ASSERT_EQ(close(open_tree_fd), 0);
+}
+
+/**
+ * Validate that idmapping a mount tree with an unsupported filesystem
+ * somehwere in the tree fails.
+ */
+TEST_F(core, idmap_mount_tree_invalid)
+{
+	int img_fd2 = -EBADF, img_mnt_fd2 = -EBADF, open_tree_fd = -EBADF;
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	/* create filesystem image */
+	img_fd2 = openat(self->test_dir_fd, IMAGE_FILE2,
+			 O_CREAT | O_WRONLY, 0600);
+	ASSERT_GE(img_fd2, 0);
+	ASSERT_EQ(ftruncate(img_fd2, 1024 * 2048), 0);
+	ASSERT_EQ(close(img_fd2), 0);
+	snprintf(self->cmdline, sizeof(self->cmdline),
+		 "mkfs.ext4 -q %s/" IMAGE_FILE2, self->test_dir_path);
+	ASSERT_EQ(system(self->cmdline), 0);
+
+	/* create mountpoint for image */
+	ASSERT_EQ(mkdirat(self->test_dir_fd, IMAGE_ROOT_MNT2, 0777), 0);
+	snprintf(self->cmdline, sizeof(self->cmdline),
+		 "mount -o loop -t ext4 %s/" IMAGE_FILE2 " %s/" IMAGE_ROOT_MNT2,
+		 self->test_dir_path, self->test_dir_path);
+	ASSERT_EQ(system(self->cmdline), 0);
+	img_mnt_fd2 = openat(self->test_dir_fd, IMAGE_ROOT_MNT2,
+			     O_DIRECTORY | O_CLOEXEC, 0);
+	ASSERT_GE(img_mnt_fd2, 0);
+
+	/* create mount of currently unsupported filesystem */
+	ASSERT_EQ(mkdirat(self->test_dir_fd, FILESYSTEM_MOUNT1, 0777), 0);
+	snprintf(self->cmdline, sizeof(self->cmdline), "%s/" FILESYSTEM_MOUNT1, self->test_dir_path);
+	ASSERT_EQ(mount(NULL, self->cmdline, "tmpfs", 0, NULL), 0);
+
+	/* Create detached mount. */
+	open_tree_fd = sys_open_tree(self->test_dir_fd, IMAGE_ROOT_MNT1,
+				     AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW |
+				     OPEN_TREE_CLOEXEC | OPEN_TREE_CLONE |
+				     AT_RECURSIVE);
+	ASSERT_GE(open_tree_fd, 0);
+
+	/* Changing mount properties on a detached mount. */
+	attr.userns_fd	= get_userns_fd(0, 10000, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_NE(sys_mount_setattr(open_tree_fd, "",
+				    AT_EMPTY_PATH | AT_RECURSIVE, &attr,
+				    sizeof(attr)), 0) {
+		TH_LOG("Managed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       open_tree_fd, self->test_dir_path);
+	}
+	ASSERT_EQ(close(attr.userns_fd), 0);
+}
+
+/**
+ * Validate that the sticky bit behaves correctly on regular mounts for unlink
+ * operations.
+ */
+TEST_F(core, sticky_bit_unlink)
+{
+	pid_t pid;
+	int dir_fd = -EBADF;
+
+	/* create directory */
+	ASSERT_EQ(mkdirat(self->img_mnt_fd, DIR1, 0000), 0);
+	dir_fd = openat(self->img_mnt_fd, DIR1, O_DIRECTORY | O_CLOEXEC);
+	ASSERT_GE(dir_fd, 0);
+	ASSERT_EQ(fchown(dir_fd, 0, 0), 0);
+	ASSERT_EQ(fchmod(dir_fd, 0777), 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE1, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE1, 0, 0, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE1, 0644, 0), 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE2, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE2, 2000, 2000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE2, 0644, 0), 0);
+
+	/*
+	 * The sticky bit is not set so we must be able to delete files not
+	 * owned by us.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		ASSERT_EQ(unlinkat(dir_fd, FILE1, 0), 0);
+		ASSERT_EQ(unlinkat(dir_fd, FILE2, 0), 0);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	/* set sticky bit */
+	ASSERT_EQ(fchmod(dir_fd, 0777 | S_ISVTX), 0);
+	/* validate sticky bit is set */
+	ASSERT_EQ(is_sticky(self->img_mnt_fd, DIR1, 0), true);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE1, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE1, 0, 0, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE1, 0644, 0), 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE2, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE2, 2000, 2000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE2, 0644, 0), 0);
+
+	/*
+	 * The sticky bit is set so we must not be able to delete files not
+	 * owned by us.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		ASSERT_NE(unlinkat(dir_fd, FILE1, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+		ASSERT_NE(unlinkat(dir_fd, FILE2, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	/*
+	 * The sticky bit is set and we own the files so we must be able to
+	 * delete the files now.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		/* change ownership */
+		ASSERT_EQ(fchownat(dir_fd, FILE1, 1000, -1, 0), 0);
+		ASSERT_EQ(expected_uid_gid(dir_fd, FILE1, 0, 1000, 0), true);
+		ASSERT_EQ(fchownat(dir_fd, FILE2, 1000, -1, 0), 0);
+		ASSERT_EQ(expected_uid_gid(dir_fd, FILE2, 0, 1000, 2000), true);
+
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		ASSERT_EQ(unlinkat(dir_fd, FILE1, 0), 0);
+		ASSERT_EQ(unlinkat(dir_fd, FILE2, 0), 0);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	/* change uid to unprivileged user */
+	ASSERT_EQ(fchown(dir_fd, 1000, -1), 0);
+	ASSERT_EQ(fchmod(dir_fd, 0777 | S_ISVTX), 0);
+	/* validate sticky bit is set */
+	ASSERT_EQ(is_sticky(self->img_mnt_fd, DIR1, 0), true);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE1, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE1, 0, 0, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE1, 0644, 0), 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE2, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE2, 2000, 2000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE2, 0644, 0), 0);
+
+	/*
+	 * The sticky bit is set and we own the directory so we must be able to
+	 * delete the files now.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		ASSERT_EQ(unlinkat(dir_fd, FILE1, 0), 0);
+		ASSERT_EQ(unlinkat(dir_fd, FILE2, 0), 0);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	ASSERT_EQ(close(dir_fd), 0);
+}
+
+/**
+ * Validate that the sticky bit behaves correctly on idmapped mounts for unlink
+ * operations.
+ */
+TEST_F(core, sticky_bit_unlink_idmapped)
+{
+	pid_t pid;
+	int dir_fd = -EBADF, open_tree_fd = -EBADF;
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	/* create directory */
+	ASSERT_EQ(mkdirat(self->img_mnt_fd, DIR1, 0000), 0);
+	dir_fd = openat(self->img_mnt_fd, DIR1, O_DIRECTORY | O_CLOEXEC);
+	ASSERT_GE(dir_fd, 0);
+	ASSERT_EQ(fchown(dir_fd, 10000, 10000), 0);
+	ASSERT_EQ(fchmod(dir_fd, 0777), 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE1, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE1, 10000, 10000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE1, 0644, 0), 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE2, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE2, 12000, 12000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE2, 0644, 0), 0);
+
+	/* Create detached mount. */
+	open_tree_fd = sys_open_tree(self->img_mnt_fd, DIR1,
+				     AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW |
+				     OPEN_TREE_CLOEXEC | OPEN_TREE_CLONE |
+				     AT_RECURSIVE);
+	ASSERT_GE(open_tree_fd, 0);
+
+	/* Changing mount properties on a detached mount. */
+	attr.userns_fd	= get_userns_fd(10000, 0, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(open_tree_fd, "",
+				    AT_EMPTY_PATH | AT_RECURSIVE, &attr,
+				    sizeof(attr)), 0) {
+		TH_LOG("%m - Failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       open_tree_fd, self->test_dir_path);
+	}
+
+	/*
+	 * The sticky bit is not set so we must be able to delete files not
+	 * owned by us.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		ASSERT_EQ(unlinkat(open_tree_fd, FILE1, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+		ASSERT_EQ(unlinkat(open_tree_fd, FILE2, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	/* set sticky bit */
+	ASSERT_EQ(fchmod(dir_fd, 0777 | S_ISVTX), 0);
+	/* validate sticky bit is set */
+	ASSERT_EQ(is_sticky(self->img_mnt_fd, DIR1, 0), true);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE1, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE1, 10000, 10000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE1, 0644, 0), 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE2, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE2, 12000, 12000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE2, 0644, 0), 0);
+
+	/*
+	 * The sticky bit is set so we must not be able to delete files not
+	 * owned by us.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		ASSERT_NE(unlinkat(open_tree_fd, FILE1, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+		ASSERT_NE(unlinkat(open_tree_fd, FILE2, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	/*
+	 * The sticky bit is set and we own the files so we must be able to
+	 * delete the files now.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		/* change ownership */
+		ASSERT_EQ(fchownat(dir_fd, FILE1, 11000, -1, 0), 0);
+		ASSERT_EQ(expected_uid_gid(dir_fd, FILE1, 0, 11000, 10000), true);
+		ASSERT_EQ(fchownat(dir_fd, FILE2, 11000, -1, 0), 0);
+		ASSERT_EQ(expected_uid_gid(dir_fd, FILE2, 0, 11000, 12000), true);
+
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		ASSERT_EQ(unlinkat(open_tree_fd, FILE1, 0), 0);
+		ASSERT_EQ(unlinkat(open_tree_fd, FILE2, 0), 0);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	/* change uid to unprivileged user */
+	ASSERT_EQ(fchown(dir_fd, 11000, -1), 0);
+	ASSERT_EQ(fchmod(dir_fd, 0777 | S_ISVTX), 0);
+	/* validate sticky bit is set */
+	ASSERT_EQ(is_sticky(self->img_mnt_fd, DIR1, 0), true);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE1, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE1, 10000, 10000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE1, 0644, 0), 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE2, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE2, 12000, 12000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE2, 0644, 0), 0);
+
+	/*
+	 * The sticky bit is set and we own the directory so we must be able to
+	 * delete the files now.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		ASSERT_EQ(unlinkat(open_tree_fd, FILE1, 0), 0);
+		ASSERT_EQ(unlinkat(open_tree_fd, FILE2, 0), 0);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	ASSERT_EQ(close(dir_fd), 0);
+	ASSERT_EQ(close(attr.userns_fd), 0);
+}
+
+/**
+ * Validate that the sticky bit behaves correctly on idmapped mounts for unlink
+ * operations in a user namespace.
+ */
+TEST_F(core, sticky_bit_unlink_idmapped_userns)
+{
+	pid_t pid;
+	int dir_fd = -EBADF, open_tree_fd = -EBADF;
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	/* create directory */
+	ASSERT_EQ(mkdirat(self->img_mnt_fd, DIR1, 0000), 0);
+	dir_fd = openat(self->img_mnt_fd, DIR1, O_DIRECTORY | O_CLOEXEC);
+	ASSERT_GE(dir_fd, 0);
+	ASSERT_EQ(fchown(dir_fd, 0, 0), 0);
+	ASSERT_EQ(fchmod(dir_fd, 0777), 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE1, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE1, 0, 0, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE1, 0644, 0), 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE2, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE2, 2000, 2000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE2, 0644, 0), 0);
+
+	/* Create detached mount. */
+	open_tree_fd = sys_open_tree(self->img_mnt_fd, DIR1,
+				     AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW |
+				     OPEN_TREE_CLOEXEC | OPEN_TREE_CLONE |
+				     AT_RECURSIVE);
+	ASSERT_GE(open_tree_fd, 0);
+
+	/* Changing mount properties on a detached mount. */
+	attr.userns_fd	= get_userns_fd(0, 10000, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(open_tree_fd, "",
+				    AT_EMPTY_PATH | AT_RECURSIVE, &attr,
+				    sizeof(attr)), 0) {
+		TH_LOG("%m - Failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       open_tree_fd, self->test_dir_path);
+	}
+
+	/*
+	 * The sticky bit is not set so we must be able to delete files not
+	 * owned by us.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setns(attr.userns_fd, CLONE_NEWUSER), 0);
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		ASSERT_EQ(unlinkat(open_tree_fd, FILE1, 0), 0);
+		ASSERT_EQ(unlinkat(open_tree_fd, FILE2, 0), 0);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	/* set sticky bit */
+	ASSERT_EQ(fchmod(dir_fd, 0777 | S_ISVTX), 0);
+	/* validate sticky bit is set */
+	ASSERT_EQ(is_sticky(self->img_mnt_fd, DIR1, 0), true);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE1, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE1, 0, 0, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE1, 0644, 0), 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE2, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE2, 2000, 2000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE2, 0644, 0), 0);
+
+	/*
+	 * The sticky bit is set so we must not be able to delete files not
+	 * owned by us.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setns(attr.userns_fd, CLONE_NEWUSER), 0);
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		ASSERT_NE(unlinkat(dir_fd, FILE1, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+		ASSERT_NE(unlinkat(dir_fd, FILE2, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+
+		ASSERT_NE(unlinkat(open_tree_fd, FILE1, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+		ASSERT_NE(unlinkat(open_tree_fd, FILE2, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	/*
+	 * The sticky bit is set and we own the files so we must be able to
+	 * delete the files now.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		/* change ownership */
+		ASSERT_EQ(fchownat(dir_fd, FILE1, 1000, -1, 0), 0);
+		ASSERT_EQ(expected_uid_gid(dir_fd, FILE1, 0, 1000, 0), true);
+		ASSERT_EQ(fchownat(dir_fd, FILE2, 1000, -1, 0), 0);
+		ASSERT_EQ(expected_uid_gid(dir_fd, FILE2, 0, 1000, 2000), true);
+
+		ASSERT_EQ(setns(attr.userns_fd, CLONE_NEWUSER), 0);
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		/* we don't own the file from the original mount */
+		ASSERT_NE(unlinkat(dir_fd, FILE1, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+		ASSERT_NE(unlinkat(dir_fd, FILE2, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+
+		/* we own the file from the idmapped mount */
+		ASSERT_EQ(unlinkat(open_tree_fd, FILE1, 0), 0);
+		ASSERT_EQ(unlinkat(open_tree_fd, FILE2, 0), 0);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	/* change uid to unprivileged user */
+	ASSERT_EQ(fchown(dir_fd, 1000, -1), 0);
+	ASSERT_EQ(fchmod(dir_fd, 0777 | S_ISVTX), 0);
+	/* validate sticky bit is set */
+	ASSERT_EQ(is_sticky(self->img_mnt_fd, DIR1, 0), true);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE1, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE1, 0, 0, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE1, 0644, 0), 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE2, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE2, 2000, 2000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE2, 0644, 0), 0);
+
+	/*
+	 * The sticky bit is set and we own the directory so we must be able to
+	 * delete the files now.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setns(attr.userns_fd, CLONE_NEWUSER), 0);
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		/* we don't own the directory from the original mount */
+		ASSERT_NE(unlinkat(dir_fd, FILE1, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+		ASSERT_NE(unlinkat(dir_fd, FILE2, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+
+		/* we own the file from the idmapped mount */
+		ASSERT_EQ(unlinkat(open_tree_fd, FILE1, 0), 0);
+		ASSERT_EQ(unlinkat(open_tree_fd, FILE2, 0), 0);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	ASSERT_EQ(close(dir_fd), 0);
+	ASSERT_EQ(close(attr.userns_fd), 0);
+}
+
+/**
+ * Validate that the sticky bit behaves correctly on regular mounts for rename
+ * operations.
+ */
+TEST_F(core, sticky_bit_rename)
+{
+	pid_t pid;
+	int dir_fd = -EBADF;
+
+	/* create directory */
+	ASSERT_EQ(mkdirat(self->img_mnt_fd, DIR1, 0000), 0);
+	dir_fd = openat(self->img_mnt_fd, DIR1, O_DIRECTORY | O_CLOEXEC);
+	ASSERT_GE(dir_fd, 0);
+	ASSERT_EQ(fchown(dir_fd, 0, 0), 0);
+	ASSERT_EQ(fchmod(dir_fd, 0777), 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE1, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE1, 0, 0, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE1, 0644, 0), 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE2, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE2, 2000, 2000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE2, 0644, 0), 0);
+
+	/*
+	 * The sticky bit is not set so we must be able to delete files not
+	 * owned by us.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		ASSERT_EQ(renameat2(dir_fd, FILE1, dir_fd, FILE1_RENAME, 0), 0);
+		ASSERT_EQ(renameat2(dir_fd, FILE1_RENAME, dir_fd, FILE1, 0), 0);
+		ASSERT_EQ(renameat2(dir_fd, FILE2, dir_fd, FILE2_RENAME, 0), 0);
+		ASSERT_EQ(renameat2(dir_fd, FILE2_RENAME, dir_fd, FILE2, 0), 0);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	/* set sticky bit */
+	ASSERT_EQ(fchmod(dir_fd, 0777 | S_ISVTX), 0);
+	/* validate sticky bit is set */
+	ASSERT_EQ(is_sticky(self->img_mnt_fd, DIR1, 0), true);
+
+	/*
+	 * The sticky bit is set so we must not be able to delete files not
+	 * owned by us.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		ASSERT_NE(renameat2(dir_fd, FILE1, dir_fd, FILE1_RENAME, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+		ASSERT_NE(renameat2(dir_fd, FILE2, dir_fd, FILE2_RENAME, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	/*
+	 * The sticky bit is set and we own the files so we must be able to
+	 * delete the files now.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		/* change ownership */
+		ASSERT_EQ(fchownat(dir_fd, FILE1, 1000, -1, 0), 0);
+		ASSERT_EQ(expected_uid_gid(dir_fd, FILE1, 0, 1000, 0), true);
+		ASSERT_EQ(fchownat(dir_fd, FILE2, 1000, -1, 0), 0);
+		ASSERT_EQ(expected_uid_gid(dir_fd, FILE2, 0, 1000, 2000), true);
+
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		ASSERT_EQ(renameat2(dir_fd, FILE1, dir_fd, FILE1_RENAME, 0), 0);
+		ASSERT_EQ(renameat2(dir_fd, FILE1_RENAME, dir_fd, FILE1, 0), 0);
+		ASSERT_EQ(renameat2(dir_fd, FILE2, dir_fd, FILE2_RENAME, 0), 0);
+		ASSERT_EQ(renameat2(dir_fd, FILE2_RENAME, dir_fd, FILE2, 0), 0);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	/* change uid to unprivileged user */
+	ASSERT_EQ(fchown(dir_fd, 1000, -1), 0);
+	ASSERT_EQ(fchmod(dir_fd, 0777 | S_ISVTX), 0);
+	/* validate sticky bit is set */
+	ASSERT_EQ(is_sticky(self->img_mnt_fd, DIR1, 0), true);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(fchownat(dir_fd, FILE1, 0, 0, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE1, 0644, 0), 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(fchownat(dir_fd, FILE2, 2000, 2000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE2, 0644, 0), 0);
+
+	/*
+	 * The sticky bit is set and we own the directory so we must be able to
+	 * delete the files now.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+
+		ASSERT_EQ(renameat2(dir_fd, FILE1, dir_fd, FILE1_RENAME, 0), 0);
+		ASSERT_EQ(renameat2(dir_fd, FILE1_RENAME, dir_fd, FILE1, 0), 0);
+		ASSERT_EQ(renameat2(dir_fd, FILE2, dir_fd, FILE2_RENAME, 0), 0);
+		ASSERT_EQ(renameat2(dir_fd, FILE2_RENAME, dir_fd, FILE2, 0), 0);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	ASSERT_EQ(close(dir_fd), 0);
+}
+
+/**
+ * Validate that the sticky bit behaves correctly on idmapped mounts for rename
+ * operations.
+ */
+TEST_F(core, sticky_bit_rename_idmapped)
+{
+	pid_t pid;
+	int dir_fd = -EBADF, open_tree_fd = -EBADF;
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	/* create directory */
+	ASSERT_EQ(mkdirat(self->img_mnt_fd, DIR1, 0000), 0);
+	dir_fd = openat(self->img_mnt_fd, DIR1, O_DIRECTORY | O_CLOEXEC);
+	ASSERT_GE(dir_fd, 0);
+	ASSERT_EQ(fchown(dir_fd, 10000, 10000), 0);
+	ASSERT_EQ(fchmod(dir_fd, 0777), 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE1, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE1, 10000, 10000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE1, 0644, 0), 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE2, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE2, 12000, 12000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE2, 0644, 0), 0);
+
+	/* Create detached mount. */
+	open_tree_fd = sys_open_tree(self->img_mnt_fd, DIR1,
+				     AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW |
+				     OPEN_TREE_CLOEXEC | OPEN_TREE_CLONE |
+				     AT_RECURSIVE);
+	ASSERT_GE(open_tree_fd, 0);
+
+	/* Changing mount properties on a detached mount. */
+	attr.userns_fd	= get_userns_fd(10000, 0, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(open_tree_fd, "",
+				    AT_EMPTY_PATH | AT_RECURSIVE, &attr,
+				    sizeof(attr)), 0) {
+		TH_LOG("%m - Failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       open_tree_fd, self->test_dir_path);
+	}
+
+	/*
+	 * The sticky bit is not set so we must be able to delete files not
+	 * owned by us.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		ASSERT_EQ(renameat2(open_tree_fd, FILE1, open_tree_fd, FILE1_RENAME, 0), 0);
+		ASSERT_EQ(renameat2(open_tree_fd, FILE1_RENAME, open_tree_fd, FILE1, 0), 0);
+		ASSERT_EQ(renameat2(open_tree_fd, FILE2, open_tree_fd, FILE2_RENAME, 0), 0);
+		ASSERT_EQ(renameat2(open_tree_fd, FILE2_RENAME, open_tree_fd, FILE2, 0), 0);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	/* set sticky bit */
+	ASSERT_EQ(fchmod(dir_fd, 0777 | S_ISVTX), 0);
+	/* validate sticky bit is set */
+	ASSERT_EQ(is_sticky(self->img_mnt_fd, DIR1, 0), true);
+
+	ASSERT_EQ(fchownat(dir_fd, FILE1, 10000, 10000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE1, 0644, 0), 0);
+
+	ASSERT_EQ(fchownat(dir_fd, FILE2, 12000, 12000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE2, 0644, 0), 0);
+
+	/*
+	 * The sticky bit is set so we must not be able to delete files not
+	 * owned by us.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		ASSERT_NE(renameat2(open_tree_fd, FILE1, open_tree_fd, FILE1_RENAME, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+		ASSERT_NE(renameat2(open_tree_fd, FILE2, open_tree_fd, FILE2_RENAME, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	/*
+	 * The sticky bit is set and we own the files so we must be able to
+	 * delete the files now.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		/* change ownership */
+		ASSERT_EQ(fchownat(dir_fd, FILE1, 11000, -1, 0), 0);
+		ASSERT_EQ(expected_uid_gid(dir_fd, FILE1, 0, 11000, 10000), true);
+		ASSERT_EQ(fchownat(dir_fd, FILE2, 11000, -1, 0), 0);
+		ASSERT_EQ(expected_uid_gid(dir_fd, FILE2, 0, 11000, 12000), true);
+
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		ASSERT_EQ(renameat2(open_tree_fd, FILE1, open_tree_fd, FILE1_RENAME, 0), 0);
+		ASSERT_EQ(renameat2(open_tree_fd, FILE1_RENAME, open_tree_fd, FILE1, 0), 0);
+		ASSERT_EQ(renameat2(open_tree_fd, FILE2, open_tree_fd, FILE2_RENAME, 0), 0);
+		ASSERT_EQ(renameat2(open_tree_fd, FILE2_RENAME, open_tree_fd, FILE2, 0), 0);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	/* change uid to unprivileged user */
+	ASSERT_EQ(fchown(dir_fd, 11000, -1), 0);
+	ASSERT_EQ(fchmod(dir_fd, 0777 | S_ISVTX), 0);
+	/* validate sticky bit is set */
+	ASSERT_EQ(is_sticky(self->img_mnt_fd, DIR1, 0), true);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(fchownat(dir_fd, FILE1, 10000, 10000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE1, 0644, 0), 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(fchownat(dir_fd, FILE2, 12000, 12000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE2, 0644, 0), 0);
+
+	/*
+	 * The sticky bit is set and we own the directory so we must be able to
+	 * delete the files now.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		ASSERT_EQ(renameat2(open_tree_fd, FILE1, open_tree_fd, FILE1_RENAME, 0), 0);
+		ASSERT_EQ(renameat2(open_tree_fd, FILE1_RENAME, open_tree_fd, FILE1, 0), 0);
+		ASSERT_EQ(renameat2(open_tree_fd, FILE2, open_tree_fd, FILE2_RENAME, 0), 0);
+		ASSERT_EQ(renameat2(open_tree_fd, FILE2_RENAME, open_tree_fd, FILE2, 0), 0);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	ASSERT_EQ(close(dir_fd), 0);
+	ASSERT_EQ(close(attr.userns_fd), 0);
+}
+
+/**
+ * Validate that the sticky bit behaves correctly on idmapped mounts for rename
+ * operations in a user namespace.
+ */
+TEST_F(core, sticky_bit_rename_idmapped_userns)
+{
+	pid_t pid;
+	int dir_fd = -EBADF, open_tree_fd = -EBADF;
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	/* create directory */
+	ASSERT_EQ(mkdirat(self->img_mnt_fd, DIR1, 0000), 0);
+	dir_fd = openat(self->img_mnt_fd, DIR1, O_DIRECTORY | O_CLOEXEC);
+	ASSERT_GE(dir_fd, 0);
+	ASSERT_EQ(fchown(dir_fd, 0, 0), 0);
+	ASSERT_EQ(fchmod(dir_fd, 0777), 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE1, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE1, 0, 0, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE1, 0644, 0), 0);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE2, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE2, 2000, 2000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE2, 0644, 0), 0);
+
+	/* Create detached mount. */
+	open_tree_fd = sys_open_tree(self->img_mnt_fd, DIR1,
+				     AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW |
+				     OPEN_TREE_CLOEXEC | OPEN_TREE_CLONE |
+				     AT_RECURSIVE);
+	ASSERT_GE(open_tree_fd, 0);
+
+	/* Changing mount properties on a detached mount. */
+	attr.userns_fd	= get_userns_fd(0, 10000, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(open_tree_fd, "",
+				    AT_EMPTY_PATH | AT_RECURSIVE, &attr,
+				    sizeof(attr)), 0) {
+		TH_LOG("%m - Failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       open_tree_fd, self->test_dir_path);
+	}
+
+	/*
+	 * The sticky bit is not set so we must be able to delete files not
+	 * owned by us.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setns(attr.userns_fd, CLONE_NEWUSER), 0);
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		ASSERT_EQ(renameat2(open_tree_fd, FILE1, open_tree_fd, FILE1_RENAME, 0), 0);
+		ASSERT_EQ(renameat2(open_tree_fd, FILE1_RENAME, open_tree_fd, FILE1, 0), 0);
+		ASSERT_EQ(renameat2(open_tree_fd, FILE2, open_tree_fd, FILE2_RENAME, 0), 0);
+		ASSERT_EQ(renameat2(open_tree_fd, FILE2_RENAME, open_tree_fd, FILE2, 0), 0);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	/* set sticky bit */
+	ASSERT_EQ(fchmod(dir_fd, 0777 | S_ISVTX), 0);
+	/* validate sticky bit is set */
+	ASSERT_EQ(is_sticky(self->img_mnt_fd, DIR1, 0), true);
+
+	ASSERT_EQ(fchownat(dir_fd, FILE1, 0, 0, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE1, 0644, 0), 0);
+
+	ASSERT_EQ(fchownat(dir_fd, FILE2, 2000, 2000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE2, 0644, 0), 0);
+
+	/*
+	 * The sticky bit is set so we must not be able to delete files not
+	 * owned by us.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setns(attr.userns_fd, CLONE_NEWUSER), 0);
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		ASSERT_NE(renameat2(dir_fd, FILE1, dir_fd, FILE1_RENAME, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+		ASSERT_NE(renameat2(dir_fd, FILE2, dir_fd, FILE2_RENAME, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+
+		ASSERT_NE(renameat2(open_tree_fd, FILE1, open_tree_fd, FILE1_RENAME, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+		ASSERT_NE(renameat2(open_tree_fd, FILE2, open_tree_fd, FILE2_RENAME, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	/*
+	 * The sticky bit is set and we own the files so we must be able to
+	 * delete the files now.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		/* change ownership */
+		ASSERT_EQ(fchownat(dir_fd, FILE1, 1000, -1, 0), 0);
+		ASSERT_EQ(expected_uid_gid(dir_fd, FILE1, 0, 1000, 0), true);
+		ASSERT_EQ(fchownat(dir_fd, FILE2, 1000, -1, 0), 0);
+		ASSERT_EQ(expected_uid_gid(dir_fd, FILE2, 0, 1000, 2000), true);
+
+		ASSERT_EQ(setns(attr.userns_fd, CLONE_NEWUSER), 0);
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		/* we don't own the file from the original mount */
+		ASSERT_NE(renameat2(dir_fd, FILE1, dir_fd, FILE1_RENAME, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+		ASSERT_NE(renameat2(dir_fd, FILE2, dir_fd, FILE2_RENAME, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+
+		/* we own the file from the idmapped mount */
+		ASSERT_EQ(renameat2(open_tree_fd, FILE1, open_tree_fd, FILE1_RENAME, 0), 0);
+		ASSERT_EQ(renameat2(open_tree_fd, FILE1_RENAME, open_tree_fd, FILE1, 0), 0);
+		ASSERT_EQ(renameat2(open_tree_fd, FILE2, open_tree_fd, FILE2_RENAME, 0), 0);
+		ASSERT_EQ(renameat2(open_tree_fd, FILE2_RENAME, open_tree_fd, FILE2, 0), 0);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	/* change uid to unprivileged user */
+	ASSERT_EQ(fchown(dir_fd, 1000, -1), 0);
+	ASSERT_EQ(fchmod(dir_fd, 0777 | S_ISVTX), 0);
+	/* validate sticky bit is set */
+	ASSERT_EQ(is_sticky(self->img_mnt_fd, DIR1, 0), true);
+
+	ASSERT_EQ(fchownat(dir_fd, FILE1, 0, 0, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE1, 0644, 0), 0);
+
+	ASSERT_EQ(fchownat(dir_fd, FILE2, 2000, 2000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE2, 0644, 0), 0);
+
+	/*
+	 * The sticky bit is set and we own the directory so we must be able to
+	 * delete the files now.
+	 */
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setns(attr.userns_fd, CLONE_NEWUSER), 0);
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		/* we don't own the directory from the original mount */
+		ASSERT_NE(renameat2(dir_fd, FILE1, dir_fd, FILE1_RENAME, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+		ASSERT_NE(renameat2(dir_fd, FILE2, dir_fd, FILE2_RENAME, 0), 0);
+		ASSERT_EQ(errno, EPERM);
+
+		/* we own the file from the idmapped mount */
+		ASSERT_EQ(renameat2(open_tree_fd, FILE1, open_tree_fd, FILE1_RENAME, 0), 0);
+		ASSERT_EQ(renameat2(open_tree_fd, FILE1_RENAME, open_tree_fd, FILE1, 0), 0);
+		ASSERT_EQ(renameat2(open_tree_fd, FILE2, open_tree_fd, FILE2_RENAME, 0), 0);
+		ASSERT_EQ(renameat2(open_tree_fd, FILE2_RENAME, open_tree_fd, FILE2, 0), 0);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	ASSERT_EQ(close(dir_fd), 0);
+	ASSERT_EQ(close(attr.userns_fd), 0);
+}
+
+/**
+ * Validate that protected symlinks work correctly.
+ */
+TEST_F(core, follow_symlinks)
+{
+	int dir_fd = -EBADF, fd = -EBADF;
+	pid_t pid;
+
+	if (!symlinks_protected())
+		SKIP(return, "Symlinks are not protected. Skipping test");
+
+	/* create directory */
+	ASSERT_EQ(mkdirat(self->img_mnt_fd, DIR1, 0000), 0);
+	dir_fd = openat(self->img_mnt_fd, DIR1, O_DIRECTORY | O_CLOEXEC);
+	ASSERT_GE(dir_fd, 0);
+	ASSERT_EQ(fchown(dir_fd, 0, 0), 0);
+	ASSERT_EQ(fchmod(dir_fd, 0777 | S_ISVTX), 0);
+	/* validate sticky bit is set */
+	ASSERT_EQ(is_sticky(self->img_mnt_fd, DIR1, 0), true);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE1, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE1, 0, 0, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE1, 0644, 0), 0);
+
+	/* create symlinks */
+	ASSERT_EQ(symlinkat(FILE1, dir_fd, SYMLINK_USER1), 0);
+	ASSERT_EQ(fchownat(dir_fd, SYMLINK_USER1, 0, 0, AT_SYMLINK_NOFOLLOW), 0);
+	ASSERT_EQ(expected_uid_gid(dir_fd, SYMLINK_USER1, AT_SYMLINK_NOFOLLOW, 0, 0), true);
+	ASSERT_EQ(expected_uid_gid(dir_fd, FILE1, 0, 0, 0), true);
+
+	ASSERT_EQ(symlinkat(FILE1, dir_fd, SYMLINK_USER2), 0);
+	ASSERT_EQ(fchownat(dir_fd, SYMLINK_USER2, 1000, 1000, AT_SYMLINK_NOFOLLOW), 0);
+	ASSERT_EQ(expected_uid_gid(dir_fd, SYMLINK_USER2, AT_SYMLINK_NOFOLLOW, 1000, 1000), true);
+	ASSERT_EQ(expected_uid_gid(dir_fd, FILE1, 0, 0, 0), true);
+
+	ASSERT_EQ(symlinkat(FILE1, dir_fd, SYMLINK_USER3), 0);
+	ASSERT_EQ(fchownat(dir_fd, SYMLINK_USER3, 2000, 2000, AT_SYMLINK_NOFOLLOW), 0);
+	ASSERT_EQ(expected_uid_gid(dir_fd, SYMLINK_USER3, AT_SYMLINK_NOFOLLOW, 2000, 2000), true);
+	ASSERT_EQ(expected_uid_gid(dir_fd, FILE1, 0, 0, 0), true);
+
+	/* validate file can be directly read */
+	fd = openat(dir_fd, FILE1, O_RDONLY | O_CLOEXEC, 0);
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(close(fd), 0);
+
+	/* validate file can be read through own symlink */
+	fd = openat(dir_fd, SYMLINK_USER1, O_RDONLY | O_CLOEXEC, 0);
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(close(fd), 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		/* validate file can be directly read */
+		fd = openat(dir_fd, FILE1, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_GE(fd, 0);
+		ASSERT_EQ(close(fd), 0);
+
+		/* validate file can be read through own symlink */
+		fd = openat(dir_fd, SYMLINK_USER2, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_GE(fd, 0);
+		ASSERT_EQ(close(fd), 0);
+
+		/* validate file can be read through root symlink */
+		fd = openat(dir_fd, SYMLINK_USER1, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_GE(fd, 0);
+		ASSERT_EQ(close(fd), 0);
+
+		/* validate file can't be read through other users symlink */
+		fd = openat(dir_fd, SYMLINK_USER3, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_LT(fd, 0);
+		ASSERT_EQ(errno, EACCES);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setresgid(2000, 2000, 2000), 0);
+		ASSERT_EQ(setresuid(2000, 2000, 2000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		/* validate file can be directly read */
+		fd = openat(dir_fd, FILE1, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_GE(fd, 0);
+		ASSERT_EQ(close(fd), 0);
+
+		/* validate file can be read through own symlink */
+		fd = openat(dir_fd, SYMLINK_USER3, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_GE(fd, 0);
+		ASSERT_EQ(close(fd), 0);
+
+		/* validate file can be read through root symlink */
+		fd = openat(dir_fd, SYMLINK_USER1, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_GE(fd, 0);
+		ASSERT_EQ(close(fd), 0);
+
+		/* validate file can't be read through other users symlink */
+		fd = openat(dir_fd, SYMLINK_USER2, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_LT(fd, 0);
+		ASSERT_EQ(errno, EACCES);
+
+		exit(EXIT_SUCCESS);
+	}
+
+	ASSERT_EQ(wait_for_pid(pid), 0);
+}
+
+/**
+ * Validate that protected symlinks work correctly on idmapped mounts.
+ */
+TEST_F(core, follow_symlinks_idmapped)
+{
+	int dir_fd = -EBADF, fd = -EBADF, open_tree_fd = -EBADF;
+	pid_t pid;
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	if (!symlinks_protected())
+		SKIP(return, "Symlinks are not protected. Skipping test");
+
+	/* create directory */
+	ASSERT_EQ(mkdirat(self->img_mnt_fd, DIR1, 0000), 0);
+	dir_fd = openat(self->img_mnt_fd, DIR1, O_DIRECTORY | O_CLOEXEC);
+	ASSERT_GE(dir_fd, 0);
+	ASSERT_EQ(fchown(dir_fd, 10000, 10000), 0);
+	ASSERT_EQ(fchmod(dir_fd, 0777 | S_ISVTX), 0);
+	/* validate sticky bit is set */
+	ASSERT_EQ(is_sticky(self->img_mnt_fd, DIR1, 0), true);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE1, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE1, 10000, 10000, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE1, 0644, 0), 0);
+
+	/* create symlinks */
+	ASSERT_EQ(symlinkat(FILE1, dir_fd, SYMLINK_USER1), 0);
+	ASSERT_EQ(fchownat(dir_fd, SYMLINK_USER1, 10000, 10000, AT_SYMLINK_NOFOLLOW), 0);
+	ASSERT_EQ(expected_uid_gid(dir_fd, SYMLINK_USER1, AT_SYMLINK_NOFOLLOW, 10000, 10000), true);
+	ASSERT_EQ(expected_uid_gid(dir_fd, FILE1, 0, 10000, 10000), true);
+
+	ASSERT_EQ(symlinkat(FILE1, dir_fd, SYMLINK_USER2), 0);
+	ASSERT_EQ(fchownat(dir_fd, SYMLINK_USER2, 11000, 11000, AT_SYMLINK_NOFOLLOW), 0);
+	ASSERT_EQ(expected_uid_gid(dir_fd, SYMLINK_USER2, AT_SYMLINK_NOFOLLOW, 11000, 11000), true);
+	ASSERT_EQ(expected_uid_gid(dir_fd, FILE1, 0, 10000, 10000), true);
+
+	ASSERT_EQ(symlinkat(FILE1, dir_fd, SYMLINK_USER3), 0);
+	ASSERT_EQ(fchownat(dir_fd, SYMLINK_USER3, 12000, 12000, AT_SYMLINK_NOFOLLOW), 0);
+	ASSERT_EQ(expected_uid_gid(dir_fd, SYMLINK_USER3, AT_SYMLINK_NOFOLLOW, 12000, 12000), true);
+	ASSERT_EQ(expected_uid_gid(dir_fd, FILE1, 0, 10000, 10000), true);
+
+	/* Create detached mount. */
+	open_tree_fd = sys_open_tree(self->img_mnt_fd, DIR1,
+				     AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW |
+				     OPEN_TREE_CLOEXEC | OPEN_TREE_CLONE |
+				     AT_RECURSIVE);
+	ASSERT_GE(open_tree_fd, 0);
+
+	/* Changing mount properties on a detached mount. */
+	attr.userns_fd	= get_userns_fd(10000, 0, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(open_tree_fd, "",
+				    AT_EMPTY_PATH | AT_RECURSIVE, &attr,
+				    sizeof(attr)), 0) {
+		TH_LOG("%m - Failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       open_tree_fd, self->test_dir_path);
+	}
+
+	/* validate file can be directly read */
+	fd = openat(open_tree_fd, FILE1, O_RDONLY | O_CLOEXEC, 0);
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(close(fd), 0);
+
+	/* validate file can be read through own symlink */
+	fd = openat(open_tree_fd, SYMLINK_USER1, O_RDONLY | O_CLOEXEC, 0);
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(close(fd), 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		/* validate file can be directly read */
+		fd = openat(open_tree_fd, FILE1, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_GE(fd, 0);
+		ASSERT_EQ(close(fd), 0);
+
+		/* validate file can be read through own symlink */
+		fd = openat(open_tree_fd, SYMLINK_USER2, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_GE(fd, 0);
+		ASSERT_EQ(close(fd), 0);
+
+		/* validate file can be read through root symlink */
+		fd = openat(open_tree_fd, SYMLINK_USER1, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_GE(fd, 0);
+		ASSERT_EQ(close(fd), 0);
+
+		/* validate file can't be read through other users symlink */
+		fd = openat(open_tree_fd, SYMLINK_USER3, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_LT(fd, 0);
+		ASSERT_EQ(errno, EACCES);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setresgid(2000, 2000, 2000), 0);
+		ASSERT_EQ(setresuid(2000, 2000, 2000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		/* validate file can be directly read */
+		fd = openat(open_tree_fd, FILE1, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_GE(fd, 0);
+		ASSERT_EQ(close(fd), 0);
+
+		/* validate file can be read through own symlink */
+		fd = openat(open_tree_fd, SYMLINK_USER3, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_GE(fd, 0);
+		ASSERT_EQ(close(fd), 0);
+
+		/* validate file can be read through root symlink */
+		fd = openat(open_tree_fd, SYMLINK_USER1, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_GE(fd, 0);
+		ASSERT_EQ(close(fd), 0);
+
+		/* validate file can't be read through other users symlink */
+		fd = openat(open_tree_fd, SYMLINK_USER2, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_LT(fd, 0);
+		ASSERT_EQ(errno, EACCES);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	ASSERT_EQ(close(dir_fd), 0);
+	ASSERT_EQ(close(open_tree_fd), 0);
+	ASSERT_EQ(close(attr.userns_fd), 0);
+}
+
+/**
+ * Validate that protected symlinks work correctly on idmapped mounts inside a
+ * user namespace.
+ */
+TEST_F(core, follow_symlinks_idmapped_userns)
+{
+	int dir_fd = -EBADF, fd = -EBADF, open_tree_fd = -EBADF;
+	pid_t pid;
+	struct mount_attr attr = {
+		.attr_set = MOUNT_ATTR_IDMAP,
+	};
+
+	if (!symlinks_protected())
+		SKIP(return, "Symlinks are not protected. Skipping test");
+
+	/* create directory */
+	ASSERT_EQ(mkdirat(self->img_mnt_fd, DIR1, 0000), 0);
+	dir_fd = openat(self->img_mnt_fd, DIR1, O_DIRECTORY | O_CLOEXEC);
+	ASSERT_GE(dir_fd, 0);
+	ASSERT_EQ(fchown(dir_fd, 0, 0), 0);
+	ASSERT_EQ(fchmod(dir_fd, 0777 | S_ISVTX), 0);
+	/* validate sticky bit is set */
+	ASSERT_EQ(is_sticky(self->img_mnt_fd, DIR1, 0), true);
+
+	/* create regular file via mknod */
+	ASSERT_EQ(mknodat(dir_fd, FILE1, S_IFREG | 0000, 0), 0);
+	ASSERT_EQ(fchownat(dir_fd, FILE1, 0, 0, 0), 0);
+	ASSERT_EQ(fchmodat(dir_fd, FILE1, 0644, 0), 0);
+
+	/* create symlinks */
+	ASSERT_EQ(symlinkat(FILE1, dir_fd, SYMLINK_USER1), 0);
+	ASSERT_EQ(fchownat(dir_fd, SYMLINK_USER1, 0, 0, AT_SYMLINK_NOFOLLOW), 0);
+	ASSERT_EQ(expected_uid_gid(dir_fd, SYMLINK_USER1, AT_SYMLINK_NOFOLLOW, 0, 0), true);
+	ASSERT_EQ(expected_uid_gid(dir_fd, FILE1, 0, 0, 0), true);
+
+	ASSERT_EQ(symlinkat(FILE1, dir_fd, SYMLINK_USER2), 0);
+	ASSERT_EQ(fchownat(dir_fd, SYMLINK_USER2, 1000, 1000, AT_SYMLINK_NOFOLLOW), 0);
+	ASSERT_EQ(expected_uid_gid(dir_fd, SYMLINK_USER2, AT_SYMLINK_NOFOLLOW, 1000, 1000), true);
+	ASSERT_EQ(expected_uid_gid(dir_fd, FILE1, 0, 0, 0), true);
+
+	ASSERT_EQ(symlinkat(FILE1, dir_fd, SYMLINK_USER3), 0);
+	ASSERT_EQ(fchownat(dir_fd, SYMLINK_USER3, 2000, 2000, AT_SYMLINK_NOFOLLOW), 0);
+	ASSERT_EQ(expected_uid_gid(dir_fd, SYMLINK_USER3, AT_SYMLINK_NOFOLLOW, 2000, 2000), true);
+	ASSERT_EQ(expected_uid_gid(dir_fd, FILE1, 0, 0, 0), true);
+
+	/* Create detached mount. */
+	open_tree_fd = sys_open_tree(self->img_mnt_fd, DIR1,
+				     AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW |
+				     OPEN_TREE_CLOEXEC | OPEN_TREE_CLONE |
+				     AT_RECURSIVE);
+	ASSERT_GE(open_tree_fd, 0);
+
+	/* Changing mount properties on a detached mount. */
+	attr.userns_fd	= get_userns_fd(0, 10000, 10000);
+	ASSERT_GE(attr.userns_fd, 0);
+	ASSERT_EQ(sys_mount_setattr(open_tree_fd, "",
+				    AT_EMPTY_PATH | AT_RECURSIVE, &attr,
+				    sizeof(attr)), 0) {
+		TH_LOG("%m - Failed to idmap mount %d(%s/" MNT_TARGET1 ")",
+		       open_tree_fd, self->test_dir_path);
+	}
+
+	/* validate file can be directly read */
+	fd = openat(open_tree_fd, FILE1, O_RDONLY | O_CLOEXEC, 0);
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(close(fd), 0);
+
+	/* validate file can be read through own symlink */
+	fd = openat(open_tree_fd, SYMLINK_USER1, O_RDONLY | O_CLOEXEC, 0);
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(close(fd), 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setns(attr.userns_fd, CLONE_NEWUSER), 0);
+		ASSERT_EQ(setresgid(1000, 1000, 1000), 0);
+		ASSERT_EQ(setresuid(1000, 1000, 1000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		/* validate file can be directly read */
+		fd = openat(open_tree_fd, FILE1, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_GE(fd, 0);
+		ASSERT_EQ(close(fd), 0);
+
+		/* validate file can be read through own symlink */
+		fd = openat(open_tree_fd, SYMLINK_USER2, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_GE(fd, 0);
+		ASSERT_EQ(close(fd), 0);
+
+		/* validate file can be read through root symlink */
+		fd = openat(open_tree_fd, SYMLINK_USER1, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_GE(fd, 0);
+		ASSERT_EQ(close(fd), 0);
+
+		/* validate file can't be read through other users symlink */
+		fd = openat(open_tree_fd, SYMLINK_USER3, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_LT(fd, 0);
+		ASSERT_EQ(errno, EACCES);
+
+		exit(EXIT_SUCCESS);
+	}
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		ASSERT_EQ(setns(attr.userns_fd, CLONE_NEWUSER), 0);
+		ASSERT_EQ(setresgid(2000, 2000, 2000), 0);
+		ASSERT_EQ(setresuid(2000, 2000, 2000), 0);
+		ASSERT_EQ(caps_down(), true);
+
+		/* validate file can be directly read */
+		fd = openat(open_tree_fd, FILE1, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_GE(fd, 0);
+		ASSERT_EQ(close(fd), 0);
+
+		/* validate file can be read through own symlink */
+		fd = openat(open_tree_fd, SYMLINK_USER3, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_GE(fd, 0);
+		ASSERT_EQ(close(fd), 0);
+
+		/* validate file can be read through root symlink */
+		fd = openat(open_tree_fd, SYMLINK_USER1, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_GE(fd, 0);
+		ASSERT_EQ(close(fd), 0);
+
+		/* validate file can't be read through other users symlink */
+		fd = openat(open_tree_fd, SYMLINK_USER2, O_RDONLY | O_CLOEXEC, 0);
+		ASSERT_LT(fd, 0);
+		ASSERT_EQ(errno, EACCES);
+
+		exit(EXIT_SUCCESS);
+	}
+
+	ASSERT_EQ(wait_for_pid(pid), 0);
+
+	ASSERT_EQ(close(dir_fd), 0);
+	ASSERT_EQ(close(open_tree_fd), 0);
+	ASSERT_EQ(close(attr.userns_fd), 0);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/idmap_mounts/internal.h b/tools/testing/selftests/idmap_mounts/internal.h
index 252803f35d71..a7d648c305cc 100644
--- a/tools/testing/selftests/idmap_mounts/internal.h
+++ b/tools/testing/selftests/idmap_mounts/internal.h
@@ -4,8 +4,16 @@
 #define __IDMAP_INTERNAL_H
 
 #define _GNU_SOURCE
-
-#include "../kselftest_harness.h"
+#include <errno.h>
+#include <linux/types.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syscall.h>
+#include <sys/types.h>
+#include <unistd.h>
 
 #ifndef __NR_mount_setattr
 	#if defined __alpha__
@@ -26,6 +34,14 @@
 		#define __NR_mount_setattr 441
 	#endif
 
+struct mount_attr {
+	__u64 attr_set;
+	__u64 attr_clr;
+	__u64 propagation;
+	__u64 userns_fd;
+};
+#endif
+
 #ifndef __NR_open_tree
 	#if defined __alpha__
 		#define __NR_open_tree 538
@@ -66,15 +82,6 @@
 	#endif
 #endif
 
-
-struct mount_attr {
-	__u64 attr_set;
-	__u64 attr_clr;
-	__u64 propagation;
-	__u64 userns;
-};
-#endif
-
 #ifndef MOVE_MOUNT_F_EMPTY_PATH
 #define MOVE_MOUNT_F_EMPTY_PATH 0x00000004 /* Empty from path permitted */
 #endif
@@ -95,6 +102,10 @@ struct mount_attr {
 #define AT_RECURSIVE 0x8000 /* Apply to the entire subtree */
 #endif
 
+#ifndef MAKE_PROPAGATION_PRIVATE
+#define MAKE_PROPAGATION_PRIVATE 2
+#endif
+
 static inline int sys_mount_setattr(int dfd, const char *path, unsigned int flags,
 				    struct mount_attr *attr, size_t size)
 {
diff --git a/tools/testing/selftests/idmap_mounts/utils.c b/tools/testing/selftests/idmap_mounts/utils.c
new file mode 100644
index 000000000000..34ea3a7f9393
--- /dev/null
+++ b/tools/testing/selftests/idmap_mounts/utils.c
@@ -0,0 +1,136 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+#include <fcntl.h>
+#include <linux/limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sched.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include "internal.h"
+
+ssize_t read_nointr(int fd, void *buf, size_t count)
+{
+	ssize_t ret;
+
+	do {
+		ret = read(fd, buf, count);
+	} while (ret < 0 && errno == EINTR);
+
+	return ret;
+}
+
+ssize_t write_nointr(int fd, const void *buf, size_t count)
+{
+	ssize_t ret;
+
+	do {
+		ret = write(fd, buf, count);
+	} while (ret < 0 && errno == EINTR);
+
+	return ret;
+}
+
+static int write_file(const char *path, const void *buf, size_t count)
+{
+	int fd;
+	ssize_t ret;
+
+	fd = open(path, O_WRONLY | O_CLOEXEC | O_NOCTTY | O_NOFOLLOW);
+	if (fd < 0)
+		return -1;
+
+	ret = write_nointr(fd, buf, count);
+	close(fd);
+	if (ret < 0 || (size_t)ret != count)
+		return -1;
+
+	return 0;
+}
+
+static int map_ids(pid_t pid, unsigned long nsid, unsigned long hostid,
+		   unsigned long range)
+{
+	char map[100], procfile[256];
+
+	snprintf(procfile, sizeof(procfile), "/proc/%d/setgroups", pid);
+	if (write_file(procfile, "deny", sizeof("deny") - 1) &&
+	    errno != ENOENT)
+		return -1;
+
+	snprintf(procfile, sizeof(procfile), "/proc/%d/uid_map", pid);
+	snprintf(map, sizeof(map), "%lu %lu %lu", nsid, hostid, range);
+	if (write_file(procfile, map, strlen(map)))
+		return -1;
+
+
+	snprintf(procfile, sizeof(procfile), "/proc/%d/gid_map", pid);
+	snprintf(map, sizeof(map), "%lu %lu %lu", nsid, hostid, range);
+	if (write_file(procfile, map, strlen(map)))
+		return -1;
+
+	return 0;
+}
+
+#define __STACK_SIZE (8 * 1024 * 1024)
+pid_t do_clone(int (*fn)(void *), void *arg, int flags)
+{
+	void *stack;
+
+	stack = malloc(__STACK_SIZE);
+	if (!stack)
+		return -ENOMEM;
+
+#ifdef __ia64__
+	return __clone2(fn, stack, __STACK_SIZE, flags | SIGCHLD, arg, NULL);
+#else
+	return clone(fn, stack + __STACK_SIZE, flags | SIGCHLD, arg, NULL);
+#endif
+}
+
+static int get_userns_fd_cb(void *data)
+{
+	return kill(getpid(), SIGSTOP);
+}
+
+int get_userns_fd(unsigned long nsid, unsigned long hostid, unsigned long range)
+{
+	int ret;
+	pid_t pid;
+	char path[256];
+
+	pid = do_clone(get_userns_fd_cb, NULL, CLONE_NEWUSER | CLONE_NEWNS);
+	if (pid < 0)
+		return -errno;
+
+	ret = map_ids(pid, nsid, hostid, range);
+	if (ret < 0)
+		return ret;
+
+	snprintf(path, sizeof(path), "/proc/%d/ns/user", pid);
+	ret = open(path, O_RDONLY | O_CLOEXEC);
+	kill(pid, SIGKILL);
+	return ret;
+}
+
+int wait_for_pid(pid_t pid)
+{
+	int status, ret;
+
+again:
+	ret = waitpid(pid, &status, 0);
+	if (ret == -1) {
+		if (errno == EINTR)
+			goto again;
+
+		return -1;
+	}
+
+	if (!WIFEXITED(status))
+		return -1;
+
+	return WEXITSTATUS(status);
+}
diff --git a/tools/testing/selftests/idmap_mounts/utils.h b/tools/testing/selftests/idmap_mounts/utils.h
new file mode 100644
index 000000000000..1f52997af99d
--- /dev/null
+++ b/tools/testing/selftests/idmap_mounts/utils.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifndef __IDMAP_UTILS_H
+#define __IDMAP_UTILS_H
+
+#define _GNU_SOURCE
+
+#include "../kselftest_harness.h"
+
+extern pid_t do_clone(int (*fn)(void *), void *arg, int flags);
+extern int get_userns_fd(unsigned long nsid, unsigned long hostid,
+			 unsigned long range);
+extern ssize_t read_nointr(int fd, void *buf, size_t count);
+extern int wait_for_pid(pid_t pid);
+extern ssize_t write_nointr(int fd, const void *buf, size_t count);
+
+#endif /* __IDMAP_UTILS_H */
diff --git a/tools/testing/selftests/idmap_mounts/xattr.c b/tools/testing/selftests/idmap_mounts/xattr.c
index 58e88f92f958..0625e3fe53ac 100644
--- a/tools/testing/selftests/idmap_mounts/xattr.c
+++ b/tools/testing/selftests/idmap_mounts/xattr.c
@@ -8,102 +8,9 @@
 #include <linux/limits.h>
 
 #include "internal.h"
+#include "utils.h"
 #include "../kselftest_harness.h"
 
-static ssize_t write_nointr(int fd, const void *buf, size_t count)
-{
-	ssize_t ret;
-
-	do {
-		ret = write(fd, buf, count);
-	} while (ret < 0 && errno == EINTR);
-
-	return ret;
-}
-
-static int write_file(const char *path, const void *buf, size_t count)
-{
-	int fd;
-	ssize_t ret;
-
-	fd = open(path, O_WRONLY | O_CLOEXEC | O_NOCTTY | O_NOFOLLOW);
-	if (fd < 0)
-		return -1;
-
-	ret = write_nointr(fd, buf, count);
-	close(fd);
-	if (ret < 0 || (size_t)ret != count)
-		return -1;
-
-	return 0;
-}
-
-static int map_ids(pid_t pid, unsigned long nsid, unsigned long hostid,
-		   unsigned long range)
-{
-	char map[100], procfile[256];
-
-	snprintf(procfile, sizeof(procfile), "/proc/%d/setgroups", pid);
-	if (write_file(procfile, "deny", sizeof("deny") - 1) &&
-	    errno != ENOENT)
-		return -1;
-
-	snprintf(procfile, sizeof(procfile), "/proc/%d/uid_map", pid);
-	snprintf(map, sizeof(map), "%lu %lu %lu", nsid, hostid, range);
-	if (write_file(procfile, map, strlen(map)))
-		return -1;
-
-
-	snprintf(procfile, sizeof(procfile), "/proc/%d/gid_map", pid);
-	snprintf(map, sizeof(map), "%lu %lu %lu", nsid, hostid, range);
-	if (write_file(procfile, map, strlen(map)))
-		return -1;
-
-	return 0;
-}
-
-#define __STACK_SIZE (8 * 1024 * 1024)
-static pid_t do_clone(int (*fn)(void *), void *arg, int flags)
-{
-	void *stack;
-
-	stack = malloc(__STACK_SIZE);
-	if (!stack)
-		return -ENOMEM;
-
-#ifdef __ia64__
-	return __clone2(fn, stack, __STACK_SIZE, flags | SIGCHLD, arg, NULL);
-#else
-	return clone(fn, stack + __STACK_SIZE, flags | SIGCHLD, arg, NULL);
-#endif
-}
-
-static int get_userns_fd_cb(void *data)
-{
-	return kill(getpid(), SIGSTOP);
-}
-
-static int get_userns_fd(unsigned long nsid, unsigned long hostid,
-			 unsigned long range)
-{
-	int ret;
-	pid_t pid;
-	char path[256];
-
-	pid = do_clone(get_userns_fd_cb, NULL, CLONE_NEWUSER | CLONE_NEWNS);
-	if (pid < 0)
-		return -errno;
-
-	ret = map_ids(pid, nsid, hostid, range);
-	if (ret < 0)
-		return ret;
-
-	snprintf(path, sizeof(path), "/proc/%d/ns/user", pid);
-	ret = open(path, O_RDONLY | O_CLOEXEC);
-	kill(pid, SIGKILL);
-	return ret;
-}
-
 struct run_as_data {
 	int userns;
 	int (*f)(void *data);
@@ -132,25 +39,6 @@ static int run_in_cb(void *data)
 	return rad->f(rad->data);
 }
 
-static int wait_for_pid(pid_t pid)
-{
-	int status, ret;
-
-again:
-	ret = waitpid(pid, &status, 0);
-	if (ret == -1) {
-		if (errno == EINTR)
-			goto again;
-
-		return -1;
-	}
-
-	if (!WIFEXITED(status))
-		return -1;
-
-	return WEXITSTATUS(status);
-}
-
 static int run_in(int userns, int (*f)(void *), void *f_data)
 {
 	pid_t pid;
@@ -245,8 +133,8 @@ TEST_F(ext4_xattr, setattr_didnt_work)
 				 MOVE_MOUNT_F_EMPTY_PATH), 0);
 
 	attr.attr_set = MOUNT_ATTR_IDMAP;
-	attr.userns = get_userns_fd(100010, 100020, 5);
-	ASSERT_GE(attr.userns, 0);
+	attr.userns_fd = get_userns_fd(100010, 100020, 5);
+	ASSERT_GE(attr.userns_fd, 0);
 	ret = sys_mount_setattr(mount_fd, "", AT_EMPTY_PATH | AT_RECURSIVE,
 				    &attr, sizeof(attr));
 	ASSERT_EQ(close(mount_fd), 0);
@@ -261,24 +149,24 @@ TEST_F(ext4_xattr, setattr_didnt_work)
 
 	snprintf(ssb.path, sizeof(ssb.path), "/tmp/ext4/source/foo");
 	ssb.uid = 4294967295;
-	EXPECT_EQ(run_in(attr.userns, getacl_should_be_uid, &ssb), 0);
+	EXPECT_EQ(run_in(attr.userns_fd, getacl_should_be_uid, &ssb), 0);
 
 	snprintf(ssb.path, sizeof(ssb.path), "/tmp/ext4/dest/foo");
 	ssb.uid = 100010;
-	EXPECT_EQ(run_in(attr.userns, getacl_should_be_uid, &ssb), 0);
+	EXPECT_EQ(run_in(attr.userns_fd, getacl_should_be_uid, &ssb), 0);
 
 	/*
 	 * now, dir is owned by someone else in the user namespace, but we can
 	 * still read it because of acls
 	 */
 	ASSERT_EQ(chown("/tmp/ext4/source/foo", 100012, 100012), 0);
-	EXPECT_EQ(run_in(attr.userns, ls_path, "/tmp/ext4/dest/foo"), 0);
+	EXPECT_EQ(run_in(attr.userns_fd, ls_path, "/tmp/ext4/dest/foo"), 0);
 
 	/*
 	 * if we delete the acls, the ls should fail because it's 700.
 	 */
 	ASSERT_EQ(system("setfacl --remove-all /tmp/ext4/source/foo"), 0);
-	EXPECT_NE(run_in(attr.userns, ls_path, "/tmp/ext4/dest/foo"), 0);
+	EXPECT_NE(run_in(attr.userns_fd, ls_path, "/tmp/ext4/dest/foo"), 0);
 }
 
 TEST_HARNESS_MAIN
-- 
2.29.2

Powered by blists - more mailing lists