[PATCH 22/27] objtool: Add options to write/read exported noreturns to/from a file

From: Josh Poimboeuf

Date: Fri Aug 28 2026 - 01:01:01 EST


Add --noreturns-write which can be used to write vmlinux's exported
noreturns to a file, and --noreturn-read which can be used to read them
when running objtool on a module.

Signed-off-by: Josh Poimboeuf <jpoimboe@xxxxxxxxxx>
---
tools/objtool/builtin-check.c | 12 ++
tools/objtool/check.c | 148 +++++++++++++++++++++++-
tools/objtool/include/objtool/builtin.h | 2 +
tools/objtool/include/objtool/elf.h | 1 +
4 files changed, 162 insertions(+), 1 deletion(-)

diff --git a/tools/objtool/builtin-check.c b/tools/objtool/builtin-check.c
index 75b11dc85010e..c4c4210f84662 100644
--- a/tools/objtool/builtin-check.c
+++ b/tools/objtool/builtin-check.c
@@ -101,6 +101,8 @@ static const struct option check_options[] = {
OPT_BOOLEAN(0, "module", &opts.module, "object is part of a kernel module"),
OPT_BOOLEAN(0, "mnop", &opts.mnop, "nop out mcount call sites"),
OPT_BOOLEAN(0, "no-unreachable", &opts.no_unreachable, "skip 'unreachable instruction' warnings"),
+ OPT_STRING(0, "noreturns-read", &opts.noreturns_read, "file", "read exported noreturns from a file"),
+ OPT_STRING(0, "noreturns-write", &opts.noreturns_write, "file", "write exported noreturns to a file"),
OPT_STRING('o', "output", &opts.output, "file", "output file name"),
OPT_BOOLEAN(0, "sec-address", &opts.sec_address, "print section addresses in warnings"),
OPT_BOOLEAN(0, "stats", &opts.stats, "print statistics"),
@@ -180,6 +182,16 @@ static bool opts_valid(void)
return false;
}

+ if (opts.noreturns_write && !opts.link) {
+ ERROR("--noreturns-write requires --link");
+ return false;
+ }
+
+ if (opts.noreturns_write && !opts.stackval && !opts.orc && !opts.uaccess) {
+ ERROR("--noreturns-write requires --stackval, --orc, or --uaccess");
+ return false;
+ }
+
if (opts.disas ||
opts.hack_jump_label ||
opts.hack_noinstr ||
diff --git a/tools/objtool/check.c b/tools/objtool/check.c
index d27303220c29d..086e8f9eca867 100644
--- a/tools/objtool/check.c
+++ b/tools/objtool/check.c
@@ -339,6 +339,120 @@ static void detect_noreturns(struct objtool_file *file)
}
}

+static bool noreturns_changed(const char *buf, size_t len)
+{
+ bool changed = true;
+ char *old = NULL;
+ FILE *fp;
+
+ fp = fopen(opts.noreturns_write, "r");
+ if (!fp)
+ return true;
+
+ old = malloc(len + 1);
+ if (!old)
+ goto out;
+
+ if (fread(old, 1, len + 1, fp) != len)
+ goto out;
+
+ if (memcmp(old, buf, len))
+ goto out;
+
+ changed = false;
+
+out:
+ free(old);
+ fclose(fp);
+ return changed;
+}
+
+/*
+ * This is called for vmlinux.o. Write the vmlinux.o noreturn list to a file
+ * so it can be read in module objtool runs by read_noreturns().
+ */
+static int write_noreturns(struct objtool_file *file)
+{
+ struct symbol *func;
+ char *buf, *pos;
+ size_t len = 0;
+ FILE *fp;
+
+ for_each_sym(file->elf, func) {
+ if (is_noreturn(func) && func->exported)
+ len += strlen(func->name) + 1;
+ }
+
+ buf = malloc(len + 1);
+ if (!buf) {
+ ERROR_GLIBC("malloc");
+ return -1;
+ }
+
+ pos = buf;
+ for_each_sym(file->elf, func) {
+ if (is_noreturn(func) && func->exported)
+ pos += sprintf(pos, "%s\n", func->name);
+ }
+
+ /*
+ * Only write the file when the contents have change to avoid relinking
+ * all the modules unnecessarily.
+ */
+ if (!noreturns_changed(buf, len)) {
+ free(buf);
+ return 0;
+ }
+
+ fp = fopen(opts.noreturns_write, "w");
+ if (!fp) {
+ ERROR_GLIBC("fopen");
+ return -1;
+ }
+
+ if (fwrite(buf, 1, len, fp) != len) {
+ ERROR_GLIBC("fwrite");
+ return -1;
+ }
+
+ free(buf);
+
+ if (fclose(fp)) {
+ ERROR_GLIBC("fclose");
+ return -1;
+ }
+
+ return 0;
+}
+
+/*
+ * This is called for modules. Read the noreturn list generated by the
+ * vmlinux.o pass and mark the corresponding undefined symbols noreturn.
+ */
+static int read_noreturns(struct objtool_file *file)
+{
+ char line[SYM_NAME_LEN];
+ struct symbol *func;
+ FILE *fp;
+
+ fp = fopen(opts.noreturns_read, "r");
+ if (!fp) {
+ ERROR("can't open '%s'", opts.noreturns_read);
+ return -1;
+ }
+
+ while (fgets(line, sizeof(line), fp)) {
+ line[strcspn(line, "\n")] = '\0';
+
+ func = find_global_symbol_by_name(file->elf, line);
+ if (func && is_undef_sym(func))
+ func->_noreturn = 1;
+ }
+
+ fclose(fp);
+ return 0;
+}
+
static void init_cfi_state(struct cfi_state *cfi)
{
int i;
@@ -2236,6 +2350,24 @@ static int add_jump_table_alts(struct objtool_file *file)
return 0;
}

+static void read_exports(struct objtool_file *file)
+{
+ struct section *sec;
+ struct symbol *func;
+ struct reloc *reloc;
+
+ sec = find_section_by_name(file->elf, ".export_symbol");
+ if (!sec || !sec->rsec)
+ return;
+
+ for_each_reloc(sec->rsec, reloc) {
+ func = find_func_by_offset(reloc->sym->sec,
+ reloc->sym->offset + reloc_addend(reloc));
+ if (func)
+ func->exported = 1;
+ }
+}
+
static void set_func_state(struct cfi_state *state)
{
state->cfa = initial_func_cfi.cfa;
@@ -2720,8 +2852,10 @@ int decode_file(struct objtool_file *file)
* both validate_functions() and validate_noinstr_sections() -- and by
* validate_unret().
*/
- if (validate_branch_enabled() || opts.noinstr || opts.unret)
+ if (validate_branch_enabled() || opts.noinstr || opts.unret) {
+ read_exports(file);
detect_noreturns(file);
+ }

if (read_unwind_hints(file))
return -1;
@@ -4888,6 +5022,12 @@ int check(struct objtool_file *file)
objtool_disas_ctx = disas_ctx;
}

+ if (opts.noreturns_read) {
+ ret = read_noreturns(file);
+ if (ret)
+ goto out;
+ }
+
ret = decode_file(file);
if (ret)
goto out;
@@ -4995,6 +5135,12 @@ int check(struct objtool_file *file)
goto out;
}

+ if (opts.noreturns_write && !opts.dryrun) {
+ ret = write_noreturns(file);
+ if (ret)
+ goto out;
+ }
+
if (opts.stats) {
printf("nr_insns_visited: %ld\n", nr_insns_visited);
printf("nr_cfi: %ld\n", nr_cfi);
diff --git a/tools/objtool/include/objtool/builtin.h b/tools/objtool/include/objtool/builtin.h
index 349690bb1c50e..fb33d57a54c95 100644
--- a/tools/objtool/include/objtool/builtin.h
+++ b/tools/objtool/include/objtool/builtin.h
@@ -41,6 +41,8 @@ struct opts {
bool mnop;
bool module;
bool no_unreachable;
+ const char *noreturns_read;
+ const char *noreturns_write;
const char *output;
bool sec_address;
bool stats;
diff --git a/tools/objtool/include/objtool/elf.h b/tools/objtool/include/objtool/elf.h
index 0e3593d993ac8..adf6e1322c6df 100644
--- a/tools/objtool/include/objtool/elf.h
+++ b/tools/objtool/include/objtool/elf.h
@@ -100,6 +100,7 @@ struct symbol {
u8 fake : 1;
u8 _noreturn : 1;
u8 ignore_noreturn : 1;
+ u8 exported : 1;
struct list_head pv_target;
struct reloc *relocs;
struct section *group_sec;
--
2.55.0