diff options
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.c | 808 |
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); +} |
