aboutsummaryrefslogtreecommitdiff
path: root/sys/contrib/openzfs/tests/zfs-tests/cmd/idmap_util.c
diff options
context:
space:
mode:
Diffstat (limited to 'sys/contrib/openzfs/tests/zfs-tests/cmd/idmap_util.c')
-rw-r--r--sys/contrib/openzfs/tests/zfs-tests/cmd/idmap_util.c808
1 files changed, 808 insertions, 0 deletions
diff --git a/sys/contrib/openzfs/tests/zfs-tests/cmd/idmap_util.c b/sys/contrib/openzfs/tests/zfs-tests/cmd/idmap_util.c
new file mode 100644
index 000000000000..49483cbaa421
--- /dev/null
+++ b/sys/contrib/openzfs/tests/zfs-tests/cmd/idmap_util.c
@@ -0,0 +1,808 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License (the "License").
+ * You may not use this file except in compliance with the License.
+ *
+ * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+ * or https://opensource.org/licenses/CDDL-1.0.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information: Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ */
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+#include <linux/types.h>
+#include <sys/wait.h>
+#include <sys/stat.h>
+#include <sys/mount.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sched.h>
+#include <syscall.h>
+#include <sys/socket.h>
+
+#include <sys/list.h>
+
+#ifndef UINT_MAX
+#define UINT_MAX 4294967295U
+#endif
+
+#ifndef __NR_Linux
+#if defined __alpha__
+#define __NR_Linux 110
+#elif defined _MIPS_SIM
+#if _MIPS_SIM == _MIPS_SIM_ABI32
+#define __NR_Linux 4000
+#endif
+#if _MIPS_SIM == _MIPS_SIM_NABI32
+#define __NR_Linux 6000
+#endif
+#if _MIPS_SIM == _MIPS_SIM_ABI64
+#define __NR_Linux 5000
+#endif
+#elif defined __ia64__
+#define __NR_Linux 1024
+#else
+#define __NR_Linux 0
+#endif
+#endif
+
+#ifndef __NR_mount_setattr
+#define __NR_mount_setattr (442 + __NR_Linux)
+#endif
+
+#ifndef __NR_open_tree
+#define __NR_open_tree (428 + __NR_Linux)
+#endif
+
+#ifndef __NR_move_mount
+#define __NR_move_mount (429 + __NR_Linux)
+#endif
+
+#ifndef MNT_DETACH
+#define MNT_DETACH 2
+#endif
+
+#ifndef MOVE_MOUNT_F_EMPTY_PATH
+#define MOVE_MOUNT_F_EMPTY_PATH 0x00000004
+#endif
+
+#ifndef MOUNT_ATTR_IDMAP
+#define MOUNT_ATTR_IDMAP 0x00100000
+#endif
+
+#ifndef OPEN_TREE_CLONE
+#define OPEN_TREE_CLONE 1
+#endif
+
+#ifndef OPEN_TREE_CLOEXEC
+#define OPEN_TREE_CLOEXEC O_CLOEXEC
+#endif
+
+#ifndef AT_RECURSIVE
+#define AT_RECURSIVE 0x8000
+#endif
+
+typedef struct {
+ __u64 attr_set;
+ __u64 attr_clr;
+ __u64 propagation;
+ __u64 userns_fd;
+} mount_attr_t;
+
+static inline int
+sys_mount_setattr(int dfd, const char *path, unsigned int flags,
+ mount_attr_t *attr, size_t size)
+{
+ return (syscall(__NR_mount_setattr, dfd, path, flags, attr, size));
+}
+
+static inline int
+sys_open_tree(int dfd, const char *filename, unsigned int flags)
+{
+ return (syscall(__NR_open_tree, dfd, filename, flags));
+}
+
+static inline int sys_move_mount(int from_dfd, const char *from_pathname,
+ int to_dfd, const char *to_pathname, unsigned int flags)
+{
+ return (syscall(__NR_move_mount, from_dfd, from_pathname, to_dfd,
+ to_pathname, flags));
+}
+
+typedef enum idmap_type_t {
+ TYPE_UID,
+ TYPE_GID,
+ TYPE_BOTH
+} idmap_type_t;
+
+struct idmap_entry {
+ __u32 first;
+ __u32 lower_first;
+ __u32 count;
+ idmap_type_t type;
+ list_node_t node;
+};
+
+static void
+log_msg(const char *msg, ...)
+{
+ va_list ap;
+
+ va_start(ap, msg);
+ vfprintf(stderr, msg, ap);
+ fputc('\n', stderr);
+ va_end(ap);
+}
+
+#define log_errno(msg, args...) \
+ do { \
+ log_msg("%s:%d:%s: [%m] " msg, __FILE__, __LINE__,\
+ __FUNCTION__, ##args); \
+ } while (0)
+
+/*
+ * Parse the idmapping in the following format
+ * and add to the list:
+ *
+ * u:nsid_first:hostid_first:count
+ * g:nsid_first:hostid_first:count
+ * b:nsid_first:hostid_first:count
+ *
+ * The delimiter can be : or space character.
+ *
+ * Return:
+ * 0 if success
+ * ENOMEM if out of memory
+ * EINVAL if wrong arg or input
+ */
+static int
+parse_idmap_entry(list_t *head, char *input)
+{
+ char *token, *savedptr = NULL;
+ struct idmap_entry *entry;
+ unsigned long ul;
+ char *delimiter = (char *)": ";
+ char c;
+
+ if (!input || !head)
+ return (EINVAL);
+ entry = malloc(sizeof (*entry));
+ if (!entry)
+ return (ENOMEM);
+
+ token = strtok_r(input, delimiter, &savedptr);
+ if (token)
+ c = token[0];
+ if (!token || (c != 'b' && c != 'u' && c != 'g'))
+ goto errout;
+ entry->type = (c == 'b') ? TYPE_BOTH :
+ ((c == 'u') ? TYPE_UID : TYPE_GID);
+
+ token = strtok_r(NULL, delimiter, &savedptr);
+ if (!token)
+ goto errout;
+ ul = strtoul(token, NULL, 10);
+ if (ul > UINT_MAX || errno != 0)
+ goto errout;
+ entry->first = (__u32)ul;
+
+ token = strtok_r(NULL, delimiter, &savedptr);
+ if (!token)
+ goto errout;
+ ul = strtoul(token, NULL, 10);
+ if (ul > UINT_MAX || errno != 0)
+ goto errout;
+ entry->lower_first = (__u32)ul;
+
+ token = strtok_r(NULL, delimiter, &savedptr);
+ if (!token)
+ goto errout;
+ ul = strtoul(token, NULL, 10);
+ if (ul > UINT_MAX || errno != 0)
+ goto errout;
+ entry->count = (__u32)ul;
+
+ list_insert_tail(head, entry);
+
+ return (0);
+
+errout:
+ free(entry);
+ return (EINVAL);
+}
+
+/*
+ * Release all the entries in the list
+ */
+static void
+free_idmap(list_t *head)
+{
+ struct idmap_entry *entry;
+
+ while ((entry = list_remove_head(head)) != NULL)
+ free(entry);
+ /* list_destroy() to be done by the caller */
+}
+
+/*
+ * Write all bytes in the buffer to fd
+ */
+static ssize_t
+write_buf(int fd, const char *buf, size_t buf_size)
+{
+ ssize_t written, total_written = 0;
+ size_t remaining = buf_size;
+ char *position = (char *)buf;
+
+ for (;;) {
+ written = write(fd, position, remaining);
+ if (written < 0 && errno == EINTR)
+ continue;
+ if (written < 0) {
+ log_errno("write");
+ return (written);
+ }
+ total_written += written;
+ if (total_written == buf_size)
+ break;
+ remaining -= written;
+ position += written;
+ }
+
+ return (total_written);
+}
+
+/*
+ * Read data from file into buffer
+ */
+static ssize_t
+read_buf(int fd, char *buf, size_t buf_size)
+{
+ int ret;
+ for (;;) {
+ ret = read(fd, buf, buf_size);
+ if (ret < 0 && errno == EINTR)
+ continue;
+ break;
+ }
+ if (ret < 0)
+ log_errno("read");
+ return (ret);
+}
+
+/*
+ * Write idmap of the given type in the buffer to the
+ * process' uid_map or gid_map proc file.
+ *
+ * Return:
+ * 0 if success
+ * errno if there's any error
+ */
+static int
+write_idmap(pid_t pid, char *buf, size_t buf_size, idmap_type_t type)
+{
+ char path[PATH_MAX];
+ int fd = -EBADF;
+ int ret;
+
+ (void) snprintf(path, sizeof (path), "/proc/%d/%cid_map",
+ pid, type == TYPE_UID ? 'u' : 'g');
+ fd = open(path, O_WRONLY | O_CLOEXEC);
+ if (fd < 0) {
+ ret = errno;
+ log_errno("open(%s)", path);
+ goto out;
+ }
+ ret = write_buf(fd, buf, buf_size);
+ if (ret < 0)
+ ret = errno;
+ else
+ ret = 0;
+out:
+ if (fd >= 0)
+ close(fd);
+ return (ret);
+}
+
+/*
+ * Write idmap info in the list to the process
+ * user namespace, i.e. its /proc/<pid>/uid_map
+ * and /proc/<pid>/gid_map file.
+ *
+ * Return:
+ * 0 if success
+ * errno if it fails
+ */
+static int
+write_pid_idmaps(pid_t pid, list_t *head)
+{
+ char *buf_uids, *buf_gids;
+ char *curr_bufu, *curr_bufg;
+ /* max 4k to be allowed for each map */
+ int size_buf_uids = 4096, size_buf_gids = 4096;
+ struct idmap_entry *entry;
+ int uid_filled, gid_filled;
+ int ret = 0;
+ int has_uids = 0, has_gids = 0;
+ size_t buf_size;
+
+ buf_uids = malloc(size_buf_uids);
+ if (!buf_uids)
+ return (ENOMEM);
+ buf_gids = malloc(size_buf_gids);
+ if (!buf_gids) {
+ free(buf_uids);
+ return (ENOMEM);
+ }
+ curr_bufu = buf_uids;
+ curr_bufg = buf_gids;
+
+ for (entry = list_head(head); entry; entry = list_next(head, entry)) {
+ if (entry->type == TYPE_UID || entry->type == TYPE_BOTH) {
+ uid_filled = snprintf(curr_bufu, size_buf_uids,
+ "%u %u %u\n", entry->first, entry->lower_first,
+ entry->count);
+ if (uid_filled <= 0 || uid_filled >= size_buf_uids) {
+ ret = E2BIG;
+ goto out;
+ }
+ curr_bufu += uid_filled;
+ size_buf_uids -= uid_filled;
+ has_uids = 1;
+ }
+ if (entry->type == TYPE_GID || entry->type == TYPE_BOTH) {
+ gid_filled = snprintf(curr_bufg, size_buf_gids,
+ "%u %u %u\n", entry->first, entry->lower_first,
+ entry->count);
+ if (gid_filled <= 0 || gid_filled >= size_buf_gids) {
+ ret = E2BIG;
+ goto out;
+ }
+ curr_bufg += gid_filled;
+ size_buf_gids -= gid_filled;
+ has_gids = 1;
+ }
+ }
+ if (has_uids) {
+ buf_size = curr_bufu - buf_uids;
+ ret = write_idmap(pid, buf_uids, buf_size, TYPE_UID);
+ if (ret)
+ goto out;
+ }
+ if (has_gids) {
+ buf_size = curr_bufg - buf_gids;
+ ret = write_idmap(pid, buf_gids, buf_size, TYPE_GID);
+ }
+
+out:
+ free(buf_uids);
+ free(buf_gids);
+ return (ret);
+}
+
+/*
+ * Wait for the child process to exit
+ * and reap it.
+ *
+ * Return:
+ * process exit code if available
+ */
+static int
+wait_for_pid(pid_t pid)
+{
+ int status;
+ int ret;
+
+ for (;;) {
+ ret = waitpid(pid, &status, 0);
+ if (ret < 0) {
+ if (errno == EINTR)
+ continue;
+ return (EXIT_FAILURE);
+ }
+ break;
+ }
+ if (!WIFEXITED(status))
+ return (EXIT_FAILURE);
+ return (WEXITSTATUS(status));
+}
+
+/*
+ * Get the file descriptor of the process user namespace
+ * given its pid.
+ *
+ * Return:
+ * fd if success
+ * -1 if it fails
+ */
+static int
+userns_fd_from_pid(pid_t pid)
+{
+ int fd;
+ char path[PATH_MAX];
+
+ (void) snprintf(path, sizeof (path), "/proc/%d/ns/user", pid);
+ fd = open(path, O_RDONLY | O_CLOEXEC);
+ if (fd < 0)
+ log_errno("open(%s)", path);
+ return (fd);
+}
+
+/*
+ * Get the user namespace file descriptor given a list
+ * of idmap info.
+ *
+ * Return:
+ * fd if success
+ * -errno if it fails
+ */
+static int
+userns_fd_from_idmap(list_t *head)
+{
+ pid_t pid;
+ int ret, fd;
+ int fds[2];
+ char c;
+ int saved_errno = 0;
+
+ /* socketpair for bidirectional communication */
+ ret = socketpair(AF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, fds);
+ if (ret) {
+ log_errno("socketpair");
+ return (-errno);
+ }
+
+ pid = fork();
+ if (pid < 0) {
+ log_errno("fork");
+ fd = -errno;
+ goto out;
+ }
+
+ if (pid == 0) {
+ /* child process */
+ ret = unshare(CLONE_NEWUSER);
+ if (ret == 0) {
+ /* notify the parent of success */
+ ret = write_buf(fds[1], "1", 1);
+ if (ret < 0)
+ saved_errno = errno;
+ else {
+ /*
+ * Until the parent has written to idmap,
+ * we cannot exit, otherwise the defunct
+ * process is owned by the real root, writing
+ * to its idmap ends up with EPERM in the
+ * context of a user ns
+ */
+ ret = read_buf(fds[1], &c, 1);
+ if (ret < 0)
+ saved_errno = errno;
+ }
+ } else {
+ saved_errno = errno;
+ log_errno("unshare");
+ ret = write_buf(fds[1], "0", 1);
+ if (ret < 0)
+ saved_errno = errno;
+ }
+ exit(saved_errno);
+ }
+
+ /* parent process */
+ ret = read_buf(fds[0], &c, 1);
+ if (ret == 1 && c == '1') {
+ ret = write_pid_idmaps(pid, head);
+ if (!ret) {
+ fd = userns_fd_from_pid(pid);
+ if (fd < 0)
+ fd = -errno;
+ } else {
+ fd = -ret;
+ }
+ /* Let child know it can exit */
+ (void) write_buf(fds[0], "1", 1);
+ } else {
+ fd = -EBADF;
+ }
+ (void) wait_for_pid(pid);
+out:
+ close(fds[0]);
+ close(fds[1]);
+ return (fd);
+}
+
+/*
+ * Check if the operating system supports idmapped mount on the
+ * given path or not.
+ *
+ * Return:
+ * true if supported
+ * false if not supported
+ */
+static bool
+is_idmap_supported(char *path)
+{
+ list_t head;
+ int ret;
+ int tree_fd = -EBADF, path_fd = -EBADF;
+ mount_attr_t attr = {
+ .attr_set = MOUNT_ATTR_IDMAP,
+ .userns_fd = -EBADF,
+ };
+
+ /* strtok_r() won't be happy with a const string */
+ /* To check if idmapped mount can be done in a user ns, map 0 to 0 */
+ char *input = strdup("b:0:0:1");
+
+ if (!input) {
+ errno = ENOMEM;
+ log_errno("strdup");
+ return (false);
+ }
+
+ list_create(&head, sizeof (struct idmap_entry),
+ offsetof(struct idmap_entry, node));
+ ret = parse_idmap_entry(&head, input);
+ if (ret) {
+ errno = ret;
+ log_errno("parse_idmap_entry(%s)", input);
+ goto out1;
+ }
+ ret = userns_fd_from_idmap(&head);
+ if (ret < 0)
+ goto out1;
+ attr.userns_fd = ret;
+ ret = openat(-EBADF, path, O_DIRECTORY | O_CLOEXEC);
+ if (ret < 0) {
+ log_errno("openat(%s)", path);
+ goto out;
+ }
+ path_fd = ret;
+ ret = sys_open_tree(path_fd, "", AT_EMPTY_PATH | AT_NO_AUTOMOUNT |
+ AT_SYMLINK_NOFOLLOW | OPEN_TREE_CLOEXEC | OPEN_TREE_CLONE);
+ if (ret < 0) {
+ log_errno("sys_open_tree");
+ goto out;
+ }
+ tree_fd = ret;
+ ret = sys_mount_setattr(tree_fd, "", AT_EMPTY_PATH, &attr,
+ sizeof (attr));
+ if (ret < 0) {
+ log_errno("sys_mount_setattr");
+ }
+out:
+ close(attr.userns_fd);
+out1:
+ free_idmap(&head);
+ list_destroy(&head);
+ if (tree_fd >= 0)
+ close(tree_fd);
+ if (path_fd >= 0)
+ close(path_fd);
+ free(input);
+ return (ret == 0);
+}
+
+/*
+ * Check if the given path is a mount point or not.
+ *
+ * Return:
+ * true if it is
+ * false otherwise
+ */
+static bool
+is_mountpoint(char *path)
+{
+ char *parent;
+ struct stat st_me, st_parent;
+ bool ret;
+
+ parent = malloc(strlen(path)+4);
+ if (!parent) {
+ errno = ENOMEM;
+ log_errno("malloc");
+ return (false);
+ }
+ strcat(strcpy(parent, path), "/..");
+ if (lstat(path, &st_me) != 0 ||
+ lstat(parent, &st_parent) != 0)
+ ret = false;
+ else
+ if (st_me.st_dev != st_parent.st_dev ||
+ st_me.st_ino == st_parent.st_ino)
+ ret = true;
+ else
+ ret = false;
+ free(parent);
+ return (ret);
+}
+
+/*
+ * Remount the source on the new target folder with the given
+ * list of idmap info. If target is NULL, the source will be
+ * unmounted and then remounted if it is a mountpoint, otherwise
+ * no unmount is done, the source is simply idmap remounted.
+ *
+ * Return:
+ * 0 if success
+ * -errno otherwise
+ */
+static int
+do_idmap_mount(list_t *idmap, char *source, char *target, int flags)
+{
+ int ret;
+ int tree_fd = -EBADF, source_fd = -EBADF;
+ mount_attr_t attr = {
+ .attr_set = MOUNT_ATTR_IDMAP,
+ .userns_fd = -EBADF,
+ };
+
+ ret = userns_fd_from_idmap(idmap);
+ if (ret < 0)
+ goto out1;
+ attr.userns_fd = ret;
+ ret = openat(-EBADF, source, O_DIRECTORY | O_CLOEXEC);
+ if (ret < 0) {
+ ret = -errno;
+ log_errno("openat(%s)", source);
+ goto out;
+ }
+ source_fd = ret;
+ ret = sys_open_tree(source_fd, "", AT_EMPTY_PATH | AT_NO_AUTOMOUNT |
+ AT_SYMLINK_NOFOLLOW | OPEN_TREE_CLOEXEC | OPEN_TREE_CLONE | flags);
+ if (ret < 0) {
+ ret = -errno;
+ log_errno("sys_open_tree");
+ goto out;
+ }
+ tree_fd = ret;
+ ret = sys_mount_setattr(tree_fd, "", AT_EMPTY_PATH | flags, &attr,
+ sizeof (attr));
+ if (ret < 0) {
+ ret = -errno;
+ log_errno("sys_mount_setattr");
+ goto out;
+ }
+ if (target == NULL && is_mountpoint(source)) {
+ ret = umount2(source, MNT_DETACH);
+ if (ret < 0) {
+ ret = -errno;
+ log_errno("umount2(%s)", source);
+ goto out;
+ }
+ }
+ ret = sys_move_mount(tree_fd, "", -EBADF, target == NULL ?
+ source : target, MOVE_MOUNT_F_EMPTY_PATH);
+ if (ret < 0) {
+ ret = -errno;
+ log_errno("sys_move_mount(%s)", target == NULL ?
+ source : target);
+ }
+out:
+ close(attr.userns_fd);
+out1:
+ if (tree_fd >= 0)
+ close(tree_fd);
+ if (source_fd >= 0)
+ close(source_fd);
+ return (ret);
+}
+
+static void
+print_usage(char *argv[])
+{
+ fprintf(stderr, "Usage: %s [-r] [-c] [-m <idmap1>] [-m <idmap2>]" \
+ " ... [<source>] [<target>]\n", argv[0]);
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -r Recursively do idmapped mount.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -c Checks if idmapped mount is supported " \
+ "on the <source> by the operating system or not.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " -m <idmap> to specify the idmap info, " \
+ "in the following format:\n");
+ fprintf(stderr, " <id_type>:<nsid_first>:<hostid_first>:<count>\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, " <id_type> can be either of 'b', 'u', and 'g'.\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, "The <source> folder will be mounted at <target> " \
+ "with the provided idmap information.\nIf no <target> is " \
+ "specified, and <source> is a mount point, " \
+ "then <source> will be unmounted and then remounted.\n");
+}
+
+int
+main(int argc, char *argv[])
+{
+ int opt;
+ list_t idmap_head;
+ int check_supported = 0;
+ int ret = EXIT_SUCCESS;
+ char *source = NULL, *target = NULL;
+ int flags = 0;
+
+ list_create(&idmap_head, sizeof (struct idmap_entry),
+ offsetof(struct idmap_entry, node));
+
+ while ((opt = getopt(argc, argv, "rcm:")) != -1) {
+ switch (opt) {
+ case 'r':
+ flags |= AT_RECURSIVE;
+ break;
+ case 'c':
+ check_supported = 1;
+ break;
+ case 'm':
+ ret = parse_idmap_entry(&idmap_head, optarg);
+ if (ret) {
+ errno = ret;
+ log_errno("parse_idmap_entry(%s)", optarg);
+ ret = EXIT_FAILURE;
+ goto out;
+ }
+ break;
+ default:
+ print_usage(argv);
+ exit(EXIT_FAILURE);
+ }
+ }
+
+ if (check_supported == 0 && list_is_empty(&idmap_head)) {
+ print_usage(argv);
+ ret = EXIT_FAILURE;
+ goto out;
+ }
+
+ if (optind >= argc) {
+ fprintf(stderr, "Expected to have <source>, <target>.\n");
+ print_usage(argv);
+ ret = EXIT_FAILURE;
+ goto out;
+ }
+
+ source = argv[optind];
+ if (optind < (argc - 1)) {
+ target = argv[optind + 1];
+ }
+
+ if (check_supported) {
+ free_idmap(&idmap_head);
+ list_destroy(&idmap_head);
+ if (is_idmap_supported(source)) {
+ printf("idmapped mount is supported on [%s].\n",
+ source);
+ return (EXIT_SUCCESS);
+ } else {
+ printf("idmapped mount is NOT supported.\n");
+ return (EXIT_FAILURE);
+ }
+ }
+
+ ret = do_idmap_mount(&idmap_head, source, target, flags);
+ if (ret)
+ ret = EXIT_FAILURE;
+out:
+ free_idmap(&idmap_head);
+ list_destroy(&idmap_head);
+
+ exit(ret);
+}