ajhahn.de
← FlashOS
Flash 403 lines
// elf: ELF64 header and program-header parser.
//
// Pure data structures — no externs, no allocation, no kernel state —
// so this module is host-testable. The ELF loader
// (sys_execve / kernel boot → prepare_move_to_user_elf in
// src/fork.zig) uses parseEhdr + iteratePhdrs to walk PT_LOAD
// segments before mapping them with map_page.
//
// Scope is deliberately narrow:
//   * ELF64, little-endian, AArch64, ET_EXEC only.
//   * Validation rejects ET_DYN — dynamic relocations / PIE land
//     later if at all.
//   * No section-header parsing. The loader does not need section
//     names; segments are enough.

const user_layout = #import("user_layout")

pub const ELF_MAGIC = [_]u8{ 0x7F, 'E', 'L', 'F' }
pub const ELFCLASS64 u8 = 2
pub const ELFDATA2LSB u8 = 1
pub const EV_CURRENT u32 = 1
pub const ET_EXEC u16 = 2
pub const EM_AARCH64 u16 = 183

pub const PT_LOAD u32 = 1

pub const PF_X u32 = 1 << 0
pub const PF_W u32 = 1 << 1
pub const PF_R u32 = 1 << 2

// Panic-bound on Phdr count. Real AArch64 ET_EXEC binaries from
// `zig build-exe` use 4-6 program headers; 16 is a generous ceiling
// that still bounds blast radius if the header is malicious.
pub const MAX_PHDRS u16 = 16

pub const Ehdr = extern struct {
    e_ident [16]u8,
    e_type u16,
    e_machine u16,
    e_version u32,
    e_entry u64,
    e_phoff u64,
    e_shoff u64,
    e_flags u32,
    e_ehsize u16,
    e_phentsize u16,
    e_phnum u16,
    e_shentsize u16,
    e_shnum u16,
    e_shstrndx u16,
}

pub const Phdr = extern struct {
    p_type u32,
    p_flags u32,
    p_offset u64,
    p_vaddr u64,
    p_paddr u64,
    p_filesz u64,
    p_memsz u64,
    p_align u64,
}

comptime {
    if #sizeOf(Ehdr) != 64 { #compileError("ELF64 Ehdr must be 64 bytes") }
    if #sizeOf(Phdr) != 56 { #compileError("ELF64 Phdr must be 56 bytes") }
}

pub const ParseError = error{
    BadMagic,
    NotElf64,
    NotLittleEndian,
    NotExecutable,
    NotAarch64,
    BadVersion,
    BadEntry,
    EntryOutOfBounds,
    PhoffOutOfBounds,
    TooManyPhdrs,
    MemszOverflow,
    VaddrOutOfBounds,
}

pub fn parseEhdr(blob []u8) ParseError!Ehdr {
    if blob.len < #sizeOf(Ehdr) { return error.BadMagic }
    var ehdr Ehdr = undefined
    const bytes [*]mut u8 = #ptrCast(&ehdr)
    #memcpy(bytes[0..#sizeOf(Ehdr)], blob[0..#sizeOf(Ehdr)])

    if ehdr.e_ident[0] != ELF_MAGIC[0] ||
        ehdr.e_ident[1] != ELF_MAGIC[1] ||
        ehdr.e_ident[2] != ELF_MAGIC[2] ||
        ehdr.e_ident[3] != ELF_MAGIC[3] { return error.BadMagic }
    if ehdr.e_ident[4] != ELFCLASS64 { return error.NotElf64 }
    if ehdr.e_ident[5] != ELFDATA2LSB { return error.NotLittleEndian }
    if ehdr.e_type != ET_EXEC { return error.NotExecutable }
    if ehdr.e_machine != EM_AARCH64 { return error.NotAarch64 }
    if ehdr.e_version != EV_CURRENT { return error.BadVersion }

    if ehdr.e_entry < user_layout.TEXT_BASE || ehdr.e_entry >= user_layout.DATA_BASE {
        return error.EntryOutOfBounds
    }

    if ehdr.e_phnum > MAX_PHDRS { return error.TooManyPhdrs }

    // Bound the program-header table itself: e_phoff..e_phoff +
    // phentsize*phnum must fit. Per-Phdr file-data bounds are checked
    // lazily in PhdrIterator.next(); a malformed blob with one bad
    // Phdr should still let earlier valid ones through to the loader.
    // Overflow checked via wraparound: `ph_end < ehdr.e_phoff` iff
    // `phentsize *% phnum` overflowed u64, since wraparound makes the
    // sum smaller than the original `e_phoff` addend.
    const phnum u64 = ehdr.e_phnum
    const phentsize u64 = ehdr.e_phentsize
    const ph_end = ehdr.e_phoff +% phentsize *% phnum
    if ph_end < ehdr.e_phoff || ph_end > blob.len { return error.PhoffOutOfBounds }

    return ehdr
}

pub const PhdrIterator = struct {
    blob []u8,
    cursor u64,
    stride u64,
    remaining u16,

    pub fn next(self *mut PhdrIterator) ParseError!?Phdr {
        if self.remaining == 0 { return null }
        if self.cursor +% #sizeOf(Phdr) > self.blob.len { return error.PhoffOutOfBounds }

        var phdr Phdr = undefined
        const bytes [*]mut u8 = #ptrCast(&phdr)
        #memcpy(bytes[0..#sizeOf(Phdr)], self.blob[self.cursor..][0..#sizeOf(Phdr)])

        // PT_LOAD is the only segment the loader copies into a user
        // page; bound-check its file region so the loader never
        // memcpy's past blob end. Other segment types are skipped at
        // the loader level, so their offsets do not matter here.
        if phdr.p_type == PT_LOAD {
            const seg_end = phdr.p_offset +% phdr.p_filesz
            if seg_end < phdr.p_offset || seg_end > self.blob.len {
                return error.PhoffOutOfBounds
            }
            // Virtual range must not wrap u64. The loader
            // (prepare_move_to_user_elf in src/fork.zig) walks e_entry against
            // [p_vaddr, p_vaddr + p_memsz) and maps that span one page at a
            // time; a wrapped sum would corrupt both the entry-mapped test and
            // the page loop. p_vaddr is otherwise unconstrained here — unlike
            // e_entry, which parseEhdr pins to [TEXT_BASE, DATA_BASE).
            const mem_end = phdr.p_vaddr +% phdr.p_memsz
            if mem_end < phdr.p_vaddr { return error.MemszOverflow }
            // The mapped span must also land inside the user range the
            // loader can populate: [TEXT_BASE, STACK_LOW). A crafted
            // p_vaddr above it would otherwise be mapped page-by-page over
            // the stack (eagerly mapped at STACK_TOP) or its guard region.
            // This is the p_vaddr counterpart to parseEhdr's e_entry pin —
            // the two were asymmetric: e_entry was range-checked, p_vaddr
            // was not.
            if phdr.p_vaddr < user_layout.TEXT_BASE || mem_end > user_layout.STACK_LOW {
                return error.VaddrOutOfBounds
            }
        }

        self.cursor += self.stride
        self.remaining -= 1
        return phdr
    }
}

pub fn iteratePhdrs(blob []u8, ehdr Ehdr) PhdrIterator {
    return .{
        .blob = blob,
        .cursor = ehdr.e_phoff,
        .stride = ehdr.e_phentsize,
        .remaining = ehdr.e_phnum,
    }
}

// ---- host tests ----------------------------------------------------

const std = #import("std")

const PHENTSIZE u16 = #sizeOf(Phdr)
const EHSIZE u16 = #sizeOf(Ehdr)

fn writeU16(buf []mut u8, off usize, v u16) void {
    buf[off] = #intCast(v & 0xFF)
    buf[off + 1] = #intCast((v >> 8) & 0xFF)
}

fn writeU32(buf []mut u8, off usize, v u32) void {
    buf[off] = #intCast(v & 0xFF)
    buf[off + 1] = #intCast((v >> 8) & 0xFF)
    buf[off + 2] = #intCast((v >> 16) & 0xFF)
    buf[off + 3] = #intCast((v >> 24) & 0xFF)
}

fn writeU64(buf []mut u8, off usize, v u64) void {
    var i usize = 0
    while i < 8 {
        buf[off + i] = #intCast((v >> #intCast(i * 8)) & 0xFF)
        i += 1
    }
}

/// Lay down a minimal valid Ehdr at offset 0 of `buf`. Caller controls
/// e_phoff / e_phnum / e_entry; the rest are pinned to a parseEhdr-
/// happy default.
fn writeEhdr(buf []mut u8, e_entry u64, e_phoff u64, e_phnum u16) void {
    #memset(buf[0..EHSIZE], 0)
    buf[0] = 0x7F
    buf[1] = 'E'
    buf[2] = 'L'
    buf[3] = 'F'
    buf[4] = ELFCLASS64
    buf[5] = ELFDATA2LSB
    buf[6] = 1 // EI_VERSION
    writeU16(buf, 16, ET_EXEC)
    writeU16(buf, 18, EM_AARCH64)
    writeU32(buf, 20, EV_CURRENT)
    writeU64(buf, 24, e_entry)
    writeU64(buf, 32, e_phoff)
    writeU64(buf, 40, 0) // e_shoff
    writeU32(buf, 48, 0) // e_flags
    writeU16(buf, 52, EHSIZE)
    writeU16(buf, 54, PHENTSIZE)
    writeU16(buf, 56, e_phnum)
    writeU16(buf, 58, 0)
    writeU16(buf, 60, 0)
    writeU16(buf, 62, 0)
}

fn writePhdr(buf []mut u8, off usize, p_type u32, p_flags u32, p_offset u64, p_filesz u64, p_memsz u64, p_vaddr u64) void {
    #memset(buf[off..][0..PHENTSIZE], 0)
    writeU32(buf, off + 0, p_type)
    writeU32(buf, off + 4, p_flags)
    writeU64(buf, off + 8, p_offset)
    writeU64(buf, off + 16, p_vaddr)
    writeU64(buf, off + 24, p_vaddr) // p_paddr = p_vaddr
    writeU64(buf, off + 32, p_filesz)
    writeU64(buf, off + 40, p_memsz)
    writeU64(buf, off + 48, 0x1000) // p_align
}

test "parseEhdr accepts a minimal valid header" {
    var buf [EHSIZE]u8 = undefined
    writeEhdr(&buf, 0x1000, 0, 0)
    const ehdr = try parseEhdr(&buf)
    try std.testing.expectEqual(#as(u16, ET_EXEC), ehdr.e_type)
    try std.testing.expectEqual(#as(u16, EM_AARCH64), ehdr.e_machine)
    try std.testing.expectEqual(#as(u64, 0x1000), ehdr.e_entry)
}

test "parseEhdr: BadMagic on truncated blob" {
    var buf [EHSIZE - 1]u8 = undefined
    #memset(&buf, 0)
    try std.testing.expectError(error.BadMagic, parseEhdr(&buf))
}

test "parseEhdr: BadMagic on flipped magic byte" {
    var buf [EHSIZE]u8 = undefined
    writeEhdr(&buf, 0x1000, 0, 0)
    buf[1] = 'X'
    try std.testing.expectError(error.BadMagic, parseEhdr(&buf))
}

test "parseEhdr: NotElf64 on ELFCLASS32" {
    var buf [EHSIZE]u8 = undefined
    writeEhdr(&buf, 0x1000, 0, 0)
    buf[4] = 1 // ELFCLASS32
    try std.testing.expectError(error.NotElf64, parseEhdr(&buf))
}

test "parseEhdr: NotLittleEndian on ELFDATA2MSB" {
    var buf [EHSIZE]u8 = undefined
    writeEhdr(&buf, 0x1000, 0, 0)
    buf[5] = 2 // ELFDATA2MSB
    try std.testing.expectError(error.NotLittleEndian, parseEhdr(&buf))
}

test "parseEhdr: NotExecutable on ET_DYN" {
    var buf [EHSIZE]u8 = undefined
    writeEhdr(&buf, 0x1000, 0, 0)
    writeU16(&buf, 16, 3) // ET_DYN
    try std.testing.expectError(error.NotExecutable, parseEhdr(&buf))
}

test "parseEhdr: NotAarch64 on EM_X86_64" {
    var buf [EHSIZE]u8 = undefined
    writeEhdr(&buf, 0x1000, 0, 0)
    writeU16(&buf, 18, 62) // EM_X86_64
    try std.testing.expectError(error.NotAarch64, parseEhdr(&buf))
}

test "parseEhdr: BadVersion on e_version=0" {
    var buf [EHSIZE]u8 = undefined
    writeEhdr(&buf, 0x1000, 0, 0)
    writeU32(&buf, 20, 0)
    try std.testing.expectError(error.BadVersion, parseEhdr(&buf))
}

test "parseEhdr: EntryOutOfBounds on e_entry >= DATA_BASE" {
    var buf [EHSIZE]u8 = undefined
    writeEhdr(&buf, user_layout.DATA_BASE, 0, 0)
    try std.testing.expectError(error.EntryOutOfBounds, parseEhdr(&buf))
}

test "parseEhdr: TooManyPhdrs above MAX_PHDRS" {
    var buf [EHSIZE]u8 = undefined
    writeEhdr(&buf, 0x1000, 0, MAX_PHDRS + 1)
    try std.testing.expectError(error.TooManyPhdrs, parseEhdr(&buf))
}

test "parseEhdr: PhoffOutOfBounds when Phdr table overruns blob" {
    var buf [EHSIZE]u8 = undefined
    // Two Phdrs declared at offset EHSIZE, but blob is exactly EHSIZE
    // bytes — the table runs off the end.
    writeEhdr(&buf, 0x1000, EHSIZE, 2)
    try std.testing.expectError(error.PhoffOutOfBounds, parseEhdr(&buf))
}

test "iteratePhdrs decodes a 2-PT_LOAD fixture" {
    var buf [EHSIZE + 2 * PHENTSIZE + 0x1000]u8 = undefined
    #memset(&buf, 0)
    writeEhdr(&buf, 0x1000, EHSIZE, 2)
    // Text segment: small, RX, file-backed at offset EHSIZE+2*PHENTSIZE
    writePhdr(&buf, EHSIZE + 0 * PHENTSIZE, PT_LOAD, PF_R | PF_X, EHSIZE + 2 * PHENTSIZE, 0x100, 0x100, 0x0)
    // Data segment: BSS-larger-than-file, RW, file-backed right after
    writePhdr(&buf, EHSIZE + 1 * PHENTSIZE, PT_LOAD, PF_R | PF_W, EHSIZE + 2 * PHENTSIZE + 0x100, 0x80, 0x200, 0x10_0000)

    const ehdr = try parseEhdr(&buf)
    var it = iteratePhdrs(&buf, ehdr)

    const p0 = (try it.next()).?
    try std.testing.expectEqual(#as(u32, PT_LOAD), p0.p_type)
    try std.testing.expectEqual(PF_R | PF_X, p0.p_flags)
    try std.testing.expectEqual(#as(u64, 0x100), p0.p_filesz)
    try std.testing.expectEqual(#as(u64, 0x0), p0.p_vaddr)

    const p1 = (try it.next()).?
    try std.testing.expectEqual(#as(u32, PT_LOAD), p1.p_type)
    try std.testing.expectEqual(PF_R | PF_W, p1.p_flags)
    try std.testing.expectEqual(#as(u64, 0x80), p1.p_filesz)
    try std.testing.expectEqual(#as(u64, 0x200), p1.p_memsz)
    try std.testing.expectEqual(#as(u64, 0x10_0000), p1.p_vaddr)

    try std.testing.expectEqual(#as(?Phdr, null), try it.next())
}

test "iteratePhdrs: PhoffOutOfBounds when PT_LOAD file range overruns blob" {
    var buf [EHSIZE + PHENTSIZE]u8 = undefined
    #memset(&buf, 0)
    writeEhdr(&buf, 0x1000, EHSIZE, 1)
    // p_filesz pushes the end of the segment past blob.len
    writePhdr(&buf, EHSIZE, PT_LOAD, PF_R, EHSIZE + PHENTSIZE, 0x1000, 0x1000, 0x0)

    const ehdr = try parseEhdr(&buf)
    var it = iteratePhdrs(&buf, ehdr)
    try std.testing.expectError(error.PhoffOutOfBounds, it.next())
}

test "iteratePhdrs: non-PT_LOAD entries are not bounds-checked" {
    var buf [EHSIZE + PHENTSIZE]u8 = undefined
    #memset(&buf, 0)
    writeEhdr(&buf, 0x1000, EHSIZE, 1)
    // PT_NOTE (4) with bogus offsets — loader skips, parser must not
    // reject. Keeps blobs with stripped notes / interp segments
    // working without forcing the loader to filter pre-iteration.
    writePhdr(&buf, EHSIZE, 4, 0, 0xFFFF_FFFF, 0xFFFF_FFFF, 0xFFFF_FFFF, 0x0)

    const ehdr = try parseEhdr(&buf)
    var it = iteratePhdrs(&buf, ehdr)
    const p = (try it.next()).?
    try std.testing.expectEqual(#as(u32, 4), p.p_type)
}

test "iteratePhdrs: MemszOverflow when p_vaddr + p_memsz wraps u64" {
    var buf [EHSIZE + PHENTSIZE]u8 = undefined
    #memset(&buf, 0)
    writeEhdr(&buf, 0x1000, EHSIZE, 1)
    // Valid file range (filesz 0, offset in-bounds) so the seg_end check
    // passes and the virtual-range wrap is what trips. p_vaddr near the top
    // of the address space + a nonzero p_memsz wraps the u64 sum.
    writePhdr(&buf, EHSIZE, PT_LOAD, PF_R, EHSIZE, 0, 0x2000, 0xFFFF_FFFF_FFFF_F000)
    const ehdr = try parseEhdr(&buf)
    var it = iteratePhdrs(&buf, ehdr)
    try std.testing.expectError(error.MemszOverflow, it.next())
}

test "iteratePhdrs: VaddrOutOfBounds when a PT_LOAD maps above STACK_LOW" {
    var buf [EHSIZE + PHENTSIZE]u8 = undefined
    #memset(&buf, 0)
    writeEhdr(&buf, 0x1000, EHSIZE, 1)
    // File range valid (filesz 0, in-bounds offset) and the virtual sum
    // does not wrap u64, so neither PhoffOutOfBounds nor MemszOverflow
    // fires — the p_vaddr range check is what must trip. p_vaddr sits at
    // STACK_TOP, so the mapped span would collide with the stack region.
    writePhdr(&buf, EHSIZE, PT_LOAD, PF_R, EHSIZE, 0, 0x1000, user_layout.STACK_TOP)
    const ehdr = try parseEhdr(&buf)
    var it = iteratePhdrs(&buf, ehdr)
    try std.testing.expectError(error.VaddrOutOfBounds, it.next())
}