Skip to content

Add an x86_64 shared object linked at a non-zero base - #188

Open
zardus wants to merge 1 commit into
masterfrom
feature/relreloc-base
Open

Add an x86_64 shared object linked at a non-zero base#188
zardus wants to merge 1 commit into
masterfrom
feature/relreloc-base

Conversation

@zardus

@zardus zardus commented Aug 22, 2026

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Problem

Every ELF here that carries relative relocations is linked at zero, so an
object's load bias and its mapped base are the same number throughout and a
loader that confuses the two is correct on all of them. There is no input that
separates the two, and on master the path does not exist:

CLEFileNotFoundError: Could not find file .../tests/x86_64/relative_reloc_nonzero_base.so

Root cause

ld links a shared object at zero unless it is told otherwise, and nothing
here asks it to, so the only ET_DYN shapes in the repository are the ones
where linked_base == 0 hides the distinction.

Fix

Adds tests/x86_64/relative_reloc_nonzero_base.so, an ET_DYN whose lowest
PT_LOAD sits at 0x400000, carrying three R_X86_64_RELATIVE relocations
that fill a pointer table in .data.rel.ro. That is what Go's linker emits for
a PIE, in a few kilobytes. tests_src/relative_reloc_nonzero_base.c holds the
source and the one-line gcc invocation that builds it.

Testing

Read back, .rela.dyn holds three type-8 entries at r_offset 0x403ec0,
0x403ec8, 0x403ed0 with addends 0x401000, 0x401010, 0x401020. Loaded
by cle at master with base_addr=0x800000, the load bias is 0x400000 and the
three slots come back holding 0xc01000, 0xc01010, 0xc01020 where the
linked value plus the bias is 0x801000, 0x801010, 0x801020 — the
confusion the fixture separates. Used by tests/test_relative_relocations.py
in angr/cle#773, which fixes it and cannot be tested without this file.

Validation: #188 (comment)

session: sharpen

@zardus

zardus commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 732431847cfc595ae286831b4e2294ae41246fb1 against baseline 003e82a2bfa641530924055695b36cec8af483ab, for this fixture. The committed file at that head reads back at the size and sha256 recorded below.

What is here already

Every ELF file in the repository at 4bf754e was classified by header —
825 files, found by walking the tree and testing for the \x7fELF magic
(regular files only; 6 ELF symlinks are not counted separately from their
targets, and the ELF members of 3 ar archives are not counted at all).

files
ET_DYN, lowest PT_LOAD p_vaddr == 0 317
ET_EXEC, lowest PT_LOAD p_vaddr == 0 8
non-zero lowest PT_LOAD p_vaddr (320 ET_EXEC + 9 ET_CORE) 329
ET_REL, no PT_LOAD at all 171
total 825
  • Every ET_DYN here has its lowest PT_LOAD at 0. None is linked high.
  • 316 files carry at least one relative relocation, and every one of them is
    an ET_DYN linked at 0: x86-64 178, s390x 66, i386 29, ARM 13, MIPS 8,
    RISC-V 8, AArch64 7, PPC64 5, PPC 2.
  • All 329 files with a non-zero lowest PT_LOAD carry zero relative
    relocations.
  • DT_RELR/SHT_RELR does not appear anywhere: zero SHT_RELR sections and
    zero DT_RELR/DT_RELRSZ tags across all 825.

"Relative relocation" means a type whose name ends in _RELATIVE, plus
R_MIPS_REL32 and R_MICROBLAZE_REL. Counted three independent ways that agree
file-for-file: pyelftools over every SHT_REL/SHT_RELA section with a numeric
per-e_machine fallback for the architectures it has no type table for
(R_RISCV_RELATIVE would otherwise be missed), pyelftools over PT_DYNAMIC
(REL/RELA/JMPREL/RELR), and GNU readelf -r 2.46.

So nothing in this repository can distinguish a loader that adds the load bias
from one that adds the mapped base: on every file here the two are the same
number.

The new file

tests/x86_64/relative_reloc_nonzero_base.so, 13,896 bytes, read from the
committed file:

ELFCLASS64, little-endian, EM_X86_64, ET_DYN, e_flags=0x0, entry 0x0
sha256 d04c667ab3c67fad0d81ef284a6ebcef567ec580d65ece171cf33fcbb53dc10d
lowest PT_LOAD p_vaddr = 0x400000  (R, filesz/memsz 0x330)
.rela.dyn:
  offset=0x403ec0  R_X86_64_RELATIVE  addend=0x401000
  offset=0x403ec8  R_X86_64_RELATIVE  addend=0x401010
  offset=0x403ed0  R_X86_64_RELATIVE  addend=0x401020
.data.rel.ro holds `table` at 0x403ec0, 24 bytes, inside PT_GNU_RELRO

How it was built

tests_src/relative_reloc_nonzero_base.c is committed beside it:

__attribute__((noinline)) static int alpha(void) { return 1; }
__attribute__((noinline)) static int beta(void)  { return 2; }
__attribute__((noinline)) static int delta(void) { return 3; }
void *const table[] = {(void *)alpha, (void *)beta, (void *)delta};
gcc -shared -fPIC -nostdlib -Wl,-Ttext-segment=0x400000 -Wl,--build-id=none \
    -o relative_reloc_nonzero_base.so relative_reloc_nonzero_base.c

-nostdlib keeps the object down to the three relocations under test, and
--build-id=none keeps a build-id note out of it. The committed file has no
build root, user name or build-id in its strings.

Rebuilding from that recipe on a different toolchain reproduces the shape but
not the bytes: gcc 15.2.0 emits 13,848 bytes against the committed 13,896, with
the same ET_DYN, the same 0x400000 base and the same three
R_X86_64_RELATIVE relocations at the same addends. The committed file is the
one the test is pinned to; the recipe is there so a reviewer can confirm what it
is rather than regenerate it byte-for-byte.

What consumes it

tests/test_relative_relocations.py in angr/cle#773 loads it at its preferred
base and rebased to 0x1000000, and checks its pointer table against the
symbols the entries name. Before that change, CLE writes
mapped_base + addend rather than the load bias plus the addend, so table[0]
reads 0x801000 instead of 0x401000 and loader.find_symbol() on that word
returns None instead of alpha. Both cases of the test fail without the fix:

E   assert 8392704 == 4198400      # 0x801000 != 0x401000
E    +  where 4198400 = <Symbol "alpha" ... at 0x401000>.rebased_addr
E   assert 20975616 == 16781312    # 0x1401000 != 0x1001000
FAILED tests/test_relative_relocations.py::test_relative_relocation_at_the_linked_base
FAILED tests/test_relative_relocations.py::test_relative_relocation_rebased
2 failed, 1 passed in 0.36s

The one that still passes is the third test, a control that maps the existing
zero-linked tests/x86_64/libc.so.6 at 0x5000000 and asserts every relative
relocation there still resolves to mapped_base + addend. It passes on both
revisions by design — it exists to hold the case the fix must not move, and it
needs no new fixture.

Full measurement of the loader defect, including its effect across 234,056
real-world ELF objects, is in angr/cle#773 (comment).

Rebased 2026-09-04. Re-keyed onto binaries master 003e82a2bfa641530924055695b36cec8af483ab. The opening named 612c425d953eca6c94346aa8053ac6df4d118f95; the branch has since passed through 7394f29129110b962dde0a2dfdd1009ac3f14ef6 and is now at 732431847cfc595ae286831b4e2294ae41246fb1. git range-diff 8646be4eafa4f1fc285d787fb2b73426a5e11d19..612c425d953eca6c94346aa8053ac6df4d118f95 003e82a2bfa641530924055695b36cec8af483ab..732431847cfc595ae286831b4e2294ae41246fb1 reports the commit unchanged, the added/removed line lists of the two git diff <merge-base>..<head> outputs are byte-identical at 14 lines each, git diff --name-status is identical, and both touched paths have the same blob at both heads — tests/x86_64/relative_reloc_nonzero_base.so a3413066 and tests_src/relative_reloc_nonzero_base.c 02ef8d7f. Master touched neither path in the range rebased over (git diff --quiet <old-base> <new-base> -- <files> passes), so the size and sha256 recorded above are readings of exactly these bytes. The replay was plain: no conflict, no hand resolution, no fixup. The move was needed because a pull request that pins this one is checked out at its branch tip rather than merged with master, so being behind master was failing unrelated tests on the consumer — angr/cle#773 was failing tests/test_gopclntab.py and tests/test_macho.py on CLEFileNotFoundError for aarch64/langdetect_go.macho and aarch64/relocatable_object.macho, two fixtures that landed on binaries master after this branch was cut. This repository schedules no checks: at the new head both check-runs and actions/runs report total_count 0, as they do for every angr/binaries pull request.

@zardus
zardus force-pushed the feature/relreloc-base branch from a93c271 to 612c425 Compare August 26, 2026 22:46
@zardus

zardus commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

tests/x86_64/relative_reloc_nonzero_base.so read back with cle at master
(a4fb8003), against a checkout of angr/binaries master and against this
branch. The command is the same on both sides:

import cle
ld = cle.Loader("tests/x86_64/relative_reloc_nonzero_base.so",
                main_opts={"base_addr": 0x800000}, auto_load_libs=False)
m = ld.main_object
for r in m.relocs:
    print(hex(r.rebased_addr), hex(m.memory.unpack_word(r.relative_addr)),
          hex(r.addend - m.linked_base + m.mapped_base))

Before — the object is not in the repository:

angr/binaries master
tests/x86_64/relative_reloc_nonzero_base.so: not present in this checkout

After — the object loads, and because its linked base is 0x400000 rather
than zero, the value cle writes into each slot and the linked value plus the
load bias are two different numbers:

with this change
e_type=ET_DYN  lowest PT_LOAD p_vaddr=0x400000
.rela.dyn:
  r_offset 0x00403ec0  type 8                    r_addend 0x00401000
  r_offset 0x00403ec8  type 8                    r_addend 0x00401010
  r_offset 0x00403ed0  type 8                    r_addend 0x00401020

loaded at base 0x800000: linked_base 0x400000  mapped_base 0x800000  load bias 0x400000
pointer table in .data.rel.ro, as cle leaves it in memory:
  slot 0x00803ec0  cle wrote 0x00c01000   linked value + load bias = 0x00801000   WRONG
  slot 0x00803ec8  cle wrote 0x00c01010   linked value + load bias = 0x00801010   WRONG
  slot 0x00803ed0  cle wrote 0x00c01020   linked value + load bias = 0x00801020   WRONG

@zardus
zardus force-pushed the feature/relreloc-base branch 3 times, most recently from 9f1de95 to dd5ab9c Compare August 30, 2026 03:42
An ELF R_*_RELATIVE relocation writes the load bias plus its addend, and the
bias is mapped_base - linked_base. Every shared object and PIE in this
repository is linked at zero, so linked_base is zero throughout and the bias is
indistinguishable from the mapped base. A loader that confuses the two is
correct on all 304 of them.

relative_reloc_nonzero_base.so is linked with -Wl,-Ttext-segment=0x400000, so
its lowest PT_LOAD is at 0x400000 and its three relative relocations carry
addends of 0x401000, 0x401010 and 0x401020. It is the shape Go's linker emits
for a PIE, in 13 KB rather than 6 MB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@zardus
zardus force-pushed the feature/relreloc-base branch from 7394f29 to 7324318 Compare September 4, 2026 16:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant