const PROGBITS: u64 = 1;
const EXCLUDE: u64 = 0x8000_0000;
const RESERVED: u64 = 0xff00;
struct Layout {
wide: bool,
table: usize,
each: usize,
count: usize,
names: usize,
}
impl Layout {
fn of(bytes: &[u8]) -> Option<Layout> {
if bytes.get(..4)? != b"\x7fELF" || *bytes.get(5)? != 1 {
return None;
}
let wide = match bytes.get(4)? {
1 => false,
2 => true,
_ => return None,
};
let (table, each, count, names) = if wide {
(get(bytes, 0x28, 8)?, 0x3a, 0x3c, 0x3e)
} else {
(get(bytes, 0x20, 4)?, 0x2e, 0x30, 0x32)
};
let layout = Layout {
wide,
table: usize::try_from(table).ok()?,
each: usize::try_from(get(bytes, each, 2)?).ok()?,
count: usize::try_from(get(bytes, count, 2)?).ok()?,
names: usize::try_from(get(bytes, names, 2)?).ok()?,
};
let size = if wide { 64 } else { 40 };
let end = layout.count.checked_mul(layout.each)?.checked_add(layout.table)?;
(layout.each == size && layout.names < layout.count && end <= bytes.len()).then_some(layout)
}
fn header(&self, nth: usize) -> usize {
self.table + nth * self.each
}
fn place(&self) -> (usize, usize, usize) {
if self.wide { (24, 32, 8) } else { (16, 20, 4) }
}
fn contents(&self, bytes: &[u8], nth: usize) -> Option<std::ops::Range<usize>> {
let (offset, size, width) = self.place();
let at = self.header(nth);
let from = usize::try_from(get(bytes, at + offset, width)?).ok()?;
let to = from.checked_add(usize::try_from(get(bytes, at + size, width)?).ok()?)?;
(to <= bytes.len()).then_some(from..to)
}
fn name<'a>(&self, bytes: &'a [u8], nth: usize) -> Option<&'a [u8]> {
let strings = &bytes[self.contents(bytes, self.names)?];
let from = usize::try_from(get(bytes, self.header(nth), 4)?).ok()?;
let rest = strings.get(from..)?;
Some(&rest[..rest.iter().position(|byte| *byte == 0)?])
}
}
fn get(bytes: &[u8], at: usize, width: usize) -> Option<u64> {
let field = bytes.get(at..at.checked_add(width)?)?;
Some(field.iter().rev().fold(0, |sum, &byte| sum << 8 | u64::from(byte)))
}
fn put(bytes: &mut [u8], at: usize, width: usize, value: u64) {
bytes[at..at + width].copy_from_slice(&value.to_le_bytes()[..width]);
}
pub fn attach(object: &mut Vec<u8>, name: &str, payload: &[u8]) -> bool {
let Some(layout) = Layout::of(object) else { return false };
let Some(strings) = layout.contents(object, layout.names) else { return false };
if layout.count == 0 || layout.count as u64 + 1 >= RESERVED || name.contains('\0') {
return false;
}
let mut names = object[strings].to_vec();
let named = names.len() as u64;
names.extend_from_slice(name.as_bytes());
names.push(0);
let mut headers = object[layout.table..layout.header(layout.count)].to_vec();
let contents = object.len();
object.extend_from_slice(payload);
let strings = object.len();
object.extend_from_slice(&names);
object.resize(object.len().next_multiple_of(8), 0);
let table = object.len();
let (offset, size, width) = layout.place();
let at = layout.names * layout.each;
put(&mut headers, at + offset, width, strings as u64);
put(&mut headers, at + size, width, names.len() as u64);
let word = if layout.wide { 8 } else { 4 };
let fields = [
(4, named),
(4, PROGBITS),
(word, EXCLUDE),
(word, 0),
(word, contents as u64),
(word, payload.len() as u64),
(4, 0),
(4, 0),
(word, 1),
(word, 0),
];
for (width, value) in fields {
headers.extend_from_slice(&value.to_le_bytes()[..width]);
}
object.extend_from_slice(&headers);
let (shoff, shnum) = if layout.wide { (0x28, 0x3c) } else { (0x20, 0x30) };
put(object, shoff, word, table as u64);
put(object, shnum, 2, layout.count as u64 + 1);
true
}
#[must_use]
pub fn carried<'a>(object: &'a [u8], name: &str) -> Option<&'a [u8]> {
let layout = Layout::of(object)?;
let nth = (0..layout.count).find(|&nth| layout.name(object, nth) == Some(name.as_bytes()))?;
Some(&object[layout.contents(object, nth)?])
}
#[cfg(test)]
mod tests {
use super::{attach, carried};
use object::elf;
use object::write::{Object, Relocation, StandardSection, Symbol, SymbolSection};
use object::{
Architecture, BinaryFormat, Endianness, Object as _, ObjectSection as _, ObjectSymbol as _,
RelocationFlags, SectionFlags, SectionKind, SymbolFlags, SymbolKind, SymbolScope,
};
fn object(architecture: Architecture, call: elf::RelocationType) -> Vec<u8> {
let mut obj = Object::new(BinaryFormat::Elf, architecture, Endianness::Little);
let text = obj.section_id(StandardSection::Text);
let at = obj.append_section_data(text, &[0xe8, 0, 0, 0, 0, 0xc3], 16);
obj.add_symbol(Symbol {
name: b"f".to_vec(),
value: at,
size: 6,
kind: SymbolKind::Text,
scope: SymbolScope::Linkage,
weak: false,
section: SymbolSection::Section(text),
flags: SymbolFlags::None,
});
let g = obj.add_symbol(Symbol {
name: b"g".to_vec(),
value: 0,
size: 0,
kind: SymbolKind::Text,
scope: SymbolScope::Unknown,
weak: false,
section: SymbolSection::Undefined,
flags: SymbolFlags::None,
});
obj.add_relocation(
text,
Relocation {
offset: at + 1,
symbol: g,
addend: -4,
flags: RelocationFlags::Elf { r_type: call },
},
)
.expect("the relocation is one the writer takes");
obj.write().expect("the object is written")
}
type Section = (String, Vec<u8>);
type Named = (String, Option<usize>);
fn read(bytes: &[u8]) -> (Vec<Section>, Vec<Named>, usize) {
let file = object::File::parse(bytes).expect("the file still parses");
let sections = file
.sections()
.filter(|s| s.name().ok() != Some(".shstrtab"))
.map(|s| (s.name().unwrap().to_string(), s.data().unwrap().to_vec()))
.collect();
let symbols = file
.symbols()
.map(|s| (s.name().unwrap().to_string(), s.section_index().map(|i| i.0)))
.collect();
let relocs = file.sections().map(|s| s.relocations().count()).sum();
(sections, symbols, relocs)
}
#[test]
fn a_section_put_in_is_read_back_and_nothing_else_moves() {
for (architecture, call) in [
(Architecture::X86_64, elf::R_X86_64_PLT32),
(Architecture::I386, elf::R_386_PC32),
(Architecture::Aarch64, elf::R_AARCH64_CALL26),
] {
let before = object(architecture, call);
let mut after = before.clone();
assert!(attach(&mut after, ".rucc.lto", b"the module"), "{architecture:?}");
assert_eq!(carried(&after, ".rucc.lto"), Some(&b"the module"[..]), "{architecture:?}");
assert_eq!(carried(&after, ".rucc.other"), None);
assert_eq!(carried(&before, ".rucc.lto"), None);
let (old, symbols, relocs) = read(&before);
let (mut new, moved, kept) = read(&after);
let added = new.pop().expect("the new section is the last one");
assert_eq!(added, (".rucc.lto".to_string(), b"the module".to_vec()));
assert_eq!(new, old, "{architecture:?}: every other section is as it was");
assert_eq!(moved, symbols, "{architecture:?}: every symbol is where it was");
assert_eq!(kept, relocs, "{architecture:?}: and so is every relocation");
let file = object::File::parse(&*after).unwrap();
let section = file.section_by_name(".rucc.lto").unwrap();
assert_eq!(section.kind(), SectionKind::Other, "{architecture:?}: not loaded");
let SectionFlags::Elf { sh_type, sh_flags } = section.flags() else {
panic!("not ELF")
};
assert_eq!(
(sh_type, sh_flags),
(elf::SHT_PROGBITS, elf::SHF_EXCLUDE),
"{architecture:?}"
);
}
}
#[test]
fn a_file_that_is_not_elf_is_left_alone() {
let mut obj = Object::new(BinaryFormat::Coff, Architecture::X86_64, Endianness::Little);
let text = obj.section_id(StandardSection::Text);
obj.append_section_data(text, &[0xc3], 16);
let before = obj.write().unwrap();
let mut after = before.clone();
assert!(!attach(&mut after, ".rucc.lto", b"the module"));
assert_eq!(after, before);
assert_eq!(carried(&after, ".rucc.lto"), None);
assert_eq!(carried(b"!<arch>\n", ".rucc.lto"), None);
let mut whole = object(Architecture::X86_64, elf::R_X86_64_PLT32);
assert!(attach(&mut whole, ".rucc.lto", b"the module"));
for len in [0, 4, 16, 64, whole.len() / 2, whole.len() - 1] {
assert_eq!(carried(&whole[..len], ".rucc.lto"), None, "{len} bytes");
}
}
}