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());
}