The additional definitions provided by the header are - EXT4_IOC_RESIZE_FS, used in resize-fs.c, - FILEID_KERNFS, used in cgroup-util.c and pidfd-util.c. Let's drop the inclusion at other places.
518 lines
19 KiB
C
518 lines
19 KiB
C
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
|
/***
|
|
Copyright © 2018 Dell Inc.
|
|
***/
|
|
|
|
#include <linux/fiemap.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/magic.h>
|
|
#include <sys/ioctl.h>
|
|
|
|
#include "alloc-util.h"
|
|
#include "blockdev-util.h"
|
|
#include "btrfs-util.h"
|
|
#include "devnum-util.h"
|
|
#include "efivars.h"
|
|
#include "env-util.h"
|
|
#include "fd-util.h"
|
|
#include "fileio.h"
|
|
#include "hibernate-util.h"
|
|
#include "log.h"
|
|
#include "parse-util.h"
|
|
#include "path-util.h"
|
|
#include "proc-cmdline.h"
|
|
#include "stat-util.h"
|
|
|
|
#define HIBERNATION_SWAP_THRESHOLD 0.98
|
|
|
|
void hibernation_device_done(HibernationDevice *device) {
|
|
assert(device);
|
|
|
|
free(device->path);
|
|
}
|
|
|
|
int read_fiemap(int fd, struct fiemap **ret) {
|
|
_cleanup_free_ struct fiemap *fiemap = NULL, *result_fiemap = NULL;
|
|
struct stat statinfo;
|
|
uint32_t result_extents = 0;
|
|
uint64_t fiemap_start = 0, fiemap_length;
|
|
const size_t n_extra = DIV_ROUND_UP(sizeof(struct fiemap), sizeof(struct fiemap_extent));
|
|
|
|
assert(fd >= 0);
|
|
assert(ret);
|
|
|
|
if (fstat(fd, &statinfo) < 0)
|
|
return log_debug_errno(errno, "Cannot determine file size: %m");
|
|
if (!S_ISREG(statinfo.st_mode))
|
|
return -ENOTTY;
|
|
fiemap_length = statinfo.st_size;
|
|
|
|
/* Zero this out in case we run on a file with no extents */
|
|
fiemap = calloc(n_extra, sizeof(struct fiemap_extent));
|
|
if (!fiemap)
|
|
return -ENOMEM;
|
|
|
|
result_fiemap = malloc_multiply(n_extra, sizeof(struct fiemap_extent));
|
|
if (!result_fiemap)
|
|
return -ENOMEM;
|
|
|
|
/* XFS filesystem has incorrect implementation of fiemap ioctl and
|
|
* returns extents for only one block-group at a time, so we need
|
|
* to handle it manually, starting the next fiemap call from the end
|
|
* of the last extent
|
|
*/
|
|
while (fiemap_start < fiemap_length) {
|
|
*fiemap = (struct fiemap) {
|
|
.fm_start = fiemap_start,
|
|
.fm_length = fiemap_length,
|
|
.fm_flags = FIEMAP_FLAG_SYNC,
|
|
};
|
|
|
|
/* Find out how many extents there are */
|
|
if (ioctl(fd, FS_IOC_FIEMAP, fiemap) < 0)
|
|
return log_debug_errno(errno, "Failed to read extents: %m");
|
|
|
|
/* Nothing to process */
|
|
if (fiemap->fm_mapped_extents == 0)
|
|
break;
|
|
|
|
/* Resize fiemap to allow us to read in the extents, result fiemap has to hold all
|
|
* the extents for the whole file. Add space for the initial struct fiemap. */
|
|
if (!greedy_realloc0((void**) &fiemap, n_extra + fiemap->fm_mapped_extents, sizeof(struct fiemap_extent)))
|
|
return -ENOMEM;
|
|
|
|
fiemap->fm_extent_count = fiemap->fm_mapped_extents;
|
|
fiemap->fm_mapped_extents = 0;
|
|
|
|
if (ioctl(fd, FS_IOC_FIEMAP, fiemap) < 0)
|
|
return log_debug_errno(errno, "Failed to read extents: %m");
|
|
|
|
/* Resize result_fiemap to allow us to copy in the extents */
|
|
if (!greedy_realloc((void**) &result_fiemap,
|
|
n_extra + result_extents + fiemap->fm_mapped_extents, sizeof(struct fiemap_extent)))
|
|
return -ENOMEM;
|
|
|
|
memcpy(result_fiemap->fm_extents + result_extents,
|
|
fiemap->fm_extents,
|
|
sizeof(struct fiemap_extent) * fiemap->fm_mapped_extents);
|
|
|
|
result_extents += fiemap->fm_mapped_extents;
|
|
|
|
/* Highly unlikely that it is zero */
|
|
if (_likely_(fiemap->fm_mapped_extents > 0)) {
|
|
uint32_t i = fiemap->fm_mapped_extents - 1;
|
|
|
|
fiemap_start = fiemap->fm_extents[i].fe_logical +
|
|
fiemap->fm_extents[i].fe_length;
|
|
|
|
if (fiemap->fm_extents[i].fe_flags & FIEMAP_EXTENT_LAST)
|
|
break;
|
|
}
|
|
}
|
|
|
|
memcpy(result_fiemap, fiemap, sizeof(struct fiemap));
|
|
result_fiemap->fm_mapped_extents = result_extents;
|
|
*ret = TAKE_PTR(result_fiemap);
|
|
return 0;
|
|
}
|
|
|
|
static int read_resume_config(dev_t *ret_devno, uint64_t *ret_offset) {
|
|
_cleanup_free_ char *devno_str = NULL, *offset_str = NULL;
|
|
uint64_t offset;
|
|
dev_t devno;
|
|
int r;
|
|
|
|
assert(ret_devno);
|
|
assert(ret_offset);
|
|
|
|
r = proc_cmdline_get_key("noresume", /* flags = */ 0, /* ret_value = */ NULL);
|
|
if (r < 0)
|
|
return log_debug_errno(r, "Failed to check if 'noresume' kernel command line option is set: %m");
|
|
if (r > 0)
|
|
return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
|
|
"'noresume' kernel command line option is set, refusing hibernation device lookup.");
|
|
|
|
r = read_one_line_file("/sys/power/resume", &devno_str);
|
|
if (r < 0)
|
|
return log_debug_errno(r, "Failed to read /sys/power/resume: %m");
|
|
|
|
r = parse_devnum(devno_str, &devno);
|
|
if (r < 0)
|
|
return log_debug_errno(r, "Failed to parse /sys/power/resume devno '%s': %m", devno_str);
|
|
|
|
r = read_one_line_file("/sys/power/resume_offset", &offset_str);
|
|
if (r < 0)
|
|
return log_debug_errno(r, "Failed to read /sys/power/resume_offset: %m");
|
|
|
|
r = safe_atou64(offset_str, &offset);
|
|
if (r < 0)
|
|
return log_debug_errno(r, "Failed to parse /sys/power/resume_offset '%s': %m", offset_str);
|
|
|
|
if (devno == 0 && offset > 0)
|
|
return log_debug_errno(SYNTHETIC_ERRNO(ENOMEDIUM),
|
|
"Found populated /sys/power/resume_offset (%" PRIu64 ") but /sys/power/resume is not set, refusing.",
|
|
offset);
|
|
|
|
*ret_devno = devno;
|
|
*ret_offset = offset;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* entry in /proc/swaps */
|
|
typedef struct SwapEntry {
|
|
char *path;
|
|
bool swapfile;
|
|
|
|
uint64_t size;
|
|
uint64_t used;
|
|
int priority;
|
|
|
|
/* Not present in original entry */
|
|
dev_t devno;
|
|
uint64_t offset;
|
|
} SwapEntry;
|
|
|
|
typedef struct SwapEntries {
|
|
SwapEntry *swaps;
|
|
size_t n_swaps;
|
|
} SwapEntries;
|
|
|
|
static void swap_entry_done(SwapEntry *entry) {
|
|
assert(entry);
|
|
|
|
free(entry->path);
|
|
}
|
|
|
|
static void swap_entries_done(SwapEntries *entries) {
|
|
assert(entries);
|
|
|
|
FOREACH_ARRAY(i, entries->swaps, entries->n_swaps)
|
|
swap_entry_done(i);
|
|
|
|
free(entries->swaps);
|
|
}
|
|
|
|
static int swap_entry_get_resume_config(SwapEntry *swap) {
|
|
_cleanup_close_ int fd = -EBADF;
|
|
uint64_t offset_raw;
|
|
struct stat st;
|
|
int r;
|
|
|
|
assert(swap);
|
|
assert(swap->path);
|
|
|
|
fd = open(swap->path, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
|
|
if (fd < 0)
|
|
return -errno;
|
|
|
|
if (fstat(fd, &st) < 0)
|
|
return -errno;
|
|
|
|
if (!swap->swapfile) {
|
|
if (!S_ISBLK(st.st_mode))
|
|
return -ENOTBLK;
|
|
|
|
swap->devno = st.st_rdev;
|
|
swap->offset = 0;
|
|
return 0;
|
|
}
|
|
|
|
r = stat_verify_regular(&st);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
r = get_block_device_fd(fd, &swap->devno);
|
|
if (r < 0)
|
|
return r;
|
|
if (r == 0)
|
|
return -EMEDIUMTYPE;
|
|
|
|
r = fd_is_fs_type(fd, BTRFS_SUPER_MAGIC);
|
|
if (r < 0)
|
|
return log_debug_errno(r, "Failed to check if swap file '%s' is on Btrfs: %m", swap->path);
|
|
if (r > 0) {
|
|
r = btrfs_get_file_physical_offset_fd(fd, &offset_raw);
|
|
if (r < 0)
|
|
return r;
|
|
} else {
|
|
_cleanup_free_ struct fiemap *fiemap = NULL;
|
|
|
|
r = read_fiemap(fd, &fiemap);
|
|
if (r < 0)
|
|
return log_debug_errno(r, "Failed to read extent map for swap file '%s': %m", swap->path);
|
|
|
|
offset_raw = fiemap->fm_extents[0].fe_physical;
|
|
}
|
|
|
|
swap->offset = offset_raw / page_size();
|
|
return 0;
|
|
}
|
|
|
|
static int read_swap_entries(SwapEntries *ret) {
|
|
_cleanup_(swap_entries_done) SwapEntries entries = {};
|
|
_cleanup_fclose_ FILE *f = NULL;
|
|
|
|
assert(ret);
|
|
|
|
f = fopen("/proc/swaps", "re");
|
|
if (!f)
|
|
return log_debug_errno(errno, "Failed to open %s: %m", "/proc/swaps");
|
|
|
|
/* Remove header */
|
|
(void) fscanf(f, "%*s %*s %*s %*s %*s\n");
|
|
|
|
for (unsigned i = 1;; i++) {
|
|
_cleanup_(swap_entry_done) SwapEntry swap = {};
|
|
_cleanup_free_ char *type = NULL;
|
|
int k;
|
|
|
|
k = fscanf(f,
|
|
"%ms " /* device/file path */
|
|
"%ms " /* type of swap */
|
|
"%" PRIu64 /* swap size */
|
|
"%" PRIu64 /* used */
|
|
"%i" /* priority */
|
|
"\n",
|
|
&swap.path, &type, &swap.size, &swap.used, &swap.priority);
|
|
if (k == EOF)
|
|
break;
|
|
if (k != 5)
|
|
return log_debug_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse /proc/swaps line %u.", i);
|
|
|
|
if (streq(type, "file")) {
|
|
if (endswith(swap.path, "\\040(deleted)")) {
|
|
log_debug("Swap file '%s' has been deleted, ignoring.", swap.path);
|
|
continue;
|
|
}
|
|
|
|
swap.swapfile = true;
|
|
|
|
} else if (streq(type, "partition")) {
|
|
const char *node;
|
|
|
|
node = path_startswith(swap.path, "/dev/");
|
|
if (node && startswith(node, "zram")) {
|
|
log_debug("Swap partition '%s' is a zram device, ignoring.", swap.path);
|
|
continue;
|
|
}
|
|
|
|
swap.swapfile = false;
|
|
|
|
} else {
|
|
log_debug("Swap type %s is not supported for hibernation, ignoring device: %s",
|
|
type, swap.path);
|
|
continue;
|
|
}
|
|
|
|
if (!GREEDY_REALLOC(entries.swaps, entries.n_swaps + 1))
|
|
return log_oom_debug();
|
|
|
|
entries.swaps[entries.n_swaps++] = TAKE_STRUCT(swap);
|
|
}
|
|
|
|
*ret = TAKE_STRUCT(entries);
|
|
return 0;
|
|
}
|
|
|
|
/* Attempt to find a suitable device for hibernation by parsing /proc/swaps, /sys/power/resume, and
|
|
* /sys/power/resume_offset.
|
|
*
|
|
* Beware:
|
|
* Never use a device or file that hasn't been somehow specified by a user who would also be entrusted
|
|
* with full system memory access (for example via /sys/power/resume) or that isn't an already active
|
|
* swap area! Otherwise various security attacks might become possible, for example an attacker could
|
|
* silently attach such a device and circumvent full disk encryption when it would be automatically used
|
|
* for hibernation. Also, having a swap area on top of encryption is not per se enough to protect from all
|
|
* such attacks.
|
|
*
|
|
* Returns:
|
|
* 1 - Values are set in /sys/power/resume and /sys/power/resume_offset.
|
|
*
|
|
* 0 - No values are set in /sys/power/resume and /sys/power/resume_offset.
|
|
* ret will represent the highest priority swap with most remaining space discovered in /proc/swaps.
|
|
*
|
|
* Negative value in the case of error */
|
|
int find_suitable_hibernation_device_full(HibernationDevice *ret_device, uint64_t *ret_size, uint64_t *ret_used) {
|
|
_cleanup_(swap_entries_done) SwapEntries entries = {};
|
|
SwapEntry *entry = NULL;
|
|
uint64_t resume_config_offset;
|
|
dev_t resume_config_devno;
|
|
int r;
|
|
|
|
assert(!ret_size == !ret_used);
|
|
|
|
r = read_resume_config(&resume_config_devno, &resume_config_offset);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
r = read_swap_entries(&entries);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
FOREACH_ARRAY(swap, entries.swaps, entries.n_swaps) {
|
|
r = swap_entry_get_resume_config(swap);
|
|
if (r == -EMEDIUMTYPE) {
|
|
assert(swap->swapfile);
|
|
|
|
log_debug_errno(r, "Unable to acquire backing block device for swap file '%s' (maybe on a RAID btrfs?), ignoring.",
|
|
swap->path);
|
|
continue;
|
|
}
|
|
if (r < 0)
|
|
return log_debug_errno(r, "Failed to get devno and offset for swap '%s': %m", swap->path);
|
|
assert(swap->devno > 0);
|
|
|
|
if (resume_config_devno > 0) {
|
|
if (swap->devno == resume_config_devno &&
|
|
(!swap->swapfile || swap->offset == resume_config_offset)) {
|
|
/* /sys/power/resume (resume=) is set, and the calculated swap file offset
|
|
* matches with /sys/power/resume_offset. */
|
|
entry = swap;
|
|
break;
|
|
}
|
|
|
|
/* If resume= is set, don't try to use other swap spaces. */
|
|
continue;
|
|
}
|
|
|
|
if (!entry ||
|
|
swap->priority > entry->priority ||
|
|
swap->size - swap->used > entry->size - entry->used)
|
|
entry = swap;
|
|
}
|
|
|
|
if (!entry) {
|
|
if (resume_config_devno > 0)
|
|
return log_debug_errno(SYNTHETIC_ERRNO(ESTALE), "Cannot find swap entry corresponding to /sys/power/resume.");
|
|
|
|
return log_debug_errno(SYNTHETIC_ERRNO(ENOSPC), "No swap space available for hibernation.");
|
|
}
|
|
|
|
if (ret_device) {
|
|
char *path;
|
|
|
|
if (entry->swapfile) {
|
|
r = device_path_make_canonical(S_IFBLK, entry->devno, &path);
|
|
if (r < 0)
|
|
return log_debug_errno(r,
|
|
"Failed to format canonical device path for devno '" DEVNUM_FORMAT_STR "': %m",
|
|
DEVNUM_FORMAT_VAL(entry->devno));
|
|
} else
|
|
path = TAKE_PTR(entry->path);
|
|
|
|
*ret_device = (HibernationDevice) {
|
|
.devno = entry->devno,
|
|
.offset = entry->offset,
|
|
.path = path,
|
|
};
|
|
}
|
|
|
|
if (ret_size) {
|
|
*ret_size = entry->size;
|
|
*ret_used = entry->used;
|
|
}
|
|
|
|
return resume_config_devno > 0;
|
|
}
|
|
|
|
static int get_proc_meminfo_active(unsigned long long *ret) {
|
|
_cleanup_free_ char *active_str = NULL;
|
|
unsigned long long active;
|
|
int r;
|
|
|
|
assert(ret);
|
|
|
|
r = get_proc_field("/proc/meminfo", "Active(anon)", &active_str);
|
|
if (r < 0)
|
|
return log_debug_errno(r, "Failed to retrieve Active(anon) from /proc/meminfo: %m");
|
|
|
|
r = safe_atollu(active_str, &active);
|
|
if (r < 0)
|
|
return log_debug_errno(r, "Failed to parse Active(anon) '%s' from /proc/meminfo: %m", active_str);
|
|
|
|
*ret = active;
|
|
return 0;
|
|
}
|
|
|
|
int hibernation_is_safe(void) {
|
|
unsigned long long active;
|
|
uint64_t size, used;
|
|
bool resume_set, bypass_space_check;
|
|
int r;
|
|
|
|
bypass_space_check = getenv_bool("SYSTEMD_BYPASS_HIBERNATION_MEMORY_CHECK") > 0;
|
|
|
|
r = find_suitable_hibernation_device_full(NULL, &size, &used);
|
|
if (IN_SET(r, -ENOSPC, -ESTALE) && bypass_space_check)
|
|
/* If we don't have any available swap space at all, or the specified resume device is missing,
|
|
* and $SYSTEMD_BYPASS_HIBERNATION_MEMORY_CHECK is set, skip all remaining checks since
|
|
* we can't do that properly anyway. It is quite possible that the user is using a setup
|
|
* similar to #30083. When we actually perform hibernation in sleep.c we'll check everything again. */
|
|
return 0;
|
|
if (r < 0)
|
|
return r;
|
|
resume_set = r > 0;
|
|
|
|
if (!resume_set && !is_efi_boot())
|
|
return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
|
|
"Not running on EFI and resume= is not set. Hibernation is not safe.");
|
|
|
|
if (bypass_space_check)
|
|
return 0;
|
|
|
|
r = get_proc_meminfo_active(&active);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
r = active <= (size - used) * HIBERNATION_SWAP_THRESHOLD;
|
|
log_debug("Detected %s swap for hibernation: Active(anon)=%llu kB, size=%" PRIu64 " kB, used=%" PRIu64 " kB, threshold=%.2g%%",
|
|
r ? "enough" : "not enough", active, size, used, 100 * HIBERNATION_SWAP_THRESHOLD);
|
|
if (!r)
|
|
return -ENOSPC;
|
|
|
|
return resume_set;
|
|
}
|
|
|
|
int write_resume_config(dev_t devno, uint64_t offset, const char *device) {
|
|
char offset_str[DECIMAL_STR_MAX(uint64_t)];
|
|
const char *devno_str;
|
|
int r;
|
|
|
|
assert(devno > 0);
|
|
assert(device);
|
|
|
|
devno_str = FORMAT_DEVNUM(devno);
|
|
xsprintf(offset_str, "%" PRIu64, offset);
|
|
|
|
r = write_string_file("/sys/power/resume_offset", offset_str, WRITE_STRING_FILE_DISABLE_BUFFER);
|
|
if (r < 0)
|
|
return log_error_errno(r, "Failed to write swap file offset %s to /sys/power/resume_offset for device '%s': %m",
|
|
offset_str, device);
|
|
log_debug("Wrote resume_offset=%s for device '%s' to /sys/power/resume_offset.",
|
|
offset_str, device);
|
|
|
|
r = write_string_file("/sys/power/resume", devno_str, WRITE_STRING_FILE_DISABLE_BUFFER);
|
|
if (r < 0)
|
|
return log_error_errno(r, "Failed to write device '%s' (%s) to /sys/power/resume: %m",
|
|
device, devno_str);
|
|
log_debug("Wrote resume=%s for device '%s' to /sys/power/resume.", devno_str, device);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int clear_efi_hibernate_location_and_warn(void) {
|
|
int r;
|
|
|
|
if (!is_efi_boot())
|
|
return 0;
|
|
|
|
r = efi_set_variable(EFI_SYSTEMD_VARIABLE_STR("HibernateLocation"), NULL, 0);
|
|
if (r == -ENOENT)
|
|
return 0;
|
|
if (r < 0)
|
|
return log_warning_errno(r, "Failed to clear HibernateLocation EFI variable: %m");
|
|
|
|
return 1;
|
|
}
|