Skip to content

Resolve relocation symbols through sh_link - #59

Open
fornwall wants to merge 2 commits into
mainfrom
fix-relocation-sh-link
Open

Resolve relocation symbols through sh_link#59
fornwall wants to merge 2 commits into
mainfrom
fix-relocation-sh-link

Conversation

@fornwall

@fornwall fornwall commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Fixes #56.

ElfRelocation#getSymbol() and ElfRelocationAddend#getSymbol() resolved the symbol index against ElfFile#getSymbolTableSection(), i.e. the first SHT_SYMTAB section of the file. A relocation entry symbol index is instead relative to the symbol table given by the sh_link field of the section containing it.

For dynamic relocation sections (.rel.dyn, .rela.dyn, .rel.plt, .rela.plt), which link to .dynsym, this meant:

  • the wrong symbol was returned when the file also has a .symtab — e.g. for android_arm_libncurses, the first .rel.plt entry resolved to $a rather than __cxa_atexit;
  • stripped files with no .symtab, such as linux_amd64_bindash, failed with a NullPointerException.

Changes

  • Resolve relocation symbols through the symbol table selected by the containing section sh_link.
  • Expose getSymbolTableSection() on relocation entries and sections, with ElfException errors for invalid links or symbol indexes.
  • Resolve symbol names through the string table selected by the containing symbol table sh_link.
  • Represent ELF unsigned 16-bit and 32-bit fields with wider Java types, including extended ELF header counts and unsigned address lookup.
  • Validate section and program-header table entry sizes and complete table extents before allocating parser state. Known-size byte-array and mapped-file backings reject tables extending past the file; unknown-size custom backings reject oversized extended tables.
  • Add tests for dynamic and static relocation tables, invalid links and indexes, unsigned ELF32/ELF64 fields, extended header counts, linked string tables, and malformed table bounds.

Relocations in relocatable object files still link to .symtab and resolve as before.

Compatibility

This PR intentionally changes the public API and JVM ABI to represent unsigned ELF fields correctly. Public fields including ElfFile.e_machine, ElfFile.e_flags, ElfSectionHeader.sh_type, ElfSectionHeader.sh_link, ElfSegment.p_type, and several ElfSymbol and ElfNote fields change from short or int to int or long. ElfRelocation#getSymbolIndex() and ElfRelocationAddend#getSymbolIndex() change their return type from int to long.

Existing binaries compiled against 0.12.0 can encounter NoSuchFieldError or NoSuchMethodError, and some source consumers will require narrowing conversions or updated switch code. Consumers must recompile and adapt to the widened types; the next release containing this change must be treated as API-incompatible.

🤖 Generated with Claude Code and OpenAI Codex

@fornwall

fornwall commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

Follow-up commit b3b5a0b after an adversarial review pass, hardening the error handling in ElfSymbolTableSection.linkedFrom():

  • An out-of-range sh_link (e.g. >= e_shnum, or a uint32 with the top bit set read as a negative int) escaped as an ArrayIndexOutOfBoundsException from the section array instead of an ElfException. The range is now validated alongside the existing zero check.
  • Building the ElfException message could itself throw a ClassCastException: it included ElfSectionHeader.toString(), which resolves the section name through e_shstrndx — but a file with a malformed sh_link may equally lack a section name string table (e_shstrndx == SHN_UNDEF, section 0 not being a string table). The messages now describe the sections by sh_type instead of by name.
  • Added @throws ElfException javadoc to the getSymbolTableSection() and getSymbol() methods, and a test patching the .rela.plt sh_link of the linux_amd64_bindash test file to each of the invalid cases.

Checked and found not to be problems: no public API/ABI break (the changed constructors are package-private and take the package-private ElfParser), no infinite recursion or MemoizedObject re-entrancy, no performance regression (getSection(int) is O(1), where the old path did a linear scan on first use), and relocations in ET_REL object files still resolve through .symtab as before.

🤖 Generated with Claude Code

@dwalluck

dwalluck commented Aug 1, 2026

Copy link
Copy Markdown
Contributor
  • An out-of-range sh_link (e.g. >= e_shnum, or a uint32 with the top bit set read as a negative int) escaped as an ArrayIndexOutOfBoundsException from the section array instead of an ElfException. The range is now validated alongside the existing zero check.

@fornwall There's a bigger issue in the code related to the sizes of types and masking so that the values stay positive and not negative. If you have tokens, have it look at:

Elf{32,64}_Half u16 -> int
Elf{32,64}_Section u16 -> int
Elf{32,64}_Word u32 -> long
Elf{32,64}_Sword i32 -> int
Elf{32,64}_Xword/Sxword u64 -> long
Elf{32,64}_Addr/Off u32/64 -> long

Maybe prompting it to make sure ELF64 is supported would automatically check all of the above. I am not sure. But, I definitely see issues where fields are short and need to be int, or int and need to be long.

You can also look at:

adding range guards (like above)
places where long sizes have to be downcast to int for use in, for example, an array index (usually safe already)

public ElfSymbol getSymbol() {
return elfFile.getSymbolTableSection().symbols[getSymbolIndex()];
public ElfSymbol getSymbol() throws ElfException {
return getSymbolTableSection().symbols[getSymbolIndex()];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add guard here:

        ElfSymbolTableSection symbolTable = getSymbolTableSection();
        int symbolIndex = getSymbolIndex();

        if (symbolIndex < 0 || symbolIndex >= symbolTable.symbols.length) {
            throw new ElfException("Relocation symbol index out of range");
        }

        return symbolTable.symbols[symbolIndex];

public ElfSymbol getSymbol() {
return elfFile.getSymbolTableSection().symbols[getSymbolIndex()];
public ElfSymbol getSymbol() throws ElfException {
return getSymbolTableSection().symbols[getSymbolIndex()];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same guard here:

        int symbolIndex = getSymbolIndex();

        if (symbolIndex < 0 || symbolIndex >= symbolTable.symbols.length) 

@@ -23,4 +23,31 @@ public class ElfSymbolTableSection extends ElfSection {
symbols[i] = new ElfSymbol(parser, symbolOffset, header.sh_type);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommend changing API here to:

symbols[i] = new ElfSymbol(parser, symbolOffset, header);

Then store the header in the ELFSymbol class:

ElfSymbol(ElfParser parser, long offset, ElfSectionHeader sectionHeader) {
    this.sectionHeader = sectionHeader;
    this.section_type = sectionHeader.sh_type;
}

public String getName() throws ElfException {
    // Check to make sure this symbol has a name.
     if (st_name == 0) return null;
     return getStringTableSection().get(st_name);
}

public ElfStringTable getStringTableSection() throws ElfException {
    return ElfStringTable.linkedFrom(elfHeader, sectionHeader);
 }

Can also add such a method ElfSymbolTableSection::getStringTableSection() to this class.

@fornwall
fornwall force-pushed the fix-relocation-sh-link branch 2 times, most recently from 01c039c to 9b4a64f Compare August 10, 2026 17:21
Resolve relocation symbol indexes through the symbol table selected by the containing section sh_link, and resolve symbol names through their linked string tables.

Represent unsigned ELF fields with wider Java types, handle extended header counts, and validate table sizes, indexes, and file bounds before allocation or lookup.

Fixes #56.

Signed-off-by: Fredrik Fornwall <fredrik@fornwall.net>
@fornwall
fornwall force-pushed the fix-relocation-sh-link branch from 9b4a64f to 909f901 Compare August 10, 2026 19:15
Throw instead of silently misparsing, and stop rejecting files that can
still be read:

- Reading the extended header counts requires an initial section header
  to read them from, so throw when e_shoff is zero instead of parsing the
  ELF header itself as a section header.
- Throw an ElfException instead of leaking a ClassCastException when the
  section pointed out by e_shstrndx is not a string table, which includes
  it being SHN_UNDEF.
- Ignore a trailing partial entry in symbol, relocation and dynamic
  sections instead of rejecting the whole section, as only whole entries
  are read anyway. The entry size is still required to be non-zero, so
  that the error message only names it when it is the invalid field.

Also merge the duplicated note parsing in the ElfNoteSection constructor
and readNotes() into a shared readNote(), which had drifted apart into
validating differently.

Signed-off-by: Fredrik Fornwall <fredrik@fornwall.net>
@fornwall

Copy link
Copy Markdown
Owner Author

@dwalluck Claude had a go at fixing things here. The first commit has been updated and squashed, and then a commit appended. Can you re-review/check this PR now?

Comment on lines 51 to 59
while (true) {
if (index == 0) return null;
ElfSymbol symbol = symbolTable.symbols[index];
if (index >= symbolTable.symbols.length || index >= chain.length) {
throw new ElfException("Hash symbol index out of range: " + index);
}
ElfSymbol symbol = symbolTable.symbols[(int) index];
if (name.equals(symbol.getName())) return symbol;
index = chain[index];
index = chain[(int) index];
}

@dwalluck dwalluck Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The range check is right, but it's still missing a bound on while(true) loop.


while (buf.hasRemaining()) {
if (channel.read(buf) == -1) {
break;

@dwalluck dwalluck Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should throw here instead of break I think.

Comment on lines +1053 to +1055
// Note that e_shstrndx being SHN_UNDEF, which means that there is no section name string table, ends
// up here as well, since section 0 is not a string table.
ElfSection section = getSection(e_shstrndx);

@dwalluck dwalluck Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Table is optional though, so I wonder about returning null instead of throwing if (e_shstrndx == 0) return null;. And in getName() similarly if (tbl == null) return null;

Comment on lines +265 to +273
int numEntries = ElfFile.arraySize(header.sh_size / header.sh_entsize, "dynamic entry count");

// Except for the DT_NULL element at the end of the array, and the relative order of DT_NEEDED elements, entries
// may appear in any order. So important to use lazy evaluation to only evaluating e.g. DT_STRTAB after the
// necessary DT_STRSZ is read.
loop:
for (int i = 0; i < numEntries; i++) {
long d_tag = parser.readIntOrLong();
final long d_val_or_ptr = parser.readIntOrLong();
long d_tag = parser.readSignedIntOrLong();
final long d_val_or_ptr = parser.readUnsignedIntOrLong();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So numEntries is fixed now, but the loop reads without checking ei_class so I think this is wrong on 64-bit.

Comment on lines 31 to 32
public void skip(int bytesToSkip) {
int target = mappedByteBuffer.position() + bytesToSkip;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to range check skip() too, just like seek().

}
try {
this.mappedByteBuffer.position((int) (offset)); // we may be limited to sub-4GB mapped files
this.mappedByteBuffer.position((int) offset);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, but you're repositioning the caller's buffer, should call duplicate().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ElfRelocation should use header.sh_link

2 participants