Skip to main content

codehelion_artifact/
elf.rs

1//! ELF implementation of the codehelion artifact backend boundary.
2//!
3//! The backend reads bytes through the safe `object` API and never maps or
4//! executes the artifact.
5
6use crate::dwarf::attach_dwarf_frames;
7use crate::native::{
8    collect_sections, collect_text_symbols, collect_undefined_imports, symbol_fingerprint,
9};
10use crate::x86::X86_NORMALIZATION_VERSION;
11use crate::{
12    ArtifactBackend, ArtifactCall, ArtifactCapabilities, ArtifactError, ArtifactFingerprint,
13    ArtifactFormat, ArtifactIr, ArtifactSymbol, UnresolvedCall,
14};
15use iced_x86::{Decoder, DecoderOptions, Mnemonic, OpKind};
16use object::{
17    Architecture, Endianness, Object, ObjectKind, ObjectSection, RelocationKind, RelocationTarget,
18    SectionKind,
19};
20use std::collections::{BTreeSet, HashMap};
21
22/// Parser backend for ELF artifacts.
23#[derive(Debug, Default, Clone, Copy)]
24pub struct ElfBackend;
25
26/// Version of the shared x86 instruction-shape normalization representation.
27pub const ELF_NORMALIZATION_VERSION: &str = X86_NORMALIZATION_VERSION;
28
29impl ArtifactBackend for ElfBackend {
30    fn format(&self) -> ArtifactFormat {
31        ArtifactFormat::Elf
32    }
33
34    fn detects(&self, bytes: &[u8]) -> bool {
35        bytes.starts_with(b"\x7fELF")
36    }
37
38    fn parse(&self, bytes: &[u8]) -> Result<ArtifactIr, ArtifactError> {
39        self.parse_with_debug_companion(bytes, None)
40    }
41
42    fn capabilities(&self) -> ArtifactCapabilities {
43        ArtifactCapabilities {
44            symbols: true,
45            call_graph: true,
46            source_mapping: false,
47            debug_info_unreadable: false,
48            normalized_duplicates: false,
49            independent_data_segments: false,
50            relocations: false,
51            data_segments: true,
52        }
53    }
54}
55
56impl ElfBackend {
57    /// Parse an ELF artifact with an optional, already-read external debug ELF.
58    ///
59    /// The companion is accepted only when both files declare the same GNU build
60    /// ID. This prevents a path supplied for one build from attributing source
61    /// locations to a different artifact. No companion means the normal
62    /// debug-information-absent fallback; neither path opens files or executes
63    /// inspected code.
64    ///
65    /// # Errors
66    ///
67    /// Returns an error when either input is not an ELF file, or when a supplied
68    /// companion does not carry the same build ID as the inspected artifact.
69    #[allow(
70        clippy::too_many_lines,
71        reason = "parsing one artifact keeps all fallible format reads in one transaction"
72    )]
73    pub fn parse_with_debug_companion(
74        &self,
75        bytes: &[u8],
76        debug_companion: Option<&[u8]>,
77    ) -> Result<ArtifactIr, ArtifactError> {
78        if !self.detects(bytes) {
79            return Err(ArtifactError::WrongFormat {
80                expected: ArtifactFormat::Elf,
81            });
82        }
83        let file = object::File::parse(bytes).map_err(|error| malformed(error.to_string()))?;
84        let debug_file = debug_companion
85            .map(|companion| {
86                let companion =
87                    object::File::parse(companion).map_err(|error| malformed(error.to_string()))?;
88                if !matching_build_id(&file, &companion) {
89                    return Err(malformed(
90                        "external debug companion does not have the artifact's build ID".to_owned(),
91                    ));
92                }
93                Ok(companion)
94            })
95            .transpose()?;
96        let mut ir = ArtifactIr::empty(ArtifactFormat::Elf, bytes);
97        let mut symbol_fingerprints = HashMap::new();
98        let mut symbol_addresses = HashMap::new();
99        let mut symbol_addresses_by_section = HashMap::new();
100        let mut symbol_addresses_by_fingerprint = HashMap::new();
101        collect_sections(&file, &mut ir).map_err(|error| malformed(error.to_string()))?;
102        collect_undefined_imports(file.symbols().chain(file.dynamic_symbols()), &mut ir);
103        let supports_global_address_join = file.kind() != ObjectKind::Relocatable;
104        for symbol in
105            collect_text_symbols(&file, &mut ir).map_err(|error| malformed(error.to_string()))?
106        {
107            symbol_fingerprints.insert(symbol.index, Some(symbol.fingerprint));
108            symbol_addresses_by_section
109                .insert((symbol.section, symbol.address), symbol.fingerprint);
110            if supports_global_address_join {
111                symbol_addresses
112                    .entry(symbol.address)
113                    .or_insert(symbol.fingerprint);
114            }
115            symbol_addresses_by_fingerprint
116                .insert(symbol.fingerprint, (symbol.address, symbol.size));
117        }
118        if ir.symbols.is_empty() {
119            infer_text_regions(&file, &mut ir)?;
120        }
121        record_entry_point(file.entry(), &symbol_addresses, &mut ir);
122        record_init_fini_roots(&file, &symbol_fingerprints, &symbol_addresses, &mut ir);
123        ir.calls = x86_direct_calls(
124            &file,
125            &ir.symbols,
126            &symbol_fingerprints,
127            &symbol_addresses_by_section,
128        );
129        attach_dwarf_frames(
130            debug_file.as_ref().unwrap_or(&file),
131            &symbol_addresses_by_fingerprint,
132            &mut ir,
133        );
134        ir.capabilities = ArtifactCapabilities {
135            symbols: !ir.symbols.is_empty(),
136            call_graph: !ir.calls.is_empty(),
137            source_mapping: !ir.source_mappings.is_empty(),
138            debug_info_unreadable: ir.capabilities.debug_info_unreadable,
139            normalized_duplicates: crate::x86::supports_normalized_duplicates(file.architecture()),
140            independent_data_segments: false,
141            relocations: !ir.relocations.is_empty(),
142            data_segments: !ir.data_segments.is_empty(),
143        };
144        Ok(ir)
145    }
146}
147
148/// Whether a separately supplied debug ELF can safely describe `artifact`.
149///
150/// An absent build ID is insufficient evidence: loading its line table would
151/// make an unrelated file look like a direct source-location correspondence.
152fn matching_build_id(artifact: &object::File<'_>, companion: &object::File<'_>) -> bool {
153    let Ok(Some(artifact_id)) = artifact.build_id() else {
154        return false;
155    };
156    let Ok(Some(companion_id)) = companion.build_id() else {
157        return false;
158    };
159    artifact_id == companion_id
160}
161
162/// Preserve an ELF entry address only after resolving it to a stable symbol ID.
163///
164/// A zero entry address is the conventional absence marker for relocatable
165/// objects. Addresses are lookup evidence only and never become part of IR
166/// identity.
167fn record_entry_point(
168    entry_address: u64,
169    addresses: &HashMap<u64, ArtifactFingerprint>,
170    ir: &mut ArtifactIr,
171) {
172    if entry_address != 0 {
173        if let Some(fingerprint) = addresses.get(&entry_address) {
174            ir.entry_points.push(*fingerprint);
175        }
176    }
177}
178
179/// Treat constructor and destructor arrays as conservative local roots.
180///
181/// A relocation in either array is loader evidence that the target can run
182/// even without a normal call edge or external export. The section and symbol
183/// indexes are used only while parsing; the IR records the stable fingerprint.
184fn record_init_fini_roots(
185    file: &object::File<'_>,
186    fingerprints: &HashMap<object::SymbolIndex, Option<ArtifactFingerprint>>,
187    addresses: &HashMap<u64, ArtifactFingerprint>,
188    ir: &mut ArtifactIr,
189) {
190    let mut roots = BTreeSet::new();
191    for section in file.sections() {
192        if !matches!(section.name().ok(), Some(".init_array" | ".fini_array")) {
193            continue;
194        }
195        for (_, relocation) in section.relocations() {
196            if let RelocationTarget::Symbol(index) = relocation.target() {
197                if let Some(Some(fingerprint)) = fingerprints.get(&index) {
198                    roots.insert(*fingerprint);
199                }
200            }
201        }
202        if let Ok(data) = section.data() {
203            roots.extend(pointer_roots(
204                data,
205                file.is_64(),
206                file.endianness(),
207                addresses,
208            ));
209        }
210    }
211    let existing: BTreeSet<_> = ir.entry_points.iter().copied().collect();
212    ir.entry_points.extend(
213        roots
214            .into_iter()
215            .filter(|fingerprint| !existing.contains(fingerprint)),
216    );
217}
218
219/// Resolve pointer-width values retained in a linked init/fini array.
220fn pointer_roots(
221    bytes: &[u8],
222    is_64: bool,
223    endianness: Endianness,
224    addresses: &HashMap<u64, ArtifactFingerprint>,
225) -> BTreeSet<ArtifactFingerprint> {
226    let width = if is_64 { 8 } else { 4 };
227    bytes
228        .chunks_exact(width)
229        .filter_map(|chunk| pointer_value(chunk, endianness))
230        .filter_map(|address| addresses.get(&address).copied())
231        .collect()
232}
233
234fn pointer_value(bytes: &[u8], endianness: Endianness) -> Option<u64> {
235    match bytes.len() {
236        4 => {
237            let bytes: [u8; 4] = bytes.try_into().ok()?;
238            Some(match endianness {
239                Endianness::Little => u64::from(u32::from_le_bytes(bytes)),
240                Endianness::Big => u64::from(u32::from_be_bytes(bytes)),
241            })
242        }
243        8 => {
244            let bytes: [u8; 8] = bytes.try_into().ok()?;
245            Some(match endianness {
246                Endianness::Little => u64::from_le_bytes(bytes),
247                Endianness::Big => u64::from_be_bytes(bytes),
248            })
249        }
250        _ => None,
251    }
252}
253
254/// Copy section, read-only-data, and relocation facts into format-neutral IR.
255/// Add one explicitly inferred region per executable section of a stripped ELF.
256fn infer_text_regions(file: &object::File<'_>, ir: &mut ArtifactIr) -> Result<(), ArtifactError> {
257    crate::native::infer_text_regions(file, ir, |section, normalized, data| {
258        symbol_fingerprint(None, section, normalized, data)
259    })
260    .map_err(|error| malformed(error.to_string()))?;
261    Ok(())
262}
263
264fn x86_direct_calls(
265    file: &object::File<'_>,
266    symbols: &[ArtifactSymbol],
267    fingerprints: &HashMap<object::SymbolIndex, Option<ArtifactFingerprint>>,
268    addresses: &HashMap<(object::SectionIndex, u64), ArtifactFingerprint>,
269) -> Vec<ArtifactCall> {
270    let bitness = match file.architecture() {
271        Architecture::I386 => 32,
272        Architecture::X86_64 => 64,
273        _ => return Vec::new(),
274    };
275    if symbols.is_empty() {
276        return Vec::new();
277    }
278    let mut calls = Vec::new();
279    for section in file
280        .sections()
281        .filter(|section| section.kind() == SectionKind::Text)
282    {
283        let (section_offset, _) = section.file_range().unwrap_or((0, 0));
284        let section_index = u32::try_from(section.index().0).ok();
285        let mut relocation_targets = HashMap::new();
286        for (offset, relocation) in section.relocations() {
287            if !matches!(
288                relocation.kind(),
289                RelocationKind::Relative | RelocationKind::PltRelative
290            ) {
291                continue;
292            }
293            relocation_targets.insert(offset, relocation.target());
294        }
295        for caller in symbols
296            .iter()
297            .filter(|symbol| symbol.section == section_index)
298        {
299            let Some(relative) = caller.offset.checked_sub(section_offset) else {
300                continue;
301            };
302            let Some(ip) = section.address().checked_add(relative) else {
303                continue;
304            };
305            let mut decoder = Decoder::with_ip(bitness, &caller.code, ip, DecoderOptions::NONE);
306            while decoder.can_decode() {
307                let instruction = decoder.decode();
308                if instruction.is_invalid()
309                    || instruction.mnemonic() != Mnemonic::Call
310                    || !matches!(
311                        instruction.op0_kind(),
312                        OpKind::NearBranch16 | OpKind::NearBranch32 | OpKind::NearBranch64
313                    )
314                {
315                    continue;
316                }
317                let Some(operand_offset) = instruction
318                    .ip()
319                    .checked_sub(section.address())
320                    .and_then(|offset| offset.checked_add(1))
321                else {
322                    continue;
323                };
324                let (target, unresolved) = relocation_targets.get(&operand_offset).map_or_else(
325                    || {
326                        let target = addresses
327                            .get(&(section.index(), instruction.near_branch_target()))
328                            .copied();
329                        (
330                            target,
331                            target
332                                .is_none()
333                                .then_some(UnresolvedCall::MissingRelocation),
334                        )
335                    },
336                    |relocation_target| match relocation_target {
337                        RelocationTarget::Symbol(index) => fingerprints
338                            .get(index)
339                            .and_then(|value| *value)
340                            .map_or((None, Some(UnresolvedCall::ExternalImport)), |target| {
341                                (Some(target), None)
342                            }),
343                        RelocationTarget::Section(_) | RelocationTarget::Absolute => {
344                            (None, Some(UnresolvedCall::MissingRelocation))
345                        }
346                        _ => (None, Some(UnresolvedCall::MissingRelocation)),
347                    },
348                );
349                calls.push(ArtifactCall {
350                    caller: caller.fingerprint,
351                    target,
352                    unresolved,
353                });
354            }
355        }
356    }
357    calls
358}
359
360const fn malformed(message: String) -> ArtifactError {
361    ArtifactError::Malformed {
362        format: ArtifactFormat::Elf,
363        message,
364    }
365}
366
367#[cfg(test)]
368#[allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
369mod tests;