[PATCH v2 2/2] elf: Add a zero terminator to the .eh_frame section of ld.so

Adrian Ratiu adrian.ratiu@collabora.com
Thu Oct 29 17:49:32 GMT 2026


ld.so is linked with -nostdlib -nostartfiles, so unlike other shared
objects it does not get the 4-byte zero terminator at the end of
.eh_frame from crtendS.o.

libc.so links elf/sofini.os last for this reason, but ld.so did not,
leaving its .eh_frame unterminated.

PT_GNU_EH_FRAME only provides the start of .eh_frame, so unwinders
which scan .eh_frame linearly rely on the terminator to find its end.
llvm-libunwind does this when a PC is not covered by the .eh_frame_hdr
search table, for example _start and _dl_start_user on x86_64 and i386,
which have no CFI (BZ #26854).

A backtrace from an ELF constructor of a SO reaches _dl_start_user
through _dl_init and the scan then continues past the end of .eh_frame.

This used to work by accident because the bytes following .eh_frame
in ld.so were zero padding. Since binutils 2.46 (commit 6f9157ad377e,
"ld/ELF: explicitly place .note.gnu.property"), GNU ld places
.note.gnu.property right after .eh_frame, and llvm-libunwind crashes
with SIGSEGV while reading it as frame records.

So we link sofini.os into ld.so after librtld.os.

sofini.os is now built as part of rtld, so that symbol-hacks.h does not
reference libc-internal __GI_* aliases, which ld.so cannot resolve. The
object only contains the terminator, so libc.so is unchanged by this.

This also shows that the reasoning in BZ #22053, which proposes removing
the terminator from libc.so because .eh_frame_hdr is always present,
does not fully hold: PT_GNU_EH_FRAME does not provide the size of
.eh_frame, so linear scans still need the terminator.

Also add tst-eh-frame-terminator-ldso and tst-eh-frame-terminator-libc
tests which check that ld.so and libc.so both end .eh_frame with a zero
terminator.

Tested on x86_64-linux-gnu with "make check subdirs=elf", and with an
aarch64-linux-gnu cross build, where both new tests now pass.
tst-eh-frame-terminator-ldso fails without the fix.

Co-authored-by: George Burgess IV <gbiv@google.com>
Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>
---
 elf/Makefile                   | 16 +++++++-
 elf/tst-eh-frame-terminator.py | 72 ++++++++++++++++++++++++++++++++++
 2 files changed, 86 insertions(+), 2 deletions(-)
 create mode 100644 elf/tst-eh-frame-terminator.py

diff --git a/elf/Makefile b/elf/Makefile
index 2c74e11946..ca78e67cf8 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -711,6 +711,17 @@ $(objpfx)tst-relro-libc.out: tst-relro-symbols.py $(..)/scripts/glibcelf.py \
 	    --required=__io_vtables \
 	  > $@ 2>&1; $(evaluate-test)
 
+tests-special += $(objpfx)tst-eh-frame-terminator-ldso.out \
+  $(objpfx)tst-eh-frame-terminator-libc.out
+$(objpfx)tst-eh-frame-terminator-ldso.out: tst-eh-frame-terminator.py \
+  $(..)/scripts/glibcelf.py $(objpfx)ld.so
+	$(PYTHON) tst-eh-frame-terminator.py $(objpfx)ld.so \
+	  > $@ 2>&1; $(evaluate-test)
+$(objpfx)tst-eh-frame-terminator-libc.out: tst-eh-frame-terminator.py \
+  $(..)/scripts/glibcelf.py $(common-objpfx)libc.so
+	$(PYTHON) tst-eh-frame-terminator.py $(common-objpfx)libc.so \
+	  > $@ 2>&1; $(evaluate-test)
+
 ifeq ($(run-built-tests),yes)
 tests-special += $(objpfx)tst-valgrind-smoke.out
 endif
@@ -1683,7 +1694,8 @@ generated += librtld.map librtld.mk rtld-libc.a librtld.os.map
 
 z-now-yes = -Wl,-z,now
 
-$(objpfx)ld.so: $(objpfx)librtld.os $(ld-map)
+# sofini.os must be placed last since it terminates .eh_frame section.
+$(objpfx)ld.so: $(objpfx)librtld.os $(objpfx)sofini.os $(ld-map)
 # Link into a temporary file so that we don't touch $@ at all
 # if the sanity check below fails.
 	$(LINK.o) -nostdlib -nostartfiles -shared -o $@.new		\
@@ -1786,7 +1798,7 @@ CFLAGS-dl-diagnostics.c += $(SYSCONF-FLAGS) \
   -D'PREFIX="$(prefix)"' \
   -D'RTLD="$(rtlddir)/$(rtld-installed-name)"'
 
-cpp-srcs-left := $(all-rtld-routines:=.os)
+cpp-srcs-left := $(all-rtld-routines:=.os) sofini.os
 lib := rtld
 include $(patsubst %,$(..)libof-iterator.mk,$(cpp-srcs-left))
 
diff --git a/elf/tst-eh-frame-terminator.py b/elf/tst-eh-frame-terminator.py
new file mode 100644
index 0000000000..f81ae45a02
--- /dev/null
+++ b/elf/tst-eh-frame-terminator.py
@@ -0,0 +1,72 @@
+#!/usr/bin/python3
+# Verify that the .eh_frame section ends with a zero terminator.
+# Copyright The GNU Toolchain Authors.
+# This file is part of the GNU C Library.
+#
+# The GNU C Library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# The GNU C Library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with the GNU C Library; if not, see
+# <https://www.gnu.org/licenses/>.
+
+"""Verify that the .eh_frame section of a shared object ends with a
+zero terminator.
+
+PT_GNU_EH_FRAME only gives the start of .eh_frame, so unwinders that
+scan .eh_frame linearly rely on the terminator to find its end.
+
+"""
+
+import os.path
+import sys
+
+# Make available glibc Python modules.
+sys.path.append(os.path.join(
+    os.path.dirname(os.path.realpath(__file__)), os.path.pardir, 'scripts'))
+
+import glibcelf
+
+def main(argv):
+    path, = argv
+    img = glibcelf.Image.readfile(path)
+    if img.ehdr.e_ident.ei_data == glibcelf.ElfData.ELFDATA2LSB:
+        byteorder = 'little'
+    else:
+        byteorder = 'big'
+
+    for shdr in img.shdrs():
+        if shdr.sh_name == b'.eh_frame':
+            break
+    else:
+        sys.stdout.write('{}: error: no .eh_frame section\n'.format(path))
+        sys.exit(1)
+
+    # Skip over the CIE and FDE records using their length fields, as
+    # a linear scan by an unwinder does, until a zero length.
+    data = img.image[shdr.sh_offset:shdr.sh_offset + shdr.sh_size]
+    offset = 0
+    while offset + 4 <= len(data):
+        length = int.from_bytes(data[offset:offset + 4], byteorder)
+        if length == 0:
+            break
+        if length == 0xffffffff:
+            # 64-bit DWARF: the actual length follows in 8 bytes.
+            length = 8 + int.from_bytes(data[offset + 4:offset + 12],
+                                        byteorder)
+        offset += 4 + length
+
+    if offset + 4 != len(data):
+        sys.stdout.write('{}: error: .eh_frame of size 0x{:x} does not end'
+                         ' with a zero terminator\n'.format(path, len(data)))
+        sys.exit(1)
+
+if __name__ == '__main__':
+    main(sys.argv[1:])
-- 
2.47.3



More information about the Libc-alpha mailing list