codspeed 4.5.0

Core instrumentation library for CodSpeed
Documentation
//! Read-only view over an in-memory ELF image, providing typed access to
//! program headers, the dynamic section, notes, and version definitions.
//!
//! ELF structure navigated by this module:
//!
//!   dl_phdr_info (one per loaded object, from dl_iterate_phdr)
//!   ├── dlpi_addr              base load address (ASLR bias)
//!   ├── dlpi_name              filesystem path
//!   └── dlpi_phdr[]            program headers
//!       │
//!       ├── PT_DYNAMIC         → array of Elf64_Dyn entries
//!       │   ├── DT_STRTAB     → string table base (relocated, absolute address)
//!       │   ├── DT_SONAME     → strtab offset → canonical library name
//!       │   └── DT_VERDEF     → file-relative offset → linked list of Verdef
//!       │       └── Verdef → Verdaux.name → strtab offset → version string
//!       │                                   (e.g. "GLIBC_2.39")
//!       │
//!       └── PT_NOTE            → flat byte array
//!           └── NT_GNU_BUILD_ID → 20-byte SHA-1 build fingerprint
//!
//! Note on relocation:
//!   DT_STRTAB is relocated to an absolute address by the dynamic linker.
//!   DT_VERDEF is NOT relocated — it remains a file-relative offset and
//!   must be added to the base load address via ptr().

const std = @import("std");
const builtin = @import("builtin");
const elf = std.elf;
const native_endian = builtin.cpu.arch.endian();

base_addr: usize,
phdrs: []const elf.Phdr,

const Self = @This();

/// Create a view for a library loaded at runtime (from dl_iterate_phdr).
pub fn init(base_addr: usize, phdr_ptr: [*]const elf.Phdr, phnum: u16) Self {
    return .{
        .base_addr = base_addr,
        .phdrs = @as([*]const elf.Phdr, @ptrCast(phdr_ptr))[0..phnum],
    };
}

/// Resolve a load-relative virtual address to a typed pointer.
pub fn ptr(self: Self, comptime T: type, vaddr: usize) *const T {
    return @ptrFromInt(self.base_addr + vaddr);
}

/// Resolve a load-relative virtual address to a byte slice.
pub fn slice(self: Self, vaddr: usize, len: usize) []const u8 {
    return @as([*]const u8, @ptrFromInt(self.base_addr + vaddr))[0..len];
}

/// Find the first program header with the given type.
pub fn findPhdr(self: Self, p_type: u32) ?elf.Phdr {
    for (self.phdrs) |phdr| {
        if (phdr.p_type == p_type) return phdr;
    }
    return null;
}

/// Get the dynamic entries array from PT_DYNAMIC.
pub fn dynamicEntries(self: Self) ?[]const elf.Dyn {
    const phdr = self.findPhdr(elf.PT_DYNAMIC) orelse return null;
    const dyn_ptr: [*]const elf.Dyn = @ptrFromInt(self.base_addr + phdr.p_vaddr);
    return dyn_ptr[0..@divExact(phdr.p_memsz, @sizeOf(elf.Dyn))];
}

/// Find the value of a dynamic entry by tag. Stops at DT_NULL.
pub fn dynVal(entries: []const elf.Dyn, tag: i64) ?usize {
    for (entries) |entry| {
        if (entry.d_tag == tag) return entry.d_val;
        if (entry.d_tag == elf.DT_NULL) break;
    }
    return null;
}

/// Get the dynamic string table pointer (DT_STRTAB, already relocated at runtime).
pub fn strtab(entries: []const elf.Dyn) ?[*]const u8 {
    const addr = dynVal(entries, elf.DT_STRTAB) orelse return null;
    return @ptrFromInt(addr);
}

/// Read a null-terminated string from a string table at the given offset.
pub fn strFromTable(table: [*]const u8, offset: usize) []const u8 {
    return std.mem.span(@as([*:0]const u8, @ptrFromInt(@intFromPtr(table) + offset)));
}

/// Extract the SONAME from the dynamic section.
pub fn soname(entries: []const elf.Dyn, table: [*]const u8) ?[]const u8 {
    const offset = dynVal(entries, elf.DT_SONAME) orelse return null;
    return strFromTable(table, offset);
}

/// Extract the GNU Build ID from PT_NOTE as raw bytes.
/// Note layout: [namesz:u32][descsz:u32][type:u32]["GNU\0"][build_id_bytes...]
pub fn buildId(self: Self) ?[]const u8 {
    for (self.phdrs) |phdr| {
        if (phdr.p_type != elf.PT_NOTE) continue;

        const note = self.slice(phdr.p_vaddr, phdr.p_memsz);
        if (note.len < 16) continue;

        const name_size = std.mem.readInt(u32, note[0..4], native_endian);
        if (name_size != 4) continue;
        const desc_size = std.mem.readInt(u32, note[4..8], native_endian);
        const note_type = std.mem.readInt(u32, note[8..12], native_endian);
        if (note_type != elf.NT_GNU_BUILD_ID) continue;
        if (!std.mem.eql(u8, note[12..16], "GNU\x00")) continue;

        return note[16..][0..desc_size];
    }
    return null;
}

// --- Tests ---
//
// All tests use libtest_fixture.so — a tiny shared library built by build.zig
// with a known SONAME, hardcoded build ID, and version definitions (TESTLIB_1.0, TESTLIB_2.0).
// It is linked into the test binary so it appears in dl_iterate_phdr at runtime.
//
// Expected values from: readelf -d -n -V libtest_fixture.so
//   SONAME:  libtest_fixture.so
//   Build ID: 0xdeadbeef (4 bytes)
//   Verdef:  3 entries — "libtest_fixture.so" (BASE), "TESTLIB_1.0", "TESTLIB_2.0"

const is_linux = builtin.os.tag == .linux;

const TestFixture = struct {
    view: Self,
};

/// Use dl_iterate_phdr to find the test fixture library.
/// Only available on Linux where dl_phdr_info exposes ELF fields.
fn testFixture() ?TestFixture {
    if (comptime !is_linux) return null;

    const Ctx = struct { result: ?TestFixture };
    var ctx = Ctx{ .result = null };

    _ = std.c.dl_iterate_phdr(&struct {
        fn callback(info: *std.c.dl_phdr_info, _: usize, data: ?*anyopaque) callconv(.c) c_int {
            const c: *Ctx = @ptrCast(@alignCast(data));
            const name = std.mem.span(info.name orelse return 0);
            if (std.mem.indexOf(u8, name, "test_fixture") != null) {
                c.result = .{
                    .view = Self.init(info.addr, info.phdr, info.phnum),
                };
                return 1;
            }
            return 0;
        }
    }.callback, @ptrCast(&ctx));

    return ctx.result;
}

// -- findPhdr --

test "findPhdr returns PT_DYNAMIC" {
    const fixture = testFixture() orelse return error.SkipZigTest;
    const phdr = fixture.view.findPhdr(elf.PT_DYNAMIC);
    try std.testing.expect(phdr != null);
    try std.testing.expectEqual(elf.PT_DYNAMIC, phdr.?.p_type);
}

test "findPhdr returns PT_NOTE" {
    const fixture = testFixture() orelse return error.SkipZigTest;
    const phdr = fixture.view.findPhdr(elf.PT_NOTE);
    try std.testing.expect(phdr != null);
    try std.testing.expectEqual(elf.PT_NOTE, phdr.?.p_type);
}

test "findPhdr returns null for absent type" {
    const fixture = testFixture() orelse return error.SkipZigTest;
    try std.testing.expectEqual(null, fixture.view.findPhdr(elf.PT_SHLIB));
}

// -- dynamicEntries --

test "dynamicEntries returns non-empty" {
    const fixture = testFixture() orelse return error.SkipZigTest;
    const entries = fixture.view.dynamicEntries();
    try std.testing.expect(entries != null);
    try std.testing.expect(entries.?.len > 0);
}

// -- dynVal --

test "dynVal finds DT_STRTAB" {
    const fixture = testFixture() orelse return error.SkipZigTest;
    const entries = fixture.view.dynamicEntries() orelse return error.NoDynamic;
    try std.testing.expect(dynVal(entries, elf.DT_STRTAB) != null);
}

test "dynVal returns null for absent tag" {
    const fixture = testFixture() orelse return error.SkipZigTest;
    const entries = fixture.view.dynamicEntries() orelse return error.NoDynamic;
    try std.testing.expectEqual(null, dynVal(entries, 0x7ffffffd));
}

// -- soname --

test "soname is libtest_fixture.so" {
    const fixture = testFixture() orelse return error.SkipZigTest;
    const entries = fixture.view.dynamicEntries() orelse return error.NoDynamic;
    const table = Self.strtab(entries) orelse return error.NoStrtab;
    const name = Self.soname(entries, table);
    try std.testing.expect(name != null);
    try std.testing.expectEqualStrings("libtest_fixture.so", name.?);
}

// -- buildId --

test "buildId returns hardcoded value" {
    const fixture = testFixture() orelse return error.SkipZigTest;
    const id = fixture.view.buildId() orelse return error.NoBuildId;

    // The test fixture is built with a fixed build ID: 0xdeadbeef (4 bytes)
    try std.testing.expectEqual(@as(usize, 4), id.len);
    try std.testing.expectEqualSlices(u8, "\xde\xad\xbe\xef", id);
}