/* SPDX-License-Identifier: BSD-3-Clause */ #include <assert.h> #include <getopt.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <common.h> #include <commonlib/bsd/elog.h> #include <flashrom.h> #include "eventlog.h" /* Only refers to the data max size. The "-1" is the checksum byte */ #define ELOG_MAX_EVENT_DATA_SIZE (ELOG_MAX_EVENT_SIZE - sizeof(struct event_header) - 1) enum elogtool_flag { ELOGTOOL_FLAG_UTC = (1 << 0), }; enum elogtool_return { ELOGTOOL_EXIT_SUCCESS = 0, ELOGTOOL_EXIT_BAD_ARGS, ELOGTOOL_EXIT_READ_ERROR, ELOGTOOL_EXIT_WRITE_ERROR, ELOGTOOL_EXIT_NOT_ENOUGH_MEMORY, ELOGTOOL_EXIT_INVALID_ELOG_FORMAT, ELOGTOOL_EXIT_NOT_ENOUGH_BUFFER_SPACE, }; static int cmd_list(const struct buffer *, enum elogtool_flag); static int cmd_clear(const struct buffer *, enum elogtool_flag); static int cmd_add(const struct buffer *, enum elogtool_flag); static const struct { const char *name; int (*func)(const struct buffer *buf, enum elogtool_flag flags); /* Whether it requires to write the buffer back */ bool write_back; } cmds[] = { {"list", cmd_list, false}, {"clear", cmd_clear, true}, {"add", cmd_add, true}, }; static char **cmd_argv; /* Command arguments */ static char *argv0; /* Used as invoked_as */ static struct option long_options[] = { {"file", required_argument, 0, 'f'}, {"help", no_argument, 0, 'h'}, {"utc", no_argument, 0, 'U'}, {NULL, 0, 0, 0}, }; static void usage(char *invoked_as) { fprintf(stderr, "elogtool: edit elog events\n\n" "USAGE:\n" "\t%s COMMAND [-f <filename>]\n\n" "where, COMMAND is:\n" " list lists all the event logs in human readable format\n" " clear clears all the event logs\n" " add <event_type> [event_data] add an entry to the event log\n" "\n" "ARGS\n" "-f, --file <filename> File that holds event log partition.\n" " If empty it will try to read/write from/to\n" " the " ELOG_RW_REGION_NAME " using flashrom.\n" "-U, --utc Print timestamps in UTC time zone\n" "-h, --help Print this help\n", invoked_as); } /* * If filename is empty, read RW_ELOG from flashrom. * Otherwise read the RW_ELOG from a file. * It fails if the ELOG header is invalid. * On success, buffer must be freed by caller. */ static int elog_read(struct buffer *buffer, const char *filename) { if (filename == NULL) { if (flashrom_host_read(buffer, ELOG_RW_REGION_NAME) != 0) { fprintf(stderr, "Could not read RW_ELOG region using flashrom\n"); return ELOGTOOL_EXIT_READ_ERROR; } } else if (buffer_from_file(buffer, filename) != 0) { fprintf(stderr, "Could not read input file: %s\n", filename); return ELOGTOOL_EXIT_READ_ERROR; } if (elog_verify_header(buffer_get(buffer)) != CB_SUCCESS) { fprintf(stderr, "FATAL: Invalid elog header\n"); buffer_delete(buffer); return ELOGTOOL_EXIT_INVALID_ELOG_FORMAT; } return ELOGTOOL_EXIT_SUCCESS; } /* * If filename is NULL, it saves the buffer using flashrom. * Otherwise, it saves the buffer in the given filename. */ static int elog_write(struct buffer *buffer, const char *filename) { if (filename == NULL) { if (flashrom_host_write(buffer, ELOG_RW_REGION_NAME) != 0) { fprintf(stderr, "Failed to write to RW_ELOG region using flashrom\n"); return ELOGTOOL_EXIT_WRITE_ERROR; } return ELOGTOOL_EXIT_SUCCESS; } if (buffer_write_file(buffer, filename) != 0) { fprintf(stderr, "Failed to write to file %s\n", filename); return ELOGTOOL_EXIT_WRITE_ERROR; } return ELOGTOOL_EXIT_SUCCESS; } /* Buffer offset must point to a valid event_header struct */ static size_t next_available_event_offset(const struct buffer *buf) { const struct event_header *event; struct buffer copy, *iter = © assert(buffer_offset(buf) >= sizeof(struct elog_header)); buffer_clone(iter, buf); while (buffer_size(iter) >= sizeof(struct event_header)) { event = buffer_get(iter); if (event->type == ELOG_TYPE_EOL || event->length == 0) break; assert(event->length <= buffer_size(iter)); buffer_seek(iter, event->length); } return buffer_offset(iter) - buffer_offset(buf); } /* * Shrinks buffer by ~bytes_to_shrink, then appends a LOG_CLEAR event, * and finally fills the remaining area with EOL events. * Buffer offset must point to a valid event_header struct. */ static int shrink_buffer(const struct buffer *buf, size_t bytes_to_shrink) { struct buffer copy, *iter = © const struct event_header *event; uint32_t cleared; int remaining; uint8_t *data; assert(buffer_offset(buf) >= sizeof(struct elog_header)); buffer_clone(©, buf); /* Save copy of first event for later */ data = buffer_get(buf); /* Set buffer offset pointing to the event right after bytes_to_shrink */ while (buffer_offset(iter) < bytes_to_shrink) { event = buffer_get(iter); assert(!(event->type == ELOG_TYPE_EOL || event->length == 0)); buffer_seek(iter, event->length); } /* Must be relative to the buffer offset */ cleared = buffer_offset(iter) - buffer_offset(buf); remaining = buffer_size(iter); /* Overlapping copy */ memmove(data, data + cleared, remaining); memset(data + remaining, ELOG_TYPE_EOL, cleared); /* Re-init copy to have a clean offset. Needed for init_event() */ buffer_clone(©, buf); buffer_seek(©, next_available_event_offset(©)); if (!eventlog_init_event(©, ELOG_TYPE_LOG_CLEAR, &cleared, sizeof(cleared))) return ELOGTOOL_EXIT_NOT_ENOUGH_BUFFER_SPACE; return ELOGTOOL_EXIT_SUCCESS; } static int cmd_list(const struct buffer *buf, enum elogtool_flag flags) { enum eventlog_timezone tz = EVENTLOG_TIMEZONE_LOCALTIME; const struct event_header *event; unsigned int count = 0; if (flags & ELOGTOOL_FLAG_UTC) tz = EVENTLOG_TIMEZONE_UTC; /* Point to the first event */ event = buffer_get(buf) + sizeof(struct elog_header); while ((const void *)(event) < buffer_end(buf)) { if (((const void *)event + sizeof(*event)) >= buffer_end(buf) || event->length <= sizeof(*event) || event->length > ELOG_MAX_EVENT_SIZE || ((const void *)event + event->length) >= buffer_end(buf) || event->type == ELOG_TYPE_EOL) break; eventlog_print_event(event, count, tz); event = elog_get_next_event(event); count++; } return ELOGTOOL_EXIT_SUCCESS; } /* * Clears the elog events from the given buffer, which is a valid RW_ELOG region. * A LOG_CLEAR event is appended. */ static int cmd_clear(const struct buffer *buf, enum elogtool_flag flags __maybe_unused) { uint32_t used_data_size; struct buffer copy; /* Clone the buffer to avoid changing the offset of the original buffer */ buffer_clone(©, buf); buffer_seek(©, sizeof(struct elog_header)); /* * Calculate the size of the "used" buffer, needed for ELOG_TYPE_LOG_CLEAR. * Then overwrite the entire buffer with ELOG_TYPE_EOL. * Finally insert a LOG_CLEAR event into the buffer. */ used_data_size = next_available_event_offset(©); memset(buffer_get(©), ELOG_TYPE_EOL, buffer_size(©)); if (!eventlog_init_event(©, ELOG_TYPE_LOG_CLEAR, &used_data_size, sizeof(used_data_size))) return ELOGTOOL_EXIT_NOT_ENOUGH_BUFFER_SPACE; return ELOGTOOL_EXIT_SUCCESS; } static void cmd_add_usage(void) { usage(argv0); fprintf(stderr, "\n\nSpecific to ADD command:\n" "\n" "<event_type>: an hexadecimal number (0-255). Prefix '0x' is optional\n" "[event_data]: (optional) a series of hexadecimal numbers. Must be:\n" " - len(event_data) %% 2 == 0\n" " - len(event_data) in bytes <= %zu\n" "\n" "Example:\n" "%s add 0x16 01ABF0 # 01ABF0 is actually three bytes: 0x01, 0xAB and 0xF0\n" "%s add 17 # 17 is in hexa\n", ELOG_MAX_EVENT_DATA_SIZE, argv0, argv0 ); } static int cmd_add_parse_args(uint8_t *type, uint8_t *data, size_t *data_size) { char byte[3] = {0}; int argc = 0; char *endptr; long value; int len; while (cmd_argv[argc] != NULL) argc++; if (argc != 1 && argc != 2) return ELOGTOOL_EXIT_BAD_ARGS; /* Force type to be an hexa value to be consistent with the data values */ value = strtol(cmd_argv[0], NULL, 16); if (value > 255) { fprintf(stderr, "Error: Event type should be between 0-0xff; " "got: 0x%04lx\n", value); return ELOGTOOL_EXIT_BAD_ARGS; } *type = value; if (argc == 1) return ELOGTOOL_EXIT_SUCCESS; /* Assuming argc == 2 */ len = strlen(cmd_argv[1]); /* Needs 2 bytes per number */ if (len % 2 != 0) { fprintf(stderr, "Error: Event data length should be an even number; got: %d\n", len); return ELOGTOOL_EXIT_BAD_ARGS; } *data_size = len / 2; if (*data_size > ELOG_MAX_EVENT_DATA_SIZE) { fprintf(stderr, "Error: Event data length (in bytes) should be <= %zu; got: %zu\n", ELOG_MAX_EVENT_DATA_SIZE, *data_size); return ELOGTOOL_EXIT_BAD_ARGS; } for (unsigned int i = 0; i < *data_size; i++) { byte[0] = *cmd_argv[1]++; byte[1] = *cmd_argv[1]++; data[i] = strtol(byte, &endptr, 16); if (endptr != &byte[2]) { fprintf(stderr, "Error: Event data length contains invalid data. " "Only hexa digits are valid\n"); return ELOGTOOL_EXIT_BAD_ARGS; } } return ELOGTOOL_EXIT_SUCCESS; } /* Appends an elog entry to EventLog buffer. */ static int cmd_add(const struct buffer *buf, enum elogtool_flag flags __maybe_unused) { uint8_t data[ELOG_MAX_EVENT_DATA_SIZE]; size_t data_size = 0; struct buffer copy; uint8_t type = 0; size_t next_event; size_t threshold; int ret; if (cmd_add_parse_args(&type, data, &data_size) != ELOGTOOL_EXIT_SUCCESS) { cmd_add_usage(); return ELOGTOOL_EXIT_BAD_ARGS; } buffer_clone(©, buf); buffer_seek(©, sizeof(struct elog_header)); threshold = buffer_size(©) * 3 / 4; next_event = next_available_event_offset(©); if (next_event > threshold) { /* Shrink ~ 1/4 of the size */ ret = shrink_buffer(©, buffer_size(buf) - threshold); if (ret != ELOGTOOL_EXIT_SUCCESS) return ret; next_event = next_available_event_offset(©); } buffer_seek(©, next_event); if (!eventlog_init_event(©, type, data, data_size)) return ELOGTOOL_EXIT_NOT_ENOUGH_BUFFER_SPACE; return ELOGTOOL_EXIT_SUCCESS; } int main(int argc, char **argv) { char *filename = NULL; enum elogtool_flag flags = 0; struct buffer buf; unsigned int i; int argflag; int ret; if (argc < 2) { usage(argv[0]); return ELOGTOOL_EXIT_BAD_ARGS; } while (1) { int option_index; argflag = getopt_long(argc, argv, "Uhf:", long_options, &option_index); if (argflag == -1) break; switch (argflag) { case 'h': case '?': usage(argv[0]); return ELOGTOOL_EXIT_SUCCESS; case 'f': if (!optarg) { usage(argv[0]); return ELOGTOOL_EXIT_BAD_ARGS; } filename = optarg; break; case 'U': flags |= ELOGTOOL_FLAG_UTC; break; default: break; } } /* At least one command must be available. */ if (optind >= argc) { usage(argv[0]); return ELOGTOOL_EXIT_BAD_ARGS; } /* Returned buffer must be freed. */ ret = elog_read(&buf, filename); if (ret) return ret; for (i = 0; i < ARRAY_SIZE(cmds); i++) { if (!strcmp(cmds[i].name, argv[optind])) { /* For commands that parse their own arguments. */ cmd_argv = &argv[optind+1]; argv0 = argv[0]; ret = cmds[i].func(&buf, flags); break; } } if (i == ARRAY_SIZE(cmds)) { usage(argv[0]); ret = ELOGTOOL_EXIT_BAD_ARGS; } if (!ret && cmds[i].write_back) ret = elog_write(&buf, filename); buffer_delete(&buf); return ret; }