From a5f7a5bb3b7f28ba7e4fa246775b29a0e5537255 Mon Sep 17 00:00:00 2001 From: Jinke Han Date: Tue, 8 Sep 2026 15:37:42 +0800 Subject: x86/kprobes: Fix crash when probing CS CALL instructions When using eBPF to probe CS CALL instructions within a function, a crash can be triggered. The eBPF tool probes offset 257 of the __hrtimer_run_queues() function: <__hrtimer_run_queues+249>: nopl 0x0(%rax,%rax,1) <__hrtimer_run_queues+254>: mov %r14,%rdi <__hrtimer_run_queues+257>: cs call <__x86_indirect_thunk_r12> <__hrtimer_run_queues+263>: mov %eax,%r12d <__hrtimer_run_queues+266>: xchg %ax,%ax <__hrtimer_run_queues+268>: mov %r13,%rdi Which triggers this crash: BUG: unable to handle page fault for address: 00000000000f41c9 #PF: supervisor write access in kernel mode #PF: error_code(0x0002) - not-present page PGD 0 P4D 0 Oops: 0002 [#1] SMP NOPTI CPU: 1 PID: 0 Comm: swapper/1 Kdump: loaded Tainted: P RIP: 0010:__hrtimer_run_queues+0x106/0x230 Note that __hrtimer_run_queues+0x106 is __hrtimer_run_queues+262, which is at the 6th byte of the above CS CALL instruction. Since the CS CALL instruction occupies 6 bytes, the exception occurred in the middle of that call instruction. The root cause is that when using eBPF tools to probe in the middle of a function, a kprobe with INT3 is used as the underlying implementation. During single-step emulation of the original CALL instruction, int3_emulate_call() assumes that the probed CALL instruction is 5 bytes long. However, the actual CS-prefixed CALL instruction occupies 6 bytes, so it constructs an incorrect exception return address. When the CPU returns from the kprobe handler, the next instruction to be executed is at the address of the last byte of that CS CALL instruction. Coincidentally, starting from that address, the CPU fetches and decodes a completely different instruction, which ultimately triggers a kernel crash. Fix the issue by using the actual instruction length obtained from the instruction decoder when constructing the exception return address, rather than relying on the hardcoded CALL_INSN_SIZE macro. [ mingo: Refined the changelog ] Fixes: 6256e668b7af ("x86/kprobes: Use int3 instead of debug trap for single-step") Suggested-by: Masami Hiramatsu (Google) Signed-off-by: Jinke Han Signed-off-by: Ingo Molnar Reviewed-by: Masami Hiramatsu (Google) Acked-by: Yafang Shao Acked-by: Borislav Petkov Cc: Peter Zijlstra Link: https://patch.msgid.link/20260908073742.GA10517@didi-ThinkCentre-M920t-N000 --- tools/bootconfig/main.c | 593 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 593 insertions(+) create mode 100644 tools/bootconfig/main.c (limited to 'tools/bootconfig/main.c') diff --git a/tools/bootconfig/main.c b/tools/bootconfig/main.c new file mode 100644 index 000000000..17d971d47 --- /dev/null +++ b/tools/bootconfig/main.c @@ -0,0 +1,593 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Boot config tool for initrd image + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define pr_err(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__) + +/* Bootconfig footer is [size][csum][BOOTCONFIG_MAGIC]. */ +#define BOOTCONFIG_FOOTER_SIZE \ + (sizeof(uint32_t) * 2 + BOOTCONFIG_MAGIC_LEN) + +static int xbc_show_value(struct xbc_node *node, bool semicolon) +{ + const char *val, *eol; + char q; + int i = 0; + + eol = semicolon ? ";\n" : "\n"; + xbc_array_for_each_value(node, val) { + if (strchr(val, '"')) + q = '\''; + else + q = '"'; + printf("%c%s%c%s", q, val, q, xbc_node_is_array(node) ? ", " : eol); + i++; + } + return i; +} + +static void xbc_show_compact_tree(void) +{ + struct xbc_node *node, *cnode = NULL, *vnode; + int depth = 0, i; + + node = xbc_root_node(); + while (node && xbc_node_is_key(node)) { + for (i = 0; i < depth; i++) + printf("\t"); + if (!cnode) + cnode = xbc_node_get_child(node); + while (cnode && xbc_node_is_key(cnode) && !cnode->next) { + vnode = xbc_node_get_child(cnode); + /* + * If @cnode has value and subkeys, this + * should show it as below. + * + * key(@node) { + * key(@cnode) = value; + * key(@cnode) { + * subkeys; + * } + * } + */ + if (vnode && xbc_node_is_value(vnode) && vnode->next) + break; + printf("%s.", xbc_node_get_data(node)); + node = cnode; + cnode = vnode; + } + if (cnode && xbc_node_is_key(cnode)) { + printf("%s {\n", xbc_node_get_data(node)); + depth++; + node = cnode; + cnode = NULL; + continue; + } else if (cnode && xbc_node_is_value(cnode)) { + printf("%s = ", xbc_node_get_data(node)); + xbc_show_value(cnode, true); + /* + * If @node has value and subkeys, continue + * looping on subkeys with same node. + */ + if (cnode->next) { + cnode = xbc_node_get_next(cnode); + continue; + } + } else { + printf("%s;\n", xbc_node_get_data(node)); + } + cnode = NULL; + + if (node->next) { + node = xbc_node_get_next(node); + continue; + } + while (!node->next) { + node = xbc_node_get_parent(node); + if (!node) + return; + if (!xbc_node_get_child(node)->next) + continue; + if (depth) { + depth--; + for (i = 0; i < depth; i++) + printf("\t"); + printf("}\n"); + } + } + node = xbc_node_get_next(node); + } +} + +static void xbc_show_list(void) +{ + char key[XBC_KEYLEN_MAX]; + struct xbc_node *leaf; + const char *val; + int ret; + + xbc_for_each_key_value(leaf, val) { + ret = xbc_node_compose_key(leaf, key, XBC_KEYLEN_MAX); + if (ret < 0) { + fprintf(stderr, "Failed to compose key %d\n", ret); + break; + } + printf("%s = ", key); + if (!val || val[0] == '\0') { + printf("\"\"\n"); + continue; + } + xbc_show_value(xbc_node_get_child(leaf), false); + } +} + +#define PAGE_SIZE 4096 + +static int load_xbc_fd(int fd, char **buf, int size) +{ + int ret; + + if (size < 0 || size > XBC_DATA_MAX) + return -EINVAL; + + *buf = malloc(size + 1); + if (!*buf) + return -ENOMEM; + + ret = read(fd, *buf, size); + if (ret < 0) + return -errno; + (*buf)[size] = '\0'; + + return ret; +} + +/* Return the read size or -errno */ +static int load_xbc_file(const char *path, char **buf) +{ + struct stat stat; + int fd, ret; + + fd = open(path, O_RDONLY); + if (fd < 0) + return -errno; + ret = fstat(fd, &stat); + if (ret < 0) { + ret = -errno; + close(fd); + return ret; + } + + if (stat.st_size > XBC_DATA_MAX) { + pr_err("%s size is too big\n", path); + ret = -E2BIG; + close(fd); + return ret; + } + + ret = load_xbc_fd(fd, buf, stat.st_size); + + close(fd); + + return ret; +} + +static int pr_errno(const char *msg, int err) +{ + pr_err("%s: %d\n", msg, err); + return err; +} + +static int load_xbc_from_initrd(int fd, char **buf) +{ + struct stat stat; + int ret; + uint32_t size = 0, csum = 0, rcsum; + char magic[BOOTCONFIG_MAGIC_LEN]; + const char *msg; + + ret = fstat(fd, &stat); + if (ret < 0) + return -errno; + + if (stat.st_size < BOOTCONFIG_FOOTER_SIZE) + return 0; + + if (lseek(fd, -(off_t)BOOTCONFIG_MAGIC_LEN, SEEK_END) < 0) + return pr_errno("Failed to lseek for magic", -errno); + + if (read(fd, magic, BOOTCONFIG_MAGIC_LEN) < 0) + return pr_errno("Failed to read", -errno); + + /* Check the bootconfig magic bytes */ + if (memcmp(magic, BOOTCONFIG_MAGIC, BOOTCONFIG_MAGIC_LEN) != 0) + return 0; + + if (lseek(fd, -(off_t)BOOTCONFIG_FOOTER_SIZE, SEEK_END) < 0) + return pr_errno("Failed to lseek for size", -errno); + + if (read(fd, &size, sizeof(uint32_t)) < 0) + return pr_errno("Failed to read size", -errno); + size = le32toh(size); + + if (read(fd, &csum, sizeof(uint32_t)) < 0) + return pr_errno("Failed to read checksum", -errno); + csum = le32toh(csum); + + /* Wrong size error */ + if (size > XBC_DATA_MAX || + size > stat.st_size - BOOTCONFIG_FOOTER_SIZE) { + pr_err("bootconfig size is too big\n"); + return -E2BIG; + } + + if (lseek(fd, stat.st_size - (size + BOOTCONFIG_FOOTER_SIZE), + SEEK_SET) < 0) + return pr_errno("Failed to lseek", -errno); + + ret = load_xbc_fd(fd, buf, size); + if (ret < 0) + return ret; + + /* Wrong Checksum */ + rcsum = xbc_calc_checksum(*buf, size); + if (csum != rcsum) { + pr_err("checksum error: %u != %u\n", csum, rcsum); + return -EINVAL; + } + + ret = xbc_init(*buf, size, &msg, NULL); + /* Wrong data */ + if (ret < 0) { + pr_err("parse error: %s.\n", msg); + return ret; + } + + return size; +} + +static void show_xbc_error(const char *data, const char *msg, int pos) +{ + int lin = 1, col, i; + + if (pos < 0) { + pr_err("Error: %s.\n", msg); + return; + } + + /* Note that pos starts from 0 but lin and col should start from 1. */ + col = pos + 1; + for (i = 0; i < pos; i++) { + if (data[i] == '\n') { + lin++; + col = pos - i; + } + } + pr_err("Parse Error: %s at %d:%d\n", msg, lin, col); + +} + +static int init_xbc_with_error(char *buf, int len) +{ + char *copy = strdup(buf); + const char *msg; + int ret, pos; + + if (!copy) + return -ENOMEM; + + ret = xbc_init(buf, len, &msg, &pos); + if (ret < 0) + show_xbc_error(copy, msg, pos); + free(copy); + + return ret; +} + +static int show_xbc_kernel_cmdline(void) +{ + struct xbc_node *root; + char *buf = NULL; + int len, ret; + + root = xbc_find_node("kernel"); + if (!root) + return 0; /* no kernel.* keys: emit empty output */ + + len = xbc_snprint_cmdline(NULL, 0, root); + if (len < 0) { + pr_err("Failed to size cmdline output: %d\n", len); + return len; + } + if (len == 0) + return 0; + + buf = malloc(len + 1); + if (!buf) + return -ENOMEM; + + ret = xbc_snprint_cmdline(buf, len + 1, root); + if (ret < 0) { + pr_err("Failed to render cmdline output: %d\n", ret); + free(buf); + return ret; + } + + fputs(buf, stdout); + free(buf); + return 0; +} + +static int show_xbc(const char *path, bool list, bool render_cmdline) +{ + int ret, fd; + char *buf = NULL; + struct stat st; + + ret = stat(path, &st); + if (ret < 0) { + ret = -errno; + pr_err("Failed to stat %s: %d\n", path, ret); + return ret; + } + + fd = open(path, O_RDONLY); + if (fd < 0) { + ret = -errno; + pr_err("Failed to open initrd %s: %d\n", path, ret); + return ret; + } + + ret = load_xbc_from_initrd(fd, &buf); + close(fd); + if (ret < 0) { + pr_err("Failed to load a boot config from initrd: %d\n", ret); + goto out; + } + /* Assume a bootconfig file if it is enough small */ + if (ret == 0 && st.st_size <= XBC_DATA_MAX) { + ret = load_xbc_file(path, &buf); + if (ret < 0) { + pr_err("Failed to load a boot config: %d\n", ret); + goto out; + } + if (init_xbc_with_error(buf, ret) < 0) + goto out; + } + if (render_cmdline) + ret = show_xbc_kernel_cmdline(); + else if (list) + xbc_show_list(); + else + xbc_show_compact_tree(); + if (ret > 0) + ret = 0; +out: + free(buf); + + return ret; +} + +static int delete_xbc(const char *path) +{ + struct stat stat; + int ret = 0, fd, size; + char *buf = NULL; + + fd = open(path, O_RDWR); + if (fd < 0) { + ret = -errno; + pr_err("Failed to open initrd %s: %d\n", path, ret); + return ret; + } + + size = load_xbc_from_initrd(fd, &buf); + if (size < 0) { + ret = size; + pr_err("Failed to load a boot config from initrd: %d\n", ret); + } else if (size > 0) { + ret = fstat(fd, &stat); + if (!ret) + ret = ftruncate(fd, stat.st_size + - size - BOOTCONFIG_FOOTER_SIZE); + if (ret) + ret = -errno; + } /* Ignore if there is no boot config in initrd */ + + close(fd); + free(buf); + + return ret; +} + +static int apply_xbc(const char *path, const char *xbc_path) +{ + struct { + uint32_t size; + uint32_t csum; + char magic[BOOTCONFIG_MAGIC_LEN]; + } footer; + char *buf, *data; + size_t total_size; + struct stat stat; + const char *msg; + uint32_t size, csum; + int pos, pad; + int ret, fd; + + ret = load_xbc_file(xbc_path, &buf); + if (ret < 0) { + pr_err("Failed to load %s : %d\n", xbc_path, ret); + return ret; + } + size = strlen(buf) + 1; + csum = xbc_calc_checksum(buf, size); + + /* Backup the bootconfig data */ + data = calloc(size + BOOTCONFIG_ALIGN + BOOTCONFIG_FOOTER_SIZE, 1); + if (!data) { + free(buf); + return -ENOMEM; + } + memcpy(data, buf, size); + + /* Check the data format */ + ret = xbc_init(buf, size, &msg, &pos); + if (ret < 0) { + show_xbc_error(data, msg, pos); + free(data); + free(buf); + + return ret; + } + printf("Apply %s to %s\n", xbc_path, path); + xbc_get_info(&ret, NULL); + printf("\tNumber of nodes: %d\n", ret); + printf("\tSize: %u bytes\n", (unsigned int)size); + printf("\tChecksum: %u\n", (unsigned int)csum); + + /* TODO: Check the options by schema */ + xbc_exit(); + free(buf); + + /* Remove old boot config if exists */ + ret = delete_xbc(path); + if (ret < 0) { + pr_err("Failed to delete previous boot config: %d\n", ret); + free(data); + return ret; + } + + /* Apply new one */ + fd = open(path, O_RDWR | O_APPEND); + if (fd < 0) { + ret = -errno; + pr_err("Failed to open %s: %d\n", path, ret); + free(data); + return ret; + } + /* TODO: Ensure the @path is initramfs/initrd image */ + if (fstat(fd, &stat) < 0) { + ret = -errno; + pr_err("Failed to get the size of %s\n", path); + goto out; + } + + /* To align up the total size to BOOTCONFIG_ALIGN, get padding size */ + total_size = stat.st_size + size + BOOTCONFIG_FOOTER_SIZE; + pad = ((total_size + BOOTCONFIG_ALIGN - 1) & (~BOOTCONFIG_ALIGN_MASK)) - total_size; + size += pad; + + /* Add a footer */ + footer.size = htole32(size); + footer.csum = htole32(csum); + memcpy(footer.magic, BOOTCONFIG_MAGIC, BOOTCONFIG_MAGIC_LEN); + static_assert(sizeof(footer) == BOOTCONFIG_FOOTER_SIZE); + memcpy(data + size, &footer, BOOTCONFIG_FOOTER_SIZE); + + total_size = size + BOOTCONFIG_FOOTER_SIZE; + + ret = write(fd, data, total_size); + if (ret < total_size) { + if (ret < 0) + ret = -errno; + pr_err("Failed to apply a boot config: %d\n", ret); + if (ret >= 0) + goto out_rollback; + } else + ret = 0; + +out: + close(fd); + free(data); + + return ret; + +out_rollback: + /* Map the partial write to -ENOSPC */ + if (ret >= 0) + ret = -ENOSPC; + if (ftruncate(fd, stat.st_size) < 0) { + ret = -errno; + pr_err("Failed to rollback the write error: %d\n", ret); + pr_err("The initrd %s may be corrupted. Recommend to rebuild.\n", path); + } + goto out; +} + +static int usage(void) +{ + printf("Usage: bootconfig [OPTIONS] \n" + "Or bootconfig \n" + " Apply, delete or show boot config to initrd.\n" + " Options:\n" + " -a : Apply boot config to initrd\n" + " -d : Delete boot config file from initrd\n" + " -l : list boot config in initrd or file\n" + " -C : render the kernel.* subtree as a flat cmdline\n" + " string (suitable for embedding in a kernel image)\n" + " and print it to stdout\n\n" + " If no option is given, show the bootconfig in the given file.\n"); + return -1; +} + +int main(int argc, char **argv) +{ + char *path = NULL; + char *apply = NULL; + bool render_cmdline = false; + bool delete = false, list = false; + int opt; + + while ((opt = getopt(argc, argv, "hda:lC")) != -1) { + switch (opt) { + case 'd': + delete = true; + break; + case 'a': + apply = optarg; + break; + case 'l': + list = true; + break; + case 'C': + render_cmdline = true; + break; + case 'h': + default: + return usage(); + } + } + + if ((!!apply + !!delete + !!list + !!render_cmdline) > 1) { + pr_err("Error: You can give one of -a, -d, -l or -C at once.\n"); + return usage(); + } + + if (optind >= argc) { + pr_err("Error: No initrd is specified.\n"); + return usage(); + } + + path = argv[optind]; + + if (apply) + return apply_xbc(path, apply); + else if (delete) + return delete_xbc(path); + + return show_xbc(path, list, render_cmdline); +} -- cgit v1.3.1