snapraid/cmdline/unixapp.c

4128 lines
98 KiB
C
Raw Normal View History

2026-06-12 09:21:18 +02:00
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2013 Andrea Mazzoleni
#include "os/portable.h"
2026-06-12 09:21:18 +02:00
#ifndef __MINGW32__ /* Only for Unix */
#include "app.h"
2026-06-12 09:21:18 +02:00
#include "support.h"
/****************************************************************************/
/* global */
/**
* Exit codes.
*/
int exit_success = 0;
int exit_failure = 1;
int exit_sync_needed = 2;
/****************************************************************************/
/* signal */
2026-07-02 09:33:14 +02:00
volatile sig_atomic_t global_interrupt = 0;
2026-06-12 09:21:18 +02:00
int os_signal_interrupt(void)
2026-06-12 09:21:18 +02:00
{
return global_interrupt;
}
/* LCOV_EXCL_START */
void app_signal_handler(int signum)
{
/* report the request of interruption with the signal received */
global_interrupt = signum;
}
/* LCOV_EXCL_STOP */
/****************************************************************************/
/* find */
#define BTRFS_SUPER_MAGIC 0x9123683E
#define BCACHEFS_SUPER_MAGIC 0xCA451A4E
#define ZFS_SUPER_MAGIC 0x2FC12FC1
#if HAVE_STATFS && HAVE_STRUCT_STATFS_F_TYPE
/**
* Return the filesystem type as an unsigned value.
*
* In glibc, 'struct statfs.f_type' has type '__fsword_t' (signed 'int' on 32-bit
* architectures and 'long' on 64-bit). Directly comparing signed 'f_type' against
* 32-bit unsigned magic constants (e.g. BTRFS_SUPER_MAGIC > 0x7FFFFFFF) triggers
* signedness comparison warnings on 32-bit systems. Converting to unsigned here
* avoids signed/unsigned comparison mismatches across all architectures.
*/
static inline unsigned statfs_type(const struct statfs* sfs)
{
return sfs->f_type;
}
#endif
2026-06-12 09:21:18 +02:00
#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 (eaccess(bcachefs_paths[i], X_OK) == 0) {
2026-06-12 09:21:18 +02:00
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 (eaccess(zfs_paths[i], X_OK) == 0) {
2026-06-12 09:21:18 +02:00
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 (eaccess(zpool_paths[i], X_OK) == 0) {
2026-06-12 09:21:18 +02:00
return zpool_paths[i];
}
}
return 0;
}
#endif
#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
/****************************************************************************/
/* sys */
/**
* 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
/*
* sysattr_vpd_pg80 — parse raw binary VPD page 0x80 (Unit Serial Number)
* from a sysfs vpd_pg80 file.
2026-06-12 09:21:18 +02:00
*/
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 */
}
size_t page_len = ((size_t)buf[2] << 8) | buf[3];
2026-06-12 09:21:18 +02:00
size_t available = (size_t)ret - 4;
/* if incomplete */
if (page_len > available) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
2026-06-12 09:21:18 +02:00
/* if empty */
if (page_len == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* if too big to store the 0 termination */
2026-06-12 09:21:18 +02:00
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. This is safe because descriptors are
* parsed individually below with strict bounds checking and exact length
* validation, so incomplete or truncated data is never returned.
*/
2026-06-12 09:21:18 +02:00
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 log error on reading.
2026-06-12 09:21:18 +02:00
* 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;
}
2026-08-10 11:33:03 +02:00
/*
* Parse a sysfs hexadecimal bitmask string (e.g. thread_siblings).
*
* Linux sysfs outputs bitmasks in big-endian 32-bit hex word groups
* (highest CPUs on the left, lowest CPUs on the right).
*
* Left-shifting digits into a 64-bit unsigned long long operates modulo 2^64,
* discarding any high-order hex words (CPUs >= 64) and leaving bits 0..63
* containing the exact mask for CPUs 0..63. Since CPUS_MAX is 64, this mask
* is always accurate for all evaluated CPUs even on systems with > 64 CPUs.
*/
2026-06-12 09:21:18 +02:00
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
/******************************************************************************/
/* resolve */
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)
{
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, "Failed ioctl BTRFS_IOC_DEV_INFO. %s.", strerror(errno));
goto bail;
2026-06-12 09:21:18 +02:00
/* 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 */
log_error(errno, "Failed stat %s. %s.", dev_info.path, strerror(errno));
goto bail;
/* LCOV_EXCL_STOP */
}
if (!S_ISBLK(st.st_mode)) {
/* LCOV_EXCL_START */
log_error(errno, "Device %s is not a block device.", dev_info.path);
goto bail;
2026-06-12 09:21:18 +02:00
/* 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);
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 (tommy_list_empty(devlist))
goto bail;
2026-06-12 09:21:18 +02:00
return 0;
bail:
tommy_list_foreach(devlist, free);
tommy_list_init(devlist);
return -1;
2026-06-12 09:21:18 +02:00
}
/**
* Decodes octal escape sequences (\ooo) in place.
*
* In /proc/self/mountinfo, special characters like spaces, tabs, newlines,
* and backslashes are encoded as 3-digit octal escape sequences (e.g. \040 for space).
* Decoding is performed in place because the decoded character is always
* shorter than the 4-byte escape sequence.
*/
static void unescape_mount(char* s)
{
char* r = s;
char* w = s;
while (*r != 0) {
if (r[0] == '\\'
&& r[1] >= '0' && r[1] <= '7'
&& r[2] >= '0' && r[2] <= '7'
&& r[3] >= '0' && r[3] <= '7') {
*w = (char)(((r[1] - '0') << 6) | ((r[2] - '0') << 3) | (r[3] - '0'));
++w;
r += 4;
} else {
*w = *r;
++w;
++r;
}
}
*w = 0;
}
2026-06-12 09:21:18 +02:00
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 */
unescape_mount(id_map[4]);
2026-06-12 09:21:18 +02:00
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;
unescape_mount(fs_map[1]);
2026-06-12 09:21:18 +02:00
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) {
/* LCOV_EXCL_START */
log_error(errno, "Failed stat %s. %s.", dev_map[i], strerror(errno));
goto bail;
/* LCOV_EXCL_STOP */
}
if (!S_ISBLK(st.st_mode)) {
/* LCOV_EXCL_START */
log_error(errno, "Device %s is not a block device.", dev_map[i]);
goto bail;
/* LCOV_EXCL_STOP */
}
2026-06-12 09:21:18 +02:00
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));
}
/* something has to be found */
if (tommy_list_empty(devlist))
goto bail;
2026-06-12 09:21:18 +02:00
return 0;
bail:
tommy_list_foreach(devlist, free);
tommy_list_init(devlist);
return -1;
2026-06-12 09:21:18 +02:00
}
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);
2026-06-12 09:21:18 +02:00
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");
2026-06-12 09:21:18 +02:00
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);
2026-06-12 09:21:18 +02:00
if (!fp) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
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) {
/* LCOV_EXCL_START */
log_error(errno, "Failed stat %s. %s.", file, strerror(errno));
os_pclose(fp);
goto bail;
/* LCOV_EXCL_STOP */
}
2026-06-12 09:21:18 +02:00
if (!S_ISBLK(st.st_mode)) {
/* LCOV_EXCL_START */
log_error(errno, "Device %s is not a block device.", file);
os_pclose(fp);
goto bail;
/* LCOV_EXCL_STOP */
}
2026-06-12 09:21:18 +02:00
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));
}
os_pclose(fp);
if (tommy_list_empty(devlist))
goto bail;
2026-06-12 09:21:18 +02:00
return 0;
bail:
tommy_list_foreach(devlist, free);
tommy_list_init(devlist);
return -1;
2026-06-12 09:21:18 +02:00
}
/**
* 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 (statfs_type(&sfs) == BTRFS_SUPER_MAGIC) {
2026-06-12 09:21:18 +02:00
ret = devdereference_btrfs(device, dir, fd, devlist);
} else if (statfs_type(&sfs) == BCACHEFS_SUPER_MAGIC) {
2026-06-12 09:21:18 +02:00
ret = devdereference_bcachefs(device, dir, devlist);
} else if (statfs_type(&sfs) == ZFS_SUPER_MAGIC) {
2026-06-12 09:21:18 +02:00
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
/****************************************************************************/
/* uuid */
/**
2026-06-29 15:42:55 +02:00
* Get the filesystem UUID using the /dev/disk/by-uuid/ links.
* It doesn't require root permission, and the UUIDs are always updated.
*
* NOTE: This method reads the filesystem-level UUID (unlike /dev/disk/by-partuuid/
* which reads the GPT partition table slot).
*
* This method WILL reliably work for standard single-device filesystems:
* - ext2 / ext3 / ext4
* - XFS
* - F2FS (Flash-Friendly File System)
* - NTFS
* - FAT32 / exFAT (via their volume serial number formatted as a UUID string)
*
* This method will NOT find an entry for:
* - Network filesystems (NFS, SMB/CIFS, CephFS)
* - Virtual/In-memory filesystems (tmpfs, proc, sysfs)
* - ZFS (bypasses the Linux block layer and uses internal GUIDs)
* - Raw flash filesystems (JFFS2, UBIFS)
*
* For multi-device filesystems like Btrfs and Bcachefs, all member devices
* share the same filesystem UUID. Because /dev/disk/by-uuid/ cannot have duplicate
* filename entries, only ONE arbitrary device from the pool will be present here.
2026-06-12 09:21:18 +02:00
*/
#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;
}
static int hexval(char c)
{
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
if (c >= 'A' && c <= 'F')
return c - 'A' + 10;
return -1;
}
/**
* Decode udev \xNN hex escapes into raw string.
* Return 0 on success, -1 if truncated.
*/
static int udev_decode(char* dst, size_t dst_size, const char* src)
{
size_t i = 0;
while (*src != 0) {
if (i + 1 >= dst_size)
return -1;
if (src[0] == '\\' && src[1] == 'x' && src[2] != 0 && src[3] != 0) {
int h1 = hexval(src[2]);
int h2 = hexval(src[3]);
if (h1 >= 0 && h2 >= 0) {
dst[i++] = (char)((h1 << 4) | h2);
src += 4;
continue;
}
}
dst[i++] = *src++;
}
dst[i] = 0;
return 0;
}
/**
* Get the filesystem LABEL using the /dev/disk/by-label/ links.
*/
static int devlabel_dev(uint64_t device, char* label, size_t label_size)
{
ssize_t ret;
DIR* d;
struct dirent* dd;
struct stat st;
/* scan the LABEL directory searching for the device */
d = opendir("/dev/disk/by-label");
if (!d) {
/* LCOV_EXCL_START */
log_tag("label:by-label:%u:%u: opendir(/dev/disk/by-label) 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("label:by-label:%u:%u: dirfd(/dev/disk/by-label) failed, %s\n", major(device), minor(device), strerror(errno));
closedir(d);
return -1;
/* LCOV_EXCL_STOP */
}
while ((dd = readdir(d)) != 0) {
/* skip "." and ".." files */
if (strcmp(dd->d_name, ".") == 0 || strcmp(dd->d_name, "..") == 0)
continue;
ret = fstatat(dir_fd, dd->d_name, &st, 0);
if (ret != 0) {
/* LCOV_EXCL_START */
log_tag("label:by-label:%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 label */
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-label/%s", dd->d_name);
ret = readlink(path, buf, sizeof(buf));
if (ret < 0 || ret >= PATH_MAX) {
/* LCOV_EXCL_START */
log_tag("label:by-label:%u:%u: readlink(/dev/disk/by-label/%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 */
if (udev_decode(label, label_size, dd->d_name) != 0) {
/* LCOV_EXCL_START */
closedir(d);
return -1;
/* LCOV_EXCL_STOP */
}
log_tag("label:by-label:%u:%u:%s: found %s\n", major(device), minor(device), label, buf);
closedir(d);
return 0;
}
}
log_tag("label:by-label:%u:%u: /dev/disk/by-label doesn't contain a matching block device\n", major(device), minor(device));
/* not found */
closedir(d);
return -1;
}
#endif
2026-06-12 09:21:18 +02:00
/**
* Get the LABEL using the generic Linux FS_IOC_GETFSLABEL ioctl.
*/
#if HAVE_LINUX_DEVICE
#ifndef FSLABEL_MAX
#define FSLABEL_MAX 256
#endif
#ifndef FS_IOC_GETFSLABEL
#define FS_IOC_GETFSLABEL _IOR(0x94, 49, char[FSLABEL_MAX])
#endif
static int devlabel_ioctl(const char* path, char* label, size_t label_size)
{
char label_buf[FSLABEL_MAX];
int fd = open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd < 0 && (errno == ENOENT || errno == ENOTDIR)) {
char dir[PATH_MAX];
char* slash;
pathcpy(dir, sizeof(dir), path);
slash = strrchr(dir, '/');
if (slash) {
if (slash == dir)
slash[1] = 0;
else
*slash = 0;
fd = open(dir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
}
}
if (fd < 0) {
/* LCOV_EXCL_START */
log_tag("label:ioctl: open(%s) failed, %s\n", path, strerror(errno));
return -1;
/* LCOV_EXCL_STOP */
}
memset(label_buf, 0, sizeof(label_buf));
if (ioctl(fd, FS_IOC_GETFSLABEL, label_buf) < 0) {
log_tag("label:ioctl: FS_IOC_GETFSLABEL(%s) failed, %s\n", path, strerror(errno));
close(fd);
return -1;
}
close(fd);
if (label_buf[0] == 0) {
log_tag("label:ioctl:%s: empty label\n", path);
return -1;
}
if (strlen(label_buf) + 1 > label_size) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
strcpy(label, label_buf);
log_tag("label:ioctl:%s: found %s\n", path, label);
return 0;
}
#endif
2026-06-12 09:21:18 +02:00
#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 (statfs_type(&sfs) != BTRFS_SUPER_MAGIC) {
2026-06-12 09:21:18 +02:00
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 (statfs_type(&sfs) != BCACHEFS_SUPER_MAGIC) {
2026-06-12 09:21:18 +02:00
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
log_tag("uuid:notfound:%u:%u:\n", major(device_id), minor(device_id));
/* not supported */
(void)uuid;
(void)uuid_size;
return -1;
}
/****************************************************************************/
/* fs */
/* 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
#define BTRFS_SUPER_MAGIC 0x9123683E
#define BCACHEFS_SUPER_MAGIC 0xCA451A4E
#define ZFS_SUPER_MAGIC 0x2FC12FC1
2026-06-12 09:21:18 +02:00
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 (statfs_type(&st)) {
2026-06-12 09:21:18 +02:00
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 (statfs_type(&st)) {
2026-06-12 09:21:18 +02:00
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 (statfs_type(&st) == FILESYSTEMS[i].id) {
2026-06-12 09:21:18 +02:00
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_LINUX_DEVICE
if (devlabel_ioctl(path, fslabel, fslabel_size) != 0) {
struct stat fst;
if (stat(path, &fst) == 0)
devlabel_dev(fst.st_dev, fslabel, fslabel_size);
}
2026-06-12 09:21:18 +02:00
#else
(void)fslabel_size;
#endif
}
return 0;
}
/****************************************************************************/
/* snapshot */
2026-06-12 09:21:18 +02:00
#if HAVE_LINUX_DEVICE
static int fssnapshot_inode(const char* path, uint32_t magic, uint64_t root_inode, struct fssnapshot_struct* fss)
{
char resolved[PATH_MAX];
2026-06-12 09:21:18 +02:00
char current_path[PATH_MAX];
if (realpath(path, resolved) == 0)
return -1;
pathslash(resolved, sizeof(resolved));
pathcpy(current_path, sizeof(current_path), resolved);
2026-06-12 09:21:18 +02:00
/* 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;
}
size_t root_len = strlen(fss->root_dir);
if (pathncmp(fss->root_dir, resolved, root_len) != 0) {
errno = EINVAL;
return -1;
}
pathcpy(fss->sub_dir, sizeof(fss->sub_dir), resolved + root_len);
2026-06-12 09:21:18 +02:00
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)
{
char resolved[PATH_MAX];
char root[PATH_MAX];
if (realpath(dir, resolved) == 0)
return -1;
2026-06-12 09:21:18 +02:00
if (extract_zfs(dir, fss->dataset, sizeof(fss->dataset), 0, 0, fss->root_dir, sizeof(fss->root_dir)) != 0)
return -1;
if (realpath(fss->root_dir, root) == 0)
return -1;
pathslash(resolved, sizeof(resolved));
pathslash(root, sizeof(root));
size_t root_len = strlen(root);
if (pathncmp(root, resolved, root_len) != 0) {
errno = EINVAL;
return -1;
}
pathcpy(fss->sub_dir, sizeof(fss->sub_dir), resolved + root_len);
pathcpy(fss->root_dir, sizeof(fss->root_dir), root);
2026-06-12 09:21:18 +02:00
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 (eaccess(fss->snapshot_dir, R_OK | X_OK) != 0) {
2026-06-12 09:21:18 +02:00
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 */
}
2026-07-30 12:40:08 +02:00
assert(fss != 0); /* silence the clang static analyzer (it's never called with a NULL fss) */
if (statfs_type(&sfs) == BTRFS_SUPER_MAGIC) {
2026-06-12 09:21:18 +02:00
/* btrfs reserved inode 256 for subvolume roots */
return fssnapshot_inode(path, BTRFS_SUPER_MAGIC, 256, fss);
} else if (statfs_type(&sfs) == BCACHEFS_SUPER_MAGIC) {
2026-06-12 09:21:18 +02:00
/* Bcachefs reserves inode 4096 for each subvolume's root directory */
return fssnapshot_inode(path, BCACHEFS_SUPER_MAGIC, 4096, fss);
} else if (statfs_type(&sfs) == ZFS_SUPER_MAGIC) {
2026-06-12 09:21:18 +02:00
return fssnapshot_zfs(path, ZFS_SUPER_MAGIC, fss);
} else {
return 1;
2026-06-12 09:21:18 +02:00
}
#else
(void)path;
(void)fss;
return 1;
2026-06-12 09:21:18 +02:00
#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
/*
* Native ZFS snapshots use a SnapRAID-specific prefix to avoid
* deleting or renaming unrelated user snapshots with generic
* names such as @scan, @pending, or @stable.
*/
#define ZFS_SNAPSHOT_PREFIX "snapraid-"
2026-06-12 09:21:18 +02:00
#if HAVE_LINUX_DEVICE
static int fssnapshot_stat_zfs(const struct fssnapshot_struct* fss, const char* name, struct stat* st)
{
char native_name[PATH_MAX];
2026-06-12 09:21:18 +02:00
char target_path[PATH_MAX];
pathcpy(native_name, sizeof(native_name), ZFS_SNAPSHOT_PREFIX);
pathcat(native_name, sizeof(native_name), name);
2026-06-12 09:21:18 +02:00
pathcpy(target_path, sizeof(target_path), fss->snapshot_dir);
pathcat(target_path, sizeof(target_path), native_name);
2026-06-12 09:21:18 +02:00
/*
* 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, native_name) == 0) {
2026-06-12 09:21:18 +02:00
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 (os_validate_exec_input(fss->root_dir) != 0 || os_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@" ZFS_SNAPSHOT_PREFIX "%s", fss->dataset, name);
2026-06-12 09:21:18 +02:00
if (os_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 (os_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@" ZFS_SNAPSHOT_PREFIX "%s", fss->dataset, name);
2026-06-12 09:21:18 +02:00
if (os_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_fs_rename(const struct fssnapshot_struct* fss, const char* old_name, const char* new_name)
2026-06-12 09:21:18 +02:00
{
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_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@" ZFS_SNAPSHOT_PREFIX "%s", fss->dataset, old_name);
snprintf(new_snapshot, sizeof(new_snapshot), "%s@" ZFS_SNAPSHOT_PREFIX "%s", fss->dataset, new_name);
2026-06-12 09:21:18 +02:00
if (os_validate_exec_input(old_snapshot) != 0 || os_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 || fss->magic == BCACHEFS_SUPER_MAGIC) {
return fssnapshot_fs_rename(fss, old_name, new_name);
2026-06-12 09:21:18 +02:00
} 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
}
int fssnapshot_path(const struct fssnapshot_struct* fss, const char* name, char* path, size_t path_size)
{
pathcpy(path, path_size, fss->snapshot_dir);
if (fss->magic == ZFS_SUPER_MAGIC)
pathcat(path, path_size, ZFS_SNAPSHOT_PREFIX);
pathcat(path, path_size, name);
pathcatc(path, path_size, '/');
pathcat(path, path_size, fss->sub_dir);
return 0;
}
2026-06-12 09:21:18 +02:00
void fssnapshot_unmount(const struct fssnapshot_struct* fss)
{
(void)fss;
}
2026-06-12 09:21:18 +02:00
/****************************************************************************/
/* dev */
/**
* 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);
pathcpy(devinfo->smartctl_info, sizeof(devinfo->smartctl_info), parent->smartctl_info);
2026-06-12 09:21:18 +02:00
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
/**
* Max number of arguments supported.
*/
#define ARGS_MAX 64
2026-06-12 09:21:18 +02:00
/**
* Get SMART attributes.
*/
#if HAVE_LINUX_DEVICE
static int devsmart(dev_t device, const char* name, const char* smartctl, const char* smartctl_info, struct smart_attr* smart, uint64_t* info, char* serial, char* family, char* model, char* interface)
2026-06-12 09:21:18 +02:00
{
char file[PATH_MAX];
OS_FILE* f;
int ret;
const char* x;
const char* info_opts = smartctl_info[0] ? smartctl_info : "-a";
2026-06-12 09:21:18 +02:00
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 */
}
const char* argv[ARGS_MAX];
unsigned argc = 0;
argv[argc++] = x;
char info_buf[SMART_MAX];
pathcpy(info_buf, sizeof(info_buf), info_opts);
argc += argsplit(argv + argc, ARGS_MAX - argc, info_buf);
if (argc >= ARGS_MAX) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Too many smartctl arguments.\n");
return -1;
/* LCOV_EXCL_STOP */
}
char extra_args[PATH_MAX];
2026-06-12 09:21:18 +02:00
if (smartctl[0]) {
pathprint(extra_args, sizeof(extra_args), smartctl, file);
argc += argsplit(argv + argc, ARGS_MAX - argc, extra_args);
if (argc >= ARGS_MAX) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Too many smartctl arguments.\n");
return -1;
/* LCOV_EXCL_STOP */
}
log_tag("smartctl:%s:%s:run: %s [info: %s] %s\n", file, name, x, info_opts, extra_args);
2026-06-12 09:21:18 +02:00
} else {
if (argc >= ARGS_MAX - 1) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Too many smartctl arguments.\n");
return -1;
/* LCOV_EXCL_STOP */
}
argv[argc++] = file;
log_tag("smartctl:%s:%s:run: %s [info: %s] %s\n", file, name, x, info_opts, file);
2026-06-12 09:21:18 +02:00
}
argv[argc++] = 0;
f = os_popen(argv);
2026-06-12 09:21:18 +02:00
if (!f) {
/* LCOV_EXCL_START */
log_tag("device:%s:%s:spawn\n", file, name);
if (smartctl[0])
log_fatal(EEXTERNAL, "Failed to run '%s %s %s'.\n", x, info_opts, extra_args);
else
log_fatal(EEXTERNAL, "Failed to run '%s %s %s'.\n", x, info_opts, file);
2026-06-12 09:21:18 +02:00
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);
if (smartctl[0])
log_fatal(EEXTERNAL, "Failed to run '%s %s %s' (not exited).\n", x, info_opts, extra_args);
else
log_fatal(EEXTERNAL, "Failed to run '%s %s %s' (not exited).\n", x, info_opts, file);
2026-06-12 09:21:18 +02:00
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));
long long v = syslong(path);
if (v >= 0)
info[INFO_SIZE] = v * 512;
2026-06-12 09:21:18 +02:00
}
if (info[INFO_ROTATION_RATE] == SMART_UNASSIGNED) {
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/queue/rotational", major(device), minor(device));
long long v = syslong(path);
if (v >= 0)
info[INFO_ROTATION_RATE] = v;
2026-06-12 09:21:18 +02:00
}
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, const char* smartctl_info, int* power, struct smart_attr* smart, uint64_t* info, char* serial, char* family, char* model, char* interface)
2026-06-12 09:21:18 +02:00
{
char file[PATH_MAX];
OS_FILE* f;
int ret;
const char* x;
const char* info_opts = smartctl_info[0] ? smartctl_info : "-a";
2026-06-12 09:21:18 +02:00
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 */
}
const char* argv[ARGS_MAX];
unsigned argc = 0;
argv[argc++] = x;
argv[argc++] = "-n";
argv[argc++] = "standby,3";
char info_buf[SMART_MAX];
pathcpy(info_buf, sizeof(info_buf), info_opts);
argc += argsplit(argv + argc, ARGS_MAX - argc, info_buf);
if (argc >= ARGS_MAX) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Too many smartctl arguments.\n");
return -1;
/* LCOV_EXCL_STOP */
}
char extra_args[PATH_MAX];
2026-06-12 09:21:18 +02:00
if (smartctl[0]) {
pathprint(extra_args, sizeof(extra_args), smartctl, file);
argc += argsplit(argv + argc, ARGS_MAX - argc, extra_args);
if (argc >= ARGS_MAX) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Too many smartctl arguments.\n");
return -1;
/* LCOV_EXCL_STOP */
}
log_tag("smartctl:%s:%s:run: %s -n standby,3 [info: %s] %s\n", file, name, x, info_opts, extra_args);
2026-06-12 09:21:18 +02:00
} else {
if (argc >= ARGS_MAX - 1) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Too many smartctl arguments.\n");
return -1;
/* LCOV_EXCL_STOP */
}
argv[argc++] = file;
log_tag("smartctl:%s:%s:run: %s -n standby,3 [info: %s] %s\n", file, name, x, info_opts, file);
2026-06-12 09:21:18 +02:00
}
argv[argc++] = 0;
f = os_popen(argv);
2026-06-12 09:21:18 +02:00
if (!f) {
/* LCOV_EXCL_START */
log_tag("device:%s:%s:spawn\n", file, name);
if (smartctl[0])
log_fatal(EEXTERNAL, "Failed to run '%s -n standby,3 %s %s'.\n", x, info_opts, extra_args);
else
log_fatal(EEXTERNAL, "Failed to run '%s -n standby,3 %s %s'.\n", x, info_opts, file);
2026-06-12 09:21:18 +02:00
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);
if (smartctl[0])
log_fatal(EEXTERNAL, "Failed to run '%s -n standby,3 %s %s' (not exited).\n", x, info_opts, extra_args);
else
log_fatal(EEXTERNAL, "Failed to run '%s -n standby,3 %s %s' (not exited).\n", x, info_opts, file);
2026-06-12 09:21:18 +02:00
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 */
}
const char* argv[ARGS_MAX];
unsigned argc = 0;
argv[argc++] = x;
argv[argc++] = "-s";
argv[argc++] = "standby,now";
2026-06-12 09:21:18 +02:00
if (smartctl[0]) {
pathprint(extra_args, sizeof(extra_args), smartctl, file);
argc += argsplit(argv + argc, ARGS_MAX - argc, extra_args);
if (argc >= ARGS_MAX) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Too many smartctl arguments.\n");
return -1;
/* LCOV_EXCL_STOP */
}
log_tag("smartctl:%s:%s:run: %s -s standby,now %s\n", file, name, x, extra_args);
2026-06-12 09:21:18 +02:00
} else {
if (argc >= ARGS_MAX - 1) {
/* LCOV_EXCL_START */
log_fatal(EEXTERNAL, "Too many smartctl arguments.\n");
return -1;
/* LCOV_EXCL_STOP */
}
argv[argc++] = file;
log_tag("smartctl:%s:%s:run: %s -s standby,now %s\n", file, name, x, file);
2026-06-12 09:21:18 +02:00
}
argv[argc++] = 0;
f = os_popen(argv);
2026-06-12 09:21:18 +02:00
if (!f) {
/* LCOV_EXCL_START */
log_tag("device:%s:%s:spawn\n", file, name);
if (smartctl[0])
log_fatal(EEXTERNAL, "Failed to run '%s -s standby,now %s'.\n", x, extra_args);
else
log_fatal(EEXTERNAL, "Failed to run '%s -s standby,now %s'.\n", x, file);
2026-06-12 09:21:18 +02:00
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);
if (smartctl[0])
log_fatal(EEXTERNAL, "Failed to run '%s -s standby,now %s' (not exited).\n", x, extra_args);
else
log_fatal(EEXTERNAL, "Failed to run '%s -s standby,now %s' (not exited).\n", x, file);
2026-06-12 09:21:18 +02:00
return -1;
/* LCOV_EXCL_STOP */
}
if (WEXITSTATUS(ret) != 0) {
/* LCOV_EXCL_START */
log_tag("device:%s:%s:exit:%d\n", file, name, WEXITSTATUS(ret));
if (smartctl[0])
log_fatal(EEXTERNAL, "Failed to run '%s -s standby,now %s' with return code %xh.\n", x, extra_args, WEXITSTATUS(ret));
else
log_fatal(EEXTERNAL, "Failed to run '%s -s standby,now %s' with return code %xh.\n", x, file, WEXITSTATUS(ret));
2026-06-12 09:21:18 +02:00
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, const char* smartctl_info, int* power)
2026-06-12 09:21:18 +02:00
{
*power = POWER_UNKNOWN;
if (devprobe(device, name, smartctl, smartctl_info, power, 0, 0, 0, 0, 0, 0) != 0)
2026-06-12 09:21:18 +02:00
return -1;
if (*power == POWER_ACTIVE)
return devdown(device, name, smartctl);
return 0;
}
#endif
/**
* Check if the device needs power management (it's a rotational one)
*/
#if HAVE_LINUX_DEVICE
static int devpower(dev_t device)
{
char path[PATH_MAX];
long long rotational;
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/queue/rotational", major(device), minor(device));
rotational = syslong(path);
if (rotational < 0)
return 1; /* assume rotational if unknown */
return rotational != 0;
}
#endif
2026-06-12 09:21:18 +02:00
/**
* 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;
/* skip not rotational devices */
if (devpower(devinfo->device) == 0)
return 0;
2026-06-12 09:21:18 +02:00
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->smartctl_info, devinfo->smart, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf) != 0) {
2026-06-12 09:21:18 +02:00
/* 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;
/* skip not rotational devices */
if (devpower(devinfo->device) == 0)
return 0;
2026-06-12 09:21:18 +02:00
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;
/* skip not rotational devices */
if (devpower(devinfo->device) == 0)
return 0;
2026-06-12 09:21:18 +02:00
start = os_tick_ms();
if (devdownifup(devinfo->device, devinfo->name, devinfo->smartctl, devinfo->smartctl_info, &power) != 0) {
2026-06-12 09:21:18 +02:00
/* 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->smartctl_info, devinfo->smart, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf) != 0) {
2026-06-12 09:21:18 +02:00
/* 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->smartctl_info, &devinfo->power, devinfo->smart, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf) != 0) {
2026-06-12 09:21:18 +02:00
/* 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;
}
void devsync(tommy_list* high)
2026-06-12 09:21:18 +02:00
{
#if HAVE_SYNCFS
tommy_node* i;
for (i = tommy_list_head(high); i != 0; i = i->next) {
devinfo_t* devinfo = i->data;
int f;
f = open(devinfo->mount, O_RDONLY);
if (f >= 0) {
syncfs(f);
close(f);
}
}
#else
(void)high;
#endif
}
2026-06-12 09:21:18 +02:00
int devquery(tommy_list* high, tommy_list* low)
{
2026-06-12 09:21:18 +02:00
#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;
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;
(void)low;
2026-06-12 09:21:18 +02:00
#endif
return 0;
}
int devrun(tommy_list* low, int operation)
{
void* (*func)(void* arg) = 0;
2026-06-12 09:21:18 +02:00
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);
}
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;
}
/****************************************************************************/
/* temperature */
#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];
2026-06-25 22:36:47 +02:00
char value[SMART_MAX];
char name[SMART_MAX];
char label[SMART_MAX];
2026-06-12 09:21:18 +02:00
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
/****************************************************************************/
/* app */
void app_init(void)
{
/* set LC_ALL=C to make smartctl ignoring the locale when printing info */
setenv("LC_ALL", "C", 1);
}
void app_done(void)
{
}
void app_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");
}
}
#endif