blob: 56b906431a8b48c3376757473975cec4f2a370b2 [file]
/*
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 */