* [PATCH 1/5] elf-parse: avoid an undefined shift when checking the ELF type
2026-10-19 0:02 [PATCH 0/5] elf-parse: validate what the host ELF tools read from the file Leizhen Zhang
@ 2026-10-19 0:02 ` Leizhen Zhang
2026-10-19 12:47 ` Steven Rostedt
2026-10-19 0:02 ` [PATCH 2/5] elf-parse: check that the section header table is inside the file Leizhen Zhang
` (4 subsequent siblings)
5 siblings, 1 reply; 9+ messages in thread
From: Leizhen Zhang @ 2026-10-19 0:02 UTC (permalink / raw)
To: nathan, nsc; +Cc: linux-kbuild, linux-kernel, rostedt, ljs, Leizhen Zhang
elf_parse() takes a bitmap of permitted ELF types and tests it with
type = elf_parser.r2(&ehdr->e32.e_type);
if (!((1 << type) & types)) {
e_type is a 16-bit field read straight out of the file, so a type of 32
or more shifts by at least the width of int and the result is undefined;
31 shifts into the sign bit. All three users of the parser -- kallsyms,
sorttable and tracepoint-update -- reach this with e_type unvalidated.
Found by fuzzing the host ELF tools with mutated object files under
ASan/UBSan. Reproducer, on any vmlinux (or any ET_REL object for
tracepoint-update), setting only e_type:
$ python3 -c 'import struct
d = bytearray(open("vmlinux","rb").read())
struct.pack_into("<H", d, 0x10, 40)
open("t.elf","wb").write(bytes(d))'
$ ./kallsyms --sysmap=/dev/null t.elf
scripts/elf-parse.c:94:11: runtime error: shift exponent 40 is too
large for 32-bit type 'int'
#0 in elf_parse scripts/elf-parse.c:94
(host tools built with -fsanitize=address,undefined)
Reject a type that cannot be in the bitmap, and shift an unsigned 1 so
that a type of 31 stays defined. No valid object changes behaviour: every
ET_* value the callers put in the bitmap is far below 32, and a file with
a larger e_type already took the error path -- it just took it through
undefined behaviour.
Fixes: b055f4c431e3 ("sorttable: Move ELF parsing into scripts/elf-parse.[ch]")
Assisted-by: LLM ASan UBSan
Signed-off-by: Leizhen Zhang <lzsx618@gmail.com>
---
scripts/elf-parse.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/scripts/elf-parse.c b/scripts/elf-parse.c
index ce1c2a670244..9e7d1a4f263f 100644
--- a/scripts/elf-parse.c
+++ b/scripts/elf-parse.c
@@ -91,7 +91,8 @@ static int elf_parse(const char *fname, void *addr, uint32_t types)
}
type = elf_parser.r2(&ehdr->e32.e_type);
- if (!((1 << type) & types)) {
+ /* e_type comes from the file and the bitmap only has 32 slots. */
+ if (type >= 32 || !((1U << type) & types)) {
fprintf(stderr, "Invalid ELF type file %s\n", fname);
return -1;
}
--
2.34.1
^ permalink raw reply [flat|nested] 9+ messages in thread* Re: [PATCH 1/5] elf-parse: avoid an undefined shift when checking the ELF type
2026-10-19 0:02 ` [PATCH 1/5] elf-parse: avoid an undefined shift when checking the ELF type Leizhen Zhang
@ 2026-10-19 12:47 ` Steven Rostedt
0 siblings, 0 replies; 9+ messages in thread
From: Steven Rostedt @ 2026-10-19 12:47 UTC (permalink / raw)
To: Leizhen Zhang; +Cc: nathan, nsc, linux-kbuild, linux-kernel, ljs
On Sun, 18 Oct 2026 19:02:14 -0500
Leizhen Zhang <lzsx618@gmail.com> wrote:
> elf_parse() takes a bitmap of permitted ELF types and tests it with
>
> type = elf_parser.r2(&ehdr->e32.e_type);
> if (!((1 << type) & types)) {
>
> e_type is a 16-bit field read straight out of the file, so a type of 32
> or more shifts by at least the width of int and the result is undefined;
> 31 shifts into the sign bit. All three users of the parser -- kallsyms,
> sorttable and tracepoint-update -- reach this with e_type unvalidated.
>
> Found by fuzzing the host ELF tools with mutated object files under
> ASan/UBSan. Reproducer, on any vmlinux (or any ET_REL object for
> tracepoint-update), setting only e_type:
>
> $ python3 -c 'import struct
> d = bytearray(open("vmlinux","rb").read())
> struct.pack_into("<H", d, 0x10, 40)
> open("t.elf","wb").write(bytes(d))'
>
> $ ./kallsyms --sysmap=/dev/null t.elf
> scripts/elf-parse.c:94:11: runtime error: shift exponent 40 is too
> large for 32-bit type 'int'
> #0 in elf_parse scripts/elf-parse.c:94
>
> (host tools built with -fsanitize=address,undefined)
>
> Reject a type that cannot be in the bitmap, and shift an unsigned 1 so
> that a type of 31 stays defined. No valid object changes behaviour: every
> ET_* value the callers put in the bitmap is far below 32, and a file with
> a larger e_type already took the error path -- it just took it through
> undefined behaviour.
>
> Fixes: b055f4c431e3 ("sorttable: Move ELF parsing into scripts/elf-parse.[ch]")
> Assisted-by: LLM ASan UBSan
> Signed-off-by: Leizhen Zhang <lzsx618@gmail.com>
> ---
> scripts/elf-parse.c | 3 ++-
> 1 file changed, 2 insertions(+), 1 deletion(-)
>
> diff --git a/scripts/elf-parse.c b/scripts/elf-parse.c
> index ce1c2a670244..9e7d1a4f263f 100644
> --- a/scripts/elf-parse.c
> +++ b/scripts/elf-parse.c
> @@ -91,7 +91,8 @@ static int elf_parse(const char *fname, void *addr, uint32_t types)
> }
>
> type = elf_parser.r2(&ehdr->e32.e_type);
> - if (!((1 << type) & types)) {
> + /* e_type comes from the file and the bitmap only has 32 slots. */
This comment is useless, nuke it.
> + if (type >= 32 || !((1U << type) & types)) {
> fprintf(stderr, "Invalid ELF type file %s\n", fname);
> return -1;
> }
Other than that, I'm fine with the patch.
-- Steve
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH 2/5] elf-parse: check that the section header table is inside the file
2026-10-19 0:02 [PATCH 0/5] elf-parse: validate what the host ELF tools read from the file Leizhen Zhang
2026-10-19 0:02 ` [PATCH 1/5] elf-parse: avoid an undefined shift when checking the ELF type Leizhen Zhang
@ 2026-10-19 0:02 ` Leizhen Zhang
2026-10-19 12:55 ` Steven Rostedt
2026-10-19 0:02 ` [PATCH 3/5] elf-parse: check the section indices that elf_open_ro() takes from " Leizhen Zhang
` (3 subsequent siblings)
5 siblings, 1 reply; 9+ messages in thread
From: Leizhen Zhang @ 2026-10-19 0:02 UTC (permalink / raw)
To: nathan, nsc; +Cc: linux-kbuild, linux-kernel, rostedt, ljs, Leizhen Zhang
elf_parse() validates e_ehsize and e_shentsize, and nothing else. The
section header table that every caller then walks is described entirely
by unchecked file data, so a truncated or corrupted object makes the
parser read outside the mapping:
- a file shorter than one ELF header is read past its end, since e_type
at offset 16 and e_ehsize at 52/54 are read before any length check;
- e_shoff may point anywhere, including far past the end of the file;
- e_shnum may claim more sections than the file can hold;
- e_shstrndx may name a section that does not exist.
elf_map() hands the mapping to sorttable and tracepoint-update, which
index the table directly, and elf_open_ro() does the same for kallsyms,
so all three tools fault on the same inputs.
Found by fuzzing the host ELF tools with mutated object files under
ASan/UBSan. Each reproducer below changes one field of an otherwise
valid file; the sanitizer output is from host tools built with
-fsanitize=address,undefined:
e_shoff = filesize + 0x1000 (offset 0x28)
./kallsyms --sysmap=/dev/null t.elf
ERROR: AddressSanitizer: unknown-crash on address 0x7f914e32cbdc
#0 in __get_unaligned_le32 tools/include/tools/le_byteshift.h:14
(reading sh_type of section 0)
./sorttable t.elf -- same
e_shnum = 0xffff (offset 0x3c)
./sorttable t.elf
ERROR: AddressSanitizer: unknown-crash on address 0x7f64023b8000
#0 in __get_unaligned_le32 tools/include/tools/le_byteshift.h:14
./tracepoint-update t.o
ERROR: AddressSanitizer: SEGV on unknown address 0x7f9fc264db28
#0 in strcmp
e_shstrndx = 0xfff0 (offset 0x3e)
./tracepoint-update t.o
ERROR: AddressSanitizer: SEGV on unknown address 0x7f0f83b29544
#0 in __get_unaligned_le32 tools/include/tools/le_byteshift.h:14
Pass the mapped size down to elf_parse() and reject a file that cannot
hold its own ELF header, a section header table that does not fit in the
mapping, and a section name index outside the table. The division keeps
the extent check free of overflow. A count of SHN_UNDEF means the real
count lives in section 0, so one entry must still be present; resolving
that count, and the SHN_XINDEX name index, stays with elf_open_ro().
The three tools keep accepting the objects they accepted before: a
vmlinux, a shared object and a .o built from the same source are parsed
unchanged, and the reproducers above now print one line and exit 1.
Fixes: b055f4c431e3 ("sorttable: Move ELF parsing into scripts/elf-parse.[ch]")
Assisted-by: LLM ASan UBSan
Signed-off-by: Leizhen Zhang <lzsx618@gmail.com>
---
scripts/elf-parse.c | 44 ++++++++++++++++++++++++++++++++++++++++----
1 file changed, 40 insertions(+), 4 deletions(-)
diff --git a/scripts/elf-parse.c b/scripts/elf-parse.c
index 9e7d1a4f263f..0e16a9cf67f4 100644
--- a/scripts/elf-parse.c
+++ b/scripts/elf-parse.c
@@ -58,10 +58,17 @@ static void *map_file(char const *fname, size_t *size, bool writable)
return addr;
}
-static int elf_parse(const char *fname, void *addr, uint32_t types)
+static int elf_parse(const char *fname, void *addr, size_t size, uint32_t types)
{
Elf_Ehdr *ehdr = addr;
- uint16_t type;
+ size_t shoff, shentsize, shnum;
+ uint16_t type, shstrndx;
+
+ /* Every field read below lives in the smaller of the two headers. */
+ if (size < sizeof(Elf32_Ehdr)) {
+ fprintf(stderr, "ELF file too short: %s\n", fname);
+ return -1;
+ }
if (memcmp(ELFMAG, ehdr->e32.e_ident, SELFMAG) != 0 ||
ehdr->e32.e_ident[EI_VERSION] != EV_CURRENT) {
@@ -153,7 +160,8 @@ static int elf_parse(const char *fname, void *addr, uint32_t types)
elf_parser.rela_addend = rela64_addend;
elf_parser.rela_write_addend = rela64_write_addend;
- if (elf_parser.r2(&ehdr->e64.e_ehsize) != sizeof(Elf64_Ehdr) ||
+ if (size < sizeof(Elf64_Ehdr) ||
+ elf_parser.r2(&ehdr->e64.e_ehsize) != sizeof(Elf64_Ehdr) ||
elf_parser.r2(&ehdr->e64.e_shentsize) != sizeof(Elf64_Shdr)) {
fprintf(stderr,
"unrecognized ET_EXEC/ET_DYN file: %s\n",
@@ -168,6 +176,34 @@ static int elf_parse(const char *fname, void *addr, uint32_t types)
ehdr->e32.e_ident[EI_CLASS], fname);
return -1;
}
+
+ /*
+ * Everything past here walks the section headers with offsets and
+ * indices taken from the file, so the table the ELF header describes
+ * has to be inside the mapping first. e_shnum == 0 means the real
+ * count lives in section 0, which must then be present to be read.
+ */
+ shoff = ehdr_shoff(ehdr);
+ shentsize = ehdr_shentsize(ehdr);
+ shnum = ehdr_shnum(ehdr);
+ if (shnum == SHN_UNDEF)
+ shnum = 1;
+ if (shoff > size || shentsize == 0 ||
+ (size - shoff) / shentsize < shnum) {
+ fprintf(stderr, "ELF section headers out of range: %s\n",
+ fname);
+ return -1;
+ }
+
+ /* SHN_XINDEX defers the index to section 0; elf_open_ro() checks it. */
+ shstrndx = ehdr_shstrndx(ehdr);
+ if (shstrndx != SHN_XINDEX && ehdr_shnum(ehdr) != SHN_UNDEF &&
+ shstrndx >= ehdr_shnum(ehdr)) {
+ fprintf(stderr, "ELF section name index out of range: %s\n",
+ fname);
+ return -1;
+ }
+
return 0;
}
@@ -195,7 +231,7 @@ static void *__elf_map(char const *fname, size_t *size, uint32_t types,
if (!addr)
return NULL;
- ret = elf_parse(fname, addr, types);
+ ret = elf_parse(fname, addr, *size, types);
if (ret < 0) {
elf_unmap(addr, *size);
return NULL;
--
2.34.1
^ permalink raw reply [flat|nested] 9+ messages in thread* Re: [PATCH 2/5] elf-parse: check that the section header table is inside the file
2026-10-19 0:02 ` [PATCH 2/5] elf-parse: check that the section header table is inside the file Leizhen Zhang
@ 2026-10-19 12:55 ` Steven Rostedt
0 siblings, 0 replies; 9+ messages in thread
From: Steven Rostedt @ 2026-10-19 12:55 UTC (permalink / raw)
To: Leizhen Zhang; +Cc: nathan, nsc, linux-kbuild, linux-kernel, ljs
On Sun, 18 Oct 2026 19:02:15 -0500
Leizhen Zhang <lzsx618@gmail.com> wrote:
> elf_parse() validates e_ehsize and e_shentsize, and nothing else. The
> section header table that every caller then walks is described entirely
> by unchecked file data, so a truncated or corrupted object makes the
> parser read outside the mapping:
>
> - a file shorter than one ELF header is read past its end, since e_type
> at offset 16 and e_ehsize at 52/54 are read before any length check;
> - e_shoff may point anywhere, including far past the end of the file;
> - e_shnum may claim more sections than the file can hold;
> - e_shstrndx may name a section that does not exist.
>
> elf_map() hands the mapping to sorttable and tracepoint-update, which
> index the table directly, and elf_open_ro() does the same for kallsyms,
> so all three tools fault on the same inputs.
>
> Found by fuzzing the host ELF tools with mutated object files under
> ASan/UBSan. Each reproducer below changes one field of an otherwise
> valid file; the sanitizer output is from host tools built with
> -fsanitize=address,undefined:
>
> e_shoff = filesize + 0x1000 (offset 0x28)
> ./kallsyms --sysmap=/dev/null t.elf
> ERROR: AddressSanitizer: unknown-crash on address 0x7f914e32cbdc
> #0 in __get_unaligned_le32 tools/include/tools/le_byteshift.h:14
> (reading sh_type of section 0)
> ./sorttable t.elf -- same
> e_shnum = 0xffff (offset 0x3c)
> ./sorttable t.elf
> ERROR: AddressSanitizer: unknown-crash on address 0x7f64023b8000
> #0 in __get_unaligned_le32 tools/include/tools/le_byteshift.h:14
> ./tracepoint-update t.o
> ERROR: AddressSanitizer: SEGV on unknown address 0x7f9fc264db28
> #0 in strcmp
> e_shstrndx = 0xfff0 (offset 0x3e)
> ./tracepoint-update t.o
> ERROR: AddressSanitizer: SEGV on unknown address 0x7f0f83b29544
> #0 in __get_unaligned_le32 tools/include/tools/le_byteshift.h:14
>
> Pass the mapped size down to elf_parse() and reject a file that cannot
> hold its own ELF header, a section header table that does not fit in the
> mapping, and a section name index outside the table. The division keeps
> the extent check free of overflow. A count of SHN_UNDEF means the real
> count lives in section 0, so one entry must still be present; resolving
> that count, and the SHN_XINDEX name index, stays with elf_open_ro().
>
> The three tools keep accepting the objects they accepted before: a
> vmlinux, a shared object and a .o built from the same source are parsed
> unchanged, and the reproducers above now print one line and exit 1.
>
> Fixes: b055f4c431e3 ("sorttable: Move ELF parsing into scripts/elf-parse.[ch]")
You can remove the above, because this doesn't "fix" anything. If the
object files are corrupted when getting here, there's much bigger issues
than this code breaking. The object files will be loaded in the vmlinux
file, and that would be a much bigger concern about corrected object files.
> Assisted-by: LLM ASan UBSan
> Signed-off-by: Leizhen Zhang <lzsx618@gmail.com>
> ---
> scripts/elf-parse.c | 44 ++++++++++++++++++++++++++++++++++++++++----
> 1 file changed, 40 insertions(+), 4 deletions(-)
>
> diff --git a/scripts/elf-parse.c b/scripts/elf-parse.c
> index 9e7d1a4f263f..0e16a9cf67f4 100644
> --- a/scripts/elf-parse.c
> +++ b/scripts/elf-parse.c
> @@ -58,10 +58,17 @@ static void *map_file(char const *fname, size_t *size, bool writable)
> return addr;
> }
>
> -static int elf_parse(const char *fname, void *addr, uint32_t types)
> +static int elf_parse(const char *fname, void *addr, size_t size, uint32_t types)
> {
> Elf_Ehdr *ehdr = addr;
> - uint16_t type;
> + size_t shoff, shentsize, shnum;
> + uint16_t type, shstrndx;
> +
> + /* Every field read below lives in the smaller of the two headers. */
> + if (size < sizeof(Elf32_Ehdr)) {
> + fprintf(stderr, "ELF file too short: %s\n", fname);
> + return -1;
> + }
>
> if (memcmp(ELFMAG, ehdr->e32.e_ident, SELFMAG) != 0 ||
> ehdr->e32.e_ident[EI_VERSION] != EV_CURRENT) {
> @@ -153,7 +160,8 @@ static int elf_parse(const char *fname, void *addr, uint32_t types)
> elf_parser.rela_addend = rela64_addend;
> elf_parser.rela_write_addend = rela64_write_addend;
>
> - if (elf_parser.r2(&ehdr->e64.e_ehsize) != sizeof(Elf64_Ehdr) ||
> + if (size < sizeof(Elf64_Ehdr) ||
> + elf_parser.r2(&ehdr->e64.e_ehsize) != sizeof(Elf64_Ehdr) ||
> elf_parser.r2(&ehdr->e64.e_shentsize) != sizeof(Elf64_Shdr)) {
> fprintf(stderr,
> "unrecognized ET_EXEC/ET_DYN file: %s\n",
> @@ -168,6 +176,34 @@ static int elf_parse(const char *fname, void *addr, uint32_t types)
> ehdr->e32.e_ident[EI_CLASS], fname);
> return -1;
> }
> +
> + /*
> + * Everything past here walks the section headers with offsets and
> + * indices taken from the file, so the table the ELF header describes
> + * has to be inside the mapping first. e_shnum == 0 means the real
> + * count lives in section 0, which must then be present to be read.
> + */
> + shoff = ehdr_shoff(ehdr);
> + shentsize = ehdr_shentsize(ehdr);
> + shnum = ehdr_shnum(ehdr);
> + if (shnum == SHN_UNDEF)
> + shnum = 1;
> + if (shoff > size || shentsize == 0 ||
> + (size - shoff) / shentsize < shnum) {
> + fprintf(stderr, "ELF section headers out of range: %s\n",
> + fname);
> + return -1;
> + }
> +
> + /* SHN_XINDEX defers the index to section 0; elf_open_ro() checks it. */
> + shstrndx = ehdr_shstrndx(ehdr);
> + if (shstrndx != SHN_XINDEX && ehdr_shnum(ehdr) != SHN_UNDEF &&
> + shstrndx >= ehdr_shnum(ehdr)) {
> + fprintf(stderr, "ELF section name index out of range: %s\n",
> + fname);
> + return -1;
> + }
I really see this as overboard. As I said, if the object files are
corrupted, you'll likely see errors everywhere. Why is this code the one to
detect it?
I think this is adding more complexity than needed.
-- Steve
> +
> return 0;
> }
>
> @@ -195,7 +231,7 @@ static void *__elf_map(char const *fname, size_t *size, uint32_t types,
> if (!addr)
> return NULL;
>
> - ret = elf_parse(fname, addr, types);
> + ret = elf_parse(fname, addr, *size, types);
> if (ret < 0) {
> elf_unmap(addr, *size);
> return NULL;
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH 3/5] elf-parse: check the section indices that elf_open_ro() takes from the file
2026-10-19 0:02 [PATCH 0/5] elf-parse: validate what the host ELF tools read from the file Leizhen Zhang
2026-10-19 0:02 ` [PATCH 1/5] elf-parse: avoid an undefined shift when checking the ELF type Leizhen Zhang
2026-10-19 0:02 ` [PATCH 2/5] elf-parse: check that the section header table is inside the file Leizhen Zhang
@ 2026-10-19 0:02 ` Leizhen Zhang
2026-10-19 0:02 ` [PATCH 4/5] elf-parse: check the symbol table before walking it Leizhen Zhang
` (2 subsequent siblings)
5 siblings, 0 replies; 9+ messages in thread
From: Leizhen Zhang @ 2026-10-19 0:02 UTC (permalink / raw)
To: nathan, nsc; +Cc: linux-kbuild, linux-kernel, rostedt, ljs, Leizhen Zhang
elf_open_ro() resolves two values that the ELF header can delegate to
section 0 -- the section count when e_shnum is SHN_UNDEF, and the section
name index when e_shstrndx is SHN_XINDEX -- and then looks up the string
table of the symbol table through its sh_link. None of the three is
checked, so each one indexes the section header table with a value taken
straight from the file.
Found by fuzzing the host ELF tools with mutated object files under
ASan/UBSan. Setting only the sh_link of the symbol table in an otherwise
valid vmlinux (the strtab index it resolves):
./kallsyms --sysmap=/dev/null t.elf
ERROR: AddressSanitizer: SEGV on unknown address
#0 in strncmp
#1 in sysmap_omits scripts/kallsyms-sysmap.c
-- elf->strtab points outside the mapping, and every symbol name is
then read through it.
With the preceding patch in place the header-supplied count and index
are already rejected before this point; the values that come out of
section 0 are not, which is what this adds, together with the sh_link of
the symbol table. elf_shnum_out_of_range() repeats the extent check from
elf_parse() against the count actually in use.
The reproducer now prints "symbol table has no string table" and exits
1, and a vmlinux and a shared object are still read unchanged.
This code is only in linux-next, from the "kbuild: significantly speed
up kernel builds" series, so there is no mainline commit to reference in
a Fixes tag.
Assisted-by: LLM ASan UBSan
Signed-off-by: Leizhen Zhang <lzsx618@gmail.com>
---
scripts/elf-parse.c | 31 +++++++++++++++++++++++++++++++
1 file changed, 31 insertions(+)
diff --git a/scripts/elf-parse.c b/scripts/elf-parse.c
index 0e16a9cf67f4..39ac25ddf188 100644
--- a/scripts/elf-parse.c
+++ b/scripts/elf-parse.c
@@ -256,6 +256,21 @@ void elf_unmap(void *addr, size_t size)
munmap(addr, size);
}
+/*
+ * True when the section header count does not fit in the mapping. The count
+ * can come from section 0, so it is not covered by the header check in
+ * elf_parse().
+ */
+static bool elf_shnum_out_of_range(const struct elf_file *elf)
+{
+ size_t shoff = (size_t)(elf->shdrs - (const char *)elf->base);
+
+ if (!elf->shnum || !elf->shentsize || shoff > elf->size)
+ return true;
+
+ return (elf->size - shoff) / elf->shentsize < elf->shnum;
+}
+
/*
* Open an ELF file and map it read-only, locating its section headers, section
* names and symbol table which are populated in the elf out parameter.
@@ -285,6 +300,17 @@ int elf_open_ro(char const *fname, uint32_t types, struct elf_file *elf)
shstrndx = ehdr_shstrndx(ehdr);
if (shstrndx == SHN_XINDEX)
shstrndx = shdr_link(first);
+
+ /*
+ * elf_parse() checked the count and the name index carried by the ELF
+ * header, but both can instead come from section 0, and that index is
+ * unchecked. Recheck whatever was actually used.
+ */
+ if (elf_shnum_out_of_range(elf) || shstrndx >= elf->shnum) {
+ fprintf(stderr, "%s: ELF section headers out of range\n", fname);
+ elf_close(elf);
+ return -1;
+ }
elf->shstrtab = (const char *)elf->base +
shdr_offset(elf_section(elf, shstrndx));
@@ -298,6 +324,11 @@ int elf_open_ro(char const *fname, uint32_t types, struct elf_file *elf)
return -1;
}
+ if (shdr_link(elf->symtab) >= elf->shnum) {
+ fprintf(stderr, "%s: symbol table has no string table\n", fname);
+ elf_close(elf);
+ return -1;
+ }
elf->strtab = (const char *)elf->base +
shdr_offset(elf_section(elf, shdr_link(elf->symtab)));
elf->nr_syms = shdr_size(elf->symtab) / shdr_entsize(elf->symtab);
--
2.34.1
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH 4/5] elf-parse: check the symbol table before walking it
2026-10-19 0:02 [PATCH 0/5] elf-parse: validate what the host ELF tools read from the file Leizhen Zhang
` (2 preceding siblings ...)
2026-10-19 0:02 ` [PATCH 3/5] elf-parse: check the section indices that elf_open_ro() takes from " Leizhen Zhang
@ 2026-10-19 0:02 ` Leizhen Zhang
2026-10-19 0:02 ` [PATCH 5/5] elf-parse: bound symbol names by the string table Leizhen Zhang
2026-10-19 12:57 ` [PATCH 0/5] elf-parse: validate what the host ELF tools read from the file Steven Rostedt
5 siblings, 0 replies; 9+ messages in thread
From: Leizhen Zhang @ 2026-10-19 0:02 UTC (permalink / raw)
To: nathan, nsc; +Cc: linux-kbuild, linux-kernel, rostedt, ljs, Leizhen Zhang
elf_open_ro() ends with
elf->nr_syms = shdr_size(elf->symtab) / shdr_entsize(elf->symtab);
on a section header taken from the file. A zero sh_entsize divides by
zero, and an sh_size that runs past the end of the mapping produces a
symbol count bounded by nothing: scripts/kallsyms-sysmap.c allocates
nr_syms entries up front and then reads every one of them.
Found by fuzzing the host ELF tools with mutated object files under
ASan/UBSan. Both reproducers change one field of the symbol table's
section header in an otherwise valid vmlinux:
sh_entsize = 0
./kallsyms --sysmap=/dev/null t.elf
scripts/elf-parse.c:266:40: runtime error: division by zero
#0 in elf_open_ro scripts/elf-parse.c:266
sh_size = 0x7fffffff0000
./kallsyms --sysmap=/dev/null t.elf
ERROR: AddressSanitizer: requested allocation size 0x7ffffffefff0
exceeds maximum supported size
#0 in malloc
#1 in xmalloc scripts/include/xalloc.h
#2 in elf_read_symbols scripts/kallsyms-sysmap.c:195
Require an entry size that can hold a symbol of the file's class, and a
table that lies inside the mapping. Both reproducers then print "symbol
table out of range" and exit 1, while a vmlinux and a shared object are
read unchanged.
One UBSan report in this area is deliberately not addressed here: an
unaligned sh_offset makes elf_symbol() form a misaligned Elf_Sym
pointer, which the header's accessors then dereference. Rejecting it on
alignment would also reject legitimate ELF32 objects, whose symbol
tables need only 4-byte alignment while the Elf_Sym union requires 8, so
that one needs the accessors to read unaligned instead of a check here.
This code is only in linux-next, from the "kbuild: significantly speed
up kernel builds" series, so there is no mainline commit to reference in
a Fixes tag.
Assisted-by: LLM ASan UBSan
Signed-off-by: Leizhen Zhang <lzsx618@gmail.com>
---
scripts/elf-parse.c | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/scripts/elf-parse.c b/scripts/elf-parse.c
index 39ac25ddf188..756843c433ee 100644
--- a/scripts/elf-parse.c
+++ b/scripts/elf-parse.c
@@ -271,6 +271,26 @@ static bool elf_shnum_out_of_range(const struct elf_file *elf)
return (elf->size - shoff) / elf->shentsize < elf->shnum;
}
+/*
+ * True when the symbol table cannot be walked inside the mapping: an entry
+ * size too small to hold a symbol divides by zero (or steps through the file
+ * in the wrong units) below, and a table running past the end of the file is
+ * read out of bounds -- its size also drives the allocation in the callers.
+ */
+static bool elf_symtab_out_of_range(const struct elf_file *elf)
+{
+ size_t offset = shdr_offset(elf->symtab);
+ size_t size = shdr_size(elf->symtab);
+ size_t entsize = shdr_entsize(elf->symtab);
+ size_t minsize = elf_map_long_size(elf->base) == 4 ?
+ sizeof(Elf32_Sym) : sizeof(Elf64_Sym);
+
+ if (entsize < minsize)
+ return true;
+
+ return offset > elf->size || elf->size - offset < size;
+}
+
/*
* Open an ELF file and map it read-only, locating its section headers, section
* names and symbol table which are populated in the elf out parameter.
@@ -324,6 +344,12 @@ int elf_open_ro(char const *fname, uint32_t types, struct elf_file *elf)
return -1;
}
+ if (elf_symtab_out_of_range(elf)) {
+ fprintf(stderr, "%s: symbol table out of range\n", fname);
+ elf_close(elf);
+ return -1;
+ }
+
if (shdr_link(elf->symtab) >= elf->shnum) {
fprintf(stderr, "%s: symbol table has no string table\n", fname);
elf_close(elf);
--
2.34.1
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH 5/5] elf-parse: bound symbol names by the string table
2026-10-19 0:02 [PATCH 0/5] elf-parse: validate what the host ELF tools read from the file Leizhen Zhang
` (3 preceding siblings ...)
2026-10-19 0:02 ` [PATCH 4/5] elf-parse: check the symbol table before walking it Leizhen Zhang
@ 2026-10-19 0:02 ` Leizhen Zhang
2026-10-19 12:57 ` [PATCH 0/5] elf-parse: validate what the host ELF tools read from the file Steven Rostedt
5 siblings, 0 replies; 9+ messages in thread
From: Leizhen Zhang @ 2026-10-19 0:02 UTC (permalink / raw)
To: nathan, nsc; +Cc: linux-kbuild, linux-kernel, rostedt, ljs, Leizhen Zhang
elf_read_symbols() builds every name as
const char *name = elf->strtab + sym_name(sym);
st_name is a 32-bit field read from the file and nothing bounds it, so a
single symbol can point the name anywhere, and sysmap_omits() then reads
a string from there.
Found by fuzzing the host ELF tools with mutated object files under
ASan/UBSan. Reproducer, setting only st_name of one object symbol in an
otherwise valid vmlinux (symbol 2 of its symbol table here, the first
one whose type makes the name be read):
./kallsyms --sysmap=/dev/null t.elf
ERROR: AddressSanitizer: SEGV on unknown address 0x7f54e308c740
#0 in strncmp
#1 in string_starts_with scripts/kallsyms.h:15
#2 in sysmap_omits scripts/kallsyms-sysmap.c:104
Record the size of the string table in struct elf_file and check, in
elf_open_ro(), that the table is inside the mapping and ends in a NUL.
A name that starts inside such a table also ends inside it, so a single
offset comparison is then enough, which elf_symbol_name() does; symbols
whose name is out of range are skipped like the other uninteresting ones
rather than failing the build, keeping the behaviour for every object a
toolchain actually produces identical.
scripts/sorttable.c reads symbol names the same way from its own
mapping, through elf_map() rather than elf_open_ro(). That path looks to
have the same gap, but every input I built was rejected earlier for
missing ORC tables, so I have no reproducer for it and have left it
alone rather than guess.
This code is only in linux-next, from the "kbuild: significantly speed
up kernel builds" series, so there is no mainline commit to reference in
a Fixes tag.
Assisted-by: LLM ASan UBSan
Signed-off-by: Leizhen Zhang <lzsx618@gmail.com>
---
scripts/elf-parse.c | 21 ++++++++++++++++++---
scripts/elf-parse.h | 14 ++++++++++++++
scripts/kallsyms-sysmap.c | 4 ++--
3 files changed, 34 insertions(+), 5 deletions(-)
diff --git a/scripts/elf-parse.c b/scripts/elf-parse.c
index 756843c433ee..8083b9ec3477 100644
--- a/scripts/elf-parse.c
+++ b/scripts/elf-parse.c
@@ -300,7 +300,8 @@ static bool elf_symtab_out_of_range(const struct elf_file *elf)
int elf_open_ro(char const *fname, uint32_t types, struct elf_file *elf)
{
Elf_Ehdr *ehdr;
- Elf_Shdr *first;
+ Elf_Shdr *first, *strtab;
+ size_t stroff;
unsigned int shstrndx, i;
memset(elf, 0, sizeof(*elf));
@@ -355,8 +356,22 @@ int elf_open_ro(char const *fname, uint32_t types, struct elf_file *elf)
elf_close(elf);
return -1;
}
- elf->strtab = (const char *)elf->base +
- shdr_offset(elf_section(elf, shdr_link(elf->symtab)));
+ strtab = elf_section(elf, shdr_link(elf->symtab));
+ stroff = shdr_offset(strtab);
+ elf->strtab_size = shdr_size(strtab);
+ elf->strtab = (const char *)elf->base + stroff;
+ /*
+ * Symbol names are read by offset into this table, so it has to be
+ * inside the mapping and has to end in a NUL for a name that starts
+ * inside it to end inside it as well.
+ */
+ if (stroff > elf->size || elf->size - stroff < elf->strtab_size ||
+ !elf->strtab_size || elf->strtab[elf->strtab_size - 1]) {
+ fprintf(stderr, "%s: symbol string table out of range\n",
+ fname);
+ elf_close(elf);
+ return -1;
+ }
elf->nr_syms = shdr_size(elf->symtab) / shdr_entsize(elf->symtab);
return 0;
diff --git a/scripts/elf-parse.h b/scripts/elf-parse.h
index c5ad754da254..548c80ead2bf 100644
--- a/scripts/elf-parse.h
+++ b/scripts/elf-parse.h
@@ -330,6 +330,7 @@ struct elf_file {
const char *shstrtab;
Elf_Shdr *symtab;
const char *strtab;
+ size_t strtab_size;
size_t nr_syms;
};
@@ -356,4 +357,17 @@ static inline Elf_Sym *elf_symbol(const struct elf_file *elf, size_t index)
index * shdr_entsize(elf->symtab));
}
+/*
+ * The name of a symbol, or NULL when st_name points outside the string table.
+ * elf_open_ro() has checked that the table is inside the mapping and ends in a
+ * NUL, so a name that starts inside it also ends inside it.
+ */
+static inline const char *elf_symbol_name(const struct elf_file *elf,
+ Elf_Sym *sym)
+{
+ size_t offset = sym_name(sym);
+
+ return offset < elf->strtab_size ? elf->strtab + offset : NULL;
+}
+
#endif /* _SCRIPTS_ELF_PARSE_H */
diff --git a/scripts/kallsyms-sysmap.c b/scripts/kallsyms-sysmap.c
index a482968eba51..efca13ab9dc3 100644
--- a/scripts/kallsyms-sysmap.c
+++ b/scripts/kallsyms-sysmap.c
@@ -198,10 +198,10 @@ static struct sysmap_symbol *elf_read_symbols(const struct elf_file *elf,
for (i = 1; i < elf->nr_syms; i++) {
Elf_Sym *sym = elf_symbol(elf, i);
- const char *name = elf->strtab + sym_name(sym);
+ const char *name = elf_symbol_name(elf, sym);
char type = elf_symbol_type(elf, sym);
- if (!type || sysmap_omits(name, type))
+ if (!name || !type || sysmap_omits(name, type))
continue;
syms[n].addr = sym_value(sym);
--
2.34.1
^ permalink raw reply [flat|nested] 9+ messages in thread* Re: [PATCH 0/5] elf-parse: validate what the host ELF tools read from the file
2026-10-19 0:02 [PATCH 0/5] elf-parse: validate what the host ELF tools read from the file Leizhen Zhang
` (4 preceding siblings ...)
2026-10-19 0:02 ` [PATCH 5/5] elf-parse: bound symbol names by the string table Leizhen Zhang
@ 2026-10-19 12:57 ` Steven Rostedt
5 siblings, 0 replies; 9+ messages in thread
From: Steven Rostedt @ 2026-10-19 12:57 UTC (permalink / raw)
To: Leizhen Zhang; +Cc: nathan, nsc, linux-kbuild, linux-kernel, ljs
On Sun, 18 Oct 2026 19:02:13 -0500
Leizhen Zhang <lzsx618@gmail.com> wrote:
> scripts/elf-parse.c takes the section header table, the section indices and
> the symbol table straight out of the file it maps, without checking any of
> them against the mapping. All three users -- kallsyms, sorttable and
> tracepoint-update -- then index with those values, so a truncated or
> corrupted object file makes the tools read outside the mapping, allocate
> from a file-supplied count, or divide by zero instead of printing an error.
>
> These are build-time tools fed by the local toolchain, so under
> Documentation/process/threat-model.rst this looks like a robustness problem
> rather than a vulnerability: reaching it takes a corrupt or hostile object
> in the build tree. I am leaving that classification for you to review.
Definitely not a security threat here. If the object files are corrupted,
then this is not where you should be detecting it. Unless we change the
purpose of this tool as a corruption detector, this code *expects* the
input to not be corrupt. The reasons for checking various ELF header info
is if someone is porting this code to a new architecture where it doesn't
support it yet, we want to warn.
I'm fine with adding robustness, but not complexity for an issue that
should never exist.
-- Steve
^ permalink raw reply [flat|nested] 9+ messages in thread