Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/znippy

plugins/native/znippy-plugin-elf/src/fixture.rs

//! A synthetic ELF64 writer, for tests only.
//!
//! Every column test needs a binary whose facts are known EXACTLY, including the
//! ones no compiler on this box would produce (a riscv64 header, an
//! `x86-64-v4` property, a receipt from a schema version in the future). A
//! checked-in fixture binary could not cover those and could not be read when it
//! drifted, so the bytes are assembled here instead.

#![allow(dead_code)]

pub const SHT_PROGBITS: u32 = 1;
pub const SHT_STRTAB: u32 = 3;
pub const SHT_NOTE: u32 = 7;
pub const SHT_NOBITS: u32 = 8;
pub const SHT_DYNAMIC: u32 = 6;
pub const SHT_GNU_VERNEED: u32 = 0x6fff_fffe;

pub const SHF_ALLOC: u64 = 0x2;
pub const SHF_EXECINSTR: u64 = 0x4;

pub const ET_REL: u16 = 1;
pub const ET_EXEC: u16 = 2;
pub const ET_DYN: u16 = 3;
pub const ET_CORE: u16 = 4;

pub const EM_X86_64: u16 = 62;
pub const EM_AARCH64: u16 = 183;
pub const EM_RISCV: u16 = 243;

pub const DT_NEEDED: i64 = 1;
pub const DT_NULL: i64 = 0;

pub struct Section {
    pub name: String,
    pub sh_type: u32,
    pub flags: u64,
    pub data: Vec<u8>,
    pub link: u32,
    pub info: u32,
    pub entsize: u64,
    /// `SHT_NOBITS` occupies `sh_size` without occupying file bytes.
    pub nobits_size: Option<u64>,
}

impl Section {
    pub fn new(name: &str, sh_type: u32, data: Vec<u8>) -> Self {
        Section {
            name: name.to_string(),
            sh_type,
            flags: 0,
            data,
            link: 0,
            info: 0,
            entsize: 0,
            nobits_size: None,
        }
    }
    pub fn flags(mut self, f: u64) -> Self {
        self.flags = f;
        self
    }
    pub fn link(mut self, l: u32) -> Self {
        self.link = l;
        self
    }
    pub fn info(mut self, i: u32) -> Self {
        self.info = i;
        self
    }
    pub fn entsize(mut self, e: u64) -> Self {
        self.entsize = e;
        self
    }
}

pub struct Builder {
    pub machine: u16,
    pub etype: u16,
    pub osabi: u8,
    pub class64: bool,
    pub interp: Option<String>,
    pub sections: Vec<Section>,
}

impl Default for Builder {
    fn default() -> Self {
        Builder {
            machine: EM_X86_64,
            etype: ET_DYN,
            osabi: 0,
            class64: true,
            interp: None,
            sections: Vec::new(),
        }
    }
}

impl Builder {
    pub fn machine(mut self, m: u16) -> Self {
        self.machine = m;
        self
    }
    pub fn etype(mut self, t: u16) -> Self {
        self.etype = t;
        self
    }
    pub fn osabi(mut self, o: u8) -> Self {
        self.osabi = o;
        self
    }
    pub fn class32(mut self) -> Self {
        self.class64 = false;
        self
    }
    pub fn interp(mut self, s: &str) -> Self {
        self.interp = Some(s.to_string());
        self
    }
    pub fn section(mut self, s: Section) -> Self {
        self.sections.push(s);
        self
    }

    /// A text section of `n` bytes (allocated + executable).
    pub fn text(self, n: usize) -> Self {
        self.section(Section::new(".text", SHT_PROGBITS, vec![0x90; n]).flags(SHF_ALLOC | SHF_EXECINSTR))
    }

    /// A `.debug_info` of `n` bytes.
    pub fn debug(self, n: usize) -> Self {
        self.section(Section::new(".debug_info", SHT_PROGBITS, vec![7; n]))
    }

    /// An ELF note section.
    pub fn note(self, section: &str, owner: &[u8], ntype: u32, desc: &[u8]) -> Self {
        self.section(Section::new(section, SHT_NOTE, note_bytes(owner, ntype, desc)))
    }

    /// `.dynamic` + `.dynstr` carrying these `DT_NEEDED` sonames.
    pub fn needed(mut self, sonames: &[&str]) -> Self {
        // .dynstr first, so its index is known when .dynamic links to it.
        let mut strtab = vec![0u8];
        let mut offsets = Vec::new();
        for s in sonames {
            offsets.push(strtab.len() as u64);
            strtab.extend_from_slice(s.as_bytes());
            strtab.push(0);
        }
        self.sections.push(Section::new(".dynstr", SHT_STRTAB, strtab));
        let dynstr_index = self.sections.len() as u32; // +1 for the NULL section

        let mut dyn_bytes = Vec::new();
        for off in &offsets {
            dyn_bytes.extend_from_slice(&DT_NEEDED.to_le_bytes());
            dyn_bytes.extend_from_slice(&off.to_le_bytes());
        }
        dyn_bytes.extend_from_slice(&DT_NULL.to_le_bytes());
        dyn_bytes.extend_from_slice(&0u64.to_le_bytes());
        self.sections.push(
            Section::new(".dynamic", SHT_DYNAMIC, dyn_bytes).link(dynstr_index).entsize(16),
        );
        self
    }

    /// `.gnu.version_r` requiring `versions` from `soname`. Must follow
    /// [`Self::needed`], whose `.dynstr` it extends.
    pub fn verneed(mut self, soname: &str, versions: &[&str]) -> Self {
        let dynstr_pos = self
            .sections
            .iter()
            .position(|s| s.name == ".dynstr")
            .expect("verneed needs a .dynstr — call .needed() first");
        let dynstr_index = (dynstr_pos + 1) as u32;

        let add = |strtab: &mut Vec<u8>, s: &str| -> u32 {
            // Reuse an existing string when it is already there, exactly as a
            // real linker would.
            let needle: Vec<u8> = s.bytes().chain(std::iter::once(0)).collect();
            if let Some(p) = strtab.windows(needle.len()).position(|w| w == needle.as_slice()) {
                return p as u32;
            }
            let off = strtab.len() as u32;
            strtab.extend_from_slice(s.as_bytes());
            strtab.push(0);
            off
        };
        let strtab = &mut self.sections[dynstr_pos].data;
        let file_off = add(strtab, soname);
        let name_offs: Vec<u32> = versions.iter().map(|v| add(strtab, v)).collect();

        let mut bytes = Vec::new();
        // Verneed: vn_version u16, vn_cnt u16, vn_file u32, vn_aux u32, vn_next u32
        bytes.extend_from_slice(&1u16.to_le_bytes());
        bytes.extend_from_slice(&(versions.len() as u16).to_le_bytes());
        bytes.extend_from_slice(&file_off.to_le_bytes());
        bytes.extend_from_slice(&16u32.to_le_bytes()); // aux starts right after
        bytes.extend_from_slice(&0u32.to_le_bytes()); // no next verneed
        for (i, off) in name_offs.iter().enumerate() {
            // Vernaux: vna_hash u32, vna_flags u16, vna_other u16, vna_name u32, vna_next u32
            bytes.extend_from_slice(&0u32.to_le_bytes());
            bytes.extend_from_slice(&0u16.to_le_bytes());
            bytes.extend_from_slice(&((i + 2) as u16).to_le_bytes());
            bytes.extend_from_slice(off.to_le_bytes().as_slice());
            let next = if i + 1 == name_offs.len() { 0u32 } else { 16u32 };
            bytes.extend_from_slice(&next.to_le_bytes());
        }
        self.sections.push(
            Section::new(".gnu.version_r", SHT_GNU_VERNEED, bytes)
                .link(dynstr_index)
                .info(1),
        );
        self
    }

    pub fn build(self) -> Vec<u8> {
        assert!(self.class64, "only ELF64 fixtures are assembled here");
        let phnum: u16 = u16::from(self.interp.is_some());
        let ehsize = 64usize;
        let phentsize = 56usize;
        let shentsize = 64usize;

        // .shstrtab is the last section and holds every name.
        let mut names: Vec<String> = vec![String::new()];
        for s in &self.sections {
            names.push(s.name.clone());
        }
        names.push(".shstrtab".to_string());
        let mut shstrtab = Vec::new();
        let mut name_off: Vec<u32> = Vec::new();
        for n in &names {
            name_off.push(shstrtab.len() as u32);
            shstrtab.extend_from_slice(n.as_bytes());
            shstrtab.push(0);
        }

        let mut body: Vec<Vec<u8>> = self.sections.iter().map(|s| s.data.clone()).collect();
        body.push(shstrtab);

        // layout: [ehdr][phdrs][interp][section bodies][shdrs]
        let mut cursor = ehsize + phentsize * phnum as usize;
        let interp_off = cursor;
        let interp_bytes: Vec<u8> = match &self.interp {
            Some(s) => s.bytes().chain(std::iter::once(0)).collect(),
            None => Vec::new(),
        };
        cursor += interp_bytes.len();

        let mut offsets = Vec::new();
        for b in &body {
            cursor = (cursor + 7) & !7;
            offsets.push(cursor);
            cursor += b.len();
        }
        cursor = (cursor + 7) & !7;
        let shoff = cursor;

        let shnum = (self.sections.len() + 2) as u16; // NULL + sections + shstrtab
        let shstrndx = shnum - 1;

        let mut out = vec![0u8; shoff + shentsize * shnum as usize];

        // ── e_ident + header ──────────────────────────────────────────────────
        out[0..4].copy_from_slice(&[0x7f, b'E', b'L', b'F']);
        out[4] = 2; // ELFCLASS64
        out[5] = 1; // ELFDATA2LSB
        out[6] = 1; // EV_CURRENT
        out[7] = self.osabi;
        put16(&mut out, 16, self.etype);
        put16(&mut out, 18, self.machine);
        put32(&mut out, 20, 1);
        put64(&mut out, 24, 0); // e_entry
        put64(&mut out, 32, if phnum > 0 { ehsize as u64 } else { 0 });
        put64(&mut out, 40, shoff as u64);
        put32(&mut out, 48, 0);
        put16(&mut out, 52, ehsize as u16);
        put16(&mut out, 54, phentsize as u16);
        put16(&mut out, 56, phnum);
        put16(&mut out, 58, shentsize as u16);
        put16(&mut out, 60, shnum);
        put16(&mut out, 62, shstrndx);

        // ── PT_INTERP ─────────────────────────────────────────────────────────
        if phnum > 0 {
            let p = ehsize;
            put32(&mut out, p, 3); // PT_INTERP
            put32(&mut out, p + 4, 4); // PF_R
            put64(&mut out, p + 8, interp_off as u64);
            put64(&mut out, p + 16, 0);
            put64(&mut out, p + 24, 0);
            put64(&mut out, p + 32, interp_bytes.len() as u64);
            put64(&mut out, p + 40, interp_bytes.len() as u64);
            put64(&mut out, p + 48, 1);
            out[interp_off..interp_off + interp_bytes.len()].copy_from_slice(&interp_bytes);
        }

        // ── section bodies ────────────────────────────────────────────────────
        for (b, off) in body.iter().zip(&offsets) {
            out[*off..*off + b.len()].copy_from_slice(b);
        }

        // ── section headers ───────────────────────────────────────────────────
        // 0: SHT_NULL, all zero — already zeroed.
        for (i, s) in self.sections.iter().enumerate() {
            let h = shoff + shentsize * (i + 1);
            put32(&mut out, h, name_off[i + 1]);
            put32(&mut out, h + 4, s.sh_type);
            put64(&mut out, h + 8, s.flags);
            put64(&mut out, h + 16, 0); // sh_addr
            put64(&mut out, h + 24, offsets[i] as u64);
            put64(&mut out, h + 32, s.nobits_size.unwrap_or(s.data.len() as u64));
            put32(&mut out, h + 40, s.link);
            put32(&mut out, h + 44, s.info);
            put64(&mut out, h + 48, 1);
            put64(&mut out, h + 56, s.entsize);
        }
        // .shstrtab
        let last = self.sections.len();
        let h = shoff + shentsize * (last + 1);
        put32(&mut out, h, name_off[last + 1]);
        put32(&mut out, h + 4, SHT_STRTAB);
        put64(&mut out, h + 24, offsets[last] as u64);
        put64(&mut out, h + 32, body[last].len() as u64);
        put64(&mut out, h + 48, 1);

        out
    }
}

/// One ELF note: `namesz`, `descsz`, `type`, name (4-aligned), desc (4-aligned).
pub fn note_bytes(owner: &[u8], ntype: u32, desc: &[u8]) -> Vec<u8> {
    let mut name: Vec<u8> = owner.to_vec();
    name.push(0);
    let mut v = Vec::new();
    v.extend_from_slice(&(name.len() as u32).to_le_bytes());
    v.extend_from_slice(&(desc.len() as u32).to_le_bytes());
    v.extend_from_slice(&ntype.to_le_bytes());
    v.extend_from_slice(&name);
    while v.len() % 4 != 0 {
        v.push(0);
    }
    v.extend_from_slice(desc);
    while v.len() % 4 != 0 {
        v.push(0);
    }
    v
}

/// A `.note.gnu.property` descriptor holding one property.
pub fn property_desc(ptype: u32, value: u32) -> Vec<u8> {
    let mut v = Vec::new();
    v.extend_from_slice(&ptype.to_le_bytes());
    v.extend_from_slice(&4u32.to_le_bytes());
    v.extend_from_slice(&value.to_le_bytes());
    // padded to 8 for an ELF64 file
    v.extend_from_slice(&[0, 0, 0, 0]);
    v
}

/// A zlib stream whose body is a single **stored** DEFLATE block — valid, and
/// assembled without pulling a compressor into a metadata plugin's test graph.
pub fn zlib_stored(payload: &[u8]) -> Vec<u8> {
    let mut v = vec![0x78, 0x01]; // CM=8, CINFO=7, FCHECK making 0x7801 % 31 == 0
    v.push(0x01); // BFINAL=1, BTYPE=00 (stored)
    v.extend_from_slice(&(payload.len() as u16).to_le_bytes());
    v.extend_from_slice(&(!(payload.len() as u16)).to_le_bytes());
    v.extend_from_slice(payload);
    // adler32 trailer — never read by a raw-DEFLATE inflate, present for shape
    v.extend_from_slice(&adler32(payload).to_be_bytes());
    v
}

fn adler32(data: &[u8]) -> u32 {
    let (mut a, mut b) = (1u32, 0u32);
    for x in data {
        a = (a + u32::from(*x)) % 65521;
        b = (b + a) % 65521;
    }
    (b << 16) | a
}

fn put16(o: &mut [u8], at: usize, v: u16) {
    o[at..at + 2].copy_from_slice(&v.to_le_bytes());
}
fn put32(o: &mut [u8], at: usize, v: u32) {
    o[at..at + 4].copy_from_slice(&v.to_le_bytes());
}
fn put64(o: &mut [u8], at: usize, v: u64) {
    o[at..at + 8].copy_from_slice(&v.to_le_bytes());
}