snapraid/cmdline/unixapp.c
Andrea Mazzoleni 4054832c7e
Some checks are pending
Build / build-mac (push) Waiting to run
Build / build-linux (push) Waiting to run
Build / build-arm32 (push) Waiting to run
Build / build-win (push) Waiting to run
Build / build-freebsd (push) Waiting to run
Build / build-musl (push) Waiting to run
CodeQL Advanced / Analyze (c-cpp) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
Add best-effort Linux disk instance mappings using boot_id and diskseq
2026-10-22 21:40:07 +02:00

5125 lines
126 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

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

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2013 Andrea Mazzoleni
#include "os/portable.h"
#ifndef __MINGW32__ /* only for Unix */
#include "app.h"
#include "support.h"
#include "raid/cpu.h"
/****************************************************************************/
/* global */
/**
* Exit codes.
*/
int exit_success = 0;
int exit_failure = 1;
int exit_sync_needed = 2;
int exit_degraded = 3;
/****************************************************************************/
/* signal */
volatile sig_atomic_t global_interrupt = 0;
int os_signal_interrupt(void)
{
return (int)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
#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) {
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) {
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) {
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.
*/
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];
size_t available = (size_t)ret - 4;
/* if incomplete */
if (page_len > available) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* if empty */
if (page_len == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* if too big to store the 0 termination */
if (4 + page_len + 1 > sizeof(buf)) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
char* attr = (char*)buf + 4;
attr[page_len] = 0;
strtrim(attr);
/* if empty */
if (!*attr) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
pathcpy(dst, dst_size, attr);
return 0;
}
#endif
/**
* sysread_vpd_pg83 — parse raw binary VPD page 0x83 (Device Identification)
* from a sysfs vpd_pg83 file and extract the first NAA (Network Address
* Authority) designator for the Logical Unit (ASSOCIATION == 0x00).
*
* The result is written into dst as a lowercase hex string,
* e.g.: "5000c500abcdef01" (8-byte / NAA type 1,2,3,5)
* "6000c500abcdef010000000000000000" (16-byte / NAA type 6)
*
* VPD page 0x83 wire format (SPC-4 §7.7.2)
*
* Page header — 4 bytes:
* [0] peripheral qualifier (7:5) | device type (4:0)
* [1] page code = 0x83
* [2] PAGE LENGTH MSB } 16-bit big-endian: number of bytes that follow
* [3] PAGE LENGTH LSB } the header (i.e. total descriptor bytes)
*
* Then a sequence of Designation Descriptors:
* [+0] PROTOCOL ID (7:4) | CODE SET (3:0)
* 0x1 = binary, 0x2 = ASCII, 0x3 = UTF-8
* [+1] PIV (7) | reserved (6) | ASSOCIATION (5:4) | DESIGNATOR TYPE (3:0)
* ASSOCIATION: 0x0 = logical unit ← we want only this
* 0x1 = target port
* 0x2 = target device
* DESIGNATOR TYPE: 0x3 = NAA ← we want only this
* [+2] reserved
* [+3] DESIGNATOR LENGTH = N (bytes that follow in this descriptor)
* [+4 … +4+N-1] DESIGNATOR (binary for NAA)
*
* NAA subtypes (top nibble of designator byte 0)
*
* 0x1 NAA IEEE Extended → 8 bytes
* 0x2 NAA Locally Assigned → 8 bytes
* 0x3 NAA IEEE Registered → 8 bytes (most common on SATA/SAS)
* 0x5 NAA IEEE Registered → 8 bytes
* 0x6 NAA IEEE Registered Ext. → 16 bytes
* others: reserved
*
* The DESIGNATOR LENGTH from the descriptor header must agree with the
* expected length for the NAA subtype. If they disagree, the descriptor
* is malformed and must be skipped.
*
* Returns 0 and fills dst on success, -1 if no valid NAA LU descriptor found.
*/
#if HAVE_LINUX_DEVICE
static int sysattr_vpd_pg83(const char* path, char* dst, size_t dst_size)
{
unsigned char buf[4096];
ssize_t ret = sysread(path, (char*)buf, sizeof(buf));
if (ret < 4) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* validate page code */
if (buf[1] != 0x83) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
size_t page_len = ((size_t)buf[2] << 8) | buf[3];
/*
* Clamp to what was actually read. 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.
*/
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.
* 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;
}
/**
* Parse Linux cpulist format (e.g. "0-3,5,8-11") into an array of CPU IDs.
*
* Returns the total number of CPUs in the list. Stores up to max_cpus entries.
*/
static unsigned parse_cpulist(const char* str, unsigned* cpus, unsigned max_cpus)
{
const char* p = str;
unsigned count = 0;
while (*p != 0) {
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
++p;
if (*p == 0)
break;
char* end;
unsigned long start = strtonl(p, &end, 10);
if (end == p)
break;
unsigned long stop = start;
p = end;
if (*p == '-') {
++p;
stop = strtonl(p, &end, 10);
if (end == p)
break;
p = end;
}
if (stop < start || stop >= CPU_SETSIZE)
break;
for (unsigned long i = start;; ++i) {
if (cpus != 0 && count < max_cpus)
cpus[count] = (unsigned)i;
++count;
if (i == stop)
break;
}
if (*p == ',')
++p;
}
return count;
}
/**
* Read online CPU IDs from /sys/devices/system/cpu/online.
*
* Falls back to consecutive 0..N-1 IDs if sysfs is unavailable.
* Returns the total number of online CPUs. Stores up to max_cpus entries in cpus.
*/
static unsigned read_online_cpus(unsigned* cpus, unsigned max_cpus)
{
char buf[1024];
if (sysattr("/sys/devices/system/cpu/online", buf, sizeof(buf)) == 0) {
unsigned count = parse_cpulist(buf, cpus, max_cpus);
if (count > 0)
return count;
}
/* fallback: consecutive IDs from 0 */
long n = sysconf(_SC_NPROCESSORS_ONLN);
if (n <= 0)
n = sysconf(_SC_NPROCESSORS_CONF);
if (n <= 0)
n = 1;
unsigned count = (unsigned)n;
unsigned store = count < max_cpus ? count : max_cpus;
for (unsigned i = 0; i < store; ++i) {
if (cpus != 0)
cpus[i] = i;
}
return count;
}
/**
* Returns 1 if the specified cpu is the lowest-numbered logical CPU in its
* physical core's SMT sibling set.
*/
static int cpu_is_primary_sibling(unsigned i)
{
char path[256];
char buf[256];
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%u/topology/thread_siblings_list", i);
if (sysattr(path, buf, sizeof(buf)) != 0)
return 1; /* assume primary */
unsigned siblings[16];
unsigned count = parse_cpulist(buf, siblings, sizeof(siblings) / sizeof(siblings[0]));
if (count == 0)
return 1;
return i == siblings[0];
}
/**
* 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];
unsigned cpus[CPU_SETSIZE];
unsigned total_cpus = read_online_cpus(cpus, CPU_SETSIZE);
unsigned num_cpus = total_cpus < CPU_SETSIZE ? total_cpus : CPU_SETSIZE;
if (num_cpus == 0)
return -1;
/* ACPI CPPC: highest_perf with nominal_perf tiebreaker */
long long max_highest = -1;
long long max_nominal = -1;
int best_cppc = -1;
for (unsigned k = 0; k < num_cpus; ++k) {
unsigned i = cpus[k];
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%u/acpi_cppc/highest_perf", i);
long long hp = syslong(path);
if (hp < 0)
continue;
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%u/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 (unsigned k = 0; k < num_cpus; ++k) {
unsigned i = cpus[k];
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%u/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 (unsigned k = 0; k < num_cpus; ++k) {
unsigned i = cpus[k];
if (global_max_freq != -1) {
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%u/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 */
}
int degraded = 0;
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;
/* LCOV_EXCL_STOP */
}
/* missing devices in degraded btrfs have empty path */
if (dev_info.path[0] == 0) {
degraded = 1;
continue;
}
/* get major:minor, use stat on the path returned */
struct stat st;
if (stat((char*)dev_info.path, &st) != 0) {
/* LCOV_EXCL_START */
if (errno == ENOENT) {
degraded = 1;
continue;
}
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;
/* 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;
return degraded ? 1 : 0;
bail:
tommy_list_foreach(devlist, free);
tommy_list_init(devlist);
return -1;
}
/**
* 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;
}
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 */
}
/*
* Example of /proc/self/mountinfo entry for bcachefs:
*
* 36 35 0:34 / /mnt/bcachefs rw,noatime shared:1 - bcachefs /dev/sda:/dev/sdb rw,degraded,metadata_replicas=2
*
* Before " - " (id_map):
* id_map[0] = mount ID (36)
* id_map[1] = parent ID (35)
* id_map[2] = major:minor (0:34)
* id_map[3] = root (/)
* id_map[4] = mount point (/mnt/bcachefs)
* id_map[5] = mount options (rw,noatime)
*
* After " - " (fs_map):
* fs_map[0] = filesystem type (bcachefs)
* fs_map[1] = mount source (/dev/sda:/dev/sdb, member devices separated by ':')
* fs_map[2] = super options (rw,degraded,metadata_replicas=2)
*/
size_t best_len = 0;
int degraded = 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\r\n", 0, 1);
fs_mac = strsplit(fs_map, 8, separator, " \t\r\n", 0, 1);
/* 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]);
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]);
pathcpy(device_list, sizeof(device_list), fs_map[1]);
/* check if mounted degraded */
if ((fs_mac >= 3 && strstr(fs_map[2], "degraded") != 0)
|| (id_mac >= 6 && strstr(id_map[5], "degraded") != 0)) {
degraded = 1;
} else {
degraded = 0;
}
}
}
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, ":", 0, 0);
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 */
if (errno == ENOENT) {
degraded = 1;
continue;
}
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 */
}
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;
return degraded ? 1 : 0;
bail:
tommy_list_foreach(devlist, free);
tommy_list_init(devlist);
return -1;
}
static int extract_zfs_mount(const char* resolved, char* dataset, size_t dataset_size, char* mount_point, size_t mount_point_size)
{
struct stat st;
if (stat(resolved, &st) != 0)
return -1;
FILE* fd = fopen("/proc/self/mountinfo", "r");
if (!fd)
return -1;
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;
unsigned mount_major;
unsigned mount_minor;
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\r\n", 0, 1);
fs_mac = strsplit(fs_map, 8, separator, " \t\r\n", 0, 1);
if (id_mac < 5 || fs_mac < 2)
continue;
/* mount point must contain the directory */
unescape_mount(id_map[4]);
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;
/* match the device backing the path, not a hidden parent mount */
if (strcmp(fs_map[0], "zfs") != 0
|| sscanf(id_map[2], "%u:%u", &mount_major, &mount_minor) != 2
|| mount_major != major(st.st_dev) || mount_minor != minor(st.st_dev))
continue;
/* use the visible mount, including a legacy mount under a parent dataset */
if (mp_len > best_len) {
best_len = mp_len;
unescape_mount(fs_map[1]);
pathcpy(dataset, dataset_size, fs_map[1]);
pathcpy(mount_point, mount_point_size, mp);
}
}
int error = ferror(fd);
if (fclose(fd) != 0 || error)
return -1;
return best_len == 0 ? -1 : 0;
}
static int extract_zfs(const char* dir, char* dataset, size_t dataset_size, char* uuid, size_t uuid_size, char* mount_point, size_t mount_point_size)
{
char resolved[PATH_MAX];
char mounted_dataset[PATH_MAX];
char mounted_point[PATH_MAX];
if (realpath(dir, resolved) == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
if (extract_zfs_mount(resolved, mounted_dataset, sizeof(mounted_dataset), mounted_point, sizeof(mounted_point)) != 0)
return -1;
if (uuid) {
const char* zfs = find_zfs();
if (!zfs)
return -1;
const char* argv[] = {
zfs,
"list",
"-H",
"-o",
"guid",
"-t",
"filesystem",
mounted_dataset,
0
};
OS_FILE* fp = os_popen(argv);
if (!fp)
return -1;
int found = 0;
while (1) {
char buf[64];
char* s = os_fgets(buf, sizeof(buf), fp);
if (s == 0)
break;
buf[strcspn(buf, "\r\n")] = 0;
if (!found && buf[0] != 0) {
pathcpy(uuid, uuid_size, buf);
found = 1;
}
}
/* read to EOF so closing the pipe does not interrupt zfs list */
int ret = os_pclose(fp);
if (ret < 0) {
/* LCOV_EXCL_START */
log_error(errno, "Failed to close ZFS dataset list for '%s'. %s.\n", dir, strerror(errno));
return -1;
/* LCOV_EXCL_STOP */
}
if (ret != 0) {
/* LCOV_EXCL_START */
if (WIFEXITED(ret))
log_error(ESOFT, "Failed to list ZFS dataset '%s' (exit status %d).\n", mounted_dataset, WEXITSTATUS(ret));
else if (WIFSIGNALED(ret))
log_error(ESOFT, "Failed to list ZFS dataset '%s' (signal %d).\n", mounted_dataset, WTERMSIG(ret));
else
log_error(ESOFT, "Failed to list ZFS dataset '%s' (unexpected wait status %d).\n", mounted_dataset, ret);
return -1;
/* LCOV_EXCL_STOP */
}
if (!found)
return -1;
}
if (dataset)
pathcpy(dataset, dataset_size, mounted_dataset);
if (mount_point)
pathcpy(mount_point, mount_point_size, mounted_point);
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);
if (!fp) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/*
* Example of 'zpool status -P' output:
*
* pool: tank
* state: DEGRADED
* config:
*
* NAME STATE READ WRITE CKSUM
* tank DEGRADED 0 0 0
* raidz1-0 DEGRADED 0 0 0
* /dev/sda1 ONLINE 0 0 0
* 15123456789012345678 UNAVAIL 0 0 0 was /dev/sdb1
*
* The state is in map[1] ("ONLINE", "DEGRADED", ...) when map[0] is
* "state:" (header) or the vdev/device name in the config table.
* Only block devices starting with "/dev/" in map[0] are added to devlist.
*/
int degraded = 0;
while (1) {
char buf[PATH_MAX * 2 + 64];
char* s = os_fgets(buf, sizeof(buf), fp);
if (s == 0)
break;
char* map[8];
unsigned mac = strsplit(map, 8, s, " \t\r\n", 0, 1);
if (mac < 2)
continue;
/* check pool state */
if (strcmp(map[0], "state:") == 0) {
if (strcmp(map[1], "ONLINE") != 0)
degraded = 1;
continue;
}
/* check device or vdev state */
if (strcmp(map[1], "DEGRADED") == 0
|| strcmp(map[1], "FAULTED") == 0
|| strcmp(map[1], "UNAVAIL") == 0
|| strcmp(map[1], "OFFLINE") == 0
|| strcmp(map[1], "REMOVED") == 0) {
degraded = 1;
}
/* only block devices under /dev/ are processed */
if (strncmp(map[0], "/dev/", 5) != 0)
continue;
const char* file = map[0];
struct stat st;
if (stat(file, &st) != 0) {
/* LCOV_EXCL_START */
if (errno == ENOENT) {
degraded = 1;
continue;
}
log_error(errno, "Failed stat %s. %s.", file, strerror(errno));
os_pclose(fp);
goto bail;
/* LCOV_EXCL_STOP */
}
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 */
}
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));
}
int ret = os_pclose(fp);
if (ret < 0) {
/* LCOV_EXCL_START */
log_error(errno, "Failed to close ZFS pool status for '%s'. %s.\n", pool, strerror(errno));
goto bail;
/* LCOV_EXCL_STOP */
}
if (ret != 0) {
/* LCOV_EXCL_START */
if (WIFEXITED(ret))
log_error(ESOFT, "Failed to get ZFS pool status for '%s' (exit status %d).\n", pool, WEXITSTATUS(ret));
else if (WIFSIGNALED(ret))
log_error(ESOFT, "Failed to get ZFS pool status for '%s' (signal %d).\n", pool, WTERMSIG(ret));
else
log_error(ESOFT, "Failed to get ZFS pool status for '%s' (unexpected wait status %d).\n", pool, ret);
goto bail;
/* LCOV_EXCL_STOP */
}
if (tommy_list_empty(devlist))
goto bail;
return degraded ? 1 : 0;
bail:
tommy_list_foreach(devlist, free);
tommy_list_init(devlist);
return -1;
}
/**
* 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) {
ret = devdereference_btrfs(device, dir, fd, devlist);
} else if (statfs_type(&sfs) == BCACHEFS_SUPER_MAGIC) {
ret = devdereference_bcachefs(device, dir, devlist);
} else if (statfs_type(&sfs) == ZFS_SUPER_MAGIC) {
ret = devdereference_zfs(device, dir, devlist);
} else {
/* insert the device itself in the list */
struct dev_struct* dev = malloc_nofail(sizeof(struct dev_struct));
dev->device = device;
tommy_list_insert_tail(devlist, &dev->node, dev);
ret = 0;
}
close(fd);
return ret;
}
#endif
/**
* Read a file extracting the specified tag TAG=VALUE format.
* Return !=0 on error.
*/
#if HAVE_LINUX_DEVICE
static int tagread(const char* path, const char* tag, char* value, size_t value_size)
{
ssize_t ret;
char buf[512];
size_t tag_len;
char* i;
char* e;
ret = sysread(path, buf, sizeof(buf));
if (ret < 0) {
/* LCOV_EXCL_START */
log_error(errno, "Failed to read '%s'.\n", path);
return -1;
/* LCOV_EXCL_STOP */
}
if ((size_t)ret + 1 > sizeof(buf)) {
/* LCOV_EXCL_START */
log_error(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_error(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_error(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 */
/**
* 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.
*/
#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
/**
* 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
#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) {
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) {
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)
{
struct statfs sfs;
/* only report ZFS lookup failures after identifying the filesystem */
if (statfs(dir, &sfs) != 0 || statfs_type(&sfs) != ZFS_SUPER_MAGIC)
return -1;
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
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)) {
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)) {
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) {
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);
}
#else
(void)fslabel_size;
#endif
}
return 0;
}
/****************************************************************************/
/* identity */
#if HAVE_STATX
#ifndef STATX_MNT_ID
#define STATX_MNT_ID 0x00001000U
#endif
#ifndef STATX_SUBVOL
#define STATX_SUBVOL 0x00008000U
#endif
struct snapraid_statx_timestamp_abi {
int64_t tv_sec;
uint32_t tv_nsec;
int32_t reserved;
};
struct snapraid_statx_abi {
/* 0x00 */
uint32_t stx_mask;
uint32_t stx_blksize;
uint64_t stx_attributes;
/* 0x10 */
uint32_t stx_nlink;
uint32_t stx_uid;
uint32_t stx_gid;
uint16_t stx_mode;
uint16_t spare0;
/* 0x20 */
uint64_t stx_ino;
uint64_t stx_size;
uint64_t stx_blocks;
uint64_t stx_attributes_mask;
/* 0x40 */
struct snapraid_statx_timestamp_abi stx_atime;
struct snapraid_statx_timestamp_abi stx_btime;
struct snapraid_statx_timestamp_abi stx_ctime;
struct snapraid_statx_timestamp_abi stx_mtime;
/* 0x80 */
uint32_t stx_rdev_major;
uint32_t stx_rdev_minor;
uint32_t stx_dev_major;
uint32_t stx_dev_minor;
/* 0x90 */
uint64_t stx_mnt_id;
uint32_t stx_dio_mem_align;
uint32_t stx_dio_offset_align;
/* 0xa0 */
uint64_t stx_subvol;
/* ABI continues to 0x100. */
unsigned char tail[0x100 - 0xa8];
};
STATIC_ASSERT(statx_abi_size, sizeof(struct snapraid_statx_abi) == 0x100);
STATIC_ASSERT(statx_abi_mnt_id, offsetof(struct snapraid_statx_abi, stx_mnt_id) == 0x90);
STATIC_ASSERT(statx_abi_subvol, offsetof(struct snapraid_statx_abi, stx_subvol) == 0xa0);
#endif
int fsidentity(const char* path, const struct stat* st, struct fsidentity* identity)
{
(void)path;
identity->device = st->st_dev;
identity->mnt_id = 0;
identity->subvol = 0;
#if HAVE_STATX
struct statx stx;
/* query mount ID and subvolume ID without following symlinks */
if (statx(AT_FDCWD, path, AT_SYMLINK_NOFOLLOW, STATX_MNT_ID | STATX_SUBVOL, &stx) != 0) {
/*
* Fall back if statx is unsupported by the kernel (ENOSYS) or blocked
* by a seccomp/security sandbox filter (EPERM/EACCES in older Docker/OCI).
*/
if (errno == ENOSYS || errno == EPERM || errno == EACCES)
return 0;
return -1;
}
/* copy to fixed ABI representation to support libc headers lacking named fields */
struct snapraid_statx_abi abi;
memcpy(&abi, &stx, sizeof(abi));
if ((abi.stx_mask & STATX_MNT_ID) != 0)
identity->mnt_id = abi.stx_mnt_id;
if ((abi.stx_mask & STATX_SUBVOL) != 0)
identity->subvol = abi.stx_subvol;
#endif
return 0;
}
/****************************************************************************/
/* snapshot */
#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];
char current_path[PATH_MAX];
if (realpath(path, resolved) == 0)
return -1;
pathslash(resolved, sizeof(resolved));
pathcpy(current_path, sizeof(current_path), resolved);
/* walk up the directory tree to find the requested subvolume root inode */
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);
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 */
}
}
}
static int fssnapshot_bcachefs(const char* path, struct fssnapshot_struct* fss)
{
char resolved[PATH_MAX];
char current_path[PATH_MAX];
struct stat st;
struct fsidentity identity;
if (realpath(path, resolved) == 0)
return -1;
pathslash(resolved, sizeof(resolved));
pathcpy(current_path, sizeof(current_path), resolved);
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 (fsidentity(current_path, &st, &identity) != 0) {
/* LCOV_EXCL_START */
log_error(errno, "Error getting filesystem identity for '%s'. %s.\n", current_path, strerror(errno));
return -1;
/* LCOV_EXCL_STOP */
}
if (identity.subvol == 0) {
/* LCOV_EXCL_START */
errno = ENOTSUP;
log_error(errno, "Bcachefs subvolume identity unavailable for '%s'. %s.\n", current_path, strerror(errno));
return -1;
/* LCOV_EXCL_STOP */
}
while (1) {
char parent_path[PATH_MAX];
struct stat parent_st;
struct fsidentity parent_identity;
pathcpy(parent_path, sizeof(parent_path), current_path);
pathup(parent_path);
if (parent_path[0] == 0)
break;
if (stat(parent_path, &parent_st) != 0) {
/* LCOV_EXCL_START */
log_error(errno, "Error stating '%s'. %s.\n", parent_path, strerror(errno));
return -1;
/* LCOV_EXCL_STOP */
}
if (fsidentity(parent_path, &parent_st, &parent_identity) != 0) {
/* LCOV_EXCL_START */
log_error(errno, "Error getting filesystem identity for '%s'. %s.\n", parent_path, strerror(errno));
return -1;
/* LCOV_EXCL_STOP */
}
/* a bind mount may share the subvolume ID with its parent */
if (parent_identity.subvol != identity.subvol
|| parent_identity.mnt_id != identity.mnt_id
|| parent_identity.device != identity.device)
break;
pathcpy(current_path, sizeof(current_path), parent_path);
st = parent_st;
}
return fssnapshot_inode(path, BCACHEFS_SUPER_MAGIC, st.st_ino, fss);
}
#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;
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);
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) {
log_error(errno, "ZFS snapshot directory not accessible: '%s'. %s.\n", fss->snapshot_dir, strerror(errno));
return -1;
}
fss->magic = magic;
return 0;
}
#endif
static void fssnapshot_retiring_name(const char* name, char* retiring_name, size_t retiring_name_size)
{
pathcpy(retiring_name, retiring_name_size, name);
pathcat(retiring_name, retiring_name_size, SNAPSHOT_RETIRING_SUFFIX);
}
static int fssnapshot_exists(struct fssnapshot_struct* fss, const char* name)
{
struct stat st;
if (fssnapshot_stat(fss, name, &st) == 0)
return 1;
if (errno == ENOENT)
return 0;
return -1;
}
/**
* Recover an interrupted snapshot replacement.
*
* The .retiring snapshot is the previous destination and must remain available until the new destination is published. If the destination exists, publication completed and the retiring snapshot is deleted. Otherwise the previous destination is restored.
*
* Any failure is propagated to fssnapshot_mount(). Callers may then fall back to the live filesystem with an explicit warning when snapshots are used only as a read source, while snapshot-modifying operations fail.
*/
static int fssnapshot_recover_replace(struct fssnapshot_struct* fss, const char* name)
{
char retiring_name[PATH_MAX];
fssnapshot_retiring_name(name, retiring_name, sizeof(retiring_name));
int retiring = fssnapshot_exists(fss, retiring_name);
if (retiring <= 0)
return retiring;
int current = fssnapshot_exists(fss, name);
if (current < 0)
return -1;
if (current != 0) {
/* the new destination was published, so the previous one can now be retired */
if (fssnapshot_delete(fss, retiring_name) != 0) {
log_error(errno, "Failed to delete retiring snapshot '%s'. %s.\n", retiring_name, strerror(errno));
return -1;
}
return 0;
}
/* the new destination was not published, so restore the previous one */
if (fssnapshot_rename(fss, retiring_name, name) != 0) {
log_error(errno, "Failed to restore retiring snapshot '%s' as '%s'. %s.\n", retiring_name, name, strerror(errno));
return -1;
}
return 0;
}
int fssnapshot_mount(const char* path, struct fssnapshot_struct* fss)
{
#if HAVE_LINUX_DEVICE
struct statfs sfs;
int ret;
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 */
}
assert(fss != 0); /* silence the clang static analyzer (it's never called with a NULL fss) */
if (statfs_type(&sfs) == BTRFS_SUPER_MAGIC) {
/* btrfs reserved inode 256 for subvolume roots */
ret = fssnapshot_inode(path, BTRFS_SUPER_MAGIC, 256, fss);
} else if (statfs_type(&sfs) == BCACHEFS_SUPER_MAGIC) {
ret = fssnapshot_bcachefs(path, fss);
} else if (statfs_type(&sfs) == ZFS_SUPER_MAGIC) {
ret = fssnapshot_zfs(path, ZFS_SUPER_MAGIC, fss);
} else {
return 1;
}
if (ret != 0)
return ret;
/* never expose an interrupted replacement to the state layer */
if (fssnapshot_recover_replace(fss, SNAPSHOT_PENDING) != 0)
return -1;
if (fssnapshot_recover_replace(fss, SNAPSHOT_STABLE) != 0)
return -1;
return 0;
#else
(void)path;
(void)fss;
return 1;
#endif
}
#if HAVE_LINUX_DEVICE
static int fssnapshot_stat_fs(const struct fssnapshot_struct* fss, const char* name, struct stat* st)
{
char target_path[PATH_MAX];
pathcpy(target_path, sizeof(target_path), fss->snapshot_dir);
pathcat(target_path, sizeof(target_path), name);
if (stat(target_path, st) != 0)
return -1;
/* it must be a directory */
if (!S_ISDIR(st->st_mode)) {
errno = ENOTDIR;
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-"
#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];
char target_path[PATH_MAX];
pathcpy(native_name, sizeof(native_name), ZFS_SNAPSHOT_PREFIX);
pathcat(native_name, sizeof(native_name), name);
pathcpy(target_path, sizeof(target_path), fss->snapshot_dir);
pathcat(target_path, sizeof(target_path), native_name);
/*
* Ensure to get into into the snapshot
*
* This is required by ZFS, otherwise we get a different st_dev
*/
pathcat(target_path, sizeof(target_path), "/.");
if (stat(target_path, st) != 0)
return -1;
/* it must be a directory */
if (!S_ISDIR(st->st_mode)) {
errno = ENOTDIR;
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) {
errno = 0;
struct dirent* entry = readdir(d);
if (!entry) {
int error = errno;
closedir(d);
if (error != 0) {
errno = error;
return -1;
}
errno = ENOENT;
return -1;
}
if (strcmp(entry->d_name, native_name) == 0) {
closedir(d);
return 0;
}
}
}
#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);
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);
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)
{
int dir_fd;
dir_fd = open(fss->snapshot_dir, O_RDONLY | O_DIRECTORY);
if (dir_fd < 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
if (renameat(dir_fd, old_name, dir_fd, new_name) < 0) {
/* LCOV_EXCL_START */
close(dir_fd);
return -1;
/* LCOV_EXCL_STOP */
}
/*
* Make the snapshot rename durable before returning.
*
* The caller may start modifying parity after this function succeeds,
* so after a crash the renamed snapshot must not revert to the previous
* directory state while the parity updates remain durable.
*/
if (fsync(dir_fd) != 0) {
/* LCOV_EXCL_START */
close(dir_fd);
return -1;
/* LCOV_EXCL_STOP */
}
if (close(dir_fd) != 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);
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);
} 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_replace(struct fssnapshot_struct* fss, const char* old_name, const char* new_name)
{
char retiring_name[PATH_MAX];
fssnapshot_retiring_name(new_name, retiring_name, sizeof(retiring_name));
/* resolve a replacement interrupted by an earlier invocation before starting another one */
if (fssnapshot_recover_replace(fss, new_name) != 0)
return -1;
int current = fssnapshot_exists(fss, new_name);
if (current < 0)
return -1;
if (current == 0)
return fssnapshot_rename(fss, old_name, new_name);
/* keep the previous destination recoverable until the source is published under the final name */
if (fssnapshot_rename(fss, new_name, retiring_name) != 0)
return -1;
/* on failure leave .retiring in place; fssnapshot_mount() will restore or complete the transition */
if (fssnapshot_rename(fss, old_name, new_name) != 0)
return -1;
/* the new destination is now published, so the previous destination is no longer required */
return fssnapshot_delete(fss, retiring_name);
}
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;
}
void fssnapshot_unmount(const struct fssnapshot_struct* fss)
{
(void)fss;
}
/****************************************************************************/
/* 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_error(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_error(errno, "Failed to read '%s'.\n", path);
return 0;
/* LCOV_EXCL_STOP */
}
if (len == sizeof(buf)) {
/* LCOV_EXCL_START */
close(f);
log_error(EEXTERNAL, "Too long read '%s'.\n", path);
return 0;
/* LCOV_EXCL_STOP */
}
ret = close(f);
if (ret != 0) {
/* LCOV_EXCL_START */
log_error(errno, "Failed to close '%s'.\n", path);
return 0;
/* LCOV_EXCL_STOP */
}
buf[len] = 0;
ma = strton(buf, &e, 10);
if (*e != ':') {
/* LCOV_EXCL_START */
log_error(EEXTERNAL, "Invalid format in '%s' for '%s'.\n", path, buf);
return 0;
/* LCOV_EXCL_STOP */
}
mi = strton(e + 1, &e, 10);
if (*e != 0 && !isspace((unsigned char)*e)) {
/* LCOV_EXCL_START */
log_error(EEXTERNAL, "Invalid format in '%s' for '%s'.\n", path, buf);
return 0;
/* LCOV_EXCL_STOP */
}
return makedev(ma, mi);
}
#endif
/**
* Check if an MD RAID array is running in degraded mode by inspecting the
* sysfs degraded attribute. Returns 1 if degraded, 0 if healthy or not an MD array.
*/
#if HAVE_LINUX_DEVICE
static int devtree_md_degraded(const char* path)
{
char buf[32];
ssize_t len = sysread(path, buf, sizeof(buf) - 1);
if (len <= 0)
return 0;
buf[len] = 0;
int val = strtoi(buf, 0, 10);
if (val > 0)
return 1;
return 0;
}
#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, int* degraded)
{
char path[PATH_MAX];
DIR* d;
int slaves = 0;
int failed = 0;
/* check if this device or parent partition is a degraded MD array */
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/md/degraded", major(device), minor(device));
if (devtree_md_degraded(path))
*degraded = 1;
pathprint(path, sizeof(path), "/sys/dev/block/%u:%u/../md/degraded", major(device), minor(device));
if (devtree_md_degraded(path))
*degraded = 1;
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;
++slaves;
/* 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 */
failed = 1;
continue;
/* LCOV_EXCL_STOP */
}
if (devtree(parent, subdev, list, degraded) != 0) {
/* LCOV_EXCL_START */
failed = 1;
continue;
/* LCOV_EXCL_STOP */
}
}
}
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_error(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);
memcpy(devinfo->smartignore, parent->smartignore, sizeof(devinfo->smartignore));
pathcpy(devinfo->file, sizeof(devinfo->file), path);
devinfo->parent = parent;
devinfo->is_array = parent->is_array;
/* insert in the list */
tommy_list_insert_tail(list, &devinfo->node, devinfo);
}
/* retain resolved siblings even when another branch failed */
return failed ? -1 : 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_error(errno, "Failed to read '%s'.\n", path);
return -1;
/* LCOV_EXCL_STOP */
}
if ((size_t)ret + 1 > sizeof(buf)) {
/* LCOV_EXCL_START */
log_error(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 = strtonll(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
/**
* 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)
{
char file[PATH_MAX];
OS_FILE* f;
int ret;
const char* x;
const char* info_opts = smartctl_info[0] ? smartctl_info : "-a";
x = find_smartctl();
if (!x) {
/* LCOV_EXCL_START */
log_error(EEXTERNAL, "Cannot find smartctl.\n");
return -1;
/* LCOV_EXCL_STOP */
}
if (devresolve(device, file, sizeof(file)) != 0) {
/* LCOV_EXCL_START */
log_error(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[SMARTCTL_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_error(EEXTERNAL, "Too many smartctl arguments.\n");
return -1;
/* LCOV_EXCL_STOP */
}
char extra_args[PATH_MAX];
char extra_split[PATH_MAX];
if (smartctl[0]) {
pathprint(extra_args, sizeof(extra_args), smartctl, file);
pathcpy(extra_split, sizeof(extra_split), extra_args);
argc += argsplit(argv + argc, ARGS_MAX - argc, extra_split);
if (argc >= ARGS_MAX) {
/* LCOV_EXCL_START */
log_error(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);
} else {
if (argc >= ARGS_MAX - 1) {
/* LCOV_EXCL_START */
log_error(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);
}
argv[argc++] = 0;
f = os_popen(argv);
if (!f) {
/* LCOV_EXCL_START */
log_tag("device:%s:%s:spawn\n", file, name);
if (smartctl[0])
log_error(EEXTERNAL, "Failed to run '%s %s %s'.\n", x, info_opts, extra_args);
else
log_error(EEXTERNAL, "Failed to run '%s %s %s'.\n", x, info_opts, file);
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_error(EEXTERNAL, "Failed to run '%s %s %s' (not exited).\n", x, info_opts, extra_args);
else
log_error(EEXTERNAL, "Failed to run '%s %s %s' (not exited).\n", x, info_opts, file);
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;
}
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;
}
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)
{
char file[PATH_MAX];
OS_FILE* f;
int ret;
const char* x;
const char* info_opts = smartctl_info[0] ? smartctl_info : "-a";
x = find_smartctl();
if (!x) {
/* LCOV_EXCL_START */
log_error(EEXTERNAL, "Cannot find smartctl.\n");
return -1;
/* LCOV_EXCL_STOP */
}
if (devresolve(device, file, sizeof(file)) != 0) {
/* LCOV_EXCL_START */
log_error(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[SMARTCTL_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_error(EEXTERNAL, "Too many smartctl arguments.\n");
return -1;
/* LCOV_EXCL_STOP */
}
char extra_args[PATH_MAX];
char extra_split[PATH_MAX];
if (smartctl[0]) {
pathprint(extra_args, sizeof(extra_args), smartctl, file);
pathcpy(extra_split, sizeof(extra_split), extra_args);
argc += argsplit(argv + argc, ARGS_MAX - argc, extra_split);
if (argc >= ARGS_MAX) {
/* LCOV_EXCL_START */
log_error(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);
} else {
if (argc >= ARGS_MAX - 1) {
/* LCOV_EXCL_START */
log_error(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);
}
argv[argc++] = 0;
f = os_popen(argv);
if (!f) {
/* LCOV_EXCL_START */
log_tag("device:%s:%s:spawn\n", file, name);
if (smartctl[0])
log_error(EEXTERNAL, "Failed to run '%s -n standby,3 %s %s'.\n", x, info_opts, extra_args);
else
log_error(EEXTERNAL, "Failed to run '%s -n standby,3 %s %s'.\n", x, info_opts, file);
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_error(EEXTERNAL, "Failed to run '%s -n standby,3 %s %s' (not exited).\n", x, info_opts, extra_args);
else
log_error(EEXTERNAL, "Failed to run '%s -n standby,3 %s %s' (not exited).\n", x, info_opts, file);
return -1;
/* LCOV_EXCL_STOP */
}
if (WEXITSTATUS(ret) == 3) {
log_tag("attr:%s:%s:power:standby\n", file, name);
*power = POWER_STANDBY;
} else {
/* store the smartctl return value */
if (smart)
smart[SMART_FLAGS].raw = WEXITSTATUS(ret);
if (WEXITSTATUS(ret) & SMARTCTL_FLAG_OPEN) {
log_tag("attr:%s:%s:power:unknown\n", file, name);
*power = POWER_UNKNOWN;
} else {
log_tag("attr:%s:%s:power:active\n", file, name);
*power = POWER_ACTIVE;
}
}
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 extra_split[PATH_MAX];
char file[PATH_MAX];
OS_FILE* f;
int ret;
const char* x;
x = find_smartctl();
if (!x) {
/* LCOV_EXCL_START */
log_error(EEXTERNAL, "Cannot find smartctl.\n");
return -1;
/* LCOV_EXCL_STOP */
}
if (devresolve(device, file, sizeof(file)) != 0) {
/* LCOV_EXCL_START */
log_error(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";
if (smartctl[0]) {
pathprint(extra_args, sizeof(extra_args), smartctl, file);
pathcpy(extra_split, sizeof(extra_split), extra_args);
argc += argsplit(argv + argc, ARGS_MAX - argc, extra_split);
if (argc >= ARGS_MAX) {
/* LCOV_EXCL_START */
log_error(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);
} else {
if (argc >= ARGS_MAX - 1) {
/* LCOV_EXCL_START */
log_error(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);
}
argv[argc++] = 0;
f = os_popen(argv);
if (!f) {
/* LCOV_EXCL_START */
log_tag("device:%s:%s:spawn\n", file, name);
if (smartctl[0])
log_error(EEXTERNAL, "Failed to run '%s -s standby,now %s'.\n", x, extra_args);
else
log_error(EEXTERNAL, "Failed to run '%s -s standby,now %s'.\n", x, file);
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_error(EEXTERNAL, "Failed to run '%s -s standby,now %s' (not exited).\n", x, extra_args);
else
log_error(EEXTERNAL, "Failed to run '%s -s standby,now %s' (not exited).\n", x, file);
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_error(EEXTERNAL, "Failed to run '%s -s standby,now %s' with return code %xh.\n", x, extra_args, WEXITSTATUS(ret));
else
log_error(EEXTERNAL, "Failed to run '%s -s standby,now %s' with return code %xh.\n", x, file, WEXITSTATUS(ret));
return -1;
/* LCOV_EXCL_STOP */
}
log_tag("attr:%s:%s:power:down\n", file, name);
return 0;
}
#endif
/**
* Spin down a specific device if it's up.
*/
#if HAVE_LINUX_DEVICE
static int devdownifup(dev_t device, const char* name, const char* smartctl, const char* smartctl_info, int* power)
{
*power = POWER_UNKNOWN;
if (devprobe(device, name, smartctl, smartctl_info, power, 0, 0, 0, 0, 0, 0) != 0)
return -1;
/* a failed probe may hide an active disk, so skip only confirmed standby */
if (*power != POWER_STANDBY)
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
/**
* 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_error(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_error(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_error(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_error(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_error(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_error(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_error(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;
int ret;
/* only for members of the array and rotational devices */
if (devinfo->is_array && devpower(devinfo->device) != 0) {
uint64_t 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);
ret = devsmart(devinfo->device, devinfo->name, devinfo->smartctl, devinfo->smartctl_info, devinfo->smart, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf);
if (ret != 0) {
/* LCOV_EXCL_START */
return (void*)-1;
/* LCOV_EXCL_STOP */
}
} else {
/* just probe others */
ret = devprobe(devinfo->device, devinfo->name, devinfo->smartctl, devinfo->smartctl_info, &devinfo->power, devinfo->smart, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf);
if (ret != 0) {
/* LCOV_EXCL_START */
return (void*)-1;
/* LCOV_EXCL_STOP */
}
}
/*
* Retrieve some attributes directly from the system.
*
* smartctl intentionally skips queries on devices in standby mode
* to prevent accidentally spinning them up.
*/
devattr(devinfo->device, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf);
return 0;
#else
(void)arg;
return (void*)-1;
#endif
}
/**
* Thread for spinning down.
*/
static void* thread_spindown(void* arg)
{
#if HAVE_LINUX_DEVICE
devinfo_t* devinfo = arg;
/* only for members of the array and rotational devices */
if (devinfo->is_array && devpower(devinfo->device) != 0) {
uint64_t 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;
/* only for members of the array and rotational devices */
if (devinfo->is_array && devpower(devinfo->device) != 0) {
uint64_t start = os_tick_ms();
int power;
if (devdownifup(devinfo->device, devinfo->name, devinfo->smartctl, devinfo->smartctl_info, &power) != 0) {
/* LCOV_EXCL_START */
return (void*)-1;
/* LCOV_EXCL_STOP */
}
if (power != POWER_STANDBY)
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) {
/* 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) {
/* LCOV_EXCL_START */
return (void*)-1;
/* LCOV_EXCL_STOP */
}
/*
* Retrieve some attributes directly from the system.
*
* smartctl intentionally skips queries on devices in standby mode
* to prevent accidentally spinning them up.
*/
devattr(devinfo->device, devinfo->info, devinfo->serial, devinfo->family, devinfo->model, devinfo->interf);
return 0;
#else
(void)arg;
return (void*)-1;
#endif
}
static int device_thread(tommy_list* list, void* (*func)(void* arg))
{
int fail = 0;
tommy_node* i;
#if HAVE_THREAD
/* start all threads */
for (i = tommy_list_head(list); i != 0; i = i->next) {
devinfo_t* devinfo = i->data;
thread_create(&devinfo->thread, func, devinfo);
}
/* join all threads */
for (i = tommy_list_head(list); i != 0; i = i->next) {
devinfo_t* devinfo = i->data;
void* retval;
thread_join(devinfo->thread, &retval);
if (retval != 0)
++fail;
}
#else
for (i = tommy_list_head(list); i != 0; i = i->next) {
devinfo_t* devinfo = i->data;
if (func(devinfo) != 0)
++fail;
}
#endif
if (fail != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
int devsync(tommy_list* high)
{
#if HAVE_SYNCFS
tommy_node* i;
int ret = 0;
/* flush every filesystem even if one fails before a forced thermal spindown */
for (i = tommy_list_head(high); i != 0; i = i->next) {
devinfo_t* devinfo = i->data;
int f;
/* only for members of the array */
if (!devinfo->is_array)
continue;
f = open(devinfo->mount, O_RDONLY);
if (f < 0) {
log_error(errno, "Failed to open filesystem at '%s'. %s.\n", devinfo->mount, strerror(errno));
ret = -1;
continue;
}
if (syncfs(f) != 0) {
int err = errno;
log_error(err, "Failed to sync filesystem at '%s'. %s.\n", devinfo->mount, strerror(err));
ret = -1;
}
if (close(f) != 0) {
log_error(errno, "Failed to close filesystem at '%s'. %s.\n", devinfo->mount, strerror(errno));
ret = -1;
}
}
return ret;
#else
(void)high;
return 0;
#endif
}
int devquery(tommy_list* high, tommy_list* low)
{
int degraded = 0;
int failed = 0;
#if HAVE_LINUX_DEVICE
tommy_node* i;
struct stat st;
/* sysfs interface is required */
if (stat("/sys/dev/block", &st) != 0) {
/* LCOV_EXCL_START */
log_error(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 */
int ret = devdereference(device, devinfo->mount, &devlist);
if (ret < 0) {
/* LCOV_EXCL_START */
log_error(EEXTERNAL, "Failed to dereference device '%u:%u' at '%s'.\n", major(device), minor(device), devinfo->mount);
failed = 1;
continue;
/* LCOV_EXCL_STOP */
}
int disk_degraded = ret > 0;
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_error(EEXTERNAL, "Failed to resolve device '%u:%u'.\n", major(dev->device), minor(dev->device));
failed = 1;
continue;
/* 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, &disk_degraded) != 0) {
/* LCOV_EXCL_START */
log_error(EEXTERNAL, "Failed to expand device '%u:%u'.\n", major(dev->device), minor(dev->device));
failed = 1;
continue;
/* LCOV_EXCL_STOP */
}
}
if (disk_degraded) {
log_error(ENXIO, "DANGER! Disk '%s' is degraded due to missing device(s).\n", devinfo->name);
log_tag("degraded:%s\n", esc_tag(devinfo->name));
degraded = 1;
}
tommy_list_foreach(&devlist, free);
}
#else
(void)high;
(void)low;
#endif
return failed ? -1 : degraded ? 1 : 0;
}
int devrun(tommy_list* low, int operation)
{
void* (*func)(void* arg) = 0;
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];
char boot_id[64];
DIR* d = opendir("/sys/block");
if (!d) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* optional boot identity: failure must not affect hardware mappings */
if (sysattr("/proc/sys/kernel/random/boot_id", boot_id, sizeof(boot_id)) != 0 || strlen(boot_id) != 36) {
boot_id[0] = 0;
} else {
for (unsigned i = 0; i < 36; ++i) {
if (i == 8 || i == 13 || i == 18 || i == 23) {
if (boot_id[i] == '-')
continue;
} else if (hexval(boot_id[i]) >= 0) {
continue;
}
boot_id[0] = 0;
break;
}
}
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));
}
/*
* This identifies the kernel device instance within one boot, not
* persistent hardware across reconnections. Missing attributes are optional.
*/
if (boot_id[0] != 0) {
pathprint(path, sizeof(path), "/sys/block/%s/diskseq", dd->d_name);
if (sysattr(path, buf, sizeof(buf)) == 0) {
char* end;
unsigned long long diskseq = strtonll(buf, &end, 10);
if (buf[0] >= '0' && buf[0] <= '9' && diskseq != 0 && end != buf && *end == 0)
log_tag("map:/dev/%s:linux-diskseq.%s.%llu\n", esc_tag(dd->d_name), esc_tag(boot_id), diskseq);
}
}
}
closedir(d);
#endif
log_tag("map_end:\n");
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];
char value[SMART_MAX];
char name[SMART_MAX];
char label[SMART_MAX];
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");
}
}
#ifdef __linux__
static void read_os_release(char* distro, size_t distro_size)
{
distro[0] = 0;
char buf[1024];
ssize_t len = sysread("/etc/os-release", buf, sizeof(buf) - 1);
if (len <= 0)
len = sysread("/usr/lib/os-release", buf, sizeof(buf) - 1);
if (len <= 0)
return;
buf[len] = 0;
char* p = buf;
while ((p = strstr(p, "PRETTY_NAME=")) != 0) {
if (p == buf || *(p - 1) == '\n')
break;
++p;
}
if (!p)
return;
p += 12;
char q = 0;
if (*p == '"' || *p == '\'') {
q = *p++;
}
size_t d = 0;
while (*p != 0 && *p != '\n' && (q != 0 ? *p != q : 1)) {
if (*p == '\\' && *(p + 1) != 0 && *(p + 1) != '\n') {
++p;
}
if (d + 1 < distro_size) {
distro[d++] = *p;
}
++p;
}
distro[d] = 0;
strtrim(distro);
}
#endif
/****************************************************************************/
/* machine */
#if defined(__linux__) || defined(__APPLE__)
static void format_freq(char* buf, size_t size, unsigned mhz)
{
if (mhz >= 1000)
snprintf(buf, size, "%.2f GHz", mhz / 1000.0);
else
snprintf(buf, size, "%u MHz", mhz);
}
#endif
#ifdef __APPLE__
static int sysctl_string(const char* name, char* buf, size_t size)
{
size_t len = size;
if (sysctlbyname(name, buf, &len, 0, 0) != 0) {
buf[0] = 0;
return -1;
}
buf[size - 1] = 0;
strtrim(buf);
return 0;
}
#endif
void machineinfo(struct machineinfo_struct* info)
{
memset(info, 0, sizeof(*info));
#if defined(__x86_64__)
snprintf(info->cpu_arch, sizeof(info->cpu_arch), "x86_64");
#elif defined(__i386__)
snprintf(info->cpu_arch, sizeof(info->cpu_arch), "x86_32");
#elif defined(__aarch64__)
snprintf(info->cpu_arch, sizeof(info->cpu_arch), "aarch64");
#elif defined(__arm__)
snprintf(info->cpu_arch, sizeof(info->cpu_arch), "arm");
#elif defined(__powerpc64__)
snprintf(info->cpu_arch, sizeof(info->cpu_arch), "ppc64");
#elif defined(__powerpc__)
snprintf(info->cpu_arch, sizeof(info->cpu_arch), "ppc");
#elif defined(__riscv)
snprintf(info->cpu_arch, sizeof(info->cpu_arch), "riscv");
#elif defined(__s390x__)
snprintf(info->cpu_arch, sizeof(info->cpu_arch), "s390x");
#endif
#ifdef CONFIG_X86
raid_cpu_info(info->cpu_vendor, &info->cpu_family, &info->cpu_model);
uint32_t reg[4];
raid_cpuid(1, 0, reg);
info->cpu_stepping = reg[0] & 0xF;
/* brand string from extended CPUID leaves 0x80000002..0x80000004 */
raid_cpuid(0x80000000, 0, reg);
if (reg[0] >= 0x80000004) {
raid_cpuid(0x80000002, 0, reg);
memcpy(info->cpu_brand + 0, reg, sizeof(reg));
raid_cpuid(0x80000003, 0, reg);
memcpy(info->cpu_brand + 16, reg, sizeof(reg));
raid_cpuid(0x80000004, 0, reg);
memcpy(info->cpu_brand + 32, reg, sizeof(reg));
info->cpu_brand[48] = 0;
strtrim(info->cpu_brand);
}
/* hypervisor detection */
raid_cpuid(1, 0, reg);
if (reg[2] & (1U << 31)) {
raid_cpuid(0x40000000, 0, reg);
memcpy(info->hypervisor + 0, &reg[1], 4);
memcpy(info->hypervisor + 4, &reg[2], 4);
memcpy(info->hypervisor + 8, &reg[3], 4);
info->hypervisor[12] = 0;
strtrim(info->hypervisor);
}
#endif
struct utsname un;
if (uname(&un) == 0) {
snprintf(info->os_name, sizeof(info->os_name), "%s %s", un.sysname, un.release);
}
long ps = sysconf(_SC_PAGESIZE);
if (ps > 0)
info->memory_page_size = (uint32_t)ps;
#ifdef _SC_NPROCESSORS_ONLN
long num_cpus = sysconf(_SC_NPROCESSORS_ONLN);
if (num_cpus <= 0)
num_cpus = sysconf(_SC_NPROCESSORS_CONF);
if (num_cpus > 0)
info->cpu_cores_logical = (unsigned)num_cpus;
#elif defined(_SC_NPROCESSORS_CONF)
long num_cpus = sysconf(_SC_NPROCESSORS_CONF);
if (num_cpus > 0)
info->cpu_cores_logical = (unsigned)num_cpus;
#endif
#ifdef _SC_LEVEL1_DCACHE_SIZE
long l1_d = sysconf(_SC_LEVEL1_DCACHE_SIZE);
if (l1_d > 0)
info->cache_l1_data = (uint64_t)l1_d;
#endif
#ifdef _SC_LEVEL1_ICACHE_SIZE
long l1_i = sysconf(_SC_LEVEL1_ICACHE_SIZE);
if (l1_i > 0)
info->cache_l1_instruction = (uint64_t)l1_i;
#endif
#ifdef _SC_LEVEL2_CACHE_SIZE
long l2 = sysconf(_SC_LEVEL2_CACHE_SIZE);
if (l2 > 0)
info->cache_l2 = (uint64_t)l2;
#endif
#ifdef _SC_LEVEL3_CACHE_SIZE
long l3 = sysconf(_SC_LEVEL3_CACHE_SIZE);
if (l3 > 0)
info->cache_l3 = (uint64_t)l3;
#endif
#ifdef __linux__
struct sysinfo si;
if (sysinfo(&si) == 0) {
info->memory_total_bytes = (uint64_t)si.totalram * si.mem_unit;
}
/*
* Group CPUs by distinct maximum frequency rather than relying on
* consecutive ID ranges. This handles non-consecutive ID layouts cleanly
* and avoids artificial cluster fragmentation.
*/
struct {
unsigned mhz;
unsigned count;
unsigned cpu;
} clusters[8];
unsigned cluster_count = 0;
unsigned cpus[CPU_SETSIZE];
unsigned online_total = read_online_cpus(cpus, sizeof(cpus) / sizeof(cpus[0]));
unsigned scan_cpus = online_total < sizeof(cpus) / sizeof(cpus[0]) ? online_total : sizeof(cpus) / sizeof(cpus[0]);
if (scan_cpus > 0) {
for (unsigned k = 0; k < scan_cpus; ++k) {
unsigned i = cpus[k];
char path[PATH_MAX];
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%u/cpufreq/cpuinfo_max_freq", i);
long long khz = syslong(path);
if (khz > 0) {
unsigned mhz = (unsigned)(khz / 1000);
int found = 0;
for (unsigned c = 0; c < cluster_count; ++c) {
if (clusters[c].mhz == mhz) {
++clusters[c].count;
found = 1;
break;
}
}
if (!found && cluster_count < sizeof(clusters) / sizeof(clusters[0])) {
clusters[cluster_count].mhz = mhz;
clusters[cluster_count].count = 1;
clusters[cluster_count].cpu = i;
++cluster_count;
}
}
}
/* sort clusters by frequency descending (fastest cores first) */
for (unsigned i = 0; i < cluster_count; ++i) {
for (unsigned j = i + 1; j < cluster_count; ++j) {
if (clusters[j].mhz > clusters[i].mhz) {
unsigned tmp_mhz = clusters[i].mhz;
unsigned tmp_count = clusters[i].count;
unsigned tmp_cpu = clusters[i].cpu;
clusters[i].mhz = clusters[j].mhz;
clusters[i].count = clusters[j].count;
clusters[i].cpu = clusters[j].cpu;
clusters[j].mhz = tmp_mhz;
clusters[j].count = tmp_count;
clusters[j].cpu = tmp_cpu;
}
}
}
}
if (cluster_count > 1) {
info->cpu_clock[0] = 0;
size_t pos = 0;
for (unsigned c = 0; c < cluster_count; ++c) {
char freq_str[32];
format_freq(freq_str, sizeof(freq_str), clusters[c].mhz);
char part[64];
if (clusters[c].count == 1)
snprintf(part, sizeof(part), "%s%s (1 cpu)", c > 0 ? ", " : "", freq_str);
else
snprintf(part, sizeof(part), "%s%s (%u cpus)", c > 0 ? ", " : "", freq_str, clusters[c].count);
if (pos < sizeof(info->cpu_clock)) {
int n = snprintf(info->cpu_clock + pos, sizeof(info->cpu_clock) - pos, "%s", part);
if (n > 0)
pos += (size_t)n;
}
}
} else if (cluster_count == 1) {
char path[PATH_MAX];
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%u/cpufreq/base_frequency", clusters[0].cpu);
long long base_khz = syslong(path);
unsigned base_mhz = base_khz > 0 ? (unsigned)(base_khz / 1000) : 0;
unsigned max_mhz = clusters[0].mhz;
char max_str[32];
char base_str[32];
format_freq(max_str, sizeof(max_str), max_mhz);
if (base_mhz > 0 && base_mhz != max_mhz) {
format_freq(base_str, sizeof(base_str), base_mhz);
snprintf(info->cpu_clock, sizeof(info->cpu_clock), "%s (max %s)", base_str, max_str);
} else {
snprintf(info->cpu_clock, sizeof(info->cpu_clock), "%s", max_str);
}
}
/* fallback if sysfs cpufreq is unavailable (e.g. Cloud VM) */
if (info->cpu_clock[0] == 0) {
#ifdef CONFIG_X86
raid_cpuid(0, 0, reg);
if (reg[0] >= 0x16) {
raid_cpuid(0x16, 0, reg);
unsigned base_mhz = reg[0] & 0xFFFF;
unsigned max_mhz = reg[1] & 0xFFFF;
if (base_mhz > 0 && max_mhz > 0 && base_mhz != max_mhz) {
char base_str[32], max_str[32];
format_freq(base_str, sizeof(base_str), base_mhz);
format_freq(max_str, sizeof(max_str), max_mhz);
snprintf(info->cpu_clock, sizeof(info->cpu_clock), "%s (max %s)", base_str, max_str);
} else if (base_mhz > 0) {
format_freq(info->cpu_clock, sizeof(info->cpu_clock), base_mhz);
} else if (max_mhz > 0) {
format_freq(info->cpu_clock, sizeof(info->cpu_clock), max_mhz);
}
}
#endif
}
if (info->cpu_clock[0] == 0) {
char buf[1024];
ssize_t len = sysread("/proc/cpuinfo", buf, sizeof(buf) - 1);
if (len > 0) {
buf[len] = 0;
char* p = strstr(buf, "cpu MHz");
if (p) {
p = strchr(p, ':');
if (p) {
double mhz = strtod(p + 1, 0);
if (mhz > 0)
format_freq(info->cpu_clock, sizeof(info->cpu_clock), (unsigned)(mhz + 0.5));
}
}
}
}
char model_buf[128];
if (sysattr("/sys/class/dmi/id/product_name", model_buf, sizeof(model_buf)) == 0 && model_buf[0]) {
char vendor_buf[128];
if (sysattr("/sys/class/dmi/id/sys_vendor", vendor_buf, sizeof(vendor_buf)) == 0 && vendor_buf[0]) {
snprintf(info->system_model, sizeof(info->system_model), "%s %s", vendor_buf, model_buf);
} else {
snprintf(info->system_model, sizeof(info->system_model), "%s", model_buf);
}
} else if (sysattr("/proc/device-tree/model", model_buf, sizeof(model_buf)) == 0 && model_buf[0]) {
snprintf(info->system_model, sizeof(info->system_model), "%s", model_buf);
}
read_os_release(info->os_distribution, sizeof(info->os_distribution));
#endif
#ifdef __APPLE__
size_t len;
if (info->cpu_brand[0] == 0)
sysctl_string("machdep.cpu.brand_string", info->cpu_brand, sizeof(info->cpu_brand));
sysctl_string("hw.model", info->system_model, sizeof(info->system_model));
unsigned val = 0;
len = sizeof(val);
if (sysctlbyname("hw.physicalcpu", &val, &len, 0, 0) == 0)
info->cpu_cores_physical = val;
len = sizeof(val);
if (sysctlbyname("hw.logicalcpu", &val, &len, 0, 0) == 0)
info->cpu_cores_logical = val;
uint64_t hz = 0;
len = sizeof(hz);
if (sysctlbyname("hw.cpufrequency", &hz, &len, 0, 0) == 0 && hz > 0)
format_freq(info->cpu_clock, sizeof(info->cpu_clock), (unsigned)(hz / 1000000));
unsigned long clen = 0;
len = sizeof(clen);
if (sysctlbyname("hw.l1dcachesize", &clen, &len, 0, 0) == 0)
info->cache_l1_data = clen;
len = sizeof(clen);
if (sysctlbyname("hw.l1icachesize", &clen, &len, 0, 0) == 0)
info->cache_l1_instruction = clen;
len = sizeof(clen);
if (sysctlbyname("hw.l2cachesize", &clen, &len, 0, 0) == 0)
info->cache_l2 = clen;
len = sizeof(clen);
if (sysctlbyname("hw.l3cachesize", &clen, &len, 0, 0) == 0)
info->cache_l3 = clen;
uint64_t mem = 0;
len = sizeof(mem);
if (sysctlbyname("hw.memsize", &mem, &len, 0, 0) == 0)
info->memory_total_bytes = mem;
char osver[32];
sysctl_string("kern.osproductversion", osver, sizeof(osver));
char osrel[32];
sysctl_string("kern.osrelease", osrel, sizeof(osrel));
if (osver[0] && osrel[0]) {
snprintf(info->os_name, sizeof(info->os_name), "macOS %s (Darwin %s)", osver, osrel);
} else if (osver[0]) {
snprintf(info->os_name, sizeof(info->os_name), "macOS %s", osver);
} else if (osrel[0]) {
snprintf(info->os_name, sizeof(info->os_name), "Darwin %s", osrel);
}
#endif
}
#endif