Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ elf.Write(outStream);
- Dynamic Linking Table (`SHT_DYNAMIC`)
- Other sections fallback to `ElfCustomSection`
- Program headers with or without sections
- `ElfFile.AddNeededLibrary` injects a `DT_NEEDED` dependency into an existing image without moving any section (address-preserving, `patchelf`-style)
- Print with `readelf` similar output
- Support for **DWARF debugging format**:
- Partial support of Version 4 (currently still the default for GCC)
Expand Down
122 changes: 122 additions & 0 deletions src/LibObjectFile.Tests/Elf/ElfDynamicEditingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.

using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using LibObjectFile.Elf;

namespace LibObjectFile.Tests.Elf;

[TestClass]
public class ElfDynamicEditingTests : ElfTestBase
{
[TestMethod]
public void AddNeededLibraryInjectsDependency()
{
// libstdc++.so is a real shared object with existing DT_NEEDED entries and a PT_NOTE to repurpose.
var elf = LoadElf("libstdc++.so");

var originalNeeded = elf.Sections.OfType<ElfDynamicLinkingTable>().Single().GetNeededLibraries().ToList();
int notesBefore = elf.Segments.Count(s => s.Type == ElfSegmentTypeCore.Note);
int loadsBefore = elf.Segments.Count(s => s.Type == ElfSegmentTypeCore.Load);
ulong dynamicVaddrBefore = elf.Segments.Single(s => s.Type == ElfSegmentTypeCore.Dynamic).VirtualAddress;

elf.AddNeededLibrary("libinject.so");

// A PT_NOTE was repurposed into a PT_LOAD, and PT_DYNAMIC now points into the appended region.
Assert.AreEqual(notesBefore - 1, elf.Segments.Count(s => s.Type == ElfSegmentTypeCore.Note));
Assert.AreEqual(loadsBefore + 1, elf.Segments.Count(s => s.Type == ElfSegmentTypeCore.Load));
Assert.IsTrue(elf.Segments.Single(s => s.Type == ElfSegmentTypeCore.Dynamic).VirtualAddress > dynamicVaddrBefore,
"PT_DYNAMIC should have moved into the appended high-address region");

// Write, re-read, then resolve DT_NEEDED exactly as the loader would: follow PT_DYNAMIC to the
// dynamic array, then DT_STRTAB to turn each DT_NEEDED offset into a name.
var stream = new MemoryStream();
elf.Write(stream);
var fileBytes = stream.ToArray();
var roundTrip = ElfFile.Read(new MemoryStream(fileBytes));

var needed = ResolveNeededViaProgramHeaders(roundTrip, fileBytes);

CollectionAssert.IsSubsetOf(originalNeeded, needed, "existing dependencies must be preserved");
CollectionAssert.Contains(needed, "libinject.so", "the injected dependency must be resolvable via DT_STRTAB");
Assert.AreEqual(originalNeeded.Count + 1, needed.Count, "exactly one DT_NEEDED should be added");

// The section view must agree with the segment/loader view: exactly one .dynamic section, and reading
// DT_NEEDED through the section (its linked .dynstr) resolves the injected library too. This is what a
// section-based re-reader (e.g. a patch applicator checking for idempotency) sees.
var dynamicSections = roundTrip.Sections.OfType<ElfDynamicLinkingTable>().ToList();
Assert.AreEqual(1, dynamicSections.Count, "there must be exactly one .dynamic section after injection");
var neededViaSection = dynamicSections[0].GetNeededLibraries().ToList();
CollectionAssert.AreEqual(needed, neededViaSection, "section view must match the program-header view");
}

[TestMethod]
public void AddNeededLibraryThrowsForNonDynamicFile()
{
// Relocatable object files have no .dynamic section, so injection is not applicable.
var elf = LoadElf("small_debug.o");
Assert.ThrowsExactly<InvalidOperationException>(() => elf.AddNeededLibrary("libinject.so"));
}

// Emulates the dynamic loader: PT_DYNAMIC -> dynamic array -> DT_STRTAB -> DT_NEEDED names.
private static List<string> ResolveNeededViaProgramHeaders(ElfFile elf, byte[] fileBytes)
{
bool little = elf.Encoding == ElfEncoding.Lsb;
bool is32 = elf.FileClass == ElfFileClass.Is32;
int entrySize = is32 ? 8 : 16;

var dynamicSegment = elf.Segments.Single(s => s.Type == ElfSegmentTypeCore.Dynamic);
int count = (int)(dynamicSegment.Size / (ulong)entrySize);

ulong strTabVaddr = 0;
var neededOffsets = new List<ulong>();
for (int i = 0; i < count; i++)
{
int offset = (int)dynamicSegment.Position + i * entrySize;
long tag = is32 ? ReadU32(fileBytes, offset, little) : (long)ReadU64(fileBytes, offset, little);
ulong value = is32 ? ReadU32(fileBytes, offset + 4, little) : ReadU64(fileBytes, offset + 8, little);

if (tag == (long)ElfDynamicTag.StrTab) strTabVaddr = value;
else if (tag == (long)ElfDynamicTag.Needed) neededOffsets.Add(value);
}

ulong strTabFileOffset = VirtualToFileOffset(elf, strTabVaddr);
return neededOffsets.Select(o => ReadCString(fileBytes, (int)(strTabFileOffset + o))).ToList();
}

private static ulong VirtualToFileOffset(ElfFile elf, ulong virtualAddress)
{
foreach (var segment in elf.Segments)
{
if (segment.Type != ElfSegmentTypeCore.Load) continue;
if (virtualAddress >= segment.VirtualAddress && virtualAddress < segment.VirtualAddress + segment.Size)
{
return segment.Position + (virtualAddress - segment.VirtualAddress);
}
}

throw new InvalidOperationException($"Virtual address 0x{virtualAddress:x} is not mapped by any PT_LOAD segment");
}

private static uint ReadU32(byte[] bytes, int offset, bool little)
=> little
? BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(offset, 4))
: BinaryPrimitives.ReadUInt32BigEndian(bytes.AsSpan(offset, 4));

private static ulong ReadU64(byte[] bytes, int offset, bool little)
=> little
? BinaryPrimitives.ReadUInt64LittleEndian(bytes.AsSpan(offset, 8))
: BinaryPrimitives.ReadUInt64BigEndian(bytes.AsSpan(offset, 8));

private static string ReadCString(byte[] bytes, int offset)
{
int end = offset;
while (end < bytes.Length && bytes[end] != 0) end++;
return System.Text.Encoding.UTF8.GetString(bytes, offset, end - offset);
}
}
175 changes: 175 additions & 0 deletions src/LibObjectFile/Elf/ElfDynamicEditing.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using LibObjectFile.Diagnostics;

namespace LibObjectFile.Elf;

/// <summary>
/// Address-preserving edits of a dynamically-linked <see cref="ElfFile"/>.
/// </summary>
/// <remarks>
/// These operations never change the byte offset of an existing section, so existing symbols, relocations and
/// code stay valid. The sections that must grow (<c>.dynamic</c> and <c>.dynstr</c>) are relocated to the end of
/// the file and mapped by a fresh <c>PT_LOAD</c> built from a spare <c>PT_NOTE</c> program header (so the
/// program-header table does not grow). This is the same technique used by <c>patchelf --add-needed</c>.
/// </remarks>
public static class ElfDynamicEditing
{
private const ulong PageSize = 0x1000;

/// <summary>
/// Injects a <c>DT_NEEDED</c> dependency on <paramref name="libraryName"/> into an existing dynamically-linked
/// image, so the dynamic loader loads that library at startup. The <c>.dynamic</c> and <c>.dynstr</c> sections
/// are relocated (as sections, kept consistent with <c>PT_DYNAMIC</c> and <c>DT_STRTAB</c>), while every other
/// section keeps its byte offset. The vacated space is backfilled so no existing reference shifts.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The file is not dynamically linked, or has no <c>PT_NOTE</c> segment to repurpose as the new <c>PT_LOAD</c>.
/// </exception>
public static void AddNeededLibrary(this ElfFile elf, string libraryName)
{
ArgumentNullException.ThrowIfNull(elf);
ArgumentException.ThrowIfNullOrEmpty(libraryName);

var dynamicSection = elf.Sections.OfType<ElfDynamicLinkingTable>().FirstOrDefault()
?? throw new InvalidOperationException("The ELF file has no dynamic section (.dynamic); it is not dynamically linked.");
var stringSection = dynamicSection.StringTable
?? throw new InvalidOperationException("The dynamic section is not linked to a string table (.dynstr).");
var dynamicSegment = elf.Segments.FirstOrDefault(s => s.Type == ElfSegmentTypeCore.Dynamic)
?? throw new InvalidOperationException("The ELF file has no PT_DYNAMIC segment.");
var noteSegment = elf.Segments.FirstOrDefault(s => s.Type == ElfSegmentTypeCore.Note)
?? throw new InvalidOperationException("The ELF file has no PT_NOTE segment to repurpose as a new PT_LOAD.");

// Original layout of the sections we relocate, captured before any mutation.
ulong dynamicOldVaddr = dynamicSection.VirtualAddress;
ulong stringOldVaddr = stringSection.VirtualAddress;
ulong dynamicOldSize = dynamicSection.Size;
ulong stringOldSize = stringSection.Size;
uint dynamicOldAlign = dynamicSection.FileAlignment;
uint stringOldAlign = stringSection.FileAlignment;

// Grow .dynstr with the library name and add the DT_NEEDED entry to .dynamic.
dynamicSection.AddNeededLibrary(libraryName);

// Relocate .dynamic and .dynstr to end-of-file, backfilling their old positions with equal-size padding
// so every other section keeps its byte offset. .dynamic goes first so PT_DYNAMIC covers the start of the
// new region; page-align it so the new PT_LOAD is vaddr/offset congruent.
RelocateToEnd(elf, dynamicSection, dynamicOldSize, dynamicOldAlign, (uint)PageSize);
RelocateToEnd(elf, stringSection, stringOldSize, stringOldAlign, 1);

var diagnostics = new DiagnosticBag();
elf.UpdateLayout(diagnostics);
if (diagnostics.HasErrors)
{
throw new ObjectFileException("Failed to update the ELF layout while injecting a dependency.", diagnostics);
}

ulong baseVaddr = AlignUp(MaxLoadedVirtualAddressEnd(elf), PageSize);
ulong firstPosition = dynamicSection.Position;
dynamicSection.VirtualAddress = baseVaddr + (dynamicSection.Position - firstPosition);
stringSection.VirtualAddress = baseVaddr + (stringSection.Position - firstPosition);

// Point DT_STRTAB/DT_STRSZ at the relocated .dynstr. These are value-only updates (no entry count change),
// so the layout computed above stays valid.
UpdateTagValue(dynamicSection.Entries, ElfDynamicTag.StrTab, stringSection.VirtualAddress);
UpdateTagValue(dynamicSection.Entries, ElfDynamicTag.StrSz, stringSection.Size);

// The _DYNAMIC symbol (and any other symbol inside the moved sections) must follow the new address.
RelocateSymbols(elf, dynamicSection, dynamicOldVaddr);
RelocateSymbols(elf, stringSection, stringOldVaddr);

// Repurpose the PT_NOTE program header into a writable PT_LOAD mapping both relocated sections. It must be
// writable because the loader writes DT_DEBUG back into .dynamic.
noteSegment.Type = ElfSegmentTypeCore.Load;
noteSegment.Flags = ElfSegmentFlagsCore.Readable | ElfSegmentFlagsCore.Writable;
noteSegment.VirtualAddress = baseVaddr;
noteSegment.PhysicalAddress = baseVaddr;
noteSegment.VirtualAddressAlignment = PageSize;
noteSegment.Range = new ElfContentRange(dynamicSection, 0, stringSection, 0);
noteSegment.SizeInMemory = stringSection.Position + stringSection.Size - dynamicSection.Position;

// Point PT_DYNAMIC at the relocated .dynamic.
dynamicSegment.VirtualAddress = dynamicSection.VirtualAddress;
dynamicSegment.PhysicalAddress = dynamicSection.VirtualAddress;
dynamicSegment.SizeInMemory = dynamicSection.Size;
dynamicSegment.Range = new ElfContentRange(dynamicSection);
}

/// <summary>
/// Moves <paramref name="section"/> to the end of the content list and backfills the space it vacated with a
/// zero-filled shadow content of the same size and alignment, so every following section keeps its byte offset.
/// The section header keeps its index (driven by <see cref="ElfSection.OrderInSectionHeaderTable"/>), only its
/// file offset moves.
/// </summary>
private static void RelocateToEnd(ElfFile elf, ElfSection section, ulong originalSize, uint originalAlignment, uint appendAlignment)
{
// RemoveAt (not Remove) is required: only RemoveAt fires the content hooks that keep the section list
// in sync, otherwise the section would be registered twice and produce a duplicate section header.
int index = elf.Content.IndexOf(section);
elf.Content.RemoveAt(index);
var padding = new ElfStreamContentData(new MemoryStream(new byte[originalSize]))
{
FileAlignment = originalAlignment,
};
elf.Content.Insert(index, padding);
section.FileAlignment = appendAlignment;
elf.Content.Add(section);
}

private static void RelocateSymbols(ElfFile elf, ElfSection section, ulong oldVirtualAddress)
{
ulong newVirtualAddress = section.VirtualAddress;
if (newVirtualAddress == oldVirtualAddress) return;

foreach (var symbolTable in elf.Sections.OfType<ElfSymbolTable>())
{
var entries = symbolTable.Entries;
for (int i = 0; i < entries.Count; i++)
{
var entry = entries[i];
if (ReferenceEquals(entry.SectionLink.Section, section))
{
entry.Value = newVirtualAddress + (entry.Value - oldVirtualAddress);
entries[i] = entry;
}
}
}
}

private static void UpdateTagValue(List<ElfDynamic> entries, ElfDynamicTag tag, ulong value)
{
int index = entries.FindIndex(e => e.TagType == tag);
if (index < 0)
{
throw new InvalidOperationException($"The dynamic section is missing the required {tag} entry.");
}

var entry = entries[index];
entry.Value = value;
entries[index] = entry;
}

private static ulong MaxLoadedVirtualAddressEnd(ElfFile elf)
{
ulong max = 0;
foreach (var segment in elf.Segments)
{
if (segment.SizeInMemory == 0) continue;
max = Math.Max(max, segment.VirtualAddress + segment.SizeInMemory);
}
foreach (var section in elf.Sections)
{
if ((section.Flags & ElfSectionFlags.Alloc) == 0) continue;
max = Math.Max(max, section.VirtualAddress + section.Size);
}
return max;
}

private static ulong AlignUp(ulong value, ulong align) => (value + align - 1) & ~(align - 1);
}
Loading