All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/2] module: close two ELF section-validation gaps
@ 2026-09-16 17:47 Fang Xieyan
  2026-09-16 17:47 ` [PATCH 1/2] module: Sanitize the undefined sh_name of SHT_NULL sections Fang Xieyan
  2026-09-16 17:47 ` [PATCH 2/2] module: Validate the __version_ext_names section offset Fang Xieyan
  0 siblings, 2 replies; 4+ messages in thread
From: Fang Xieyan @ 2026-09-16 17:47 UTC (permalink / raw)
  To: mcgrof, petr.pavlu, da.gomez, samitolvanen, atomlin
  Cc: mmaurer, masahiroy, linux-modules, linux-kernel, stable

Both patches come from the same review of the module ELF validator, and
both fix an out-of-bounds read of a section field that the validator
leaves unchecked on purpose.

elf_validity_check_sectionheaders() exempts SHT_NULL and SHT_NOBITS
sections from its bounds checks, on the assumption that a section with
no contents has nothing to dereference. The loaders that run after it do
not share that assumption: they read sh_name and sh_offset for every
section, so an exempted section carries an unvalidated field into a
string lookup or a pointer computation. Commit 9a5ff4568932 ("module:
validate string table section types") closed two such gaps for
.shstrtab and .strtab; these two patches close the ones that remain.

Patch 1 gives the undefined sh_name of a SHT_NULL section a safe value,
so the section walkers that follow read the empty string instead of
running past .shstrtab.

Patch 2 validates the sh_offset of the __version_ext_names section
before elf_validity_cache_index_versions() dereferences it, so a
SHT_NOBITS section of that name cannot point the extended version name
walk outside the module image.

Each patch stands alone and can be applied or backported separately;
they are grouped only because they share a root cause. Both were
reproduced on 704340f1cd0d with KASAN, and each is fixed by its own
change.

Fang Xieyan (2):
  module: Sanitize the undefined sh_name of SHT_NULL sections
  module: Validate the __version_ext_names section offset

 kernel/module/main.c | 23 +++++++++++++++++++++--
 1 file changed, 21 insertions(+), 2 deletions(-)

-- 
2.50.1 (Apple Git-155)


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [PATCH 1/2] module: Sanitize the undefined sh_name of SHT_NULL sections
  2026-09-16 17:47 [PATCH 0/2] module: close two ELF section-validation gaps Fang Xieyan
@ 2026-09-16 17:47 ` Fang Xieyan
  2026-09-16 17:47 ` [PATCH 2/2] module: Validate the __version_ext_names section offset Fang Xieyan
  1 sibling, 0 replies; 4+ messages in thread
From: Fang Xieyan @ 2026-09-16 17:47 UTC (permalink / raw)
  To: mcgrof, petr.pavlu, da.gomez, samitolvanen, atomlin
  Cc: mmaurer, masahiroy, linux-modules, linux-kernel, stable

elf_validity_cache_secstrings() skips the sh_name bounds check for a
SHT_NULL section:

	for (i = 0; i < info->hdr->e_shnum; i++) {
		shdr = &info->sechdrs[i];
		/* SHT_NULL means sh_name has an undefined value */
		if (shdr->sh_type == SHT_NULL)
			continue;
		if (shdr->sh_name >= strhdr->sh_size) {

A SHT_NULL section may then carry an sh_name past the end of
.shstrtab. The name lookups that run afterwards do not skip SHT_NULL.
This is find_any_unique_sec(), which load_module() uses to locate
".modinfo":

	for (i = 1; i < info->hdr->e_shnum; i++) {
		if (strcmp(info->secstrings + info->sechdrs[i].sh_name,
			   name) == 0) {

so the out-of-bounds sh_name is read as a string. A module carrying a
crafted SHT_NULL section reads past its in-memory copy:

  BUG: KASAN: vmalloc-out-of-bounds in strcmp+0xb0/0xc0
  Read of size 1 at addr ffa00000005436e0 by task insmod/81
  ...
   strcmp+0xb0/0xc0
   find_any_unique_sec+0x103/0x190
   load_module+0x5c4/0x8600

sh_name has no defined value for SHT_NULL, so give it one that is in
bounds: the empty string at index 0. The walkers then compare against
"" and, as before, ignore the section, so no valid module is affected.

Fixes: 3c5700aeabd8 ("module: Factor out elf_validity_cache_secstrings")
Cc: stable@vger.kernel.org
Assisted-by: Hawkeye:GLM-5.3-flash
Assisted-by: Qoder:Qwen3.8-Max
Signed-off-by: Fang Xieyan <fangxy@xiaopeng.com>
---

Found by reading the section-name walkers in kernel/module/main.c after
9a5ff4568932 tightened the string-table types. elf_validity_cache_secstrings()
skips the sh_name bounds check for a SHT_NULL section, but find_any_unique_sec()
still resolves every section's name as secstrings + sh_name, so the skip leaves
one path where an unchecked sh_name is read as a string. Patch 2/2 fixes the
matching SHT_NOBITS gap on __version_ext_names; the two are independent.

Reproducer: build an otherwise valid .ko and add a SHT_NULL section whose
sh_name is past the end of .shstrtab (0x2000 in the run below, aimed at the
redzone after the in-memory module copy). insmod it. Before the change the name
lookup reads out of bounds; after it the SHT_NULL section resolves to "" and, as
before, is ignored, so the module loads with rc=0.

Both cases ran on 704340f1cd0d (v7.3-rc4): x86_64 defconfig plus
CONFIG_KASAN_GENERIC and CONFIG_KASAN_VMALLOC, gcc 13.2.0, QEMU under TCG. The
unpatched and patched kernels are built from byte-identical .config files and
differ only by this patch. The tree already contains 9a5ff4568932, so this is
the gap that commit left, not a re-report of it.

One setup detail is not obvious. The payload has to match the kernel's
vermagic to reach load_module()'s name walkers, so the patched kernel is built
with LOCALVERSION pinned to keep the release string byte-identical to the
unpatched one; otherwise insmod fails on the version magic before the buggy
lookup ever runs and the run proves nothing.

 kernel/module/main.c | 13 +++++++++++--
 1 file changed, 11 insertions(+), 2 deletions(-)

diff --git a/kernel/module/main.c b/kernel/module/main.c
index d0e1e0b..e36bfe4 100644
--- a/kernel/module/main.c
+++ b/kernel/module/main.c
@@ -2060,9 +2060,18 @@ static int elf_validity_cache_secstrings(struct load_info *info)
 
 	for (i = 0; i < info->hdr->e_shnum; i++) {
 		shdr = &info->sechdrs[i];
-		/* SHT_NULL means sh_name has an undefined value */
-		if (shdr->sh_type == SHT_NULL)
+		/*
+		 * SHT_NULL means sh_name has an undefined value. The section
+		 * name walkers that follow (find_any_unique_sec(),
+		 * module_mark_ro_after_init(), ...) look the name up as
+		 * secstrings + sh_name for every section, so give the undefined
+		 * value a safe in-bounds meaning instead of skipping the check:
+		 * the empty string at index 0.
+		 */
+		if (shdr->sh_type == SHT_NULL) {
+			shdr->sh_name = 0;
 			continue;
+		}
 		if (shdr->sh_name >= strhdr->sh_size) {
 			pr_err("Invalid ELF section name in module (section %u type %u)\n",
 			       i, shdr->sh_type);
-- 
2.50.1 (Apple Git-155)


^ permalink raw reply related	[flat|nested] 4+ messages in thread

* [PATCH 2/2] module: Validate the __version_ext_names section offset
  2026-09-16 17:47 [PATCH 0/2] module: close two ELF section-validation gaps Fang Xieyan
  2026-09-16 17:47 ` [PATCH 1/2] module: Sanitize the undefined sh_name of SHT_NULL sections Fang Xieyan
@ 2026-09-16 17:47 ` Fang Xieyan
  2026-09-16 18:05   ` sashiko-bot
  1 sibling, 1 reply; 4+ messages in thread
From: Fang Xieyan @ 2026-09-16 17:47 UTC (permalink / raw)
  To: mcgrof, petr.pavlu, da.gomez, samitolvanen, atomlin
  Cc: mmaurer, masahiroy, linux-modules, linux-kernel, stable

elf_validity_check_sectionheaders() validates the size and offset of
every section, unless its type is SHT_NULL or SHT_NOBITS:

		switch (shdr->sh_type) {
		case SHT_NULL:
		case SHT_NOBITS:
			/* No contents, offset/size don't mean anything */
			continue;
		default:
			err = validate_section_offset(info, shdr);

elf_validity_cache_index_versions() then reads the extended version
names by their sh_offset, without checking that the section holds
data:

	if (vers_ext_crc) {
		crc_count = info->sechdrs[vers_ext_crc].sh_size / sizeof(u32);
		name = (void *)info->hdr +
			info->sechdrs[vers_ext_name].sh_offset;
		remaining_len = info->sechdrs[vers_ext_name].sh_size;
		while (crc_count--) {
			name_size = strnlen(name, remaining_len) + 1;

A __version_ext_names section of type SHT_NOBITS reaches here with an
unvalidated sh_offset, so the name lookup reads past the in-memory
copy of the module:

  BUG: KASAN: vmalloc-out-of-bounds in strnlen+0x73/0x80
  Read of size 1 at addr ffa00000005534ff by task insmod/79
  ...
   strnlen+0x73/0x80
   load_module+0xef6/0x8600
  The buggy address belongs to a 43-page vmalloc region starting at
  0xffa0000000529000 allocated at kernel_read_file+0x7b4/0x9f0

A real __version_ext_names section is SHT_PROGBITS and has already
passed validate_section_offset(), so this only rejects a SHT_NOBITS
section, which holds no names to read. Validate the offset before the
dereference.

Fixes: 54ac1ac8edeb ("modules: Support extended MODVERSIONS info")
Cc: stable@vger.kernel.org
Assisted-by: Hawkeye:GLM-5.3-flash
Assisted-by: Qoder:Qwen3.8-Max
Signed-off-by: Fang Xieyan <fangxy@xiaopeng.com>
---

Found in the same review as patch 1/2. elf_validity_check_sectionheaders()
exempts SHT_NOBITS from validate_section_offset(), and
elf_validity_cache_index_versions() then dereferences the __version_ext_names
section as hdr + sh_offset without re-checking it, so a section of that name and
type SHT_NOBITS carries an unvalidated offset into the extended version name
walk. This patch stands on its own; it does not depend on patch 1/2.

Reproducer: build a .ko that carries __version_ext_crcs and a
__version_ext_names section of type SHT_NOBITS whose sh_offset points past the
end of the module image. insmod it. Before the change the name walk reads out of
bounds; after it validate_section_offset() rejects the section and insmod fails
with -ENOEXEC (rc=8, "invalid module format"), which is what a module with a
malformed version section should do.

Both cases ran on 704340f1cd0d (v7.3-rc4): x86_64 defconfig plus
CONFIG_KASAN_GENERIC and CONFIG_KASAN_VMALLOC, gcc 13.2.0, QEMU under TCG. The
unpatched and patched kernels are built from byte-identical .config files and
differ only by this patch.

The same vermagic caveat as patch 1/2 applies: LOCALVERSION is pinned so the
patched kernel's release string matches the payload, and the loader reaches the
version walk instead of stopping at the version magic check.

 kernel/module/main.c | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/kernel/module/main.c b/kernel/module/main.c
index e36bfe4..adaaaca 100644
--- a/kernel/module/main.c
+++ b/kernel/module/main.c
@@ -2296,6 +2296,16 @@ static int elf_validity_cache_index_versions(struct load_info *info, int flags)
 	 * number of entries in every section.
 	 */
 	if (vers_ext_crc) {
+		/*
+		 * The name section is walked by its sh_offset and sh_size
+		 * below. elf_validity_check_sectionheaders() exempts SHT_NOBITS
+		 * sections from validate_section_offset(), so a __version_ext_names
+		 * section of that type reaches here with an unvalidated sh_offset.
+		 * Bound it before dereferencing hdr + sh_offset.
+		 */
+		if (validate_section_offset(info, &info->sechdrs[vers_ext_name]))
+			return -ENOEXEC;
+
 		crc_count = info->sechdrs[vers_ext_crc].sh_size / sizeof(u32);
 		name = (void *)info->hdr +
 			info->sechdrs[vers_ext_name].sh_offset;
-- 
2.50.1 (Apple Git-155)


^ permalink raw reply related	[flat|nested] 4+ messages in thread

* Re: [PATCH 2/2] module: Validate the __version_ext_names section offset
  2026-09-16 17:47 ` [PATCH 2/2] module: Validate the __version_ext_names section offset Fang Xieyan
@ 2026-09-16 18:05   ` sashiko-bot
  0 siblings, 0 replies; 4+ messages in thread
From: sashiko-bot @ 2026-09-16 18:05 UTC (permalink / raw)
  To: Fang Xieyan; +Cc: linux-modules

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] module: allows reading arbitrary file data for SHT_NOBITS sections
--

commit 7826bfa932aee0181b2db001dad06d62e3bc7702
Author: Fang Xieyan <fangxy@xiaopeng.com>
module: Validate the __version_ext_names section offset

This commit bounds the offset of the __version_ext_names section to prevent
an out-of-bounds read when encountering a SHT_NOBITS section. It adds a call
to validate_section_offset() in elf_validity_cache_index_versions() before
dereferencing the section's offset.

> diff --git a/kernel/module/main.c b/kernel/module/main.c
> index e36bfe4734d59..adaaaca5a4e68 100644
> --- a/kernel/module/main.c
> +++ b/kernel/module/main.c
> @@ -2296,6 +2296,16 @@ static int elf_validity_cache_index_versions(struct load_info *info, int flags)
>  	 * number of entries in every section.
>  	 */
>  	if (vers_ext_crc) {
> +		/*
> +		 * The name section is walked by its sh_offset and sh_size
> +		 * below. elf_validity_check_sectionheaders() exempts SHT_NOBITS
> +		 * sections from validate_section_offset(), so a __version_ext_names
> +		 * section of that type reaches here with an unvalidated sh_offset.
> +		 * Bound it before dereferencing hdr + sh_offset.
> +		 */
> +		if (validate_section_offset(info, &info->sechdrs[vers_ext_name]))
> +			return -ENOEXEC;

[Severity: Low]
Does this allow in-bounds SHT_NOBITS sections to pass, reading arbitrary
in-bounds file data as extended version names?

This would appear to violate ELF semantics since SHT_NOBITS sections are
not supposed to hold file data.

> +
>  		crc_count = info->sechdrs[vers_ext_crc].sh_size / sizeof(u32);
>  		name = (void *)info->hdr +
>  			info->sechdrs[vers_ext_name].sh_offset;

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260916174753.14870-1-fangxy@xiaopeng.com?part=2

^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-09-16 18:05 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-16 17:47 [PATCH 0/2] module: close two ELF section-validation gaps Fang Xieyan
2026-09-16 17:47 ` [PATCH 1/2] module: Sanitize the undefined sh_name of SHT_NULL sections Fang Xieyan
2026-09-16 17:47 ` [PATCH 2/2] module: Validate the __version_ext_names section offset Fang Xieyan
2026-09-16 18:05   ` sashiko-bot

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.