diff options
Diffstat (limited to 'libmount/src/hook_loopdev.c')
-rw-r--r-- | libmount/src/hook_loopdev.c | 530 |
1 files changed, 530 insertions, 0 deletions
diff --git a/libmount/src/hook_loopdev.c b/libmount/src/hook_loopdev.c new file mode 100644 index 0000000..8c8f7f2 --- /dev/null +++ b/libmount/src/hook_loopdev.c @@ -0,0 +1,530 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * This file is part of libmount from util-linux project. + * + * Copyright (C) 2011-2022 Karel Zak <kzak@redhat.com> + * + * libmount is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * Please, see the comment in libmount/src/hooks.c to understand how hooks work. + */ +#include <blkid.h> +#include <stdbool.h> + +#include "mountP.h" +#include "loopdev.h" +#include "strutils.h" +#include "linux_version.h" + +struct hook_data { + int loopdev_fd; +}; + +/* de-initiallize this module */ +static int hookset_deinit(struct libmnt_context *cxt, const struct libmnt_hookset *hs) +{ + void *data; + + DBG(HOOK, ul_debugobj(hs, "deinit '%s'", hs->name)); + + /* remove all our hooks */ + while (mnt_context_remove_hook(cxt, hs, 0, &data) == 0) { + free(data); + data = NULL; + } + + return 0; +} + +static inline struct hook_data *new_hook_data(void) +{ + struct hook_data *hd = calloc(1, sizeof(*hd)); + + if (!hd) + return NULL; + + hd->loopdev_fd = -1; + return hd; +} + +/* Check if there already exists a mounted loop device on the mountpoint node + * with the same parameters. + */ +static int __attribute__((nonnull)) +is_mounted_same_loopfile(struct libmnt_context *cxt, + const char *target, + const char *backing_file, + uint64_t offset) +{ + struct libmnt_table *tb = NULL; + struct libmnt_iter itr; + struct libmnt_fs *fs; + struct libmnt_cache *cache; + const char *bf; + int rc = 0; + struct libmnt_ns *ns_old; + unsigned long flags = 0; + + assert(cxt); + assert(cxt->fs); + assert((cxt->flags & MNT_FL_MOUNTFLAGS_MERGED)); + + if (mnt_context_get_mountinfo(cxt, &tb)) + return 0; + + ns_old = mnt_context_switch_target_ns(cxt); + if (!ns_old) + return -MNT_ERR_NAMESPACE; + + DBG(LOOP, ul_debugobj(cxt, "checking if %s mounted on %s", + backing_file, target)); + + rc = mnt_context_get_user_mflags(cxt, &flags); + if (rc) + return 0; + + cache = mnt_context_get_cache(cxt); + mnt_reset_iter(&itr, MNT_ITER_BACKWARD); + + bf = cache ? mnt_resolve_path(backing_file, cache) : backing_file; + + /* Search for a mountpoint node in mountinfo, proceed if any of these have the + * loop option set or the device is a loop device + */ + while (rc == 0 && mnt_table_next_fs(tb, &itr, &fs) == 0) { + const char *src = mnt_fs_get_source(fs); + const char *opts = mnt_fs_get_user_options(fs); + char *val; + size_t len; + + if (!src || !mnt_fs_match_target(fs, target, cache)) + continue; + + rc = 0; + + if (strncmp(src, "/dev/loop", 9) == 0) { + rc = loopdev_is_used((char *) src, bf, offset, 0, LOOPDEV_FL_OFFSET); + + } else if (opts && (flags & MNT_MS_LOOP) && + mnt_optstr_get_option(opts, "loop", &val, &len) == 0 && val) { + + val = strndup(val, len); + rc = loopdev_is_used((char *) val, bf, offset, 0, LOOPDEV_FL_OFFSET); + free(val); + } + } + if (rc) + DBG(LOOP, ul_debugobj(cxt, "%s already mounted", backing_file)); + + if (!mnt_context_switch_ns(cxt, ns_old)) + return -MNT_ERR_NAMESPACE; + return rc; +} + +static int setup_loopdev(struct libmnt_context *cxt, + struct libmnt_optlist *ol, struct hook_data *hd) +{ + const char *backing_file, *loopdev = NULL; + struct loopdev_cxt lc; + int rc = 0, lo_flags = 0; + uint64_t offset = 0, sizelimit = 0; + bool reuse = FALSE; + struct libmnt_opt *opt, *loopopt = NULL; + + backing_file = mnt_fs_get_srcpath(cxt->fs); + if (!backing_file) + return -EINVAL; + + DBG(LOOP, ul_debugobj(cxt, "trying to setup device for %s", backing_file)); + + ol = mnt_context_get_optlist(cxt); + if (!ol) + return -ENOMEM; + + if (mnt_optlist_is_rdonly(ol)) { + DBG(LOOP, ul_debugobj(cxt, "enabling READ-ONLY flag")); + lo_flags |= LO_FLAGS_READ_ONLY; + } + + /* + * loop= + */ + if (!rc) + loopopt = mnt_optlist_get_opt(ol, MNT_MS_LOOP, cxt->map_userspace); + + /* + * offset= + */ + if (!rc && (opt = mnt_optlist_get_opt(ol, MNT_MS_OFFSET, cxt->map_userspace)) + && mnt_opt_has_value(opt)) { + if (strtosize(mnt_opt_get_value(opt), &offset)) { + DBG(LOOP, ul_debugobj(cxt, "failed to parse offset=")); + rc = -MNT_ERR_MOUNTOPT; + } + } + + /* + * sizelimit= + */ + if (!rc && (opt = mnt_optlist_get_opt(ol, MNT_MS_SIZELIMIT, cxt->map_userspace)) + && mnt_opt_has_value(opt)) { + if (strtosize(mnt_opt_get_value(opt), &sizelimit)) { + DBG(LOOP, ul_debugobj(cxt, "failed to parse sizelimit=")); + rc = -MNT_ERR_MOUNTOPT; + } + } + + /* + * encryption= + */ + if (!rc && mnt_optlist_get_opt(ol, MNT_MS_ENCRYPTION, cxt->map_userspace)) { + DBG(LOOP, ul_debugobj(cxt, "encryption no longer supported")); + rc = -MNT_ERR_MOUNTOPT; + } + + if (!rc && is_mounted_same_loopfile(cxt, + mnt_context_get_target(cxt), + backing_file, offset)) + rc = -EBUSY; + + if (rc) + goto done_no_deinit; + + /* It is possible to mount the same file more times. If we set more + * than one loop device referring to the same file, kernel has no + * mechanism to detect it. To prevent data corruption, the same loop + * device has to be recycled. + */ + if (backing_file) { + rc = loopcxt_init(&lc, 0); + if (rc) + goto done_no_deinit; + + rc = loopcxt_find_overlap(&lc, backing_file, offset, sizelimit); + switch (rc) { + case 0: /* not found */ + DBG(LOOP, ul_debugobj(cxt, "not found overlapping loopdev")); + loopcxt_deinit(&lc); + break; + + case 1: /* overlap */ + DBG(LOOP, ul_debugobj(cxt, "overlapping %s detected", + loopcxt_get_device(&lc))); + rc = -MNT_ERR_LOOPOVERLAP; + goto done; + + case 2: /* overlap -- full size and offset match (reuse) */ + { + uint32_t lc_encrypt_type = 0; + + DBG(LOOP, ul_debugobj(cxt, "re-using existing loop device %s", + loopcxt_get_device(&lc))); + + /* Open loop device to block device autoclear... */ + if (loopcxt_get_fd(&lc) < 0) { + DBG(LOOP, ul_debugobj(cxt, "failed to get loopdev FD")); + rc = -errno; + goto done; + } + + /* + * Now that we certainly have the loop device open, + * verify the loop device was not autocleared in the + * mean time. + */ + if (!loopcxt_get_info(&lc)) { + DBG(LOOP, ul_debugobj(cxt, "lost race with %s teardown", + loopcxt_get_device(&lc))); + loopcxt_deinit(&lc); + break; + } + + /* Once a loop is initialized RO, there is no + * way to change its parameters. */ + if (loopcxt_is_readonly(&lc) + && !(lo_flags & LO_FLAGS_READ_ONLY)) { + DBG(LOOP, ul_debugobj(cxt, "%s is read-only", + loopcxt_get_device(&lc))); + rc = -EROFS; + goto done; + } + + /* This is no more supported, but check to be safe. */ + if (loopcxt_get_encrypt_type(&lc, &lc_encrypt_type) == 0 + && lc_encrypt_type != LO_CRYPT_NONE) { + DBG(LOOP, ul_debugobj(cxt, "encryption no longer supported for device %s", + loopcxt_get_device(&lc))); + rc = -MNT_ERR_LOOPOVERLAP; + goto done; + } + rc = 0; + /* loop= used with argument. Conflict will occur. */ + if (mnt_opt_has_value(loopopt)) { + rc = -MNT_ERR_LOOPOVERLAP; + goto done; + } else { + reuse = TRUE; + goto success; + } + } + default: /* error */ + goto done; + } + } + + DBG(LOOP, ul_debugobj(cxt, "not found; create a new loop device")); + rc = loopcxt_init(&lc, 0); + if (rc) + goto done_no_deinit; + if (mnt_opt_has_value(loopopt)) { + rc = loopcxt_set_device(&lc, mnt_opt_get_value(loopopt)); + if (rc == 0) + loopdev = loopcxt_get_device(&lc); + } + if (rc) + goto done; + + /* since 2.6.37 we don't have to store backing filename to mountinfo + * because kernel provides the name in /sys. + */ + if (get_linux_version() >= KERNEL_VERSION(2, 6, 37)) { + DBG(LOOP, ul_debugobj(cxt, "enabling AUTOCLEAR flag")); + lo_flags |= LO_FLAGS_AUTOCLEAR; + } + + do { + /* found free device */ + if (!loopdev) { + rc = loopcxt_find_unused(&lc); + if (rc) + goto done; + DBG(LOOP, ul_debugobj(cxt, "trying to use %s", + loopcxt_get_device(&lc))); + } + + /* set device attributes + * -- note that loopcxt_find_unused() resets "lc" + */ + rc = loopcxt_set_backing_file(&lc, backing_file); + + if (!rc && offset) + rc = loopcxt_set_offset(&lc, offset); + if (!rc && sizelimit) + rc = loopcxt_set_sizelimit(&lc, sizelimit); + if (!rc) + loopcxt_set_flags(&lc, lo_flags); + if (rc) { + DBG(LOOP, ul_debugobj(cxt, "failed to set loop attributes")); + goto done; + } + + /* setup the device */ + rc = loopcxt_setup_device(&lc); + if (!rc) + break; /* success */ + + if (loopdev || rc != -EBUSY) { + DBG(LOOP, ul_debugobj(cxt, "failed to setup device")); + rc = -MNT_ERR_LOOPDEV; + goto done; + } + DBG(LOOP, ul_debugobj(cxt, "device stolen...trying again")); + } while (1); + +success: + if (!rc) + rc = mnt_fs_set_source(cxt->fs, loopcxt_get_device(&lc)); + + if (!rc) { + if (loopopt && (reuse || loopcxt_is_autoclear(&lc))) { + /* + * autoclear flag accepted by the kernel, don't store + * the "loop=" option to utab. + */ + DBG(LOOP, ul_debugobj(cxt, "removing unnecessary loop= from utab")); + mnt_optlist_remove_opt(ol, loopopt); + loopopt = NULL; + } + + if (!mnt_optlist_is_rdonly(ol) && loopcxt_is_readonly(&lc)) + /* + * mount planned read-write, but loopdev is read-only, + * let's fix mount options... + */ + mnt_optlist_append_flags(ol, MS_RDONLY, cxt->map_linux); + + /* we have to keep the device open until mount(1), + * otherwise it will be auto-cleared by kernel + */ + hd->loopdev_fd = loopcxt_get_fd(&lc); + if (hd->loopdev_fd < 0) { + DBG(LOOP, ul_debugobj(cxt, "failed to get loopdev FD")); + rc = -errno; + } else + loopcxt_set_fd(&lc, -1, 0); + } +done: + loopcxt_deinit(&lc); +done_no_deinit: + return rc; +} + +static int delete_loopdev(struct libmnt_context *cxt, struct hook_data *hd) +{ + const char *src; + int rc; + + assert(cxt); + assert(cxt->fs); + + src = mnt_fs_get_srcpath(cxt->fs); + if (!src) + return -EINVAL; + + if (hd && hd->loopdev_fd > -1) { + close(hd->loopdev_fd); + hd->loopdev_fd = -1; + } + + rc = loopdev_delete(src); /* see lib/loopdev.c */ + + DBG(LOOP, ul_debugobj(cxt, "deleted [rc=%d]", rc)); + return rc; +} + +/* Now used by umount until context_umount.c will use hooks toosee */ +int mnt_context_delete_loopdev(struct libmnt_context *cxt) +{ + return delete_loopdev(cxt, NULL); +} + +static int is_loopdev_required(struct libmnt_context *cxt, struct libmnt_optlist *ol) +{ + const char *src, *type; + unsigned long flags = 0; + + if (cxt->action != MNT_ACT_MOUNT) + return 0; + if (!cxt->fs) + return 0; + if (mnt_optlist_is_bind(ol) + || mnt_optlist_is_move(ol) + || mnt_context_propagation_only(cxt)) + return 0; + + src = mnt_fs_get_srcpath(cxt->fs); + if (!src) + return 0; /* backing file not set */ + + /* userspace flags */ + if (mnt_context_get_user_mflags(cxt, &flags)) + return 0; + + if (flags & (MNT_MS_LOOP | MNT_MS_OFFSET | MNT_MS_SIZELIMIT)) { + DBG(LOOP, ul_debugobj(cxt, "loopdev specific options detected")); + return 1; + } + + /* Automatically create a loop device from a regular file if a + * filesystem is not specified or the filesystem is known for libblkid + * (these filesystems work with block devices only). The file size + * should be at least 1KiB, otherwise we will create an empty loopdev with + * no mountable filesystem... + * + * Note that there is no restriction (on kernel side) that would prevent a regular + * file as a mount(2) source argument. A filesystem that is able to mount + * regular files could be implemented. + */ + type = mnt_fs_get_fstype(cxt->fs); + + if (mnt_fs_is_regularfs(cxt->fs) && + (!type || strcmp(type, "auto") == 0 || blkid_known_fstype(type))) { + struct stat st; + + if (stat(src, &st) == 0 && S_ISREG(st.st_mode) && + st.st_size > 1024) { + + DBG(LOOP, ul_debugobj(cxt, "automatically enabling loop= option")); + mnt_optlist_append_flags(ol, MNT_MS_LOOP, cxt->map_userspace); + return 1; + } + } + + return 0; +} + +/* call after mount(2) */ +static int hook_cleanup_loopdev( + struct libmnt_context *cxt, + const struct libmnt_hookset *hs __attribute__((__unused__)), + void *data) +{ + struct hook_data *hd = (struct hook_data *) data; + + if (!hd || hd->loopdev_fd < 0) + return 0; + + if (mnt_context_get_status(cxt) == 0) { + /* + * mount(2) failed, delete loopdev + */ + delete_loopdev(cxt, hd); + + } else { + /* + * mount(2) success, close the device + */ + DBG(LOOP, ul_debugobj(cxt, "closing FD")); + close(hd->loopdev_fd); + hd->loopdev_fd = -1; + } + + return 0; +} + +/* call to prepare mount source */ +static int hook_prepare_loopdev( + struct libmnt_context *cxt, + const struct libmnt_hookset *hs, + void *data __attribute__((__unused__))) +{ + struct libmnt_optlist *ol; + struct hook_data *hd; + int rc; + + assert(cxt); + + ol = mnt_context_get_optlist(cxt); + if (!ol) + return -ENOMEM; + if (!is_loopdev_required(cxt, ol)) + return 0; + hd = new_hook_data(); + if (!hd) + return -ENOMEM; + + rc = setup_loopdev(cxt, ol, hd); + if (!rc) + rc = mnt_context_append_hook(cxt, hs, + MNT_STAGE_MOUNT_POST, + hd, hook_cleanup_loopdev); + if (rc) { + delete_loopdev(cxt, hd); + free(hd); + } + return rc; +} + + +const struct libmnt_hookset hookset_loopdev = +{ + .name = "__loopdev", + + .firststage = MNT_STAGE_PREP_SOURCE, + .firstcall = hook_prepare_loopdev, + + .deinit = hookset_deinit +}; |