From e44340a48569c33aac4b235972259d7053d3f680 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Thu, 23 Jul 2026 14:12:39 -0400 Subject: [PATCH 1/3] Add address-preserving DT_NEEDED injection to ELF files There was no way to add a dependency to an existing dynamically-linked image. Growing .dynamic and .dynstr in place is not viable: it would shift every later section's file offset while its virtual address stays fixed, breaking the loader's vaddr-to-offset mapping and every stored address reference. Adds ElfFile.AddNeededLibrary, which never changes an existing section's byte offset. It relocates the .dynamic and .dynstr sections to end-of-file (backfilling their old offsets with equal-size padding so no other section moves), maps them with a PT_LOAD built from a spare PT_NOTE program header, and repoints PT_DYNAMIC, DT_STRTAB/DT_STRSZ and the _DYNAMIC symbol at the new location. Section and segment stay consistent, so a re-read sees the injected dependency. --- src/LibObjectFile/Elf/ElfDynamicEditing.cs | 175 +++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 src/LibObjectFile/Elf/ElfDynamicEditing.cs diff --git a/src/LibObjectFile/Elf/ElfDynamicEditing.cs b/src/LibObjectFile/Elf/ElfDynamicEditing.cs new file mode 100644 index 0000000..d070d25 --- /dev/null +++ b/src/LibObjectFile/Elf/ElfDynamicEditing.cs @@ -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; + +/// +/// Address-preserving edits of a dynamically-linked . +/// +/// +/// These operations never change the byte offset of an existing section, so existing symbols, relocations and +/// code stay valid. The sections that must grow (.dynamic and .dynstr) are relocated to the end of +/// the file and mapped by a fresh PT_LOAD built from a spare PT_NOTE program header (so the +/// program-header table does not grow). This is the same technique used by patchelf --add-needed. +/// +public static class ElfDynamicEditing +{ + private const ulong PageSize = 0x1000; + + /// + /// Injects a DT_NEEDED dependency on into an existing dynamically-linked + /// image, so the dynamic loader loads that library at startup. The .dynamic and .dynstr sections + /// are relocated (as sections, kept consistent with PT_DYNAMIC and DT_STRTAB), while every other + /// section keeps its byte offset. The vacated space is backfilled so no existing reference shifts. + /// + /// + /// The file is not dynamically linked, or has no PT_NOTE segment to repurpose as the new PT_LOAD. + /// + public static void AddNeededLibrary(this ElfFile elf, string libraryName) + { + ArgumentNullException.ThrowIfNull(elf); + ArgumentException.ThrowIfNullOrEmpty(libraryName); + + var dynamicSection = elf.Sections.OfType().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); + } + + /// + /// Moves 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 ), only its + /// file offset moves. + /// + 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()) + { + 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 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); +} From d8a33af43ace71a10ffc846245cbb35c631b7ed4 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Thu, 23 Jul 2026 14:12:39 -0400 Subject: [PATCH 2/3] Add tests for DT_NEEDED injection Covers ElfFile.AddNeededLibrary in-process: injecting into a real shared object repurposes a PT_NOTE into a PT_LOAD and moves PT_DYNAMIC, and resolving DT_NEEDED the way the loader does (PT_DYNAMIC to the dynamic array, then DT_STRTAB) shows the existing dependencies preserved and the injected one resolvable, with the section view agreeing. Also checks that a non-dynamic object file is rejected. --- .../Elf/ElfDynamicEditingTests.cs | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 src/LibObjectFile.Tests/Elf/ElfDynamicEditingTests.cs diff --git a/src/LibObjectFile.Tests/Elf/ElfDynamicEditingTests.cs b/src/LibObjectFile.Tests/Elf/ElfDynamicEditingTests.cs new file mode 100644 index 0000000..d74e141 --- /dev/null +++ b/src/LibObjectFile.Tests/Elf/ElfDynamicEditingTests.cs @@ -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().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().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(() => elf.AddNeededLibrary("libinject.so")); + } + + // Emulates the dynamic loader: PT_DYNAMIC -> dynamic array -> DT_STRTAB -> DT_NEEDED names. + private static List 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(); + 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); + } +} From 9b9e131102ca1f58e8e422cd21cc6ea3f0e3d766 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Thu, 23 Jul 2026 14:12:39 -0400 Subject: [PATCH 3/3] Document DT_NEEDED injection in readme Adds ElfFile.AddNeededLibrary to the ELF feature list. --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index c491ae8..ad098e5 100644 --- a/readme.md +++ b/readme.md @@ -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)