| /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
| |
| /* Copyright © 2019 Oracle and/or its affiliates. */ |
| |
| /* Generally speaking, the pstore contains a small number of files |
| * that in turn contain a small amount of data. */ |
| #include <errno.h> |
| #include <stdio.h> |
| #include <stdio_ext.h> |
| #include <sys/prctl.h> |
| #include <sys/xattr.h> |
| #include <unistd.h> |
| |
| #include "sd-daemon.h" |
| #include "sd-journal.h" |
| #include "sd-login.h" |
| #include "sd-messages.h" |
| |
| #include "acl-util.h" |
| #include "alloc-util.h" |
| #include "capability-util.h" |
| #include "cgroup-util.h" |
| #include "compress.h" |
| #include "conf-parser.h" |
| #include "copy.h" |
| #include "dirent-util.h" |
| #include "escape.h" |
| #include "fd-util.h" |
| #include "fileio.h" |
| #include "fs-util.h" |
| #include "io-util.h" |
| #include "journal-importer.h" |
| #include "log.h" |
| #include "macro.h" |
| #include "main-func.h" |
| #include "mkdir.h" |
| #include "parse-util.h" |
| #include "process-util.h" |
| #include "signal-util.h" |
| #include "socket-util.h" |
| #include "special.h" |
| #include "sort-util.h" |
| #include "string-table.h" |
| #include "string-util.h" |
| #include "strv.h" |
| #include "tmpfile-util.h" |
| #include "user-util.h" |
| |
| /* Command line argument handling */ |
| typedef enum PStoreStorage { |
| PSTORE_STORAGE_NONE, |
| PSTORE_STORAGE_EXTERNAL, |
| PSTORE_STORAGE_JOURNAL, |
| _PSTORE_STORAGE_MAX, |
| _PSTORE_STORAGE_INVALID = -EINVAL, |
| } PStoreStorage; |
| |
| static const char* const pstore_storage_table[_PSTORE_STORAGE_MAX] = { |
| [PSTORE_STORAGE_NONE] = "none", |
| [PSTORE_STORAGE_EXTERNAL] = "external", |
| [PSTORE_STORAGE_JOURNAL] = "journal", |
| }; |
| |
| DEFINE_PRIVATE_STRING_TABLE_LOOKUP(pstore_storage, PStoreStorage); |
| static DEFINE_CONFIG_PARSE_ENUM(config_parse_pstore_storage, pstore_storage, PStoreStorage, "Failed to parse storage setting"); |
| |
| static PStoreStorage arg_storage = PSTORE_STORAGE_EXTERNAL; |
| |
| static bool arg_unlink = true; |
| static const char *arg_sourcedir = "/sys/fs/pstore"; |
| static const char *arg_archivedir = "/var/lib/systemd/pstore"; |
| |
| static int parse_config(void) { |
| static const ConfigTableItem items[] = { |
| { "PStore", "Unlink", config_parse_bool, 0, &arg_unlink }, |
| { "PStore", "Storage", config_parse_pstore_storage, 0, &arg_storage }, |
| {} |
| }; |
| |
| return config_parse_many_nulstr( |
| PKGSYSCONFDIR "/pstore.conf", |
| CONF_PATHS_NULSTR("systemd/pstore.conf.d"), |
| "PStore\0", |
| config_item_table_lookup, items, |
| CONFIG_PARSE_WARN, |
| NULL, |
| NULL); |
| } |
| |
| /* File list handling - PStoreEntry is the struct and |
| * and PStoreEntry is the type that contains all info |
| * about a pstore entry. */ |
| typedef struct PStoreEntry { |
| struct dirent dirent; |
| bool is_binary; |
| bool handled; |
| char *content; |
| size_t content_size; |
| } PStoreEntry; |
| |
| typedef struct PStoreList { |
| PStoreEntry *entries; |
| size_t n_entries; |
| } PStoreList; |
| |
| static void pstore_entries_reset(PStoreList *list) { |
| for (size_t i = 0; i < list->n_entries; i++) |
| free(list->entries[i].content); |
| free(list->entries); |
| list->n_entries = 0; |
| } |
| |
| static int compare_pstore_entries(const PStoreEntry *a, const PStoreEntry *b) { |
| return strcmp(a->dirent.d_name, b->dirent.d_name); |
| } |
| |
| static int move_file(PStoreEntry *pe, const char *subdir1, const char *subdir2) { |
| _cleanup_free_ char *ifd_path = NULL, *ofd_path = NULL; |
| _cleanup_free_ void *field = NULL; |
| const char *suffix, *message; |
| struct iovec iovec[2]; |
| int n_iovec = 0, r; |
| |
| if (pe->handled) |
| return 0; |
| |
| ifd_path = path_join(arg_sourcedir, pe->dirent.d_name); |
| if (!ifd_path) |
| return log_oom(); |
| |
| ofd_path = path_join(arg_archivedir, subdir1, subdir2, pe->dirent.d_name); |
| if (!ofd_path) |
| return log_oom(); |
| |
| /* Always log to the journal */ |
| suffix = arg_storage == PSTORE_STORAGE_EXTERNAL ? strjoina(" moved to ", ofd_path) : (char *)"."; |
| message = strjoina("MESSAGE=PStore ", pe->dirent.d_name, suffix); |
| iovec[n_iovec++] = IOVEC_MAKE_STRING(message); |
| |
| if (pe->content_size > 0) { |
| size_t field_size; |
| |
| field_size = strlen("FILE=") + pe->content_size; |
| field = malloc(field_size); |
| if (!field) |
| return log_oom(); |
| memcpy(stpcpy(field, "FILE="), pe->content, pe->content_size); |
| iovec[n_iovec++] = IOVEC_MAKE(field, field_size); |
| } |
| |
| r = sd_journal_sendv(iovec, n_iovec); |
| if (r < 0) |
| return log_error_errno(r, "Failed to log pstore entry: %m"); |
| |
| if (arg_storage == PSTORE_STORAGE_EXTERNAL) { |
| /* Move file from pstore to external storage */ |
| r = mkdir_parents(ofd_path, 0755); |
| if (r < 0) |
| return log_error_errno(r, "Failed to create directory %s: %m", ofd_path); |
| r = copy_file_atomic(ifd_path, ofd_path, 0600, 0, 0, COPY_REPLACE); |
| if (r < 0) |
| return log_error_errno(r, "Failed to copy_file_atomic: %s to %s", ifd_path, ofd_path); |
| } |
| |
| /* If file copied properly, remove it from pstore */ |
| if (arg_unlink) |
| (void) unlink(ifd_path); |
| |
| pe->handled = true; |
| |
| return 0; |
| } |
| |
| static int append_dmesg(PStoreEntry *pe, const char *subdir1, const char *subdir2) { |
| /* Append dmesg chunk to end, create if needed */ |
| _cleanup_free_ char *ofd_path = NULL; |
| _cleanup_close_ int ofd = -EBADF; |
| ssize_t wr; |
| |
| assert(pe); |
| |
| if (pe->content_size == 0) |
| return 0; |
| |
| ofd_path = path_join(arg_archivedir, subdir1, subdir2, "dmesg.txt"); |
| if (!ofd_path) |
| return log_oom(); |
| |
| ofd = open(ofd_path, O_CREAT|O_NOFOLLOW|O_NOCTTY|O_CLOEXEC|O_APPEND|O_WRONLY, 0640); |
| if (ofd < 0) |
| return log_error_errno(ofd, "Failed to open file %s: %m", ofd_path); |
| wr = write(ofd, pe->content, pe->content_size); |
| if (wr < 0) |
| return log_error_errno(errno, "Failed to store dmesg to %s: %m", ofd_path); |
| if ((size_t)wr != pe->content_size) |
| return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to store dmesg to %s. %zu bytes are lost.", ofd_path, pe->content_size - wr); |
| |
| return 0; |
| } |
| |
| static int process_dmesg_files(PStoreList *list) { |
| /* Move files, reconstruct dmesg.txt */ |
| _cleanup_free_ char *erst_subdir = NULL; |
| uint64_t last_record_id = 0; |
| |
| /* When dmesg is written into pstore, it is done so in small chunks, whatever the exchange buffer |
| * size is with the underlying pstore backend (ie. EFI may be ~2KiB), which means an example |
| * pstore with approximately 64KB of storage may have up to roughly 32 dmesg files, some likely |
| * related. |
| * |
| * Here we look at the dmesg filename and try to discern if files are part of a related group, |
| * meaning the same original dmesg. |
| * |
| * The dmesg- filename contains the backend-type and the Common Platform Error Record, CPER, |
| * record id, a 64-bit number. |
| * |
| * Files are processed in reverse lexigraphical order so as to properly reconstruct original dmesg. */ |
| |
| for (size_t n = list->n_entries; n > 0; n--) { |
| PStoreEntry *pe; |
| char *p; |
| |
| pe = &list->entries[n-1]; |
| |
| if (pe->handled) |
| continue; |
| if (endswith(pe->dirent.d_name, ".enc.z")) /* indicates a problem */ |
| continue; |
| if (!startswith(pe->dirent.d_name, "dmesg-")) |
| continue; |
| |
| if ((p = startswith(pe->dirent.d_name, "dmesg-efi-"))) { |
| /* For the EFI backend, the 3 least significant digits of record id encodes a |
| * "count" number, the next 2 least significant digits for the dmesg part |
| * (chunk) number, and the remaining digits as the timestamp. See |
| * linux/drivers/firmware/efi/efi-pstore.c in efi_pstore_write(). */ |
| _cleanup_free_ char *subdir1 = NULL, *subdir2 = NULL; |
| size_t plen = strlen(p); |
| |
| if (plen < 6) |
| continue; |
| |
| /* Extract base record id */ |
| subdir1 = strndup(p, plen - 5); |
| if (!subdir1) |
| return log_oom(); |
| /* Extract "count" field */ |
| subdir2 = strndup(p + plen - 3, 3); |
| if (!subdir2) |
| return log_oom(); |
| |
| /* Now move file from pstore to archive storage */ |
| (void) move_file(pe, subdir1, subdir2); |
| |
| /* Append to the dmesg */ |
| (void) append_dmesg(pe, subdir1, subdir2); |
| } else if ((p = startswith(pe->dirent.d_name, "dmesg-erst-"))) { |
| /* For the ERST backend, the record is a monotonically increasing number, seeded as |
| * a timestamp. See linux/drivers/acpi/apei/erst.c in erst_writer(). */ |
| uint64_t record_id; |
| |
| if (safe_atou64(p, &record_id) < 0) |
| continue; |
| if (last_record_id - 1 != record_id) |
| /* A discontinuity in the number has been detected, this current record id |
| * will become the directory name for all pieces of the dmesg in this |
| * series. */ |
| if (free_and_strdup(&erst_subdir, p) < 0) |
| return log_oom(); |
| |
| /* Now move file from pstore to archive storage */ |
| (void) move_file(pe, erst_subdir, NULL); |
| |
| /* Append to the dmesg */ |
| (void) append_dmesg(pe, erst_subdir, NULL); |
| |
| /* Update, but keep erst_subdir for next file */ |
| last_record_id = record_id; |
| } else |
| log_debug("Unknown backend, ignoring \"%s\".", pe->dirent.d_name); |
| } |
| return 0; |
| } |
| |
| static int list_files(PStoreList *list, const char *sourcepath) { |
| _cleanup_(closedirp) DIR *dirp = NULL; |
| int r; |
| |
| dirp = opendir(sourcepath); |
| if (!dirp) |
| return log_error_errno(errno, "Failed to opendir %s: %m", sourcepath); |
| |
| FOREACH_DIRENT(de, dirp, return log_error_errno(errno, "Failed to iterate through %s: %m", sourcepath)) { |
| _cleanup_free_ char *ifd_path = NULL; |
| |
| ifd_path = path_join(sourcepath, de->d_name); |
| if (!ifd_path) |
| return log_oom(); |
| |
| _cleanup_free_ char *buf = NULL; |
| size_t buf_size; |
| |
| /* Now read contents of pstore file */ |
| r = read_full_virtual_file(ifd_path, &buf, &buf_size); |
| if (r < 0) { |
| log_warning_errno(r, "Failed to read file %s, skipping: %m", ifd_path); |
| continue; |
| } |
| |
| if (!GREEDY_REALLOC(list->entries, list->n_entries + 1)) |
| return log_oom(); |
| |
| list->entries[list->n_entries++] = (PStoreEntry) { |
| .dirent = *de, |
| .content = TAKE_PTR(buf), |
| .content_size = buf_size, |
| .is_binary = true, |
| .handled = false, |
| }; |
| } |
| |
| return 0; |
| } |
| |
| static int run(int argc, char *argv[]) { |
| _cleanup_(pstore_entries_reset) PStoreList list = {}; |
| int r; |
| |
| log_setup(); |
| |
| if (argc == 3) { |
| arg_sourcedir = argv[1]; |
| arg_archivedir = argv[2]; |
| } else if (argc > 1) |
| return log_error_errno(SYNTHETIC_ERRNO(EINVAL), |
| "This program takes zero or two arguments."); |
| |
| /* Ignore all parse errors */ |
| (void) parse_config(); |
| |
| log_debug("Selected storage: %s.", pstore_storage_to_string(arg_storage)); |
| log_debug("Selected unlink: %s.", yes_no(arg_unlink)); |
| |
| if (arg_storage == PSTORE_STORAGE_NONE) |
| /* Do nothing, intentionally, leaving pstore untouched */ |
| return 0; |
| |
| /* Obtain list of files in pstore */ |
| r = list_files(&list, arg_sourcedir); |
| if (r < 0) |
| return r; |
| |
| /* Handle each pstore file */ |
| /* Sort files lexicographically ascending, generally needed by all */ |
| typesafe_qsort(list.entries, list.n_entries, compare_pstore_entries); |
| |
| /* Process known file types */ |
| (void) process_dmesg_files(&list); |
| |
| /* Move left over files out of pstore */ |
| for (size_t n = 0; n < list.n_entries; n++) |
| (void) move_file(&list.entries[n], NULL, NULL); |
| |
| return 0; |
| } |
| |
| DEFINE_MAIN_FUNCTION(run); |