[PATCH v1 1/1] ld: microblaze: don't index the local symbol cache with a global symbol index
Neal Frager
neal.frager@amd.com
Mon Aug 10 06:00:11 GMT 2026
From: Sam Price <samuel.r.price@nasa.gov>
microblaze_elf_relax_section() was copied from sh_elf_relax_delete_bytes()
in 2009 (7ba29e2a41) but the copy dropped a bounds check that was already
present in the sh original. This adds the missing two-line guard from
bfd/elf32-sh.c:1227-1228, preventing an out-of-bounds read that silently
corrupts relocation addends.
The bug: when scanning relocations in other sections, the code indexes
isymbuf with a global symbol index without checking it against sh_info.
isymbuf (from symtab_hdr->contents) holds only sh_info local symbols, so
global symbol indices read past the end of the allocation.
If the out-of-bounds bytes happen to look like a local section symbol for
the section being relaxed, the relocation's addend is incorrectly adjusted,
losing 4 bytes per deleted IMM instruction. This corrupts structure member
accesses built at -O2:
lwi rD, r0, sym + offsetof(struct s, member)
This was found as a silent miscompilation in RTEMS where
Per_CPU_Control::executing was fetched as Per_CPU_Control::dispatch_necessary.
The fix copies the guard from bfd/elf32-sh.c:1227-1228, present since the
first binutils commit (252b5132c7, 1999-05-03).
Three tests are added (the first for this target), checking that addends
survive relaxation. relax-addend.d and relax-addend-data.d use
--gc-sections; relax-addend-eh.d uses .eh_frame editing (the route live on
current master).
Test behavior depends on how ld was built:
ld built normally all three pass with and without the fix, because
whether the out-of-bounds read corrupts the addend
depends on what is in the adjacent heap
ld built with ASan relax-addend-eh.d FAILS without the fix, because the
read itself is unconditional and ASan traps it, and
passes with the fix
ld/testsuite results on microblaze-elf are otherwise unchanged.
bfd/
* elf32-microblaze.c (microblaze_elf_relax_section): Skip
relocations against global symbols when scanning the relocations
of other sections.
ld/
* testsuite/ld-microblaze/microblaze.exp: New file.
* testsuite/ld-microblaze/relax-addend.s: New file.
* testsuite/ld-microblaze/relax-addend-support.s: New file.
* testsuite/ld-microblaze/relax-addend.ld: New file.
* testsuite/ld-microblaze/relax-addend.d: New test.
* testsuite/ld-microblaze/relax-addend-data.d: New test.
* testsuite/ld-microblaze/relax-addend-eh.s: New file.
* testsuite/ld-microblaze/relax-addend-eh-support.s: New file.
* testsuite/ld-microblaze/relax-addend-eh.ld: New file.
* testsuite/ld-microblaze/relax-addend-eh.d: New test.
Signed-off-by: Neal Frager <neal.frager@amd.com>
Signed-off-by: Sam Price <samuel.r.price@nasa.gov>
Assisted-by: Claude (Anthropic)
---
bfd/elf32-microblaze.c | 3 +
ld/testsuite/ld-microblaze/microblaze.exp | 28 +++++++
.../ld-microblaze/relax-addend-data.d | 17 +++++
.../ld-microblaze/relax-addend-eh-support.s | 16 ++++
ld/testsuite/ld-microblaze/relax-addend-eh.d | 27 +++++++
ld/testsuite/ld-microblaze/relax-addend-eh.ld | 9 +++
ld/testsuite/ld-microblaze/relax-addend-eh.s | 76 +++++++++++++++++++
.../ld-microblaze/relax-addend-support.s | 24 ++++++
ld/testsuite/ld-microblaze/relax-addend.d | 26 +++++++
ld/testsuite/ld-microblaze/relax-addend.ld | 23 ++++++
ld/testsuite/ld-microblaze/relax-addend.s | 55 ++++++++++++++
11 files changed, 304 insertions(+)
create mode 100644 ld/testsuite/ld-microblaze/microblaze.exp
create mode 100644 ld/testsuite/ld-microblaze/relax-addend-data.d
create mode 100644 ld/testsuite/ld-microblaze/relax-addend-eh-support.s
create mode 100644 ld/testsuite/ld-microblaze/relax-addend-eh.d
create mode 100644 ld/testsuite/ld-microblaze/relax-addend-eh.ld
create mode 100644 ld/testsuite/ld-microblaze/relax-addend-eh.s
create mode 100644 ld/testsuite/ld-microblaze/relax-addend-support.s
create mode 100644 ld/testsuite/ld-microblaze/relax-addend.d
create mode 100644 ld/testsuite/ld-microblaze/relax-addend.ld
create mode 100644 ld/testsuite/ld-microblaze/relax-addend.s
diff --git a/bfd/elf32-microblaze.c b/bfd/elf32-microblaze.c
index a52358a9a12..06cfb42cd6f 100644
--- a/bfd/elf32-microblaze.c
+++ b/bfd/elf32-microblaze.c
@@ -2084,6 +2084,9 @@ microblaze_elf_relax_section (bfd *abfd,
irelscanend = irelocs + o->reloc_count;
for (irelscan = irelocs; irelscan < irelscanend; irelscan++)
{
+ if (ELF32_R_SYM (irelscan->r_info) >= symtab_hdr->sh_info)
+ continue;
+
if ((ELF32_R_TYPE (irelscan->r_info) == (int) R_MICROBLAZE_32)
|| (ELF32_R_TYPE (irelscan->r_info) == (int) R_MICROBLAZE_32_NONE))
{
diff --git a/ld/testsuite/ld-microblaze/microblaze.exp b/ld/testsuite/ld-microblaze/microblaze.exp
new file mode 100644
index 00000000000..919fc2c2a49
--- /dev/null
+++ b/ld/testsuite/ld-microblaze/microblaze.exp
@@ -0,0 +1,28 @@
+# Expect script for MicroBlaze ELF linker tests.
+# Copyright (C) 2026 Free Software Foundation, Inc.
+#
+# This file is part of the GNU Binutils.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston,
+# MA 02110-1301, USA.
+
+if { ![istarget "microblaze*-*-*"] } {
+ return
+}
+
+foreach test [lsort [glob -nocomplain $srcdir/$subdir/*.d]] {
+ verbose [file rootname $test]
+ run_dump_test [file rootname $test]
+}
diff --git a/ld/testsuite/ld-microblaze/relax-addend-data.d b/ld/testsuite/ld-microblaze/relax-addend-data.d
new file mode 100644
index 00000000000..c4b5232d870
--- /dev/null
+++ b/ld/testsuite/ld-microblaze/relax-addend-data.d
@@ -0,0 +1,17 @@
+#source: relax-addend.s
+#source: relax-addend-support.s
+#as: -EL
+#ld: -EL -relax --gc-sections -T $srcdir/$subdir/relax-addend.ld
+#readelf: -x .checkdata
+#name: MicroBlaze relaxation preserves R_MICROBLAZE_32 addends
+
+# The same check for the R_MICROBLAZE_32 arm of the same loop, using a data
+# word rather than an instruction so that readelf alone can verify it.
+# .checkdata holds a single word initialised to gvar + 0x18; with .data pinned
+# by the linker script that is 0x90001018, little endian 18 10 00 90.
+# A linker which corrupts the addend stores 14 10 00 90.
+
+#...
+Hex dump of section '.checkdata':
+[ ]*0x90001100 18100090 .*
+#pass
diff --git a/ld/testsuite/ld-microblaze/relax-addend-eh-support.s b/ld/testsuite/ld-microblaze/relax-addend-eh-support.s
new file mode 100644
index 00000000000..d06df4b83e8
--- /dev/null
+++ b/ld/testsuite/ld-microblaze/relax-addend-eh-support.s
@@ -0,0 +1,16 @@
+ .section .text.zz_support,"ax",@progbits
+ .globl near_callee
+near_callee:
+ rtsd r15, 8
+ nop
+ .globl _start
+_start:
+ brlid r15, aaa_relaxed
+ nop
+ brlid r15, zzz_victim
+ nop
+ bri 0
+ .data
+ .globl gvar
+ .align 2
+gvar: .space 64
diff --git a/ld/testsuite/ld-microblaze/relax-addend-eh.d b/ld/testsuite/ld-microblaze/relax-addend-eh.d
new file mode 100644
index 00000000000..d061fd04fb1
--- /dev/null
+++ b/ld/testsuite/ld-microblaze/relax-addend-eh.d
@@ -0,0 +1,27 @@
+#source: relax-addend-eh.s
+#source: relax-addend-eh-support.s
+#as: -EL
+#ld: -EL -relax --gc-sections -T $srcdir/$subdir/relax-addend-eh.ld
+#objdump: -d
+#name: MicroBlaze relaxation preserves addends across .eh_frame editing
+
+# Same defect as relax-addend.d, reached the other way.
+#
+# _bfd_elf_discard_section_eh_frame installs a locals-only symbol cache in
+# symtab_hdr->contents when editing .eh_frame moves a local symbol defined
+# inside it; the generic ELF emulation then calls lang_relax_sections from the
+# same after_allocation. dead_fn is unreferenced, so --gc-sections drops it,
+# its FDE is removed, the section shrinks and ehlocal moves -- which is what
+# makes the cache appear. The linker script must KEEP .eh_frame or it is swept
+# and none of this happens.
+#
+# gvar lands at 0x90000074, so the reference to gvar + 0x18 must be 0x9000008c,
+# encoded as IMM 0x9000 followed by LWI with 0x008c. A linker which corrupts
+# the addend emits e8600088.
+
+.*: +file format .*
+#...
+9000001c <zzz_victim>:
+[ ]*9000001c:[ ]+b0009000[ ]+imm[ ]+-28672
+[ ]*90000020:[ ]+e860008c[ ]+lwi[ ]+r3, r0, 140
+#pass
diff --git a/ld/testsuite/ld-microblaze/relax-addend-eh.ld b/ld/testsuite/ld-microblaze/relax-addend-eh.ld
new file mode 100644
index 00000000000..c0d11cc9cb4
--- /dev/null
+++ b/ld/testsuite/ld-microblaze/relax-addend-eh.ld
@@ -0,0 +1,9 @@
+ENTRY(_start)
+SECTIONS
+{
+ . = 0x90000000;
+ .text : { *(.text) *(.text.*) }
+ .eh_frame : { KEEP (*(.eh_frame)) }
+ .data : { *(.data) *(.data.*) }
+ /DISCARD/ : { *(.comment) *(.note*) }
+}
diff --git a/ld/testsuite/ld-microblaze/relax-addend-eh.s b/ld/testsuite/ld-microblaze/relax-addend-eh.s
new file mode 100644
index 00000000000..5900f8cc956
--- /dev/null
+++ b/ld/testsuite/ld-microblaze/relax-addend-eh.s
@@ -0,0 +1,76 @@
+/* dead_fn is dropped by --gc-sections; its FDE is then removed from .eh_frame,
+ which shifts ehlocal and makes adjust_eh_frame_local_symbols() cache a
+ locals-only symbol buffer in symtab_hdr->contents. */
+ .section .text.dead,"ax",@progbits
+ .globl dead_fn
+ .type dead_fn,@function
+dead_fn:
+ rtsd r15, 8
+ nop
+ .size dead_fn, .-dead_fn
+
+ .section .text.aaa_relaxed,"ax",@progbits
+ .globl aaa_relaxed
+ .type aaa_relaxed,@function
+aaa_relaxed:
+ addik r1, r1, -28
+ swi r15, r1, 0
+ brlid r15, near_callee
+ nop
+ lwi r15, r1, 0
+ rtsd r15, 8
+ addik r1, r1, 28
+ .size aaa_relaxed, .-aaa_relaxed
+
+ .section .text.zzz_victim,"ax",@progbits
+ .globl zzz_victim
+ .type zzz_victim,@function
+zzz_victim:
+ lwi r3, r0, gvar+24
+ rtsd r15, 8
+ nop
+ .size zzz_victim, .-zzz_victim
+
+ .section .eh_frame,"a",@progbits
+/* Hand-assembled so that no label-difference expressions are used. MicroBlaze
+ GAS emits an R_MICROBLAZE_NONE marker for every resolved label difference,
+ which lands in .rela.eh_frame and trips the
+ BFD_ASSERT (cookie->rel->r_offset == ent->offset + 8) in
+ _bfd_elf_discard_section_eh_frame. Literal lengths and CIE pointers keep
+ .rela.eh_frame to just the two FDE initial-location relocations. */
+
+ /* CIE at 0x00, total 20 bytes */
+ .4byte 16 /* length */
+ .4byte 0 /* CIE id */
+ .byte 1 /* version */
+ .asciz "zR" /* augmentation */
+ .uleb128 1 /* code alignment factor */
+ .sleb128 -4 /* data alignment factor */
+ .byte 15 /* return address register */
+ .uleb128 1 /* augmentation data length */
+ .byte 0x00 /* FDE encoding: DW_EH_PE_absptr */
+ .byte 0x0c, 0x01, 0x00 /* DW_CFA_def_cfa r1, 0 */
+
+ /* FDE for dead_fn at 0x14, total 20 bytes. dead_fn is dropped by
+ --gc-sections, so this FDE is removed and everything after it moves. */
+ .4byte 16 /* length */
+ .4byte 0x18 /* CIE pointer: this field's offset - 0 */
+ .4byte dead_fn /* initial location <- the only reloc */
+ .4byte 8 /* address range */
+ .uleb128 0 /* augmentation data length */
+ .byte 0, 0, 0 /* padding to 20 bytes */
+
+ehlocal: /* local symbol inside .eh_frame; it is
+ this symbol moving that makes
+ adjust_eh_frame_local_symbols() cache
+ a locals-only symbol buffer */
+
+ /* FDE for aaa_relaxed at 0x28, total 20 bytes */
+ .4byte 16
+ .4byte 0x2c
+ .4byte aaa_relaxed /* <- the only other reloc */
+ .4byte 32
+ .uleb128 0
+ .byte 0, 0, 0
+
+ .4byte 0 /* terminator */
diff --git a/ld/testsuite/ld-microblaze/relax-addend-support.s b/ld/testsuite/ld-microblaze/relax-addend-support.s
new file mode 100644
index 00000000000..03d5eb0ffba
--- /dev/null
+++ b/ld/testsuite/ld-microblaze/relax-addend-support.s
@@ -0,0 +1,24 @@
+# Placed in .text.zz_support so that it is laid out after the sections of
+# relax-addend.s and the call from aaa_relaxed is a forward reference. A
+# backward reference is not relaxed and the test would not exercise anything.
+
+ .section .text.zz_support,"ax",@progbits
+ .globl near_callee
+ .type near_callee,@function
+near_callee:
+ rtsd r15, 8
+ nop
+ .size near_callee, .-near_callee
+
+ .globl _start
+_start:
+ brlid r15, aaa_relaxed
+ nop
+ brlid r15, zzz_victim
+ nop
+ bri 0
+
+ .data
+ .globl gvar
+ .align 2
+gvar: .space 64
diff --git a/ld/testsuite/ld-microblaze/relax-addend.d b/ld/testsuite/ld-microblaze/relax-addend.d
new file mode 100644
index 00000000000..951c8737696
--- /dev/null
+++ b/ld/testsuite/ld-microblaze/relax-addend.d
@@ -0,0 +1,26 @@
+#source: relax-addend.s
+#source: relax-addend-support.s
+#as: -EL
+#ld: -EL -relax --gc-sections -T $srcdir/$subdir/relax-addend.ld
+#objdump: -d
+#name: MicroBlaze relaxation preserves R_MICROBLAZE_64 addends
+
+# .text.aaa_relaxed contains an IMM that relaxation deletes. .text.zzz_victim,
+# a sibling section of the same object, refers to gvar + 0x18. The linker must
+# not let the deletion disturb that addend.
+#
+# The linker script pins .data, so gvar is at 0x90001000 and the reference must
+# resolve to 0x90001018, encoded as IMM 0x9000 followed by LWI with 0x1018.
+# A linker which corrupts the addend emits e8601014 instead.
+
+.*: +file format .*
+#...
+90000000 <aaa_relaxed>:
+[ ]*90000000:[ ]+3021ffe4[ ]+addik[ ]+r1, r1, -28
+[ ]*90000004:[ ]+f9e10000[ ]+swi[ ]+r15, r1, 0
+[ ]*90000008:[ ]+b9f40024[ ]+brlid[ ]+r15, 36
+#...
+9000001c <zzz_victim>:
+[ ]*9000001c:[ ]+b0009000[ ]+imm[ ]+-28672
+[ ]*90000020:[ ]+e8601018[ ]+lwi[ ]+r3, r0, 4120
+#pass
diff --git a/ld/testsuite/ld-microblaze/relax-addend.ld b/ld/testsuite/ld-microblaze/relax-addend.ld
new file mode 100644
index 00000000000..ca4d3603989
--- /dev/null
+++ b/ld/testsuite/ld-microblaze/relax-addend.ld
@@ -0,0 +1,23 @@
+/* Fixed layout so that the expected dump is deterministic. gvar is the first
+ thing in .data, which is pinned, so the reference to gvar + 0x18 has a known
+ value regardless of how the sections happen to be ordered. */
+ENTRY(_start)
+SECTIONS
+{
+ . = 0x90000000;
+ .text : {
+ *(.text.aaa_relaxed)
+ *(.text.zzz_victim)
+ *(.text.zz_support)
+ *(.text)
+ *(.text.*)
+ }
+ . = 0x90001000;
+ .data : {
+ *(.data)
+ *(.data.*)
+ }
+ . = 0x90001100;
+ .checkdata : { KEEP (*(.checkdata)) }
+ /DISCARD/ : { *(.comment) *(.note*) }
+}
diff --git a/ld/testsuite/ld-microblaze/relax-addend.s b/ld/testsuite/ld-microblaze/relax-addend.s
new file mode 100644
index 00000000000..16e500170fb
--- /dev/null
+++ b/ld/testsuite/ld-microblaze/relax-addend.s
@@ -0,0 +1,55 @@
+# Linker relaxation must not disturb the addend of an R_MICROBLAZE_64
+# relocation that refers to a symbol outside the section being relaxed.
+#
+# .text.aaa_relaxed contains an IMM that relaxation deletes. .text.zzz_victim,
+# a sibling section in the same object file, contains
+#
+# R_MICROBLAZE_64 gvar + 0x18
+#
+# While relaxing .text.aaa_relaxed, microblaze_elf_relax_section() walks the
+# relocations of every other section of the same BFD and, for R_MICROBLAZE_64,
+# indexes isymbuf with ELF32_R_SYM (irelscan->r_info) without checking it
+# against symtab_hdr->sh_info. gvar is global, so its symbol index is >=
+# sh_info and the read is out of bounds -- isymbuf holds only sh_info entries
+# once --gc-sections has installed the locals-only cache. If the bytes read
+# happen to satisfy
+#
+# isym->st_shndx == shndx && ELF32_ST_TYPE (isym->st_info) == STT_SECTION
+#
+# the addend is decremented by calc_fixup(), and gvar + 0x18 links as
+# gvar + 0x14.
+#
+# The out-of-bounds read itself is unconditional; run ld under ASan or
+# valgrind to observe it. Whether it corrupts the addend depends on what is
+# in the adjacent heap, so the check below is a correctness assertion rather
+# than a reliable trigger.
+
+ .section .text.aaa_relaxed,"ax",@progbits
+ .globl aaa_relaxed
+ .type aaa_relaxed,@function
+aaa_relaxed:
+ addik r1, r1, -28
+ swi r15, r1, 0
+ brlid r15, near_callee # IMM + BRLID; the IMM is deleted
+ nop
+ lwi r15, r1, 0
+ rtsd r15, 8
+ addik r1, r1, 28
+ .size aaa_relaxed, .-aaa_relaxed
+
+ .section .text.zzz_victim,"ax",@progbits
+ .globl zzz_victim
+ .type zzz_victim,@function
+zzz_victim:
+ lwi r3, r0, gvar+24 # IMM + LWI; R_MICROBLAZE_64 gvar+0x18
+ rtsd r15, 8
+ nop
+ .size zzz_victim, .-zzz_victim
+
+/* A data reference to the same symbol with the same non-zero addend. This
+ goes through the R_MICROBLAZE_32 arm of the same loop, and unlike the
+ instruction above it can be checked with readelf alone. */
+ .section .checkdata,"aw",@progbits
+ .globl check_word
+check_word:
+ .4byte gvar + 0x18
--
2.25.1
More information about the Binutils
mailing list