snapraid/cmdline/unix.c

4751 lines
112 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2013 Andrea Mazzoleni
#include "portable.h"
#ifndef __MINGW32__ /* Only for Unix */
#include "support.h"
/****************************************************************************/
/* access */
#if !HAVE_EACCESS
/**
* Check effective user's permissions for a file.
* Conceptually identical to access(), but uses the effective UID/GID.
*/
int eaccess(const char* pathname, int mode)
{
return faccessat(AT_FDCWD, pathname, mode, AT_EACCESS);
}
#endif
/****************************************************************************/
/* signal */
volatile int global_interrupt = 0;
int os_global_interrupt(void)
{
return global_interrupt;
}
/* LCOV_EXCL_START */
static void signal_handler(int signum)
{
/* report the request of interruption with the signal received */
global_interrupt = signum;
}
/* LCOV_EXCL_STOP */
void os_signal_restore_after_fork(void)
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = SIG_DFL;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGTERM, &sa, 0);
sigaction(SIGINT, &sa, 0);
sigaction(SIGHUP, &sa, 0);
/* do not restore SIGPIPE */
/* ensure signals are unblocked */
sigset_t mask;
sigemptyset(&mask);
sigprocmask(SIG_SETMASK, &mask, NULL); /* cannot use pthread_sigmask after fork */
}
void os_signal_init(void)
{
struct sigaction sa;
sa.sa_handler = signal_handler;
sigemptyset(&sa.sa_mask);
/* use the SA_RESTART to automatically restart interrupted system calls */
sa.sa_flags = SA_RESTART;
sigaction(SIGHUP, &sa, 0);
sigaction(SIGTERM, &sa, 0);
sigaction(SIGINT, &sa, 0);
sa.sa_handler = SIG_IGN; /* ignore the signal */
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGPIPE, &sa, 0);
}
/****************************************************************************/
/* exec */
/*
* Securely verify and open an executable file.
*
* Performs a series of security checks on the file at @exec_path before
* returning an open file descriptor suitable for use with fexecve(2), or
* falling back to execve(2) on systems that lack it.
*
* Security model:
* - @exec_path must be absolute.
* - The path is resolved via realpath(3) to canonicalize it and eliminate
* symlinks before any further checks are performed.
* - The parent directory is opened with O_PATH | O_NOFOLLOW to pin its
* inode, and the file is opened with openat(2) relative to that pinned
* descriptor. This closes the TOCTOU window between path resolution
* and file open.
* - Both the parent directory and the file must be owned by root or the
* daemon's real/effective UID, must not be world-writable, and must not
* be group-writable by a group other than the daemon's real/effective GID.
* - The file must be a regular file with at least one execute bit set.
* - The setuid and setgid bits must not be set.
* - The file must not have more than one hard link, to prevent an attacker
* from linking a controlled file into a trusted directory.
* - On systems with fexecve(2) support, the returned fd is opened without
* O_CLOEXEC so it can be passed directly to fexecve(2). On other systems
* O_CLOEXEC is set and execve(2) must be used with @resolved_path.
*
* @exec_path Absolute path to the executable to verify.
* @resolved_path Caller-allocated buffer of at least PATH_MAX bytes. On
* success, filled with the canonicalized path from realpath(3).
*
* Returns an open file descriptor (>= 0) on success. The caller is
* responsible for closing it. Returns -1 on any verification failure;
* the specific reason is emitted via log_error(...).
*/
static int verify_executable(const char* exec_path, char* resolved_path)
{
struct stat st;
uid_t process_uid, process_euid;
gid_t process_gid, process_egid;
process_uid = getuid();
process_euid = geteuid();
process_gid = getgid();
process_egid = getegid();
/* verify path is absolute */
if (exec_path[0] != '/') {
log_error(EINVAL, "Path %s must be absolute", exec_path);
return -1;
}
/* resolve the path to prevent symlink attacks */
if (!realpath(exec_path, resolved_path)) {
log_error(errno, "Failed to resolve %s. %s.", exec_path, strerror(errno));
return -1;
}
char* last_slash = strrchr(resolved_path, '/');
if (last_slash == 0) {
log_error(EINVAL, "Relative execution of %s not allowed", resolved_path);
return -1;
}
if (last_slash == resolved_path) {
log_error(EINVAL, "Root dir execution of %s not allowed", resolved_path);
return -1;
}
const char* exec_name = last_slash + 1;
if (exec_name[0] == 0) {
log_error(EINVAL, "No executable name in %s", exec_path);
return -1;
}
char dir_path[PATH_MAX];
size_t dir_len = last_slash - resolved_path;
memcpy(dir_path, resolved_path, dir_len);
dir_path[dir_len] = 0;
int dir_fd = open(dir_path, O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (dir_fd < 0) {
log_error(errno, "Failed to open directory %s. %s.", dir_path, strerror(errno));
return -1;
}
if (fstat(dir_fd, &st) != 0) {
log_error(errno, "Failed to stat directory %s. %s.", dir_path, strerror(errno));
close(dir_fd);
return -1;
}
/* directory must be owned by root or the daemon's real user */
if (st.st_uid != process_uid && st.st_uid != process_euid && st.st_uid != 0) {
log_error(EINVAL, "Directory %s owner must match the daemon owner or be root", dir_path);
close(dir_fd);
return -1;
}
/* directory must be not group-writable unless group matches daemon */
if ((st.st_mode & S_IWGRP) && st.st_gid != process_gid && st.st_gid != process_egid && st.st_gid != 0) {
log_error(EINVAL, "Directory %s must be not group-writable unless group matches daemon owner or root", dir_path);
close(dir_fd);
return -1;
}
/* directory must be not world-writable */
if (st.st_mode & S_IWOTH) {
log_error(EINVAL, "Directory %s must be not world-writable", dir_path);
close(dir_fd);
return -1;
}
/*
* Open the executable
* O_NOFOLLOW prevents following symlinks to mitigate redirection attacks
*/
int fd = openat(dir_fd, exec_name, O_RDONLY | O_NOFOLLOW
#if !HAVE_FEXECVE
| O_CLOEXEC /* with fexecve cannot use O_CLOEXEC (Close on Exec) */
#endif
);
if (fd < 0) {
log_error(errno, "Failed to open %s. %s.", resolved_path, strerror(errno));
close(dir_fd);
return -1;
}
close(dir_fd);
/* get the file handle (TOCTOU Protection) */
if (fstat(fd, &st) == -1) {
log_error(errno, "Failed to stat %s. %s.", resolved_path, strerror(errno));
close(fd);
return -1;
}
/* ensure it's a regular file */
if (!S_ISREG(st.st_mode)) {
log_error(EINVAL, "File %s is not a regular file", resolved_path);
close(fd);
return -1;
}
/* ensure it has execute permissions */
if (!(st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) {
log_error(EINVAL, "File %s is not an executable", resolved_path);
close(fd);
return -1;
}
/* must be owned by root or the daemon's real user */
if (st.st_uid != process_uid && st.st_uid != process_euid && st.st_uid != 0) {
log_error(EINVAL, "File %s owner must match the daemon owner or be root", resolved_path);
close(fd);
return -1;
}
/* must be not group-writable unless group matches daemon */
if ((st.st_mode & S_IWGRP) && st.st_gid != process_gid && st.st_gid != process_egid && st.st_gid != 0) {
log_error(EINVAL, "File %s must be not group-writable unless group matches daemon owner or root", resolved_path);
close(fd);
return -1;
}
/* must be not world-writable */
if (st.st_mode & S_IWOTH) {
log_error(EINVAL, "File %s must be not world-writable", resolved_path);
close(fd);
return -1;
}
/* must be not setuid / setgid */
if (st.st_mode & (S_ISUID | S_ISGID)) {
log_error(EINVAL, "File %s has setuid/setgid bits set", resolved_path);
close(fd);
return -1;
}
/* verify the file has not been hardlinked multiple times */
if (st.st_nlink > 1) {
log_error(EINVAL, "File %s has multiple hard links", resolved_path);
close(fd);
return -1;
}
return fd;
}
/**
* Validates a string for exec.
* Returns -1 if dangerous characters are detected, 0 otherwise.
*/
int validate_exec_input(const char* str)
{
/* reject paths trying to go up levels */
if (strstr(str, "..") != 0)
return -1;
/* reject inputs starting with '-' to prevent Flag Injection */
if (str[0] == '-')
return -1;
return 0;
}
/*
* Scrubbed environment
* Only provide the bare essentials.
*/
static char* const envp_scrubbed[] = {
"PATH="
#ifdef __APPLE__
"/opt/homebrew/bin:"
#endif
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=dumb",
"LANG=C",
"IFS= \t\n",
NULL
};
static int pipe_cloexec(int pipefd[2])
{
#ifdef HAVE_PIPE2
return pipe2(pipefd, O_CLOEXEC);
#else
if (pipe(pipefd) < 0)
return -1;
for (int i = 0; i < 2; i++) {
int flags = fcntl(pipefd[i], F_GETFD);
if (flags < 0)
goto bail;
if (fcntl(pipefd[i], F_SETFD, flags | FD_CLOEXEC) < 0)
goto bail;
}
return 0;
bail:
close(pipefd[0]);
close(pipefd[1]);
return -1;
#endif
}
/*
* Fork and execute a verified executable, capturing stdout.
*
* Spawns @argv[0] in a new process with stdout connected to a pipe whose
* read end is returned in @stdout_fd. stdin and stderr are redirected to
* /dev/null. Use this when the child's error output needs to be read and
* processed by the daemon.
*
* The child is placed in its own process group (setpgid) to isolate it
* from signals sent to the daemon's process group. The daemon can terminate
* the child and all its descendants with kill(-pid, SIGTERM).
*
* The pipe is created with O_CLOEXEC on both ends. The write end is closed
* in the parent after fork. The read end's buffer is reduced to 4096 bytes
* to improve read responsiveness on low-volume output.
*
* @argv NULL-terminated argument vector. argv[0] must be the absolute
* path to the executable.
* @stdout_fd On success, set to the read end of the stdout pipe. The caller
* is responsible for closing it when done.
*
* Returns the child PID on success, or -1 on failure.
*/
static pid_t os_spawn_stdout(const char** argv, int* stdout_fd)
{
char resolved_path[PATH_MAX];
int out_pipe[2];
pid_t pid;
int fd = verify_executable(argv[0], resolved_path);
if (fd < 0) {
return -1;
}
if (pipe_cloexec(out_pipe) < 0) {
close(fd);
return -1;
}
pid = fork();
if (pid < 0) {
close(out_pipe[0]);
close(out_pipe[1]);
close(fd);
return -1;
}
if (pid == 0) {
/* child process */
/*
* Create a new process group for the child.
* This isolates the child from signals sent to the daemon's process group
* and allows the daemon to kill this process and all its future children
* (the entire group) using kill(-pid, SIGTERM).
*/
setpgid(0, 0);
/* io sandboxing */
int null_fd = open("/dev/null", O_RDWR | O_CLOEXEC);
if (null_fd < 0)
_exit(126);
/* stdin -> /dev/null */
/* stdout -> pipe */
/* stderr -> /dev/null */
if (dup2(null_fd, STDIN_FILENO) < 0
|| dup2(out_pipe[1], STDOUT_FILENO) < 0
|| dup2(null_fd, STDERR_FILENO) < 0)
_exit(126);
close(out_pipe[0]);
close(out_pipe[1]);
/* if the fd we opened is not one of the standard ones, close it */
if (null_fd > STDERR_FILENO)
close(null_fd);
#if defined(CLOSE_RANGE_CLOEXEC) && defined(HAVE_CLOSE_RANGE)
/*
* Set all fd to be closed on exec as extra safety measure
*
* fallback: if it fails, we assume to be still safe, as all fds and
* sockets should be already created with CLOEXEC.
*/
close_range(3, fd - 1, CLOSE_RANGE_CLOEXEC);
close_range(fd + 1, ~0U, CLOSE_RANGE_CLOEXEC);
#endif
/* restore and unblock signals */
os_signal_restore_after_fork();
/* use the resolved path for execution */
argv[0] = resolved_path;
/*
* Direct Execution via File Descriptor
* The kernel uses the shebang in the FD to find the interpreter.
*/
#if HAVE_FEXECVE
fexecve(fd, (char**)argv, envp_scrubbed);
#else
/* fallback: unfortunately must use the path */
execve(resolved_path, (char**)argv, envp_scrubbed);
#endif
_exit(127);
}
/* parent */
close(fd);
#ifdef F_SETPIPE_SZ /* not available on macOS */
/* set the pipe buffer to the minimum to improve responsiveness */
if (fcntl(out_pipe[0], F_SETPIPE_SZ, 4096) == -1) {
log_error(errno, "Failed to set pipe size. %s.", strerror(errno));
/* log non-fatal error or ignore */
}
#endif
close(out_pipe[1]);
*stdout_fd = out_pipe[0];
return pid;
}
static int os_wait(pid_t pid, int* status)
{
int ret;
do {
ret = waitpid(pid, status, 0);
} while (ret == -1 && errno == EINTR);
return ret;
}
/*
* Fork and execute a verified executable, discarding all I/O.
*
* Spawns @argv[0] in a new process with stdin, stdout and stderr all
* redirected to /dev/null. Use this for fire-and-forget tasks where the
* child's output is not needed.
*
* The child is placed in its own process group (setpgid) to isolate it
* from signals sent to the daemon's process group.
*
* @argv NULL-terminated argument vector. argv[0] must be the absolute path
* to the executable.
*
* Returns the child execit status on success, or -1 on failure.
*/
int os_spawn_and_wait(const char** argv)
{
char resolved_path[PATH_MAX];
pid_t pid;
int fd = verify_executable(argv[0], resolved_path);
if (fd < 0)
return -1;
pid = fork();
if (pid < 0) {
log_error(errno, "Fork failed. %s.", strerror(errno));
close(fd);
return -1;
}
if (pid == 0) {
/* child process */
/*
* Create a new process group for the child.
* This isolates the child from signals sent to the daemon's process group
* and allows the daemon to kill this process and all its future children
* (the entire group) using kill(-pid, SIGTERM).
*/
setpgid(0, 0);
/* io sandboxing */
int null_fd = open("/dev/null", O_RDWR | O_CLOEXEC);
if (null_fd < 0)
_exit(126);
/* stdin -> /dev/null */
/* stdout -> /dev/null */
/* stderr -> /dev/null */
if (dup2(null_fd, STDIN_FILENO) < 0
|| dup2(null_fd, STDOUT_FILENO) < 0
|| dup2(null_fd, STDERR_FILENO) < 0)
_exit(126);
/* if the fd we opened is not one of the standard ones, close it */
if (null_fd > STDERR_FILENO)
close(null_fd);
#if defined(CLOSE_RANGE_CLOEXEC) && defined(HAVE_CLOSE_RANGE)
/*
* Mark all remaining open fds (>= 3) as close-on-exec as an
* extra safety measure against leaking inherited fds into the
* child executable.
* Note: if fd == 3, the first range (3, fd-1) = (3, 2) is
* intentionally invalid and will fail harmlessly; fd itself
* is preserved for fexecve.
*/
close_range(3, fd - 1, CLOSE_RANGE_CLOEXEC);
close_range(fd + 1, ~0U, CLOSE_RANGE_CLOEXEC);
#endif
/* restore and unblock signals */
os_signal_restore_after_fork();
/* use the resolved path for execution */
argv[0] = resolved_path;
/*
* Direct Execution via File Descriptor
* The kernel uses the shebang in the FD to find the interpreter.
*/
#if HAVE_FEXECVE
fexecve(fd, (char**)argv, envp_scrubbed);
#else
/* fallback: unfortunately must use the path */
execve(resolved_path, (char**)argv, envp_scrubbed);
#endif
_exit(127);
}
/* parent */
close(fd);
int status;
int ret = os_wait(pid, &status);
if (ret == -1) {
/* LCOV_EXCL_START */
log_error(errno, "Wait %s failed. %s.", resolved_path, strerror(errno));
return -1;
/* LCOV_EXCL_STOP */
}
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
} else if (WIFSIGNALED(status)) {
return 128 + WTERMSIG(status);
}
return -1;
}
#define ARGV_MAX 64
OS_FILE* os_popen(const char** argv, char* extra_args)
{
const char* combined_argv[ARGV_MAX];
unsigned argc = 0;
while (argv[argc] && argc < ARGV_MAX) {
combined_argv[argc] = argv[argc];
++argc;
}
if (extra_args)
argc += argsplit(combined_argv + argc, ARGV_MAX - argc, extra_args);
if (argc >= ARGV_MAX) {
errno = E2BIG;
return NULL;
}
combined_argv[argc] = 0;
int stdout_fd = -1;
pid_t pid = os_spawn_stdout(combined_argv, &stdout_fd);
if (pid < 0) {
return 0;
}
FILE* fp = fdopen(stdout_fd, "r");
if (!fp) {
/* LCOV_EXCL_START */
int saved_errno = errno;
close(stdout_fd);
int status;
os_wait(pid, &status);
errno = saved_errno;
return 0;
/* LCOV_EXCL_STOP */
}
OS_FILE* os_file = malloc_nofail(sizeof(OS_FILE));
os_file->fp = fp;
os_file->pid = pid;
return os_file;
}
char* os_fgets(char* s, int size, OS_FILE* stream)
{
return fgets(s, size, stream->fp);
}
int os_pclose(OS_FILE* stream)
{
/* leep handles locally to free container block immediately */
FILE* fp = stream->fp;
pid_t pid = stream->pid;
free(stream);
/* close buffered stream, releasing the underlying fd */
fclose(fp);
int status = 0;
if (os_wait(pid, &status) < 0) {
return -1;
}
return status;
}
/**
* Exit codes.
*/
int exit_success = 0;
int exit_failure = 1;
int exit_sync_needed = 2;
#define BTRFS_SUPER_MAGIC 0x9123683E
#define BCACHEFS_SUPER_MAGIC 0xCA451A4E
#define ZFS_SUPER_MAGIC 0x2FC12FC1
#if HAVE_LINUX_DEVICE
static const char* const bcachefs_paths[] = {
#ifdef BCACHEFS_PATH
/* Path configured at build time (e.g. on NixOS). */
BCACHEFS_PATH,
#else
"/usr/sbin/bcachefs",
"/sbin/bcachefs",
"/usr/local/sbin/bcachefs",
"/usr/bin/bcachefs",
"/bin/bcachefs",
"/usr/local/bin/bcachefs",
#ifdef __APPLE__
/* macOS (Intel & Apple Silicon) */
"/opt/homebrew/sbin/bcachefs",
#endif
#endif
0
};
static const char* find_bcachefs(void)
{
for (int i = 0; bcachefs_paths[i]; ++i) {
if (access(bcachefs_paths[i], X_OK) == 0) {
return bcachefs_paths[i];
}
}
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
static const char* const zfs_paths[] = {
#ifdef ZFS_PATH
/* Path configured at build time (e.g. on NixOS). */
ZFS_PATH,
#else
"/usr/sbin/zfs",
"/sbin/zfs",
"/usr/local/sbin/zfs",
"/usr/bin/zfs",
"/bin/zfs",
"/usr/local/bin/zfs",
#ifdef __APPLE__
/* macOS (Intel & Apple Silicon) */
"/opt/homebrew/sbin/zfs",
#endif
#endif
0
};
static const char* find_zfs(void)
{
for (int i = 0; zfs_paths[i]; ++i) {
if (access(zfs_paths[i], X_OK) == 0) {
return zfs_paths[i];
}
}
return 0;
}
static const char* const zpool_paths[] = {
#ifdef ZPOOL_PATH
/* Path configured at build time (e.g. on NixOS). */
ZPOOL_PATH,
#else
"/usr/sbin/zpool",
"/sbin/zpool",
"/usr/local/sbin/zpool",
"/usr/bin/zpool",
"/bin/zpool",
"/usr/local/bin/zpool",
#ifdef __APPLE__
/* macOS (Intel & Apple Silicon) */
"/opt/homebrew/sbin/zpool",
#endif
#endif
0
};
static const char* find_zpool(void)
{
for (int i = 0; zpool_paths[i]; ++i) {
if (access(zpool_paths[i], X_OK) == 0) {
return zpool_paths[i];
}
}
return 0;
}
#endif
#if HAVE_POSIX_FADVISE
/**
* Wrapper around posix_fadvise() that handles ENOENT by converting it to
* ENOSYS.
*
* Why can posix_fadvise() return ENOENT?
*
* According to POSIX and the Linux kernel's standard fadvise64 implementation
* (mm/fadvise.c), posix_fadvise() should only return EBADF (bad file
* descriptor), ESPIPE (fd refers to a pipe or FIFO), or EINVAL (invalid
* advice value or arguments). ENOENT is never generated by the kernel's
* generic path.
*
* However, the VFS layer allows individual filesystems to provide their own
* .fadvise() method, which is called instead of (or in addition to) the
* generic implementation. Certain filesystem drivers can return ENOENT in
* this method for reasons that are completely unrelated to the file not
* existing (the file is clearly open, we have a valid fd):
*
* - NFS: A stale NFS inode can be revalidated against the server while
* fadvise is being processed. If the server-side file has been removed or
* renamed, the revalidation discovers the inode is gone and returns
* -ENOENT, even though the client still holds an open file descriptor.
*
* - FUSE-based filesystems (mergerfs, sshfs, rclone, etc.): The FUSE daemon
* receives the fadvise request and may forward it to an underlying backend.
* If that backend cannot find the file (e.g., the backing path has changed,
* the FUSE daemon is partially initialised, or the daemon translates the
* request and the target layer returns an error), the daemon may return
* ENOENT to the kernel.
*
* - Overlayfs (used by Docker, Podman, LXC): The overlayfs .fadvise()
* implementation delegates to the lower or upper layer. If the lookup
* through the overlay stack fails to find the real inode (e.g., the upper
* directory entry is absent and the lower copy is in a weird state), the
* delegation can return ENOENT.
*
* - Other network or virtual filesystems: Any out-of-tree or in-tree
* filesystem that implements .fadvise() and performs a path or inode
* lookup as part of processing the hint can propagate ENOENT if that
* internal lookup fails.
*
* Because posix_fadvise() is always an advisory hint, it never affects
* correctness, only performance, it is safe to treat unexpected errors,
* including ENOENT, as "not supported" (ENOSYS) and continue.
*/
int posix_fadvise_wrapper(int fd, off_t offset, off_t len, int advice)
{
int ret = posix_fadvise(fd, offset, len, advice);
if (ret == ENOENT)
return ENOSYS;
return ret;
}
#endif
int open_noatime(const char* file, int flags)
{
#ifdef O_NOATIME
int f = open(file, flags | O_NOATIME);
/* only root is allowed to use O_NOATIME, in case retry without it */
if (f == -1 && errno == EPERM)
f = open(file, flags);
return f;
#else
return open(file, flags);
#endif
}
int dirent_hidden(struct dirent* dd)
{
return dd->d_name[0] == '.';
}
const char* stat_desc(struct stat* st)
{
if (S_ISREG(st->st_mode))
return "regular";
if (S_ISDIR(st->st_mode))
return "directory";
if (S_ISCHR(st->st_mode))
return "character";
if (S_ISBLK(st->st_mode))
return "block-device";
if (S_ISFIFO(st->st_mode))
return "fifo";
if (S_ISLNK(st->st_mode))
return "symlink";
if (S_ISSOCK(st->st_mode))
return "socket";
return "unknown";
}
#if HAVE_LINUX_DEVICE
static const char* smartctl_paths[] = {
#ifdef SMARTCTL_PATH
/* Path configured at build time (e.g. on NixOS). */
SMARTCTL_PATH,
#else
/* Linux & BSD */
"/usr/sbin/smartctl",
"/sbin/smartctl",
"/usr/local/sbin/smartctl",
"/usr/bin/smartctl",
"/usr/local/bin/smartctl",
#ifdef __APPLE__
/* macOS (Intel & Apple Silicon) */
"/opt/homebrew/sbin/smartctl",
#endif
#endif
0
};
static const char* find_smartctl(void)
{
for (int i = 0; smartctl_paths[i]; ++i) {
if (eaccess(smartctl_paths[i], X_OK) == 0) {
return smartctl_paths[i];
}
}
return 0;
}
#endif
/**
* Read a file from sys
*
* Return -1 on error, otherwise the size of data read
*/
#if HAVE_LINUX_DEVICE
static ssize_t sysread(const char* path, char* buf, size_t buf_size)
{
int f;
int ret;
ssize_t len;
f = open(path, O_RDONLY);
if (f == -1) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
len = read(f, buf, buf_size);
if (len < 0) {
/* LCOV_EXCL_START */
close(f);
return -1;
/* LCOV_EXCL_STOP */
}
ret = close(f);
if (ret != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return len;
}
#endif
#if HAVE_LINUX_DEVICE
/*
* sysread_vpd_pg83 — parse raw binary VPD page 0x83 (Device Identification)
* from a sysfs vpd_pg83 file and extract the first NAA (Network Address
* Authority) designator for the Logical Unit (ASSOCIATION == 0x00).
*/
static int sysattr_vpd_pg80(const char* path, char* dst, size_t dst_size)
{
unsigned char buf[512];
ssize_t ret = sysread(path, (char*)buf, sizeof(buf));
/* need at least the header */
if (ret < 4) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* validate page code */
if (buf[1] != 0x80) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* clamp to what was actually read */
size_t page_len = buf[3];
size_t available = (size_t)ret - 4;
if (available < page_len)
page_len = available;
/* if empty */
if (page_len == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* if too bit to store the 0 termination */
if (4 + page_len + 1 > sizeof(buf)) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
char* attr = (char*)buf + 4;
attr[page_len] = 0;
strtrim(attr);
/* if empty */
if (!*attr) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
pathcpy(dst, dst_size, attr);
return 0;
}
#endif
/*
* sysread_vpd_pg83 — parse raw binary VPD page 0x83 (Device Identification)
* from a sysfs vpd_pg83 file and extract the first NAA (Network Address
* Authority) designator for the Logical Unit (ASSOCIATION == 0x00).
*
* The result is written into dst as a lowercase hex string,
* e.g.: "5000c500abcdef01" (8-byte / NAA type 1,2,3,5)
* "6000c500abcdef010000000000000000" (16-byte / NAA type 6)
*
* VPD page 0x83 wire format (SPC-4 §7.7.2)
*
* Page header — 4 bytes:
* [0] peripheral qualifier (7:5) | device type (4:0)
* [1] page code = 0x83
* [2] PAGE LENGTH MSB } 16-bit big-endian: number of bytes that follow
* [3] PAGE LENGTH LSB } the header (i.e. total descriptor bytes)
*
* Then a sequence of Designation Descriptors:
* [+0] PROTOCOL ID (7:4) | CODE SET (3:0)
* 0x1 = binary, 0x2 = ASCII, 0x3 = UTF-8
* [+1] PIV (7) | reserved (6) | ASSOCIATION (5:4) | DESIGNATOR TYPE (3:0)
* ASSOCIATION: 0x0 = logical unit ← we want only this
* 0x1 = target port
* 0x2 = target device
* DESIGNATOR TYPE: 0x3 = NAA ← we want only this
* [+2] reserved
* [+3] DESIGNATOR LENGTH = N (bytes that follow in this descriptor)
* [+4 … +4+N-1] DESIGNATOR (binary for NAA)
*
* NAA subtypes (top nibble of designator byte 0)
*
* 0x1 NAA IEEE Extended → 8 bytes
* 0x2 NAA Locally Assigned → 8 bytes
* 0x3 NAA IEEE Registered → 8 bytes (most common on SATA/SAS)
* 0x5 NAA IEEE Registered → 8 bytes
* 0x6 NAA IEEE Registered Ext. → 16 bytes
* others: reserved
*
* The DESIGNATOR LENGTH from the descriptor header must agree with the
* expected length for the NAA subtype. If they disagree, the descriptor
* is malformed and must be skipped.
*
* Returns 0 and fills dst on success, -1 if no valid NAA LU descriptor found.
*/
#if HAVE_LINUX_DEVICE
static int sysattr_vpd_pg83(const char* path, char* dst, size_t dst_size)
{
unsigned char buf[4096];
ssize_t ret = sysread(path, (char*)buf, sizeof(buf));
if (ret < 4) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* validate page code */
if (buf[1] != 0x83) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
size_t page_len = ((size_t)buf[2] << 8) | buf[3];
/* clamp to what was actually read */
size_t available = (size_t)ret - 4;
if (page_len > available)
page_len = available;
/* if empty */
if (page_len == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/*
* Walk every Designation Descriptor looking for the first NAA descriptor
* that is associated with the Logical Unit (ASSOCIATION == 0x00).
*
* We take the first one found rather than scoring: all NAA subtypes are
* globally unique by construction; the first LU-associated NAA is the
* canonical device WWN.
*/
size_t offset = 0;
while (offset + 4 <= page_len) {
const unsigned char* desc = buf + 4 + offset;
unsigned code_set = desc[0] & 0x0f;
unsigned assoc = (desc[1] >> 4) & 0x03;
unsigned dtype = desc[1] & 0x0f;
size_t id_len = desc[3];
/* bounds check: the full descriptor must lie within page_len */
if (offset + 4 + id_len > page_len)
break; /* malformed page — stop iterating */
/* skip anything that is not an LU-associated NAA descriptor */
if (assoc != 0x00 || dtype != 0x03)
goto next;
/*
* NAA is always binary (CODE SET == 0x01).
* Reject if the device mis-advertises it as ASCII (defensive).
*/
if (code_set != 0x01)
goto next;
/* if empty */
if (id_len == 0)
goto next;
/*
* Validate NAA subtype vs. expected byte length.
* The NAA value is the top nibble of the first byte of the
* designator (NOT the descriptor header byte).
*/
unsigned naa = (desc[4] >> 4) & 0x0f;
size_t expected = 0;
switch (naa) {
case 0x1 : /* IEEE Extended */
case 0x2 : /* Locally Assigned */
case 0x3 : /* IEEE Registered */
case 0x5 : /* IEEE Registered */
expected = 8;
break;
case 0x6 : /* IEEE Registered Extended */
expected = 16;
break;
default :
goto next;
}
/* DESIGNATOR LENGTH must match the NAA subtype exactly */
if (id_len != expected)
goto next;
if (dst_size < id_len * 2 + 1)
return -1;
for (size_t i = 0; i < id_len; i++)
snprintf(dst + i * 2, 3, "%02x", desc[4 + i]);
dst[id_len * 2] = 0;
return 0;
next:
offset += 4 + id_len;
}
return -1;
}
#endif
/**
* Read a file from sys.
* Trim spaces.
* Always put an ending 0.
* Do not report error on reading.
* Return an error if truncated
*
* Return -1 on error, 0 on success
*/
#if HAVE_LINUX_DEVICE
static int sysattr(const char* path, char* buf, size_t buf_size)
{
ssize_t len;
len = sysread(path, buf, buf_size);
if (len < 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
if ((size_t)len + 1 > buf_size) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
buf[len] = 0;
strtrim(buf);
return 0;
}
#endif
/******************************************************************************/
/* cpu */
#if HAVE_LINUX_DEVICE
static long long syslong(const char* path)
{
char buf[32];
if (sysattr(path, buf, sizeof(buf)) != 0)
return -1;
char* end;
long long v = strtoll(buf, &end, 10);
if (end == buf)
return -1;
return v;
}
static unsigned long long syshex(const char* path)
{
char buf[1152]; /* enough for 1024 CPUs */
if (sysattr(path, buf, sizeof(buf)) != 0)
return 0;
unsigned long long v = 0;
for (char* p = buf; *p != 0 && *p != '\n'; ++p) {
if (*p == ',')
continue;
v <<= 4;
if (*p >= '0' && *p <= '9')
v |= *p - '0';
else if (*p >= 'a' && *p <= 'f')
v |= *p - 'a' + 10;
else if (*p >= 'A' && *p <= 'F')
v |= *p - 'A' + 10;
else
break; /* hit unexpected character */
}
return v;
}
static int cpu_is_online(int i)
{
char path[256];
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/online", i);
long long v = syslong(path);
if (v < 0)
return 1; /* cpu0 has no "online" knob — always on */
return v;
}
/*
* Returns 1 if the specified cpu is the lowest-numbered logical CPU in its
* physical core's SMT sibling set.
*
* thread_siblings is a hex bitmask where bit N = logical cpu N.
* The lowest set bit identifies the primary sibling.
* Safe for up to 64 logical CPUs (fits in unsigned long).
*/
static int cpu_is_primary_sibling(int i)
{
char path[256];
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/topology/thread_siblings", i);
unsigned long long mask = syshex(path);
if (mask == 0) /* assume primary */
return 1;
return __builtin_ctzll(mask) == i;
}
#define CPUS_MAX 64
/**
* os_get_optimal_cpu() – returns the logical CPU index of the "fastest"
* core using a three-tier strategy:
*
* Tier 1 – ACPI CPPC preferred-core ranking (hardware-binned quality)
* Tier 2 – cpufreq max-frequency (P-core vs E-core discrimination)
* Tier 3 – SMT topology isolation (primary sibling only)
*/
int os_get_optimal_cpu(void)
{
char path[256];
int num_cpus = sysconf(_SC_NPROCESSORS_CONF);
if (num_cpus <= 0)
return -1;
if (num_cpus > CPUS_MAX)
num_cpus = CPUS_MAX;
/* ACPI CPPC: highest_perf with nominal_perf tiebreaker */
long long max_highest = -1;
long long max_nominal = -1;
int best_cppc = -1;
for (int i = 0; i < num_cpus; ++i) {
if (!cpu_is_online(i))
continue;
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/acpi_cppc/highest_perf", i);
long long hp = syslong(path);
if (hp < 0)
continue;
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/acpi_cppc/nominal_perf", i);
long long np = syslong(path);
/* np may be missing */
/* tiebreaker priority: highest_perf > nominal_perf > non-CPU0 */
int better = (hp > max_highest)
|| (hp == max_highest && np > max_nominal)
|| (hp == max_highest && np == max_nominal && best_cppc == 0);
if (better) {
max_highest = hp;
max_nominal = np;
best_cppc = i;
}
}
if (best_cppc >= 0)
return best_cppc;
/* max frequency, then SMT isolation */
/* find the global maximum frequency among online CPUs */
long long global_max_freq = -1;
for (int i = 0; i < num_cpus; ++i) {
if (!cpu_is_online(i))
continue;
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", i);
long long freq = syslong(path);
if (freq < 0)
continue;
if (freq > global_max_freq)
global_max_freq = freq;
}
/* first online CPU at max freq that is a primary sibling, preferring non-CPU0 */
int best_isolated = -1; /* primary sibling, not CPU 0 */
int best_any = -1; /* any matching CPU, not CPU 0 */
int best_c0 = -1; /* CPU 0 fallback (always a primary sibling) */
for (int i = 0; i < num_cpus; ++i) {
if (!cpu_is_online(i))
continue;
if (global_max_freq != -1) {
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", i);
if (syslong(path) != global_max_freq)
continue;
}
if (i == 0) {
best_c0 = 0;
} else {
if (best_isolated == -1 && cpu_is_primary_sibling(i))
best_isolated = i;
if (best_any == -1)
best_any = i;
}
}
/* prefer non-CPU0, fall back to CPU0 only if no alternative exists */
if (best_isolated != -1)
return best_isolated;
if (best_any != -1)
return best_any;
if (best_c0 != -1)
return best_c0;
return -1;
}
#endif
/******************************************************************************/
/* dev */
struct dev_struct {
uint64_t device; /**< Device ID. */
tommy_node node;
};
#if HAVE_LINUX_DEVICE
static int devdereference_btrfs(uint64_t device, const char* dir, int fd, tommy_list* devlist)
{
int found = 0;
struct btrfs_ioctl_fs_info_args fs_info;
memset(&fs_info, 0, sizeof(fs_info));
if (ioctl(fd, BTRFS_IOC_FS_INFO, &fs_info) < 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
if (fs_info.max_id == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
for (__u64 i = 1; i <= fs_info.max_id; ++i) {
struct btrfs_ioctl_dev_info_args dev_info;
memset(&dev_info, 0, sizeof(dev_info));
dev_info.devid = i;
if (ioctl(fd, BTRFS_IOC_DEV_INFO, &dev_info) != 0) {
/* LCOV_EXCL_START */
/* btrfs device IDs (devid) are not guaranteed to be contiguous */
if (errno == ENODEV || errno == ENOENT)
continue; /* this is the expected error on missing id */
log_error(errno, "Ioctl BTRFS_IOC_DEV_INFO failed. %s.", strerror(errno));
continue;
/* LCOV_EXCL_STOP */
}
/* get major:minor, use stat on the path returned */
struct stat st;
if (stat((char*)dev_info.path, &st) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
struct dev_struct* dev = malloc_nofail(sizeof(struct dev_struct));
dev->device = st.st_rdev;
tommy_list_insert_tail(devlist, &dev->node, dev);
++found;
log_tag("dereference:btrfs:%s:%u:%u:%u:%u\n", dir, major(device), minor(device), major(dev->device), minor(dev->device));
}
/* something has to be found */
if (found == 0)
return -1;
return 0;
}
static int devdereference_bcachefs(uint64_t device, const char* dir, tommy_list* devlist)
{
char resolved[PATH_MAX];
char device_list[PATH_MAX * 8];
if (realpath(dir, resolved) == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
FILE* fd = fopen("/proc/self/mountinfo", "r");
if (!fd) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/*
* mountinfo format
* 0 - mount ID
* 1 - parent ID
* 2 - major:minor
* 3 - root
* 4 - mount point
* 5 - options
* 6 - "-" (separator)
* 7 - fs
* 8 - mount source - /dev/device
*/
size_t best_len = 0;
while (1) {
char buf[PATH_MAX * 2 + 64];
char* id_map[8];
unsigned id_mac;
char* fs_map[8];
unsigned fs_mac;
char* s = fgets(buf, sizeof(buf), fd);
if (s == 0)
break;
/* find the separator position */
char* separator = strstr(s, " - ");
if (!separator)
continue;
/* skip the separator */
*separator = 0;
separator += 3;
/* split the line */
id_mac = strsplit(id_map, 8, s, " \t\n");
fs_mac = strsplit(fs_map, 8, separator, " \t\n");
/* if too short, it's the wrong line */
if (id_mac < 5)
continue;
if (fs_mac < 2)
continue;
/* mount point must contain the directory */
const char* mp = id_map[4];
size_t mp_len = strlen(mp);
if (strncmp(resolved, mp, mp_len) != 0)
continue;
if (mp_len > 1 && resolved[mp_len] != '/' && resolved[mp_len] != 0)
continue;
/* it should be bcachefs */
const char* fs = fs_map[0];
if (strcmp(fs, "bcachefs") != 0)
continue;
/* keep the longest (innermost) match */
if (mp_len > best_len) {
best_len = mp_len;
pathcpy(device_list, sizeof(device_list), fs_map[1]);
}
}
fclose(fd);
if (best_len == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* split the device list */
char* dev_map[64];
unsigned dev_mac = strsplit(dev_map, 64, device_list, ":");
for (unsigned i = 0; i < dev_mac; ++i) {
/* get major:minor, use stat on the path returned */
struct stat st;
if (stat(dev_map[i], &st) != 0)
continue;
struct dev_struct* dev = malloc_nofail(sizeof(struct dev_struct));
dev->device = st.st_rdev;
tommy_list_insert_tail(devlist, &dev->node, dev);
log_tag("dereference:bcachefs:%s:%u:%u:%u:%u\n", dir, major(device), minor(device), major(dev->device), minor(dev->device));
}
return 0;
}
static int extract_zfs(const char* dir, char* dataset, size_t dataset_size, char* uuid, size_t uuid_size, char* mount_point, size_t mount_point_size)
{
char resolved[PATH_MAX];
const char* zfs = find_zfs();
if (!zfs) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
if (realpath(dir, resolved) == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* list all ZFS filesystems in one shot */
const char* argv[] = {
zfs,
"list",
"-H",
"-o",
"name,guid,mountpoint",
"-t",
"filesystem",
0
};
OS_FILE* fp = os_popen(argv, 0);
if (!fp) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
size_t best_len = 0;
while (1) {
char buf[PATH_MAX * 2 + 64];
char* map[3];
unsigned mac;
char* s = os_fgets(buf, sizeof(buf), fp);
if (s == 0)
break;
/* split the line */
mac = strsplit(map, 3, s, " \t\n");
if (mac < 3)
continue;
const char* mp = map[2];
size_t mp_len = strlen(mp);
if (strncmp(resolved, mp, mp_len) != 0)
continue;
if (mp_len > 1 && resolved[mp_len] != '/' && resolved[mp_len] != 0)
continue;
/* keep the longest (innermost) match */
if (mp_len > best_len) {
best_len = mp_len;
if (dataset)
pathcpy(dataset, dataset_size, map[0]);
if (uuid)
pathcpy(uuid, uuid_size, map[1]);
if (mount_point)
pathcpy(mount_point, mount_point_size, map[2]);
}
}
os_pclose(fp);
if (best_len == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
static int devdereference_zfs(uint64_t device, const char* dir, tommy_list* devlist)
{
char pool[PATH_MAX];
const char* zpool = find_zpool();
if (!zpool) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
if (extract_zfs(dir, pool, sizeof(pool), 0, 0, 0, 0) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* extract pool name */
char* slash = strchr(pool, '/');
if (slash)
*slash = 0;
const char* argv[] = {
zpool,
"status",
"-P",
pool,
0
};
OS_FILE* fp = os_popen(argv, 0);
if (!fp) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
size_t count = 0;
while (1) {
char buf[PATH_MAX * 2 + 64];
char* s = os_fgets(buf, sizeof(buf), fp);
if (s == 0)
break;
char* file = strstr(s, "/dev/");
if (!file)
continue;
size_t i = 0;
while (file[i] != 0 && file[i] != ' ' && file[i] != '\t' && file[i] != '\n')
++i;
file[i] = 0;
struct stat st;
if (stat(file, &st) != 0)
continue;
if (!S_ISBLK(st.st_mode))
continue;
struct dev_struct* dev = malloc_nofail(sizeof(struct dev_struct));
dev->device = st.st_rdev;
tommy_list_insert_tail(devlist, &dev->node, dev);
log_tag("dereference:zfs:%s:%u:%u:%u:%u\n", dir, major(device), minor(device), major(dev->device), minor(dev->device));
++count;
}
os_pclose(fp);
if (count == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
/**
* Get the devices of a virtual device.
*
* This is intended to resolve the case of Btrfs/ZFS filesystems that
* create a virtual superblock (major==0) not backed by any low
* level device.
*
* See:
* Bug 711881 - too funny btrfs st_dev numbers
* https://bugzilla.redhat.com/show_bug.cgi?id=711881
*/
static int devdereference(uint64_t device, const char* dir, tommy_list* devlist)
{
struct statfs sfs;
int fd = open(dir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd < 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
if (fstatfs(fd, &sfs) != 0) {
/* LCOV_EXCL_START */
close(fd);
return -1;
/* LCOV_EXCL_STOP */
}
int ret;
if (sfs.f_type == BTRFS_SUPER_MAGIC) {
ret = devdereference_btrfs(device, dir, fd, devlist);
} else if (sfs.f_type == BCACHEFS_SUPER_MAGIC) {
ret = devdereference_bcachefs(device, dir, devlist);
} else if (sfs.f_type == ZFS_SUPER_MAGIC) {
ret = devdereference_zfs(device, dir, devlist);
} else {
/* insert the device itself in the list */
struct dev_struct* dev = malloc_nofail(sizeof(struct dev_struct));
dev->device = device;
tommy_list_insert_tail(devlist, &dev->node, dev);
ret = 0;
}
close(fd);
return ret;
}
#endif
/**
* Read a file extracting the specified tag TAG=VALUE format.
* Return !=0 on error.
*/
#if HAVE_LINUX_DEVICE
static int tagread(const char* path, const char* tag, char* value, size_t value_size)
{
ssize_t ret;
char buf[512];
size_t tag_len;
char* i;
char* e;
ret = sysread(path, buf, sizeof(buf));
if (ret < 0) {
/* LCOV_EXCL_START */
log_fatal(errno, "Failed to read '%s'.\n", path);
return -1;
/* LCOV_EXCL_STOP */
}
if ((size_t)ret + 1 > sizeof(buf)) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Too long read '%s'.\n", path);
return -1;
/* LCOV_EXCL_STOP */
}
/* ending 0 */
buf[ret] = 0;
tag_len = strlen(tag);
for (i = buf; *i; ++i) {
char* p = i;
/* start with a space */
if (p != buf) {
if (!isspace((unsigned char)*p))
continue;
++p;
}
if (strncmp(p, tag, tag_len) != 0)
continue;
p += tag_len;
/* end with a = */
if (*p != '=')
continue;
++p;
/* found */
i = p;
break;
}
if (!*i) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Missing tag '%s' for '%s'.\n", tag, path);
return -1;
/* LCOV_EXCL_STOP */
}
/* terminate at the first space */
e = i;
while (*e != 0 && !isspace((unsigned char)*e))
++e;
*e = 0;
if (!*i) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Empty tag '%s' for '%s'.\n", tag, path);
return -1;
/* LCOV_EXCL_STOP */
}
pathprint(value, value_size, "%s", i);
return 0;
}
#endif
/**
* Get the device file from the device number.
*
* It uses /sys/dev/block/.../uevent.
*
* For null device (major==0) it fails.
*/
#if HAVE_LINUX_DEVICE
static int devresolve_sys(dev_t device, char* path, size_t path_size)
{
struct stat st;
char buf[PATH_MAX];
/* default device path from device number */
pathprint(path, path_size, "/sys/dev/block/%u:%u/uevent", major(device), minor(device));
if (tagread(path, "DEVNAME", buf, sizeof(buf)) != 0) {
/* LCOV_EXCL_START */
log_tag("resolve:sys:%u:%u: failed to read DEVNAME tag '%s'\n", major(device), minor(device), path);
return -1;
/* LCOV_EXCL_STOP */
}
/* set the real device path */
pathprint(path, path_size, "/dev/%s", buf);
/* check the device */
if (stat(path, &st) != 0) {
/* LCOV_EXCL_START */
log_tag("resolve:sys:%u:%u: failed to stat '%s'\n", major(device), minor(device), path);
return -1;
/* LCOV_EXCL_STOP */
}
if (st.st_rdev != device) {
/* LCOV_EXCL_START */
log_tag("resolve:sys:%u:%u: unexpected device '%u:%u' for '%s'.\n", major(device), minor(device), major(st.st_rdev), minor(st.st_rdev), path);
return -1;
/* LCOV_EXCL_STOP */
}
log_tag("resolve:sys:%u:%u:%s: found\n", major(device), minor(device), path);
return 0;
}
#endif
/**
* Get the device file from the device number.
*/
#if HAVE_LINUX_DEVICE
static int devresolve(uint64_t device, char* path, size_t path_size)
{
if (devresolve_sys(device, path, path_size) == 0)
return 0;
return -1;
}
#endif
/**
* Cache used by blkid.
*/
#if HAVE_BLKID
static blkid_cache cache = 0;
#endif
/**
* Get the UUID using the /dev/disk/by-uuid/ links.
* It doesn't require root permission, and the uuid are always updated.
* It doesn't work with Btrfs file-systems that don't export the main UUID
* in /dev/disk/by-uuid/.
*/
#if HAVE_LINUX_DEVICE
static int devuuid_dev(uint64_t device, char* uuid, size_t uuid_size)
{
ssize_t ret;
DIR* d;
struct dirent* dd;
struct stat st;
/* scan the UUID directory searching for the device */
d = opendir("/dev/disk/by-uuid");
if (!d) {
/* LCOV_EXCL_START */
log_tag("uuid:by-uuid:%u:%u: opendir(/dev/disk/by-uuid) failed, %s\n", major(device), minor(device), strerror(errno));
/* directory missing?, likely we are not in Linux */
return -1;
/* LCOV_EXCL_STOP */
}
int dir_fd = dirfd(d);
if (dir_fd == -1) {
/* LCOV_EXCL_START */
log_tag("uuid:by-uuid:%u:%u: dirfd(/dev/disk/by-uuid) failed, %s\n", major(device), minor(device), strerror(errno));
closedir(d);
return -1;
/* LCOV_EXCL_STOP */
}
while ((dd = readdir(d)) != 0) {
/* skip "." and ".." files, UUIDs never start with '.' */
if (dd->d_name[0] == '.')
continue;
ret = fstatat(dir_fd, dd->d_name, &st, 0);
if (ret != 0) {
/* LCOV_EXCL_START */
log_tag("uuid:by-uuid:%u:%u: fstatat(%s) failed, %s\n", major(device), minor(device), dd->d_name, strerror(errno));
/* generic error, ignore and continue the search */
continue;
/* LCOV_EXCL_STOP */
}
/* if it matches, we have the uuid */
if (S_ISBLK(st.st_mode) && st.st_rdev == (dev_t)device) {
char buf[PATH_MAX];
char path[PATH_MAX];
/* resolve the link */
pathprint(path, sizeof(path), "/dev/disk/by-uuid/%s", dd->d_name);
ret = readlink(path, buf, sizeof(buf));
if (ret < 0 || ret >= PATH_MAX) {
/* LCOV_EXCL_START */
log_tag("uuid:by-uuid:%u:%u: readlink(/dev/disk/by-uuid/%s) failed, %s\n", major(device), minor(device), dd->d_name, strerror(errno));
/* generic error, ignore and continue the search */
continue;
/* LCOV_EXCL_STOP */
}
buf[ret] = 0;
/* found */
pathcpy(uuid, uuid_size, dd->d_name);
log_tag("uuid:by-uuid:%u:%u:%s: found %s\n", major(device), minor(device), uuid, buf);
closedir(d);
return 0;
}
}
log_tag("uuid:by-uuid:%u:%u: /dev/disk/by-uuid doesn't contain a matching block device\n", major(device), minor(device));
/* not found */
closedir(d);
return -1;
}
#endif
/**
* Get the UUID using libblkid.
* It uses a cache to work without root permission, resulting in UUID
* not necessarily recent.
* We could call blkid_probe_all() to refresh the UUID, but it would
* require root permission to read the superblocks, and resulting in
* all the disks spinning.
*/
#if HAVE_BLKID
static int devuuid_blkid(uint64_t device, char* uuid, size_t uuid_size)
{
char* devname;
char* uuidname;
devname = blkid_devno_to_devname(device);
if (!devname) {
/* LCOV_EXCL_START */
log_tag("uuid:blkid:%u:%u: blkid_devno_to_devname() failed, %s\n", major(device), minor(device), strerror(errno));
/* device mapping failed */
return -1;
/* LCOV_EXCL_STOP */
}
uuidname = blkid_get_tag_value(cache, "UUID", devname);
if (!uuidname) {
/* LCOV_EXCL_START */
log_tag("uuid:blkid:%u:%u: blkid_get_tag_value(UUID,%s) failed, %s\n", major(device), minor(device), devname, strerror(errno));
/* uuid mapping failed */
free(devname);
return -1;
/* LCOV_EXCL_STOP */
}
pathcpy(uuid, uuid_size, uuidname);
log_tag("uuid:blkid:%u:%u:%s: found %s\n", major(device), minor(device), uuid, devname);
free(devname);
free(uuidname);
return 0;
}
#endif
/**
* Get the LABEL using libblkid.
* It uses a cache to work without root permission, resulting in LABEL
* not necessarily recent.
*/
#if HAVE_BLKID
static int devuuid_label(uint64_t device, char* label, size_t label_size)
{
char* devname;
char* labelname;
devname = blkid_devno_to_devname(device);
if (!devname) {
/* LCOV_EXCL_START */
log_tag("label:blkid:%u:%u: blkid_devno_to_devname() failed, %s\n", major(device), minor(device), strerror(errno));
/* device mapping failed */
return -1;
/* LCOV_EXCL_STOP */
}
labelname = blkid_get_tag_value(cache, "LABEL", devname);
if (!labelname) {
/* LCOV_EXCL_START */
log_tag("label:blkid:%u:%u: blkid_get_tag_value(LABEL,%s) failed, %s\n", major(device), minor(device), devname, strerror(errno));
free(devname);
/* label mapping failed */
return -1;
/* LCOV_EXCL_STOP */
}
pathcpy(label, label_size, labelname);
log_tag("label:blkid:%u:%u:%s: found %s\n", major(device), minor(device), label, devname);
free(devname);
free(labelname);
return 0;
}
#endif
#ifdef __APPLE__
static int devuuid_darwin(const char* path, char* uuid, size_t uuid_size)
{
CFStringRef path_apple = 0;
DASessionRef session = 0;
CFURLRef path_appler = 0;
int result = -1;
*uuid = 0;
path_apple = CFStringCreateWithCString(kCFAllocatorDefault, path, kCFStringEncodingUTF8);
if (!path_apple)
goto bail;
session = DASessionCreate(kCFAllocatorDefault);
if (!session)
goto bail;
path_appler = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, path_apple, kCFURLPOSIXPathStyle, false);
if (!path_appler)
goto bail;
/* find the mount point */
DADiskRef disk = NULL;
while (path_appler) {
disk = DADiskCreateFromVolumePath(kCFAllocatorDefault, session, path_appler);
if (disk)
break;
CFURLRef parent = CFURLCreateCopyDeletingLastPathComponent(kCFAllocatorDefault, path_appler);
if (!parent)
break;
/* check if we hit the root (parent == child) */
if (CFEqual(parent, path_appler)) {
CFRelease(parent);
break;
}
CFRelease(path_appler);
path_appler = parent;
}
/* safely extract the UUID */
if (disk) {
CFDictionaryRef description = DADiskCopyDescription(disk);
if (description) {
/* key might not exist for NTFS/ExFAT on some drivers */
const void* value = (CFUUIDRef)CFDictionaryGetValue(description, kDADiskDescriptionVolumeUUIDKey);
CFStringRef uuid_string = NULL;
if (value) {
if (CFGetTypeID(value) == CFUUIDGetTypeID()) {
uuid_string = CFUUIDCreateString(kCFAllocatorDefault, (CFUUIDRef)value);
} else if (CFGetTypeID(value) == CFStringGetTypeID()) {
/* if it's already a string, retain it so we can release it later consistently */
uuid_string = (CFStringRef)CFRetain(value);
}
}
if (uuid_string) {
if (CFStringGetCString(uuid_string, uuid, uuid_size, kCFStringEncodingUTF8)) {
result = 0; /* success */
}
CFRelease(uuid_string);
}
CFRelease(description);
}
CFRelease(disk);
}
bail:
/* clean up */
if (path_appler) CFRelease(path_appler);
if (session) CFRelease(session);
if (path_apple) CFRelease(path_apple);
return result;
}
#endif
#if HAVE_LINUX_DEVICE
static int devuuid_btrfs(uint64_t device, const char* dir, char* uuid, size_t uuid_size)
{
struct statfs sfs;
int fd = open(dir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd < 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP*/
}
if (fstatfs(fd, &sfs) != 0) {
/* LCOV_EXCL_START */
close(fd);
return -1;
/* LCOV_EXCL_STOP */
}
if (sfs.f_type != BTRFS_SUPER_MAGIC) {
close(fd);
return -1;
}
struct btrfs_ioctl_fs_info_args info;
memset(&info, 0, sizeof(info));
if (ioctl(fd, BTRFS_IOC_FS_INFO, &info) < 0) {
/* LCOV_EXCL_START */
close(fd);
return -1;
/* LCOV_EXCL_STOP */
}
snprintf(uuid, uuid_size,
"%02x%02x%02x%02x-"
"%02x%02x-"
"%02x%02x-"
"%02x%02x-"
"%02x%02x%02x%02x%02x%02x",
info.fsid[0], info.fsid[1], info.fsid[2], info.fsid[3],
info.fsid[4], info.fsid[5],
info.fsid[6], info.fsid[7],
info.fsid[8], info.fsid[9],
info.fsid[10], info.fsid[11], info.fsid[12], info.fsid[13], info.fsid[14], info.fsid[15]
);
log_tag("uuid:by-btrfs:%u:%u:%s: found %s\n", major(device), minor(device), uuid, dir);
close(fd);
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
struct bch_ioctl_query_uuid {
__u8 uuid[16];
};
#define BCH_IOCTL_QUERY_UUID _IOR(0xbc, 1, struct bch_ioctl_query_uuid)
static int devuuid_bcachefs(uint64_t device, const char* dir, char* uuid, size_t uuid_size)
{
struct statfs sfs;
int fd = open(dir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd < 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP*/
}
if (fstatfs(fd, &sfs) != 0) {
/* LCOV_EXCL_START */
close(fd);
return -1;
/* LCOV_EXCL_STOP */
}
if (sfs.f_type != BCACHEFS_SUPER_MAGIC) {
close(fd);
return -1;
}
struct bch_ioctl_query_uuid info;
memset(&info, 0, sizeof(info));
if (ioctl(fd, BCH_IOCTL_QUERY_UUID, &info) < 0) {
/* LCOV_EXCL_START */
close(fd);
return -1;
/* LCOV_EXCL_STOP */
}
close(fd);
snprintf(uuid, uuid_size,
"%02x%02x%02x%02x-"
"%02x%02x-"
"%02x%02x-"
"%02x%02x-"
"%02x%02x%02x%02x%02x%02x",
info.uuid[0], info.uuid[1], info.uuid[2], info.uuid[3],
info.uuid[4], info.uuid[5],
info.uuid[6], info.uuid[7],
info.uuid[8], info.uuid[9],
info.uuid[10], info.uuid[11], info.uuid[12], info.uuid[13], info.uuid[14], info.uuid[15]
);
log_tag("uuid:by-bcachefs:%u:%u:%s: found %s\n", major(device), minor(device), uuid, dir);
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
int devuuid_zfs(uint64_t device, const char* dir, char* uuid, size_t uuid_size)
{
if (extract_zfs(dir, 0, 0, uuid, uuid_size, 0, 0) != 0)
return -1;
log_tag("uuid:by-zfs:%u:%u:%s: found %s\n", major(device), minor(device), uuid, dir);
return 0;
}
#endif
int devuuid(uint64_t device_id, const char* device_path, char* uuid, size_t uuid_size)
{
(void)device_path;
(void)device_id;
#ifdef __APPLE__
if (devuuid_darwin(device_path, uuid, uuid_size) == 0)
return 0;
#endif
#if HAVE_LINUX_DEVICE
if (devuuid_btrfs(device_id, device_path, uuid, uuid_size) == 0)
return 0;
#endif
#if HAVE_LINUX_DEVICE
if (devuuid_bcachefs(device_id, device_path, uuid, uuid_size) == 0)
return 0;
#endif
#if HAVE_LINUX_DEVICE
if (devuuid_zfs(device_id, device_path, uuid, uuid_size) == 0)
return 0;
#endif
#if HAVE_LINUX_DEVICE
if (devuuid_dev(device_id, uuid, uuid_size) == 0)
return 0;
#else
log_tag("uuid:by-uuid:%u:%u: by-uuid not supported\n", major(device_id), minor(device_id));
#endif
#if HAVE_BLKID
if (devuuid_blkid(device_id, uuid, uuid_size) == 0)
return 0;
#else
log_tag("uuid:blkid:%u:%u: blkid support not compiled in\n", major(device_id), minor(device_id));
#endif
log_tag("uuid:notfound:%u:%u:\n", major(device_id), minor(device_id));
/* not supported */
(void)uuid;
(void)uuid_size;
return -1;
}
int filephy(const char* path, uint64_t size, uint64_t* physical)
{
#if HAVE_LINUX_FIEMAP_H
/*
* In Linux get the real physical address of the file
* Note that FIEMAP doesn't require root permission
*/
int f;
struct fiemap* fiemap;
size_t fiemap_size;
unsigned int blknum;
f = open(path, O_RDONLY);
if (f == -1) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/*
* First try with FIEMAP
* if works for ext2, ext3, ext4, xfs, btrfs
*/
fiemap_size = sizeof(struct fiemap) + sizeof(struct fiemap_extent);
fiemap = malloc_nofail(fiemap_size);
memset(fiemap, 0, fiemap_size);
fiemap->fm_start = 0;
fiemap->fm_length = ~0ULL;
fiemap->fm_flags = FIEMAP_FLAG_SYNC; /* required to ensure that just created files report a valid address and not 0 */
fiemap->fm_extent_count = 1; /* we are interested only at the first block */
if (ioctl(f, FS_IOC_FIEMAP, fiemap) != -1) {
uint32_t flags = fiemap->fm_extents[0].fe_flags;
uint64_t offset = fiemap->fm_extents[0].fe_physical;
/* check some condition for validating the offset */
if (flags & FIEMAP_EXTENT_DATA_INLINE) {
/* if the data is inline, we don't have an offset to report */
*physical = FILEPHY_WITHOUT_OFFSET;
} else if (flags & FIEMAP_EXTENT_UNKNOWN) {
/* if the offset is unknown, we don't have an offset to report */
*physical = FILEPHY_WITHOUT_OFFSET;
} else if (offset == 0) {
/*
* 0 is the general fallback for file-systems when
* they don't have an offset to report
*/
*physical = FILEPHY_WITHOUT_OFFSET;
} else {
/* finally report the real offset */
*physical = offset + FILEPHY_REAL_OFFSET;
}
free(fiemap);
if (close(f) == -1)
return -1;
return 0;
}
free(fiemap);
/* if the file is empty, FIBMAP doesn't work, and we don't even try to use it */
if (size == 0) {
*physical = FILEPHY_WITHOUT_OFFSET;
if (close(f) == -1)
return -1;
return 0;
}
/*
* Then try with FIBMAP
* it works for jfs, reiserfs, ntfs-3g
* in exfat it always returns 0, that it's anyway better than the fake inodes
*/
blknum = 0; /* first block */
if (ioctl(f, FIBMAP, &blknum) != -1) {
*physical = blknum + FILEPHY_REAL_OFFSET;
if (close(f) == -1)
return -1;
return 0;
}
/*
* Otherwise don't use anything, and keep the directory traversal order
* at now this should happen only for vfat
* and it's surely better than using fake inodes
*/
*physical = FILEPHY_UNREPORTED_OFFSET;
if (close(f) == -1)
return -1;
#else
/*
* In a generic Unix use a dummy value for all the files
* We don't want to risk to use the inode without knowing
* if it really improves performance.
* In this way we keep them in the directory traversal order
* that at least keeps files in the same directory together.
* Note also that in newer file-system with snapshot, like ZFS,
* the inode doesn't represent even more the disk position, because files
* are not overwritten in place, but rewritten in another location
* of the disk.
*/
*physical = FILEPHY_UNREPORTED_OFFSET;
(void)path; /* not used here */
(void)size;
#endif
return 0;
}
/* from man statfs */
#define ADFS_SUPER_MAGIC 0xadf5
#define AFFS_SUPER_MAGIC 0xADFF
#define BDEVFS_MAGIC 0x62646576
#define BEFS_SUPER_MAGIC 0x42465331
#define BFS_MAGIC 0x1BADFACE
#define BINFMTFS_MAGIC 0x42494e4d
#define CGROUP_SUPER_MAGIC 0x27e0eb
#define CIFS_MAGIC_NUMBER 0xFF534D42
#define CODA_SUPER_MAGIC 0x73757245
#define COH_SUPER_MAGIC 0x012FF7B7
#define CRAMFS_MAGIC 0x28cd3d45
#define DEBUGFS_MAGIC 0x64626720
#define DEVFS_SUPER_MAGIC 0x1373
#define DEVPTS_SUPER_MAGIC 0x1cd1
#define EFIVARFS_MAGIC 0xde5e81e4
#define EFS_SUPER_MAGIC 0x00414A53
#define EXT_SUPER_MAGIC 0x137D
#define EXT2_OLD_SUPER_MAGIC 0xEF51
#define EXT4_SUPER_MAGIC 0xEF53 /* also ext2/ext3 */
#define FUSE_SUPER_MAGIC 0x65735546
#define FUTEXFS_SUPER_MAGIC 0xBAD1DEA
#define HFS_SUPER_MAGIC 0x4244
#define HFSPLUS_SUPER_MAGIC 0x482b
#define HOSTFS_SUPER_MAGIC 0x00c0ffee
#define HPFS_SUPER_MAGIC 0xF995E849
#define HUGETLBFS_MAGIC 0x958458f6
#define ISOFS_SUPER_MAGIC 0x9660
#define JFFS2_SUPER_MAGIC 0x72b6
#define JFS_SUPER_MAGIC 0x3153464a
#define MINIX_SUPER_MAGIC 0x137F
#define MINIX_SUPER_MAGIC2 0x138F
#define MINIX2_SUPER_MAGIC 0x2468
#define MINIX2_SUPER_MAGIC2 0x2478
#define MINIX3_SUPER_MAGIC 0x4d5a
#define MQUEUE_MAGIC 0x19800202
#define MSDOS_SUPER_MAGIC 0x4d44
#define NCP_SUPER_MAGIC 0x564c
#define NFS_SUPER_MAGIC 0x6969
#define NILFS_SUPER_MAGIC 0x3434
#define NTFS_SB_MAGIC 0x5346544e
#define OCFS2_SUPER_MAGIC 0x7461636f
#define OPENPROM_SUPER_MAGIC 0x9fa1
#define PIPEFS_MAGIC 0x50495045
#define PROC_SUPER_MAGIC 0x9fa0
#define PSTOREFS_MAGIC 0x6165676C
#define QNX4_SUPER_MAGIC 0x002f
#define QNX6_SUPER_MAGIC 0x68191122
#define RAMFS_MAGIC 0x858458f6
#define REISERFS_SUPER_MAGIC 0x52654973
#define ROMFS_MAGIC 0x7275
#define SELINUX_MAGIC 0xf97cff8c
#define SMACK_MAGIC 0x43415d53
#define SMB_SUPER_MAGIC 0x517B
#define SOCKFS_MAGIC 0x534F434B
#define SQUASHFS_MAGIC 0x73717368
#define SYSFS_MAGIC 0x62656572
#define SYSV2_SUPER_MAGIC 0x012FF7B6
#define SYSV4_SUPER_MAGIC 0x012FF7B5
#define TMPFS_MAGIC 0x01021994
#define UDF_SUPER_MAGIC 0x15013346
#define UFS_MAGIC 0x00011954
#define USBDEVICE_SUPER_MAGIC 0x9fa2
#define V9FS_MAGIC 0x01021997
#define VXFS_SUPER_MAGIC 0xa501FCF5
#define XENFS_SUPER_MAGIC 0xabba1974
#define XENIX_SUPER_MAGIC 0x012FF7B4
#define XFS_SUPER_MAGIC 0x58465342
#define _XIAFS_SUPER_MAGIC 0x012FD16D
#define AFS_SUPER_MAGIC 0x5346414F
#define AUFS_SUPER_MAGIC 0x61756673
#define ANON_INODE_FS_SUPER_MAGIC 0x09041934
#define CEPH_SUPER_MAGIC 0x00C36400
#define ECRYPTFS_SUPER_MAGIC 0xF15F
#define FAT_SUPER_MAGIC 0x4006
#define FHGFS_SUPER_MAGIC 0x19830326
#define FUSEBLK_SUPER_MAGIC 0x65735546
#define FUSECTL_SUPER_MAGIC 0x65735543
#define GFS_SUPER_MAGIC 0x1161970
#define GPFS_SUPER_MAGIC 0x47504653
#define MTD_INODE_FS_SUPER_MAGIC 0x11307854
#define INOTIFYFS_SUPER_MAGIC 0x2BAD1DEA
#define ISOFS_R_WIN_SUPER_MAGIC 0x4004
#define ISOFS_WIN_SUPER_MAGIC 0x4000
#define JFFS_SUPER_MAGIC 0x07C0
#define KAFS_SUPER_MAGIC 0x6B414653
#define LUSTRE_SUPER_MAGIC 0x0BD00BD0
#define NFSD_SUPER_MAGIC 0x6E667364
#define PANFS_SUPER_MAGIC 0xAAD7AAEA
#define RPC_PIPEFS_SUPER_MAGIC 0x67596969
#define SECURITYFS_SUPER_MAGIC 0x73636673
#define UFS_BYTESWAPPED_SUPER_MAGIC 0x54190100
#define VMHGFS_SUPER_MAGIC 0xBACBACBC
#define VZFS_SUPER_MAGIC 0x565A4653
struct filesystem_entry {
unsigned id;
const char* name;
int remote;
} FILESYSTEMS[] = {
{ ADFS_SUPER_MAGIC, "adfs", 0 },
{ AFFS_SUPER_MAGIC, "affs", 0 },
{ AFS_SUPER_MAGIC, "afs", 1 },
{ AUFS_SUPER_MAGIC, "aufs", 1 },
{ BEFS_SUPER_MAGIC, "befs", 0 },
{ BDEVFS_MAGIC, "bdevfs", 0 },
{ BFS_MAGIC, "bfs", 0 },
{ BINFMTFS_MAGIC, "binfmt_misc", 0 },
{ BTRFS_SUPER_MAGIC, "btrfs", 0 },
{ BCACHEFS_SUPER_MAGIC, "bcachefs", 0 },
{ CEPH_SUPER_MAGIC, "ceph", 1 },
{ CGROUP_SUPER_MAGIC, "cgroupfs", 0 },
{ CIFS_MAGIC_NUMBER, "cifs", 1 },
{ CODA_SUPER_MAGIC, "coda", 1 },
{ COH_SUPER_MAGIC, "coh", 0 },
{ CRAMFS_MAGIC, "cramfs", 0 },
{ DEBUGFS_MAGIC, "debugfs", 0 },
{ DEVFS_SUPER_MAGIC, "devfs", 0 },
{ DEVPTS_SUPER_MAGIC, "devpts", 0 },
{ ECRYPTFS_SUPER_MAGIC, "ecryptfs", 0 },
{ EFS_SUPER_MAGIC, "efs", 0 },
{ EXT_SUPER_MAGIC, "ext", 0 },
{ EXT4_SUPER_MAGIC, "ext4", 0 },
{ EXT2_OLD_SUPER_MAGIC, "ext2", 0 },
{ FAT_SUPER_MAGIC, "fat", 0 },
{ FHGFS_SUPER_MAGIC, "fhgfs", 1 },
{ FUSEBLK_SUPER_MAGIC, "fuseblk", 1 },
{ FUSECTL_SUPER_MAGIC, "fusectl", 1 },
{ FUTEXFS_SUPER_MAGIC, "futexfs", 0 },
{ GFS_SUPER_MAGIC, "gfs/gfs2", 1 },
{ GPFS_SUPER_MAGIC, "gpfs", 1 },
{ HFS_SUPER_MAGIC, "hfs", 0 },
{ HFSPLUS_SUPER_MAGIC, "hfsplus", 0 },
{ HPFS_SUPER_MAGIC, "hpfs", 0 },
{ HUGETLBFS_MAGIC, "hugetlbfs", 0 },
{ MTD_INODE_FS_SUPER_MAGIC, "inodefs", 0 },
{ INOTIFYFS_SUPER_MAGIC, "inotifyfs", 0 },
{ ISOFS_SUPER_MAGIC, "isofs", 0 },
{ ISOFS_R_WIN_SUPER_MAGIC, "isofs", 0 },
{ ISOFS_WIN_SUPER_MAGIC, "isofs", 0 },
{ JFFS_SUPER_MAGIC, "jffs", 0 },
{ JFFS2_SUPER_MAGIC, "jffs2", 0 },
{ JFS_SUPER_MAGIC, "jfs", 0 },
{ KAFS_SUPER_MAGIC, "k-afs", 1 },
{ LUSTRE_SUPER_MAGIC, "lustre", 1 },
{ MINIX_SUPER_MAGIC, "minix", 0 },
{ MINIX_SUPER_MAGIC2, "minix", 0 },
{ MINIX2_SUPER_MAGIC, "minix2", 0 },
{ MINIX2_SUPER_MAGIC2, "minix2", 0 },
{ MINIX3_SUPER_MAGIC, "minix3", 0 },
{ MQUEUE_MAGIC, "mqueue", 0 },
{ MSDOS_SUPER_MAGIC, "msdos", 0 },
{ NCP_SUPER_MAGIC, "novell", 1 },
{ NFS_SUPER_MAGIC, "nfs", 1 },
{ NFSD_SUPER_MAGIC, "nfsd", 1 },
{ NILFS_SUPER_MAGIC, "nilfs", 0 },
{ NTFS_SB_MAGIC, "ntfs", 0 },
{ OPENPROM_SUPER_MAGIC, "openprom", 0 },
{ OCFS2_SUPER_MAGIC, "ocfs2", 1 },
{ PANFS_SUPER_MAGIC, "panfs", 1 },
{ PIPEFS_MAGIC, "pipefs", 1 },
{ PROC_SUPER_MAGIC, "proc", 0 },
{ PSTOREFS_MAGIC, "pstorefs", 0 },
{ QNX4_SUPER_MAGIC, "qnx4", 0 },
{ QNX6_SUPER_MAGIC, "qnx6", 0 },
{ RAMFS_MAGIC, "ramfs", 0 },
{ REISERFS_SUPER_MAGIC, "reiserfs", 0 },
{ ROMFS_MAGIC, "romfs", 0 },
{ RPC_PIPEFS_SUPER_MAGIC, "rpc_pipefs", 0 },
{ SECURITYFS_SUPER_MAGIC, "securityfs", 0 },
{ SELINUX_MAGIC, "selinux", 0 },
{ SMB_SUPER_MAGIC, "smb", 1 },
{ SOCKFS_MAGIC, "sockfs", 0 },
{ SQUASHFS_MAGIC, "squashfs", 0 },
{ SYSFS_MAGIC, "sysfs", 0 },
{ SYSV2_SUPER_MAGIC, "sysv2", 0 },
{ SYSV4_SUPER_MAGIC, "sysv4", 0 },
{ TMPFS_MAGIC, "tmpfs", 0 },
{ UDF_SUPER_MAGIC, "udf", 0 },
{ UFS_MAGIC, "ufs", 0 },
{ UFS_BYTESWAPPED_SUPER_MAGIC, "ufs", 0 },
{ USBDEVICE_SUPER_MAGIC, "usbdevfs", 0 },
{ V9FS_MAGIC, "v9fs", 0 },
{ VMHGFS_SUPER_MAGIC, "vmhgfs", 1 },
{ VXFS_SUPER_MAGIC, "vxfs", 0 },
{ VZFS_SUPER_MAGIC, "vzfs", 0 },
{ XENFS_SUPER_MAGIC, "xenfs", 0 },
{ XENIX_SUPER_MAGIC, "xenix", 0 },
{ XFS_SUPER_MAGIC, "xfs", 0 },
{ _XIAFS_SUPER_MAGIC, "xia", 0 },
{ ZFS_SUPER_MAGIC, "zfs", 0 },
{ 0 }
};
int fsinfo(const char* path, int* has_persistent_inode, int* has_syncronized_hardlinks, uint64_t* total_space, uint64_t* free_space, char* fstype, size_t fstype_size, char* fslabel, size_t fslabel_size)
{
#if HAVE_STATFS
struct statfs st;
if (statfs(path, &st) != 0) {
char dir[PATH_MAX];
char* slash;
if (errno != ENOENT) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/*
* If it doesn't exist, we assume a file
* and we check for the containing dir
*/
if (strlen(path) + 1 > sizeof(dir)) {
/* LCOV_EXCL_START */
errno = ENAMETOOLONG;
return -1;
/* LCOV_EXCL_STOP */
}
strcpy(dir, path);
slash = strrchr(dir, '/');
if (!slash) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
*slash = 0;
if (statfs(dir, &st) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
}
#endif
/* to get the fs type check "man stat" or "stat -f -t FILE" */
if (has_persistent_inode) {
#if HAVE_STATFS && HAVE_STRUCT_STATFS_F_TYPE
switch (st.f_type) {
case FUSEBLK_SUPER_MAGIC : /* FUSE, "fuseblk" in the stat command */
case MSDOS_SUPER_MAGIC : /* VFAT, "msdos" in the stat command */
*has_persistent_inode = 0;
break;
default :
/* by default assume yes */
*has_persistent_inode = 1;
break;
}
#else
/* in Unix inodes are persistent by default */
*has_persistent_inode = 1;
#endif
}
if (has_syncronized_hardlinks) {
#if HAVE_STATFS && HAVE_STRUCT_STATFS_F_TYPE
switch (st.f_type) {
case NTFS_SB_MAGIC : /* NTFS */
case MSDOS_SUPER_MAGIC : /* VFAT, "msdos" in the stat command */
*has_syncronized_hardlinks = 0;
break;
default :
/* by default assume yes */
*has_syncronized_hardlinks = 1;
break;
}
#else
/* in Unix hardlinks share the same metadata by default */
*has_syncronized_hardlinks = 1;
#endif
}
if (total_space) {
#if HAVE_STATFS
*total_space = st.f_bsize * (uint64_t)st.f_blocks;
#else
*total_space = 0;
#endif
}
if (free_space) {
#if HAVE_STATFS
*free_space = st.f_bsize * (uint64_t)st.f_bfree;
#else
*free_space = 0;
#endif
}
const char* ptype = 0;
#if HAVE_STATFS && HAVE_STRUCT_STATFS_F_FSTYPENAME
/* get the filesystem type directly from the struct (Mac OS X) */
ptype = st.f_fstypename;
#elif HAVE_STATFS && HAVE_STRUCT_STATFS_F_TYPE
/*
* Get the filesystem type from f_type (Linux)
* from: https://github.com/influxdata/gopsutil/blob/master/disk/disk_linux.go
*/
for (int i = 0; FILESYSTEMS[i].id != 0; ++i) {
if (st.f_type == FILESYSTEMS[i].id) {
ptype = FILESYSTEMS[i].name;
break;
}
}
#endif
if (fstype) {
fstype[0] = 0;
if (ptype)
snprintf(fstype, fstype_size, "%s", ptype);
}
if (fslabel) {
fslabel[0] = 0;
#if HAVE_BLKID
struct stat fst;
if (stat(path, &fst) == 0)
devuuid_label(fst.st_dev, fslabel, fslabel_size);
#else
(void)fslabel_size;
#endif
}
return 0;
}
#if HAVE_LINUX_DEVICE
static int fssnapshot_inode(const char* path, uint32_t magic, uint64_t root_inode, struct fssnapshot_struct* fss)
{
char current_path[PATH_MAX];
pathcpy(current_path, sizeof(current_path), path);
/* walk up the directory tree to find the subvolume root (Inode 256) */
while (1) {
struct stat st;
if (stat(current_path, &st) != 0) {
/* LCOV_EXCL_START */
log_error(errno, "Error stating '%s'. %s.\n", current_path, strerror(errno));
return -1;
/* LCOV_EXCL_STOP */
}
if (st.st_ino == root_inode) {
/* copy the subvol root */
pathcpy(fss->root_dir, sizeof(fss->root_dir), current_path);
pathcpy(fss->snapshot_dir, sizeof(fss->snapshot_dir), current_path);
pathcat(fss->snapshot_dir, sizeof(fss->snapshot_dir), SNAPSHOT_CONTAINER "/");
/* create the snapshot container directory if not existing */
int ret = mkdir(fss->snapshot_dir, S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
if (ret != 0 && errno != EEXIST) {
log_error(errno, "Error creating '%s'. %s.\n", fss->snapshot_dir, strerror(errno));
return -1;
}
fss->magic = magic;
return 0;
}
/* move to parent directory */
pathup(current_path);
if (current_path[0] == 0) {
/* LCOV_EXCL_START */
log_error(ENOENT, "No subvolume root found for '%s'. %s.\n", path, strerror(errno));
return -1;
/* LCOV_EXCL_STOP */
}
}
}
#endif
#if HAVE_LINUX_DEVICE
static int fssnapshot_zfs(const char* dir, uint32_t magic, struct fssnapshot_struct* fss)
{
if (extract_zfs(dir, fss->dataset, sizeof(fss->dataset), 0, 0, fss->root_dir, sizeof(fss->root_dir)) != 0)
return -1;
pathslash(fss->root_dir, sizeof(fss->root_dir));
pathcpy(fss->snapshot_dir, sizeof(fss->snapshot_dir), fss->root_dir);
pathcat(fss->snapshot_dir, sizeof(fss->snapshot_dir), ".zfs/snapshot/");
/* ensure ZFS snapshot directory is visible */
if (access(fss->snapshot_dir, R_OK | X_OK) != 0) {
log_error(errno, "ZFS snapshot directory not accessible: '%s'. %s.\n", fss->snapshot_dir, strerror(errno));
return -1;
}
fss->magic = magic;
return 0;
}
#endif
int fssnapshot_mount(const char* path, struct fssnapshot_struct* fss)
{
#if HAVE_LINUX_DEVICE
struct statfs sfs;
if (statfs(path, &sfs) != 0) {
/* LCOV_EXCL_START */
log_error(errno, "Error stating filesystem '%s'. %s.\n", path, strerror(errno));
return -1;
/* LCOV_EXCL_STOP */
}
if (sfs.f_type == BTRFS_SUPER_MAGIC) {
/* btrfs reserved inode 256 for subvolume roots */
return fssnapshot_inode(path, BTRFS_SUPER_MAGIC, 256, fss);
} else if (sfs.f_type == BCACHEFS_SUPER_MAGIC) {
/* Bcachefs reserves inode 4096 for each subvolume's root directory */
return fssnapshot_inode(path, BCACHEFS_SUPER_MAGIC, 4096, fss);
} else if (sfs.f_type == ZFS_SUPER_MAGIC) {
return fssnapshot_zfs(path, ZFS_SUPER_MAGIC, fss);
} else {
return -1;
}
#else
(void)path;
(void)fss;
return -1;
#endif
}
#if HAVE_LINUX_DEVICE
static int fssnapshot_stat_fs(const struct fssnapshot_struct* fss, const char* name, struct stat* st)
{
char target_path[PATH_MAX];
pathcpy(target_path, sizeof(target_path), fss->snapshot_dir);
pathcat(target_path, sizeof(target_path), name);
if (stat(target_path, st) != 0)
return -1;
/* it must be a directory */
if (!S_ISDIR(st->st_mode))
return -1;
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
static int fssnapshot_stat_zfs(const struct fssnapshot_struct* fss, const char* name, struct stat* st)
{
char target_path[PATH_MAX];
pathcpy(target_path, sizeof(target_path), fss->snapshot_dir);
pathcat(target_path, sizeof(target_path), name);
/*
* Ensure to get into into the snapshot
*
* This is required by ZFS, otherwise we get a different st_dev
*/
pathcat(target_path, sizeof(target_path), "/.");
if (stat(target_path, st) != 0)
return -1;
/* it must be a directory */
if (!S_ISDIR(st->st_mode))
return -1;
/**
* When verifying if a ZFS snapshot has been successfully deleted, a direct
* path lookup (stat, access, open) is unreliable. A directory listing
* (readdir) of the .zfs/snapshot directory must be used instead.
*
* On Linux, the Virtual File System (VFS) maintains a 'dentry cache'
* (dcache). If a snapshot path was accessed recently, the kernel caches
* the directory entry in RAM. When ZFS destroys the snapshot, the kernel
* dcache is not always immediately synchronized. Consequently, stat()
* may hit the stale cache and report the snapshot still exists.
*
* Unlike stat(), a directory listing (ls/readdir) forces the kernel to
* query the ZFS module for the current set of valid entries, bypassing
* the stale dcache for a "ground truth" check.
*
* This behavior can be confirmed by forcing the kernel to purge its
* dcache manually using the following command:
* sync; echo 2 > /proc/sys/vm/drop_caches
*
* After dropping caches, stat() will correctly return ENOENT. To avoid
* requiring root privileges or impacting system performance with
* drop_caches, always use readdir() for existence checks in this context.
*/
DIR* d = opendir(fss->snapshot_dir);
if (!d) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
while (1) {
struct dirent* entry = readdir(d);
if (!entry)
break;
if (strcmp(entry->d_name, name) == 0) {
closedir(d);
return 0;
}
}
closedir(d);
return -1;
}
#endif
int fssnapshot_stat(struct fssnapshot_struct* fss, const char* name, struct stat* st)
{
#if HAVE_LINUX_DEVICE
if (fss->magic == ZFS_SUPER_MAGIC) {
return fssnapshot_stat_zfs(fss, name, st);
} else {
return fssnapshot_stat_fs(fss, name, st);
}
#else
(void)fss;
(void)name;
(void)st;
return 0;
#endif
}
#if HAVE_LINUX_DEVICE
int fssnapshot_btrfs_create(const struct fssnapshot_struct* fss, const char* name)
{
struct btrfs_ioctl_vol_args_v2 args;
int fd_source;
int fd_dest_parent;
int ret;
/* open the source subvolume to get a file descriptor */
fd_source = open(fss->root_dir, O_RDONLY | O_DIRECTORY);
if (fd_source < 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
fd_dest_parent = open(fss->snapshot_dir, O_RDONLY | O_DIRECTORY);
if (fd_dest_parent < 0) {
/* LCOV_EXCL_START */
close(fd_source);
return -1;
/* LCOV_EXCL_STOP */
}
memset(&args, 0, sizeof(args));
args.fd = fd_source;
args.flags = BTRFS_SUBVOL_RDONLY;
pathcpy(args.name, BTRFS_SUBVOL_NAME_MAX, name);
/* issue the snapshot command to the PARENT directory of the destination */
ret = ioctl(fd_dest_parent, BTRFS_IOC_SNAP_CREATE_V2, &args);
if (ret < 0) {
/* LCOV_EXCL_START */
close(fd_source);
close(fd_dest_parent);
return -1;
/* LCOV_EXCL_STOP */
}
close(fd_source);
close(fd_dest_parent);
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
static int fssnapshot_bcachefs_create(const struct fssnapshot_struct* fss, const char* name)
{
char target_path[PATH_MAX];
int ret;
const char* bcachefs = find_bcachefs();
if (!bcachefs) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
pathcpy(target_path, sizeof(target_path), fss->snapshot_dir);
pathcat(target_path, sizeof(target_path), name);
if (validate_exec_input(fss->root_dir) != 0 || validate_exec_input(target_path) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
const char* argv[] = {
bcachefs,
"subvolume",
"snapshot",
"-r",
fss->root_dir,
target_path,
0
};
ret = os_spawn_and_wait(argv);
if (ret != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
static int fssnapshot_zfs_create(const struct fssnapshot_struct* fss, const char* name)
{
char snapshot[PATH_MAX + 64];
int ret;
const char* zfs = find_zfs();
if (!zfs) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
snprintf(snapshot, sizeof(snapshot), "%s@%s", fss->dataset, name);
if (validate_exec_input(snapshot) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
const char* argv[] = {
zfs,
"snapshot",
snapshot,
0
};
ret = os_spawn_and_wait(argv);
if (ret != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
#endif
int fssnapshot_create(const struct fssnapshot_struct* fss, const char* name)
{
#if HAVE_LINUX_DEVICE
if (fss->magic == BTRFS_SUPER_MAGIC) {
return fssnapshot_btrfs_create(fss, name);
} else if (fss->magic == BCACHEFS_SUPER_MAGIC) {
return fssnapshot_bcachefs_create(fss, name);
} else if (fss->magic == ZFS_SUPER_MAGIC) {
return fssnapshot_zfs_create(fss, name);
} else {
return -1;
}
#else
(void)fss;
(void)name;
return -1;
#endif
}
#if HAVE_LINUX_DEVICE
int fssnapshot_btrfs_delete(const struct fssnapshot_struct* fss, const char* name)
{
struct btrfs_ioctl_vol_args args;
int fd_parent;
int ret;
fd_parent = open(fss->snapshot_dir, O_RDONLY | O_DIRECTORY);
if (fd_parent < 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
memset(&args, 0, sizeof(args));
pathcpy(args.name, BTRFS_PATH_NAME_MAX, name);
ret = ioctl(fd_parent, BTRFS_IOC_SNAP_DESTROY, &args);
if (ret < 0) {
/* LCOV_EXCL_START */
close(fd_parent);
return -1;
/* LCOV_EXCL_STOP */
}
close(fd_parent);
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
static int fssnapshot_bcachefs_delete(const struct fssnapshot_struct* fss, const char* name)
{
char target_path[PATH_MAX];
int ret;
const char* bcachefs = find_bcachefs();
if (!bcachefs) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
pathcpy(target_path, sizeof(target_path), fss->snapshot_dir);
pathcat(target_path, sizeof(target_path), name);
if (validate_exec_input(target_path) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
const char* argv[] = {
bcachefs,
"subvolume",
"delete",
target_path,
0,
};
ret = os_spawn_and_wait(argv);
if (ret != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
static int fssnapshot_zfs_delete(const struct fssnapshot_struct* fss, const char* name)
{
char snapshot[PATH_MAX + 64];
int ret;
const char* zfs = find_zfs();
if (!zfs) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
snprintf(snapshot, sizeof(snapshot), "%s@%s", fss->dataset, name);
if (validate_exec_input(snapshot) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
const char* argv[] = {
zfs,
"destroy",
snapshot,
0
};
ret = os_spawn_and_wait(argv);
if (ret != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
#endif
int fssnapshot_delete(const struct fssnapshot_struct* fss, const char* name)
{
#if HAVE_LINUX_DEVICE
if (fss->magic == BTRFS_SUPER_MAGIC) {
return fssnapshot_btrfs_delete(fss, name);
} else if (fss->magic == BCACHEFS_SUPER_MAGIC) {
return fssnapshot_bcachefs_delete(fss, name);
} else if (fss->magic == ZFS_SUPER_MAGIC) {
return fssnapshot_zfs_delete(fss, name);
} else {
return -1;
}
#else
(void)fss;
(void)name;
return -1;
#endif
}
#if HAVE_LINUX_DEVICE
static int fssnapshot_btrfs_rename(const struct fssnapshot_struct* fss, const char* old_name, const char* new_name)
{
char old_path[PATH_MAX];
char new_path[PATH_MAX];
pathcpy(old_path, sizeof(old_path), fss->snapshot_dir);
pathcat(old_path, sizeof(old_path), old_name);
pathcpy(new_path, sizeof(new_path), fss->snapshot_dir);
pathcat(new_path, sizeof(new_path), new_name);
if (rename(old_path, new_path) < 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
static int fssnapshot_bcachefs_rename(const struct fssnapshot_struct* fss, const char* old_name, const char* new_name)
{
if (fssnapshot_bcachefs_create(fss, new_name) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
if (fssnapshot_bcachefs_delete(fss, old_name) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
static int fssnapshot_zfs_rename(const struct fssnapshot_struct* fss, const char* old_name, const char* new_name)
{
char old_snapshot[PATH_MAX + 64];
char new_snapshot[PATH_MAX + 64];
int ret;
const char* zfs = find_zfs();
if (!zfs) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
snprintf(old_snapshot, sizeof(old_snapshot), "%s@%s", fss->dataset, old_name);
snprintf(new_snapshot, sizeof(new_snapshot), "%s@%s", fss->dataset, new_name);
if (validate_exec_input(old_snapshot) != 0 || validate_exec_input(new_snapshot) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
const char* argv[] = {
zfs,
"rename",
old_snapshot,
new_snapshot,
0
};
ret = os_spawn_and_wait(argv);
if (ret != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
#endif
int fssnapshot_rename(const struct fssnapshot_struct* fss, const char* old_name, const char* new_name)
{
#if HAVE_LINUX_DEVICE
if (fss->magic == BTRFS_SUPER_MAGIC) {
return fssnapshot_btrfs_rename(fss, old_name, new_name);
} else if (fss->magic == BCACHEFS_SUPER_MAGIC) {
return fssnapshot_bcachefs_rename(fss, old_name, new_name);
} else if (fss->magic == ZFS_SUPER_MAGIC) {
return fssnapshot_zfs_rename(fss, old_name, new_name);
} else {
return -1;
}
#else
(void)fss;
(void)old_name;
(void)new_name;
return -1;
#endif
}
void fssnapshot_unmount(const struct fssnapshot_struct* fss)
{
(void)fss;
}
uint64_t os_tick(void)
{
#if HAVE_MACH_ABSOLUTE_TIME
/* for Mac OS X */
return mach_absolute_time();
#elif HAVE_CLOCK_GETTIME && (defined(CLOCK_MONOTONIC) || defined(CLOCK_MONOTONIC_RAW))
/* for Linux */
struct timespec tv;
/* nanosecond precision with clock_gettime() */
#if defined(CLOCK_MONOTONIC_RAW)
if (clock_gettime(CLOCK_MONOTONIC_RAW, &tv) != 0) {
#else
if (clock_gettime(CLOCK_MONOTONIC, &tv) != 0) {
#endif
/* LCOV_EXCL_START */
return 0;
/* LCOV_EXCL_STOP */
}
return tv.tv_sec * 1000000000ULL + tv.tv_nsec;
#else
/* other platforms */
struct timeval tv;
/* microsecond precision with gettimeofday() */
if (gettimeofday(&tv, 0) != 0) {
/* LCOV_EXCL_START */
return 0;
/* LCOV_EXCL_STOP */
}
return tv.tv_sec * 1000000ULL + tv.tv_usec;
#endif
}
uint64_t os_tick_ms(void)
{
struct timeval tv;
if (gettimeofday(&tv, 0) != 0) {
/* LCOV_EXCL_START */
return 0;
/* LCOV_EXCL_STOP */
}
return tv.tv_sec * 1000ULL + tv.tv_usec / 1000;
}
int randomize(void* ptr, size_t size)
{
int f;
ssize_t ret;
f = open("/dev/urandom", O_RDONLY);
if (f == -1) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
ret = read(f, ptr, size);
if (ret < 0 || (size_t)ret != size) {
/* LCOV_EXCL_START */
close(f);
return -1;
/* LCOV_EXCL_STOP */
}
if (close(f) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
/**
* Read a file extracting the contained device number in %u:%u format.
* Return 0 on error.
*/
#if HAVE_LINUX_DEVICE
static dev_t devread(const char* path)
{
int f;
int ret;
ssize_t len;
char buf[64];
char* e;
unsigned ma;
unsigned mi;
f = open(path, O_RDONLY);
if (f == -1) {
/* LCOV_EXCL_START */
log_fatal(errno, "Failed to open '%s'.\n", path);
return 0;
/* LCOV_EXCL_STOP */
}
len = read(f, buf, sizeof(buf));
if (len < 0) {
/* LCOV_EXCL_START */
close(f);
log_fatal(errno, "Failed to read '%s'.\n", path);
return 0;
/* LCOV_EXCL_STOP */
}
if (len == sizeof(buf)) {
/* LCOV_EXCL_START */
close(f);
log_fatal(EEXTERNAL, "Too long read '%s'.\n", path);
return 0;
/* LCOV_EXCL_STOP */
}
ret = close(f);
if (ret != 0) {
/* LCOV_EXCL_START */
log_fatal(errno, "Failed to close '%s'.\n", path);
return 0;
/* LCOV_EXCL_STOP */
}
buf[len] = 0;
ma = strtou(buf, &e, 10);
if (*e != ':') {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Invalid format in '%s' for '%s'.\n", path, buf);
return 0;
/* LCOV_EXCL_STOP */
}
mi = strtou(e + 1, &e, 10);
if (*e != 0 && !isspace((unsigned char)*e)) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Invalid format in '%s' for '%s'.\n", path, buf);
return 0;
/* LCOV_EXCL_STOP */
}
return makedev(ma, mi);
}
#endif
/**
* Read a device tree filling the specified list of disk_t entries.
*/
#if HAVE_LINUX_DEVICE
static int devtree(devinfo_t* parent, dev_t device, tommy_list* list)
{
char path[PATH_MAX];
DIR* d;
int slaves = 0;
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/slaves", major(device), minor(device));
/* check if there is a slaves list */
d = opendir(path);
if (d != 0) {
struct dirent* dd;
while ((dd = readdir(d)) != 0) {
if (dd->d_name[0] != '.') {
dev_t subdev;
/* for each slave, expand the full potential tree */
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/slaves/%s/dev", major(device), minor(device), dd->d_name);
subdev = devread(path);
if (!subdev) {
/* LCOV_EXCL_START */
closedir(d);
return -1;
/* LCOV_EXCL_STOP */
}
if (devtree(parent, subdev, list) != 0) {
/* LCOV_EXCL_START */
closedir(d);
return -1;
/* LCOV_EXCL_STOP */
}
++slaves;
}
}
closedir(d);
}
/* if no slaves found */
if (!slaves) {
/* this is a raw device */
devinfo_t* devinfo;
/* check if it's a real device */
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/device", major(device), minor(device));
if (eaccess(path, F_OK) != 0) {
/* get the parent device */
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/../dev", major(device), minor(device));
device = devread(path);
if (!device) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
}
/* get the device file */
if (devresolve(device, path, sizeof(path)) != 0) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Failed to resolve device '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
devinfo = calloc_nofail(1, sizeof(devinfo_t));
devinfo->device = device;
pathcpy(devinfo->name, sizeof(devinfo->name), parent->name);
pathcpy(devinfo->smartctl, sizeof(devinfo->smartctl), parent->smartctl);
memcpy(devinfo->smartignore, parent->smartignore, sizeof(devinfo->smartignore));
pathcpy(devinfo->file, sizeof(devinfo->file), path);
devinfo->parent = parent;
/* insert in the list */
tommy_list_insert_tail(list, &devinfo->node, devinfo);
}
return 0;
}
#endif
/**
* Compute disk usage by aggregating access statistics.
*/
#if HAVE_LINUX_DEVICE
static int devstat(dev_t device, uint64_t* count)
{
char path[PATH_MAX];
char buf[512];
int token;
ssize_t ret;
char* i;
*count = 0;
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/stat", major(device), minor(device));
ret = sysread(path, buf, sizeof(buf));
if (ret < 0) {
/* LCOV_EXCL_START */
log_fatal(errno, "Failed to read '%s'.\n", path);
return -1;
/* LCOV_EXCL_STOP */
}
if ((size_t)ret + 1 > sizeof(buf)) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Too long read '%s'.\n", path);
return -1;
/* LCOV_EXCL_STOP */
}
/* ending 0 */
buf[ret] = 0;
i = buf;
token = 1; /* token number */
while (*i) {
char* n;
char* e;
unsigned long long v;
/* skip spaces */
while (*i && isspace((unsigned char)*i))
++i;
/* read digits */
n = i;
while (*i && isdigit((unsigned char)*i))
++i;
if (i == n) /* if no digit, abort */
break;
if (*i == 0 || !isspace((unsigned char)*i)) /* if no space, abort */
break;
*i++ = 0; /* put a terminator */
v = strtoull(n, &e, 10);
if (*e != 0) {
/* LCOV_EXCL_START */
break;
/* LCOV_EXCL_STOP */
}
/* sum reads and writes completed */
if (token == 1 || token == 5) {
*count += v;
if (token == 5)
break; /* stop here */
}
++token;
}
return 0;
}
#endif
/**
* Get SMART attributes.
*/
#if HAVE_LINUX_DEVICE
static int devsmart(dev_t device, const char* name, const char* smartctl, struct smart_attr* smart, uint64_t* info, char* serial, char* family, char* model, char* interface)
{
char extra_args[PATH_MAX];
char file[PATH_MAX];
OS_FILE* f;
int ret;
const char* x;
x = find_smartctl();
if (!x) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Cannot find smartctl.\n");
return -1;
/* LCOV_EXCL_STOP */
}
if (devresolve(device, file, sizeof(file)) != 0) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Failed to resolve device '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
/* if there is a custom smartctl command */
const char* args = "-a";
if (smartctl[0]) {
pathprint(extra_args, sizeof(extra_args), smartctl, file);
const char* argv[] = {
x,
"-a",
0
};
log_tag("smartctl:%s:%s:run: %s %s %s\n", file, name, x, args, extra_args);
f = os_popen(argv, extra_args);
} else {
pathprint(extra_args, sizeof(extra_args), "%s", file);
const char* argv[] = {
x,
"-a",
file,
0
};
log_tag("smartctl:%s:%s:run: %s %s %s\n", file, name, x, args, extra_args);
f = os_popen(argv, 0);
}
if (!f) {
/* LCOV_EXCL_START */
log_tag("device:%s:%s:spawn\n", file, name);
log_fatal(EEXTERNAL, "Failed to run '%s %s %s'.\n", x, args, extra_args);
return -1;
/* LCOV_EXCL_STOP */
}
if (smartctl_attribute(f, file, name, smart, info, serial, family, model, interface) != 0) {
/* LCOV_EXCL_START */
os_pclose(f);
log_tag("device:%s:%s:spawn\n", file, name);
return -1;
/* LCOV_EXCL_STOP */
}
ret = os_pclose(f);
log_tag("smartctl:%s:%s:ret: %x\n", file, name, ret);
if (!WIFEXITED(ret)) {
/* LCOV_EXCL_START */
log_tag("device:%s:%s:abort\n", file, name);
log_fatal(EEXTERNAL, "Failed to run '%s %s %s' (not exited).\n", x, args, extra_args);
return -1;
/* LCOV_EXCL_STOP */
}
/* store the return smartctl return value */
smart[SMART_FLAGS].raw = WEXITSTATUS(ret);
return 0;
}
#endif
/**
* Get device attributes.
*/
#if HAVE_LINUX_DEVICE
static void devattr(dev_t device, uint64_t* info, char* serial, char* family, char* model, char* interface)
{
char path[PATH_MAX];
char buf[512];
ssize_t ret;
(void)family; /* not available, smartctl uses an internal database to get it */
if (info[INFO_SIZE] == SMART_UNASSIGNED) {
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/size", major(device), minor(device));
if (sysattr(path, buf, sizeof(buf)) == 0) {
char* e;
uint64_t v;
v = strtoul(buf, &e, 10);
if (*e == 0)
info[INFO_SIZE] = v * 512;
}
}
if (info[INFO_ROTATION_RATE] == SMART_UNASSIGNED) {
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/queue/rotational", major(device), minor(device));
if (sysattr(path, buf, sizeof(buf)) == 0) {
char* e;
uint64_t v;
v = strtoul(buf, &e, 10);
if (*e == 0)
info[INFO_ROTATION_RATE] = v;
}
}
if (*model == 0) {
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/device/model", major(device), minor(device));
if (sysattr(path, buf, sizeof(buf)) == 0) {
if (buf[0] != 0)
pathcpy(model, SMART_MAX, buf);
}
}
if (*serial == 0) {
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/device/serial", major(device), minor(device));
if (sysattr(path, buf, sizeof(buf)) == 0) {
if (buf[0] != 0)
pathcpy(serial, SMART_MAX, buf);
}
}
if (*serial == 0) {
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/device/vpd_pg80", major(device), minor(device));
if (sysattr_vpd_pg80(path, buf, sizeof(buf)) == 0) {
if (buf[0] != 0)
pathcpy(serial, SMART_MAX, buf);
}
}
/* always override interface if it's usb */
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u", major(device), minor(device));
ret = readlink(path, buf, sizeof(buf));
if (ret > 0 && (unsigned)ret < sizeof(buf)) {
buf[ret] = 0;
if (strstr(buf, "usb") != 0)
strcpy(interface, "USB");
}
}
#endif
/**
* Get POWER state.
*/
#if HAVE_LINUX_DEVICE
static int devprobe(dev_t device, const char* name, const char* smartctl, int* power, struct smart_attr* smart, uint64_t* info, char* serial, char* family, char* model, char* interface)
{
char extra_args[PATH_MAX];
char file[PATH_MAX];
OS_FILE* f;
int ret;
const char* x;
x = find_smartctl();
if (!x) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Cannot find smartctl.\n");
return -1;
/* LCOV_EXCL_STOP */
}
if (devresolve(device, file, sizeof(file)) != 0) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Failed to resolve device '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
/* if there is a custom smartctl command */
const char* args = "-n standby,3 -a";
if (smartctl[0]) {
pathprint(extra_args, sizeof(extra_args), smartctl, file);
const char* argv[] = {
x,
"-n",
"standby,3",
"-a",
0
};
log_tag("smartctl:%s:%s:run: %s %s %s\n", file, name, x, args, extra_args);
f = os_popen(argv, extra_args);
} else {
pathprint(extra_args, sizeof(extra_args), "%s", file);
const char* argv[] = {
x,
"-n",
"standby,3",
"-a",
file,
0
};
log_tag("smartctl:%s:%s:run: %s %s %s\n", file, name, x, args, extra_args);
f = os_popen(argv, 0);
}
if (!f) {
/* LCOV_EXCL_START */
log_tag("device:%s:%s:spawn\n", file, name);
log_fatal(EEXTERNAL, "Failed to run '%s %s %s'.\n", x, args, extra_args);
return -1;
/* LCOV_EXCL_STOP */
}
if (smartctl_attribute(f, file, name, smart, info, serial, family, model, interface) != 0) {
/* LCOV_EXCL_START */
os_pclose(f);
log_tag("device:%s:%s:spawn\n", file, name);
return -1;
/* LCOV_EXCL_STOP */
}
ret = os_pclose(f);
log_tag("smartctl:%s:%s:ret: %x\n", file, name, ret);
if (!WIFEXITED(ret)) {
/* LCOV_EXCL_START */
log_tag("device:%s:%s:abort\n", file, name);
log_fatal(EEXTERNAL, "Failed to run '%s %s %s' (not exited).\n", x, args, extra_args);
return -1;
/* LCOV_EXCL_STOP */
}
if (WEXITSTATUS(ret) == 3) {
log_tag("attr:%s:%s:power:standby\n", file, name);
*power = POWER_STANDBY;
} else {
log_tag("attr:%s:%s:power:active\n", file, name);
*power = POWER_ACTIVE;
/* store the smartctl return value */
if (smart)
smart[SMART_FLAGS].raw = WEXITSTATUS(ret);
}
return 0;
}
#endif
/**
* Spin down a specific device.
*/
#if HAVE_LINUX_DEVICE
static int devdown(dev_t device, const char* name, const char* smartctl)
{
char extra_args[PATH_MAX];
char file[PATH_MAX];
OS_FILE* f;
int ret;
const char* x;
x = find_smartctl();
if (!x) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Cannot find smartctl.\n");
return -1;
/* LCOV_EXCL_STOP */
}
if (devresolve(device, file, sizeof(file)) != 0) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Failed to resolve device '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
/* if there is a custom smartctl command */
const char* args = "-s standby,now";
if (smartctl[0]) {
pathprint(extra_args, sizeof(extra_args), smartctl, file);
const char* argv[] = {
x,
"-s",
"standby,now",
0
};
log_tag("smartctl:%s:%s:run: %s %s %s\n", file, name, x, args, extra_args);
f = os_popen(argv, extra_args);
} else {
pathprint(extra_args, sizeof(extra_args), "%s", file);
const char* argv[] = {
x,
"-s",
"standby,now",
file,
0
};
log_tag("smartctl:%s:%s:run: %s %s %s\n", file, name, x, args, extra_args);
f = os_popen(argv, 0);
}
if (!f) {
/* LCOV_EXCL_START */
log_tag("device:%s:%s:spawn\n", file, name);
log_fatal(EEXTERNAL, "Failed to run '%s %s %s'.\n", x, args, extra_args);
return -1;
/* LCOV_EXCL_STOP */
}
if (smartctl_flush(f, file, name) != 0) {
/* LCOV_EXCL_START */
os_pclose(f);
log_tag("device:%s:%s:spawn\n", file, name);
return -1;
/* LCOV_EXCL_STOP */
}
ret = os_pclose(f);
log_tag("smartctl:%s:%s:ret: %x\n", file, name, ret);
if (!WIFEXITED(ret)) {
/* LCOV_EXCL_START */
log_tag("device:%s:%s:abort\n", file, name);
log_fatal(EEXTERNAL, "Failed to run '%s %s %s' (not exited).\n", x, args, extra_args);
return -1;
/* LCOV_EXCL_STOP */
}
if (WEXITSTATUS(ret) != 0) {
/* LCOV_EXCL_START */
log_tag("device:%s:%s:exit:%d\n", file, name, WEXITSTATUS(ret));
log_fatal(EEXTERNAL, "Failed to run '%s %s %s' with return code %xh.\n", x, args, extra_args, WEXITSTATUS(ret));
return -1;
/* LCOV_EXCL_STOP */
}
log_tag("attr:%s:%s:power:down\n", file, name);
return 0;
}
#endif
/**
* Spin down a specific device if it's up.
*/
#if HAVE_LINUX_DEVICE
static int devdownifup(dev_t device, const char* name, const char* smartctl, int* power)
{
*power = POWER_UNKNOWN;
if (devprobe(device, name, smartctl, power, 0, 0, 0, 0, 0, 0) != 0)
return -1;
if (*power == POWER_ACTIVE)
return devdown(device, name, smartctl);
return 0;
}
#endif
/**
* Spin up a specific device.
*/
#if HAVE_LINUX_DEVICE
static int devup(dev_t device, const char* name)
{
char file[PATH_MAX];
ssize_t ret;
int f;
void* buf;
uint64_t size;
uint64_t offset;
if (devresolve(device, file, sizeof(file)) != 0) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Failed to resolve device '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
/* O_DIRECT requires memory aligned to the block size */
if (posix_memalign(&buf, 4096, 4096) != 0) {
/* LCOV_EXCL_START */
log_fatal(errno, "Failed to allocate aligned memory for device '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
f = open(file, O_RDONLY | O_DIRECT);
if (f < 0) {
/* LCOV_EXCL_START */
free(buf);
log_tag("device:%s:%s:error:%d\n", file, name, errno);
log_fatal(errno, "Failed to open device '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
if (ioctl(f, BLKGETSIZE64, &size) < 0) {
/* LCOV_EXCL_START */
close(f);
free(buf);
log_tag("device:%s:%s:error:%d\n", file, name, errno);
log_fatal(errno, "Failed to get device size '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
/* select a random offset */
uint64_t blocks = size / 4096;
offset = blocks > 0 ? (random_u64() % blocks) * 4096 : 0;
#if HAVE_POSIX_FADVISE
/* clear cache */
ret = posix_fadvise_wrapper(f, offset, 4096, POSIX_FADV_DONTNEED);
if (ret == ENOSYS) {
/* call is not supported */
ret = 0;
}
if (ret != 0) {
/* LCOV_EXCL_START */
close(f);
free(buf);
log_tag("device:%s:%s:error:%d\n", file, name, errno);
log_fatal(errno, "Failed to advise device '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
#endif
ret = pread(f, buf, 4096, offset);
if (ret < 0) {
/* LCOV_EXCL_START */
close(f);
free(buf);
log_tag("device:%s:%s:error:%d\n", file, name, errno);
log_fatal(errno, "Failed to read device '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
ret = close(f);
if (ret < 0) {
/* LCOV_EXCL_START */
free(buf);
log_tag("device:%s:%s:error:%d\n", file, name, errno);
log_fatal(errno, "Failed to close device '%u:%u'.\n", major(device), minor(device));
return -1;
/* LCOV_EXCL_STOP */
}
log_tag("attr:%s:%s:power:up\n", file, name);
free(buf);
return 0;
}
#endif
/**
* Thread for spinning up.
*
* Note that filling up the devinfo object is done inside this thread,
* to avoid to block the main thread if the device need to be spin up
* to handle stat/resolve requests.
*/
static void* thread_spinup(void* arg)
{
#if HAVE_LINUX_DEVICE
devinfo_t* devinfo = arg;
uint64_t start;
start = os_tick_ms();
if (devup(devinfo->device, devinfo->name) != 0) {
/* LCOV_EXCL_START */
return (void*)-1;
/* LCOV_EXCL_STOP */
}
msg_status("Spunup device '%s' for disk '%s' in %" PRIu64 " ms.\n", devinfo->file, devinfo->name, os_tick_ms() - start);
/* after the spin up, get SMART info */
if (devsmart(devinfo->device, devinfo->name, devinfo->smartctl, devinfo->smart, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf) != 0) {
/* LCOV_EXCL_START */
return (void*)-1;
/* LCOV_EXCL_STOP */
}
/*
* Retrieve some attributes directly from the system.
*
* smartctl intentionally skips queries on devices in standby mode
* to prevent accidentally spinning them up.
*/
devattr(devinfo->device, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf);
return 0;
#else
(void)arg;
return (void*)-1;
#endif
}
/**
* Thread for spinning down.
*/
static void* thread_spindown(void* arg)
{
#if HAVE_LINUX_DEVICE
devinfo_t* devinfo = arg;
uint64_t start;
start = os_tick_ms();
if (devdown(devinfo->device, devinfo->name, devinfo->smartctl) != 0) {
/* LCOV_EXCL_START */
return (void*)-1;
/* LCOV_EXCL_STOP */
}
msg_status("Spundown device '%s' for disk '%s' in %" PRIu64 " ms.\n", devinfo->file, devinfo->name, os_tick_ms() - start);
return 0;
#else
(void)arg;
return (void*)-1;
#endif
}
/**
* Thread for spinning down.
*/
static void* thread_spindownifup(void* arg)
{
#if HAVE_LINUX_DEVICE
devinfo_t* devinfo = arg;
uint64_t start;
int power;
start = os_tick_ms();
if (devdownifup(devinfo->device, devinfo->name, devinfo->smartctl, &power) != 0) {
/* LCOV_EXCL_START */
return (void*)-1;
/* LCOV_EXCL_STOP */
}
if (power == POWER_ACTIVE)
msg_status("Spundown device '%s' for disk '%s' in %" PRIu64 " ms.\n", devinfo->file, devinfo->name, os_tick_ms() - start);
return 0;
#else
(void)arg;
return (void*)-1;
#endif
}
/**
* Thread for getting smart info.
*/
static void* thread_smart(void* arg)
{
#if HAVE_LINUX_DEVICE
devinfo_t* devinfo = arg;
if (devsmart(devinfo->device, devinfo->name, devinfo->smartctl, devinfo->smart, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf) != 0) {
/* LCOV_EXCL_START */
return (void*)-1;
/* LCOV_EXCL_STOP */
}
/*
* Retrieve some attributes directly from the system.
*
* smartctl intentionally skips queries on devices in standby mode
* to prevent accidentally spinning them up.
*/
devattr(devinfo->device, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf);
return 0;
#else
(void)arg;
return (void*)-1;
#endif
}
/**
* Thread for getting power info.
*/
static void* thread_probe(void* arg)
{
#if HAVE_LINUX_DEVICE
devinfo_t* devinfo = arg;
if (devprobe(devinfo->device, devinfo->name, devinfo->smartctl, &devinfo->power, devinfo->smart, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf) != 0) {
/* LCOV_EXCL_START */
return (void*)-1;
/* LCOV_EXCL_STOP */
}
/*
* Retrieve some attributes directly from the system.
*
* smartctl intentionally skips queries on devices in standby mode
* to prevent accidentally spinning them up.
*/
devattr(devinfo->device, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf);
return 0;
#else
(void)arg;
return (void*)-1;
#endif
}
static int device_thread(tommy_list* list, void* (*func)(void* arg))
{
int fail = 0;
tommy_node* i;
#if HAVE_THREAD
/* start all threads */
for (i = tommy_list_head(list); i != 0; i = i->next) {
devinfo_t* devinfo = i->data;
thread_create(&devinfo->thread, func, devinfo);
}
/* join all threads */
for (i = tommy_list_head(list); i != 0; i = i->next) {
devinfo_t* devinfo = i->data;
void* retval;
thread_join(devinfo->thread, &retval);
if (retval != 0)
++fail;
}
#else
for (i = tommy_list_head(list); i != 0; i = i->next) {
devinfo_t* devinfo = i->data;
if (func(devinfo) != 0)
++fail;
}
#endif
if (fail != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
int devquery(tommy_list* high, tommy_list* low, int operation)
{
void* (*func)(void* arg) = 0;
#if HAVE_LINUX_DEVICE
tommy_node* i;
struct stat st;
/* sysfs interface is required */
if (stat("/sys/dev/block", &st) != 0) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Missing interface /sys/dev/block.\n");
return -1;
/* LCOV_EXCL_STOP */
}
/* for each high device */
for (i = tommy_list_head(high); i != 0; i = i->next) {
devinfo_t* devinfo = i->data;
uint64_t device = devinfo->device;
#if HAVE_SYNCFS
if (operation == DEVICE_DOWN) {
/* flush the high level filesystem before spinning down */
int f = open(devinfo->mount, O_RDONLY);
if (f >= 0) {
syncfs(f);
close(f);
}
}
#endif
tommy_list devlist;
tommy_list_init(&devlist);
/* obtain the real devices */
if (devdereference(device, devinfo->mount, &devlist) != 0) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Failed to dereference device '%u:%u' at '%s'.\n", major(device), minor(device), devinfo->mount);
return -1;
/* LCOV_EXCL_STOP */
}
devinfo->file[0] = 0;
for (tommy_node* j = tommy_list_head(&devlist); j != 0; j = j->next) {
struct dev_struct* dev = j->data;
uint64_t access_stat;
/* retrieve access stat for the high level device */
if (devstat(dev->device, &access_stat) == 0) {
/* cumulate access stat in the first split */
if (devinfo->split)
devinfo->split->access_stat += access_stat;
else
devinfo->access_stat += access_stat;
}
/* get the device file */
char file[PATH_MAX];
if (devresolve(dev->device, file, sizeof(file)) != 0) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Failed to resolve device '%u:%u'.\n", major(dev->device), minor(dev->device));
return -1;
/* LCOV_EXCL_STOP */
}
/* add to the list of device files */
if (devinfo->file[0] != 0)
pathcat(devinfo->file, sizeof(devinfo->file), ",");
pathcat(devinfo->file, sizeof(devinfo->file), file);
/* expand the tree of devices */
if (devtree(devinfo, dev->device, low) != 0) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Failed to expand device '%u:%u'.\n", major(dev->device), minor(dev->device));
return -1;
/* LCOV_EXCL_STOP */
}
}
tommy_list_foreach(&devlist, free);
}
#else
(void)high;
#endif
switch (operation) {
case DEVICE_UP : func = thread_spinup; break;
case DEVICE_DOWN : func = thread_spindown; break;
case DEVICE_SMART : func = thread_smart; break;
case DEVICE_PROBE : func = thread_probe; break;
case DEVICE_DOWNIFUP : func = thread_spindownifup; break;
}
if (!func)
return 0;
return device_thread(low, func);
}
void os_default_conf(char* conf, size_t conf_size, const char* argv0)
{
(void)argv0;
#ifdef SYSCONFDIR
/* if it exists, give precedence at sysconfdir, usually /usr/local/etc */
if (eaccess(SYSCONFDIR "/" PACKAGE ".conf", F_OK) == 0) {
pathcpy(conf, conf_size, SYSCONFDIR "/" PACKAGE ".conf");
} else /* otherwise fallback to plain /etc */
#endif
{
pathcpy(conf, conf_size, "/etc/" PACKAGE ".conf");
}
}
void os_init(int opt)
{
#if HAVE_BLKID
int ret;
ret = blkid_get_cache(&cache, NULL);
if (ret != 0) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "WARNING Failed to get blkid cache\n");
/* LCOV_EXCL_STOP */
}
#endif
/* set LC_ALL=C to make smartctl ignoring the locale when printing info */
setenv("LC_ALL", "C", 1);
(void)opt;
}
void os_done(void)
{
#if HAVE_BLKID
if (cache != 0)
blkid_put_cache(cache);
#endif
}
/* LCOV_EXCL_START */
void os_abort(void)
{
#if HAVE_BACKTRACE && HAVE_BACKTRACE_SYMBOLS
void* stack[32];
char** messages;
unsigned size;
unsigned i;
#endif
printf("Stacktrace of " PACKAGE " v" VERSION);
#ifdef _linux
printf(", linux");
#endif
#ifdef __GNUC__
printf(", gcc " __VERSION__);
#endif
printf(", %d-bit", (int)sizeof(void*) * 8);
printf(", PATH_MAX=%d", PATH_MAX);
printf("\n");
#if HAVE_BACKTRACE && HAVE_BACKTRACE_SYMBOLS
size = backtrace(stack, 32);
messages = backtrace_symbols(stack, size);
for (i = 1; i < size; ++i) {
const char* msg;
if (messages)
msg = messages[i];
else
msg = "<unknown>";
printf("[bt] %02u: %s\n", i, msg);
if (messages) {
int ret;
char addr2line[1024];
size_t j = 0;
while (msg[j] != '(' && msg[j] != ' ' && msg[j] != 0)
++j;
snprintf(addr2line, sizeof(addr2line), "addr2line %p -e %.*s", stack[i], (unsigned)j, msg);
ret = system(addr2line);
if (WIFEXITED(ret) && WEXITSTATUS(ret) != 0)
printf("exit:%d\n", WEXITSTATUS(ret));
if (WIFSIGNALED(ret))
printf("signal:%d\n", WTERMSIG(ret));
}
}
#endif
printf("Please report this error to the SnapRAID Issues:\n");
printf("https://github.com/amadvance/snapraid/issues\n");
abort();
}
/* LCOV_EXCL_STOP */
void os_clear(void)
{
/* ANSI codes */
printf("\033[H"); /* cursor at topleft */
printf("\033[2J"); /* clear screen */
}
size_t direct_size(void)
{
long size;
size = sysconf(_SC_PAGESIZE);
if (size == -1) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "No page size\n");
exit(EXIT_FAILURE);
/* LCOV_EXCL_STOP */
}
return size;
}
#if HAVE_LINUX_DEVICE
/* List of possible ambient temperature labels */
const char* AMBIENT_LABEL[] = {
"systin",
"auxtin",
"mb",
"m/b",
"board",
"motherboard",
"system",
"chassis",
"case",
"room",
"ambient",
0
};
int ambient_temperature(void)
{
DIR* dir;
struct dirent* entry;
int lowest_temp = 0;
dir = opendir("/sys/class/hwmon");
if (!dir) {
/* LCOV_EXCL_START */
return 0;
/* LCOV_EXCL_STOP */
}
/* iterate through hwmon devices */
while ((entry = readdir(dir)) != NULL) {
char path[PATH_MAX];
DIR* hwmon_dir;
struct dirent* hwmon_entry;
if (strncmp(entry->d_name, "hwmon", 5) != 0)
continue;
pathprint(path, sizeof(path), "/sys/class/hwmon/%s", entry->d_name);
/* iterate through temp*_input files */
hwmon_dir = opendir(path);
if (!hwmon_dir) {
/* LCOV_EXCL_START */
continue;
/* LCOV_EXCL_STOP */
}
while ((hwmon_entry = readdir(hwmon_dir)) != NULL) {
char hwmon_name[PATH_MAX];
char value[128];
char name[128];
char label[128];
char* dash;
char* e;
int temp;
if (strncmp(hwmon_entry->d_name, "temp", 4) != 0)
continue;
/* make a copy to allow modifications in place */
pathcpy(hwmon_name, sizeof(hwmon_name), hwmon_entry->d_name);
dash = strrchr(hwmon_name, '_');
if (dash == 0) {
/* LCOV_EXCL_START */
continue;
/* LCOV_EXCL_STOP */
}
if (strcmp(dash, "_input") != 0)
continue;
/* read the temperature */
pathprint(path, sizeof(path), "/sys/class/hwmon/%s/%s", entry->d_name, hwmon_name);
if (sysattr(path, value, sizeof(value)) != 0) {
/* LCOV_EXCL_START */
continue;
/* LCOV_EXCL_STOP */
}
temp = strtoi(value, &e, 10) / 1000;
if (*e != 0 && !isspace((unsigned char)*e)) {
/* LCOV_EXCL_START */
continue;
/* LCOV_EXCL_STOP */
}
/* cut the file name at "_input" */
*dash = 0;
/* read the corresponding name */
pathprint(path, sizeof(path), "/sys/class/hwmon/%s/name", entry->d_name);
if (sysattr(path, name, sizeof(name)) != 0) {
/* LCOV_EXCL_START */
/* fallback to using the hwmon name */
pathcpy(name, sizeof(name), entry->d_name);
/* LCOV_EXCL_STOP */
}
/* read the corresponding label file */
pathprint(path, sizeof(path), "/sys/class/hwmon/%s/%s_label", entry->d_name, hwmon_name);
if (sysattr(path, label, sizeof(label)) != 0) {
/* LCOV_EXCL_START */
/* fallback to using the temp* name (e.g., temp1, temp2) */
pathcpy(label, sizeof(label), hwmon_name);
/* LCOV_EXCL_STOP */
}
log_tag("thermal:ambient:device:%s:%s:%s:%s:%d\n", entry->d_name, name, hwmon_name, label, temp);
/* check if temperature is in reasonable range */
if (temp < 15 || temp > 40)
continue;
/* lower case */
strlwr(label);
/* check if label matches possible ambient labels */
for (int i = 0; AMBIENT_LABEL[i]; ++i) {
if (worddigitstr(label, AMBIENT_LABEL[i]) != 0) {
log_tag("thermal:ambient:candidate:%d\n", temp);
if (lowest_temp == 0 || lowest_temp > temp)
lowest_temp = temp;
break;
}
}
/* accept also generic "temp1" */
if (strcmp(label, "temp1") == 0) {
log_tag("thermal:ambient:candidate:%d\n", temp);
if (lowest_temp == 0 || lowest_temp > temp)
lowest_temp = temp;
}
}
closedir(hwmon_dir);
}
closedir(dir);
return lowest_temp;
}
#else
int ambient_temperature(void)
{
return 0;
}
#endif
int devmap(void)
{
#if HAVE_LINUX_DEVICE
char path[PATH_MAX];
DIR* d = opendir("/sys/block");
if (!d) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
struct dirent* dd;
while (1) {
char buf[256];
dd = readdir(d);
if (dd == 0)
break;
if (dd->d_name[0] == '.')
continue;
/* verify it has a hardware device link (excludes virtual disks) */
pathprint(path, sizeof(path), "/sys/block/%s/device", dd->d_name);
if (eaccess(path, F_OK) != 0)
continue;
/* device/serial */
pathprint(path, sizeof(path), "/sys/block/%s/device/serial", dd->d_name);
if (sysattr(path, buf, sizeof(buf)) == 0 && buf[0] != 0) {
log_tag("map:/dev/%s:%s\n", dd->d_name, esc_tag(buf));
}
/* device/vpd_pg80 */
pathprint(path, sizeof(path), "/sys/block/%s/device/vpd_pg80", dd->d_name);
if (sysattr_vpd_pg80(path, buf, sizeof(buf)) == 0 && buf[0] != 0) {
log_tag("map:/dev/%s:%s\n", dd->d_name, esc_tag(buf));
}
/* device/vpd_pg83 */
pathprint(path, sizeof(path), "/sys/block/%s/device/vpd_pg83", dd->d_name);
if (sysattr_vpd_pg83(path, buf, sizeof(buf)) == 0 && buf[0] != 0) {
log_tag("map:/dev/%s:naa.%s\n", dd->d_name, esc_tag(buf));
}
/* device/WWN */
pathprint(path, sizeof(path), "/sys/block/%s/device/wwn", dd->d_name);
if (sysattr(path, buf, sizeof(buf)) == 0 && buf[0] != 0) {
log_tag("map:/dev/%s:%s\n", dd->d_name, esc_tag(buf));
}
/* device/WWID */
pathprint(path, sizeof(path), "/sys/block/%s/device/wwid", dd->d_name);
if (sysattr(path, buf, sizeof(buf)) == 0 && buf[0] != 0) {
log_tag("map:/dev/%s:%s\n", dd->d_name, esc_tag(buf));
}
/* WWN */
pathprint(path, sizeof(path), "/sys/block/%s/wwn", dd->d_name);
if (sysattr(path, buf, sizeof(buf)) == 0 && buf[0] != 0) {
log_tag("map:/dev/%s:%s\n", dd->d_name, esc_tag(buf));
}
/* WWID */
pathprint(path, sizeof(path), "/sys/block/%s/wwid", dd->d_name);
if (sysattr(path, buf, sizeof(buf)) == 0 && buf[0] != 0) {
log_tag("map:/dev/%s:%s\n", dd->d_name, esc_tag(buf));
}
/* uuid */
pathprint(path, sizeof(path), "/sys/block/%s/uuid", dd->d_name);
if (sysattr(path, buf, sizeof(buf)) == 0 && buf[0] != 0) {
log_tag("map:/dev/%s:%s\n", dd->d_name, esc_tag(buf));
}
}
closedir(d);
#endif
return 0;
}
#endif