| /* |
| FUSE: Filesystem in Userspace |
| Copyright (C) 2025 Oracle. |
| Author: Darrick J. Wong <djwong@kernel.org> |
| |
| Library functions for handling loopback devices on linux. |
| |
| This program can be distributed under the terms of the GNU LGPLv2. |
| See the file LGPL2.txt |
| */ |
| |
| #define _GNU_SOURCE |
| #include "fuse_config.h" |
| #include "fuse_loopdev.h" |
| |
| #ifdef FUSE_LOOPDEV_ENABLED |
| #include <stdint.h> |
| #include <stdio.h> |
| #include <fcntl.h> |
| #include <unistd.h> |
| #include <string.h> |
| #include <stdlib.h> |
| #include <limits.h> |
| #include <stdbool.h> |
| #include <errno.h> |
| #include <dirent.h> |
| #include <signal.h> |
| #include <time.h> |
| #include <sys/stat.h> |
| #include <sys/ioctl.h> |
| #include <sys/file.h> |
| #include <sys/types.h> |
| #include <sys/time.h> |
| #include <linux/loop.h> |
| |
| #include "fuse_log.h" |
| |
| #define _PATH_LOOPCTL "/dev/loop-control" |
| #define _PATH_SYS_BLOCK "/sys/block" |
| |
| #ifdef STATX_SUBVOL |
| # define STATX_SUBVOL_FLAG STATX_SUBVOL |
| #else |
| # define STATX_SUBVOL_FLAG 0 |
| #endif |
| |
| static int lock_file(int fd, const char *path) |
| { |
| int ret; |
| |
| ret = flock(fd, LOCK_EX); |
| if (ret) { |
| fuse_log(FUSE_LOG_DEBUG, "%s: %s\n", path, strerror(errno)); |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| static double gettime_monotonic(void) |
| { |
| #ifdef CLOCK_MONOTONIC |
| struct timespec ts; |
| #endif |
| struct timeval tv; |
| static double fake_ret = 0; |
| int ret; |
| |
| #ifdef CLOCK_MONOTONIC |
| ret = clock_gettime(CLOCK_MONOTONIC, &ts); |
| if (ret == 0) |
| return ts.tv_sec + (ts.tv_nsec / 1000000000.0); |
| #endif |
| ret = gettimeofday(&tv, NULL); |
| if (ret == 0) |
| return tv.tv_sec + (tv.tv_usec / 1000000.0); |
| |
| fake_ret += 1.0; |
| return fake_ret; |
| } |
| |
| static int lock_file_timeout(int fd, const char *path, unsigned int timeout) |
| { |
| double deadline, now; |
| int ret; |
| |
| now = gettime_monotonic(); |
| deadline = now + timeout; |
| |
| /* Use a tight sleeping loop here to avoid signal handlers */ |
| while (now <= deadline) { |
| ret = flock(fd, LOCK_EX | LOCK_NB); |
| if (ret == 0) |
| return 0; |
| if (errno != EWOULDBLOCK) { |
| fuse_log(FUSE_LOG_DEBUG, "%s: %s\n", path, |
| strerror(errno)); |
| return -1; |
| } |
| |
| /* sleep 0.1s before trying again */ |
| usleep(100000); |
| |
| now = gettime_monotonic(); |
| } |
| |
| fuse_log(FUSE_LOG_DEBUG, "%s: could not lock file\n", path); |
| errno = EWOULDBLOCK; |
| return -1; |
| } |
| |
| static int unlock_file(int fd, const char *path) |
| { |
| int ret; |
| |
| ret = flock(fd, LOCK_UN); |
| if (ret) { |
| fuse_log(FUSE_LOG_DEBUG, "%s: %s\n", path, strerror(errno)); |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| static int want_loopdev(int file_fd, const char *path) |
| { |
| struct stat statbuf; |
| int ret; |
| |
| ret = fstat(file_fd, &statbuf); |
| if (ret < 0) { |
| fuse_log(FUSE_LOG_DEBUG, "%s: fstat failed: %s\n", |
| path, strerror(errno)); |
| return -1; |
| } |
| |
| /* |
| * Keep quiet about block devices, the client can probably still read |
| * and write that. |
| */ |
| if (S_ISBLK(statbuf.st_mode)) |
| return 0; |
| |
| ret = S_ISREG(statbuf.st_mode) && statbuf.st_size >= 512; |
| if (!ret) |
| fuse_log(FUSE_LOG_DEBUG, |
| "%s: file not compatible with loop device\n", path); |
| return ret; |
| } |
| |
| static int same_backing_file(int dir_fd, const char *name, |
| const struct statx *file_stat) |
| { |
| struct statx backing_stat; |
| char backing_name[NAME_MAX + 18 + 1]; |
| char path[PATH_MAX + 1]; |
| ssize_t bytes; |
| int fd; |
| int ret; |
| |
| snprintf(backing_name, sizeof(backing_name), "%s/loop/backing_file", |
| name); |
| |
| fd = openat(dir_fd, backing_name, O_RDONLY); |
| if (fd < 0) { |
| /* unconfigured loop devices don't have backing_file attr */ |
| if (errno == ENOENT) |
| return 0; |
| fuse_log(FUSE_LOG_DEBUG, "%s: %s\n", backing_name, |
| strerror(errno)); |
| return -1; |
| } |
| |
| bytes = pread(fd, path, sizeof(path) - 1, 0); |
| if (bytes < 0) { |
| fuse_log(FUSE_LOG_DEBUG, "%s: %s\n", backing_name, |
| strerror(errno)); |
| ret = -1; |
| goto out_backing; |
| } else if (bytes == 0) { |
| fuse_log(FUSE_LOG_DEBUG, "%s: no path in backing file?\n", |
| backing_name); |
| ret = -1; |
| goto out_backing; |
| } |
| |
| if (path[bytes - 1] == '\n') |
| path[bytes - 1] = 0; |
| |
| ret = statx(AT_FDCWD, path, 0, STATX_BASIC_STATS | STATX_SUBVOL_FLAG, |
| &backing_stat); |
| if (ret) { |
| /* |
| * backing file deleted, assume nobody's doing procfd |
| * shenanigans |
| */ |
| if (errno == ENOENT) { |
| ret = 0; |
| goto out_backing; |
| } |
| fuse_log(FUSE_LOG_DEBUG, "%s: %s\n", path, strerror(errno)); |
| goto out_backing; |
| } |
| |
| /* different devices */ |
| if (backing_stat.stx_dev_major != file_stat->stx_dev_major) |
| goto out_backing; |
| if (backing_stat.stx_dev_minor != file_stat->stx_dev_minor) |
| goto out_backing; |
| |
| /* different inode number */ |
| if (backing_stat.stx_ino != file_stat->stx_ino) |
| goto out_backing; |
| |
| #ifdef STATX_SUBVOL |
| /* different subvol (or subvol state) */ |
| if ((backing_stat.stx_mask ^ file_stat->stx_mask) & STATX_SUBVOL) |
| goto out_backing; |
| |
| if ((backing_stat.stx_mask & STATX_SUBVOL) && |
| backing_stat.stx_subvol != file_stat->stx_subvol) |
| goto out_backing; |
| #endif |
| |
| ret = 1; |
| |
| out_backing: |
| close(fd); |
| return ret; |
| } |
| |
| static int has_existing_loopdev(int file_fd, const char *path) |
| { |
| struct statx file_stat; |
| DIR *dir; |
| struct dirent *d; |
| int blockfd; |
| int ret; |
| |
| ret = statx(file_fd, "", AT_EMPTY_PATH, |
| STATX_BASIC_STATS | STATX_SUBVOL_FLAG, &file_stat); |
| if (ret) { |
| fuse_log(FUSE_LOG_DEBUG, "%s: %s\n", path, strerror(errno)); |
| return -1; |
| } |
| |
| dir = opendir(_PATH_SYS_BLOCK); |
| if (!dir) { |
| fuse_log(FUSE_LOG_DEBUG, "%s: %s\n", _PATH_SYS_BLOCK, |
| strerror(errno)); |
| return -1; |
| } |
| |
| blockfd = dirfd(dir); |
| |
| while ((d = readdir(dir)) != NULL) { |
| if (strcmp(d->d_name, ".") == 0 |
| || strcmp(d->d_name, "..") == 0 |
| || strncmp(d->d_name, "loop", 4) != 0) |
| continue; |
| |
| ret = same_backing_file(blockfd, d->d_name, &file_stat); |
| if (ret != 0) |
| break; |
| } |
| |
| closedir(dir); |
| return ret; |
| } |
| |
| static int open_loopdev(int file_fd, int open_flags, char *loopdev, |
| size_t loopdev_sz) |
| { |
| struct loop_config lc = { |
| .info.lo_flags = LO_FLAGS_DIRECT_IO | LO_FLAGS_AUTOCLEAR, |
| }; |
| int ctl_fd = -1; |
| int loop_fd = -1; |
| int loopno; |
| int ret; |
| |
| if ((open_flags & O_ACCMODE) == O_RDONLY) |
| lc.info.lo_flags |= LO_FLAGS_READ_ONLY; |
| |
| ctl_fd = open(_PATH_LOOPCTL, O_RDONLY); |
| if (ctl_fd < 0) { |
| fuse_log(FUSE_LOG_DEBUG, "%s: %s\n", _PATH_LOOPCTL, |
| strerror(errno)); |
| return -1; |
| } |
| |
| ret = ioctl(ctl_fd, LOOP_CTL_GET_FREE); |
| if (ret < 0) { |
| fuse_log(FUSE_LOG_DEBUG, "%s: %s\n", _PATH_LOOPCTL, |
| strerror(errno)); |
| goto out_ctl; |
| } |
| loopno = ret; |
| snprintf(loopdev, loopdev_sz, "/dev/loop%d", loopno); |
| |
| loop_fd = open(loopdev, open_flags); |
| if (loop_fd < 0) { |
| fuse_log(FUSE_LOG_DEBUG, "%s: %s\n", loopdev, strerror(errno)); |
| ret = -1; |
| goto out_ctl; |
| } |
| |
| lc.fd = file_fd; |
| |
| ret = ioctl(loop_fd, LOOP_CONFIGURE, &lc); |
| if (ret < 0) { |
| fuse_log(FUSE_LOG_DEBUG, "%s: %s\n", loopdev, strerror(errno)); |
| goto out_loop; |
| } |
| |
| close(ctl_fd); |
| return loop_fd; |
| |
| out_loop: |
| ioctl(ctl_fd, LOOP_CTL_REMOVE, loopno); |
| close(loop_fd); |
| out_ctl: |
| close(ctl_fd); |
| return ret; |
| } |
| |
| int fuse_loopdev_setup(int file_fd, int open_flags, const char *path, |
| unsigned int timeout, int *loop_fd, char **loop_dev) |
| { |
| char loopdev[PATH_MAX]; |
| int loopfd = -1; |
| int ret; |
| |
| *loop_fd = -1; |
| if (loop_dev) |
| *loop_dev = NULL; |
| |
| if (timeout) |
| ret = lock_file_timeout(file_fd, path, timeout); |
| else |
| ret = lock_file(file_fd, path); |
| if (ret) |
| return ret; |
| |
| ret = want_loopdev(file_fd, path); |
| if (ret <= 0) |
| goto out_unlock; |
| |
| ret = has_existing_loopdev(file_fd, path); |
| if (ret < 0) |
| goto out_unlock; |
| if (ret == 1) { |
| fuse_log(FUSE_LOG_DEBUG, |
| "%s: attached to another loop device\n", path); |
| ret = -1; |
| errno = EBUSY; |
| goto out_unlock; |
| } |
| |
| loopfd = open_loopdev(file_fd, open_flags, loopdev, sizeof(loopdev)); |
| if (loopfd < 0) |
| goto out_unlock; |
| |
| ret = unlock_file(file_fd, path); |
| if (ret) |
| goto out_loop; |
| |
| if (loop_dev) { |
| char *ldev = strdup(loopdev); |
| if (!ldev) |
| goto out_loop; |
| |
| *loop_fd = loopfd; |
| *loop_dev = ldev; |
| } else { |
| *loop_fd = loopfd; |
| } |
| |
| return 0; |
| |
| out_loop: |
| close(loopfd); |
| out_unlock: |
| unlock_file(file_fd, path); |
| return ret; |
| } |
| #else |
| #include <stdlib.h> |
| |
| #include "util.h" |
| |
| int fuse_loopdev_setup(int file_fd FUSE_VAR_UNUSED, |
| int open_flags FUSE_VAR_UNUSED, |
| const char *path FUSE_VAR_UNUSED, |
| unsigned int timeout FUSE_VAR_UNUSED, |
| int *loop_fd, char **loop_dev) |
| { |
| *loop_fd = -1; |
| if (loop_dev) |
| *loop_dev = NULL; |
| return 0; |
| } |
| #endif /* FUSE_LOOPDEV_ENABLED */ |