1use std::collections::{BTreeSet, HashMap};
9use std::io::Cursor;
10
11use crate::native::{
12 collect_sections, collect_text_symbols, collect_undefined_imports, symbol_fingerprint,
13};
14use crate::x86::X86_NORMALIZATION_VERSION;
15use crate::{
16 ArtifactBackend, ArtifactCapabilities, ArtifactError, ArtifactFingerprint, ArtifactFormat,
17 ArtifactIr,
18};
19use object::Object;
20use pdb::{FallibleIterator, PDB};
21
22#[cfg(test)]
23use crate::ArtifactImportKind;
24
25#[derive(Debug, Default, Clone, Copy)]
27pub struct PeCoffBackend;
28
29pub const PE_COFF_NORMALIZATION_VERSION: &str = X86_NORMALIZATION_VERSION;
31
32impl ArtifactBackend for PeCoffBackend {
33 fn format(&self) -> ArtifactFormat {
34 ArtifactFormat::PeCoff
35 }
36
37 fn detects(&self, bytes: &[u8]) -> bool {
38 bytes.starts_with(b"MZ") || is_coff_machine(bytes)
42 }
43
44 fn parse(&self, bytes: &[u8]) -> Result<ArtifactIr, ArtifactError> {
45 self.parse_with_pdb(bytes, None)
46 }
47
48 fn capabilities(&self) -> ArtifactCapabilities {
49 ArtifactCapabilities {
50 symbols: true,
51 call_graph: false,
52 source_mapping: false,
53 debug_info_unreadable: false,
54 normalized_duplicates: false,
55 independent_data_segments: false,
56 relocations: true,
57 data_segments: true,
58 }
59 }
60}
61
62impl PeCoffBackend {
63 pub fn parse_with_pdb(
74 &self,
75 bytes: &[u8],
76 pdb_bytes: Option<&[u8]>,
77 ) -> Result<ArtifactIr, ArtifactError> {
78 if !self.detects(bytes) {
79 return Err(ArtifactError::WrongFormat {
80 expected: ArtifactFormat::PeCoff,
81 });
82 }
83 let file = object::File::parse(bytes).map_err(|error| malformed(error.to_string()))?;
84 if !matches!(
85 file.format(),
86 object::BinaryFormat::Coff | object::BinaryFormat::Pe
87 ) {
88 return Err(ArtifactError::WrongFormat {
89 expected: ArtifactFormat::PeCoff,
90 });
91 }
92 let mut ir = ArtifactIr::empty(ArtifactFormat::PeCoff, bytes);
93 collect_sections(&file, &mut ir).map_err(|error| malformed(error.to_string()))?;
94 collect_undefined_imports(file.symbols(), &mut ir);
95 let named = collect_symbols(&file, &mut ir)?;
96 let symbol_ranges = if ir.symbols.is_empty() {
101 infer_text_regions(&file, &mut ir)?
102 } else {
103 named
104 };
105 if let Some(pdb_bytes) = pdb_bytes {
106 collect_pdb_frames(&file, pdb_bytes, &symbol_ranges, &mut ir)?;
107 }
108 ir.capabilities = ArtifactCapabilities {
109 symbols: !ir.symbols.is_empty(),
110 call_graph: false,
111 source_mapping: !ir.source_mappings.is_empty(),
112 debug_info_unreadable: false,
113 normalized_duplicates: crate::x86::supports_normalized_duplicates(file.architecture()),
114 independent_data_segments: false,
115 relocations: !ir.relocations.is_empty(),
116 data_segments: !ir.data_segments.is_empty(),
117 };
118 Ok(ir)
119 }
120}
121
122fn symbol_address_of(file: &object::File<'_>, relative_address: u32) -> u64 {
130 use object::Object as _;
131
132 file.relative_address_base()
133 .saturating_add(u64::from(relative_address))
134}
135
136#[derive(Debug, Clone, Copy)]
138struct SymbolRange {
139 fingerprint: ArtifactFingerprint,
140 start: u64,
141 end: u64,
142}
143
144fn is_coff_machine(bytes: &[u8]) -> bool {
145 matches!(
146 bytes.get(..2),
147 Some([0x4c, 0x01] | [0x64, 0x86 | 0xaa] | [0xaa, 0x64])
148 )
149}
150
151fn collect_symbols(
152 file: &object::File<'_>,
153 ir: &mut ArtifactIr,
154) -> Result<Vec<SymbolRange>, ArtifactError> {
155 collect_text_symbols(file, ir)
156 .map(|ranges| {
157 ranges
158 .into_iter()
159 .map(|range| SymbolRange {
160 fingerprint: range.fingerprint,
161 start: range.address,
162 end: range.address.saturating_add(range.size),
163 })
164 .collect()
165 })
166 .map_err(|error| malformed(error.to_string()))
167}
168
169fn infer_text_regions(
175 file: &object::File<'_>,
176 ir: &mut ArtifactIr,
177) -> Result<Vec<SymbolRange>, ArtifactError> {
178 let ranges = crate::native::infer_text_regions(file, ir, |section, normalized, data| {
179 symbol_fingerprint(None, section, normalized, data)
180 })
181 .map_err(|error| malformed(error.to_string()))?;
182 Ok(ranges
183 .into_iter()
184 .map(|(fingerprint, address, size)| SymbolRange {
185 fingerprint,
186 start: address,
187 end: address.saturating_add(size),
188 })
189 .collect())
190}
191
192#[allow(
198 clippy::too_many_lines,
199 reason = "the identity check, fallible PDB iteration, and stable-ID attachment form one safety boundary"
200)]
201fn collect_pdb_frames(
202 file: &object::File<'_>,
203 pdb_bytes: &[u8],
204 symbol_ranges: &[SymbolRange],
205 ir: &mut ArtifactIr,
206) -> Result<(), ArtifactError> {
207 if file.format() != object::BinaryFormat::Pe {
208 return Err(malformed(
209 "a PDB can only describe a PE image, not a COFF object".to_owned(),
210 ));
211 }
212 let image_pdb = file
213 .pdb_info()
214 .map_err(|error| malformed(error.to_string()))?
215 .ok_or_else(|| malformed("PE image has no CodeView PDB identity".to_owned()))?;
216 let mut pdb =
217 PDB::open(Cursor::new(pdb_bytes)).map_err(|error| malformed(error.to_string()))?;
218 let pdb_info = pdb
219 .pdb_information()
220 .map_err(|error| malformed(error.to_string()))?;
221 if !pdb_identity_matches(
222 pdb_info.guid.to_bytes_le(),
223 pdb_info.age,
224 image_pdb.guid(),
225 image_pdb.age(),
226 ) {
227 return Err(malformed(
228 "PDB GUID or age does not match the PE image".to_owned(),
229 ));
230 }
231 let address_map = pdb
232 .address_map()
233 .map_err(|error| malformed(error.to_string()))?;
234 let string_table = pdb
235 .string_table()
236 .map_err(|error| malformed(error.to_string()))?;
237 let debug_information = pdb
238 .debug_information()
239 .map_err(|error| malformed(error.to_string()))?;
240 let mut modules = debug_information
241 .modules()
242 .map_err(|error| malformed(error.to_string()))?;
243 let mut frames = Vec::new();
244 while let Some(module) = modules
245 .next()
246 .map_err(|error| malformed(error.to_string()))?
247 {
248 let Some(module_info) = pdb
249 .module_info(&module)
250 .map_err(|error| malformed(error.to_string()))?
251 else {
252 continue;
253 };
254 let Ok(program) = module_info.line_program() else {
255 continue;
256 };
257 let mut lines = program.lines();
258 while let Some(line) = lines.next().map_err(|error| malformed(error.to_string()))? {
259 let Some(rva) = line.offset.to_rva(&address_map) else {
260 continue;
261 };
262 let Ok(file_info) = program.get_file_info(line.file_index) else {
263 continue;
264 };
265 let Ok(source) = file_info.name.to_string_lossy(&string_table) else {
266 continue;
267 };
268 frames.push((
269 symbol_address_of(file, rva.0),
270 crate::ArtifactInlineFrame {
271 evidence_kind: crate::ArtifactSourceLocationEvidenceKind::Pdb,
272 source: source.into_owned(),
273 line: Some(line.line_start),
274 column: line.column_start.filter(|column| *column != 0),
275 },
276 ));
277 }
278 }
279 frames.sort_by_key(|(address, _)| *address);
280 let symbol_rows: HashMap<_, _> = ir
281 .symbols
282 .iter()
283 .enumerate()
284 .map(|(index, symbol)| (symbol.fingerprint, index))
285 .collect();
286 for range in symbol_ranges {
287 let frame_start = frames.partition_point(|(address, _)| *address < range.start);
288 let mut symbol_frames: Vec<_> = frames[frame_start..]
289 .iter()
290 .take_while(|(address, _)| *address < range.end)
291 .map(|(_, frame)| frame.clone())
292 .collect();
293 symbol_frames.sort_by(|left, right| {
294 (&left.source, left.line, left.column).cmp(&(&right.source, right.line, right.column))
295 });
296 symbol_frames.dedup();
297 if symbol_frames.is_empty() {
298 continue;
299 }
300 if let Some(index) = symbol_rows.get(&range.fingerprint) {
301 ir.symbols[*index].inline_stack = symbol_frames;
302 }
303 }
304 ir.source_mappings = ir
305 .symbols
306 .iter()
307 .flat_map(|symbol| symbol.inline_stack.iter().map(|frame| frame.source.clone()))
308 .collect::<BTreeSet<_>>()
309 .into_iter()
310 .map(|uri| crate::ArtifactSourceMapping { uri })
311 .collect();
312 Ok(())
313}
314
315fn pdb_identity_matches(
321 pdb_guid: [u8; 16],
322 pdb_age: u32,
323 image_guid: [u8; 16],
324 image_age: u32,
325) -> bool {
326 pdb_guid == image_guid && pdb_age >= image_age
327}
328
329const fn malformed(message: String) -> ArtifactError {
330 ArtifactError::Malformed {
331 format: ArtifactFormat::PeCoff,
332 message,
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 #![allow(clippy::expect_used, clippy::unwrap_used)]
339
340 use super::*;
341 use object::write::{Object as WriteObject, StandardSection, Symbol, SymbolSection};
342 use object::{Architecture, BinaryFormat, Endianness, SymbolFlags, SymbolKind, SymbolScope};
343 use proptest::prelude::*;
344
345 fn coff_fixture() -> Vec<u8> {
346 let mut object =
347 WriteObject::new(BinaryFormat::Coff, Architecture::X86_64, Endianness::Little);
348 let text = object.section_id(StandardSection::Text);
349 let offset = object.append_section_data(text, &[0x90, 0xc3], 1);
350 object.add_symbol(Symbol {
351 name: b"render".to_vec(),
352 value: offset,
353 size: 2,
354 kind: SymbolKind::Text,
355 scope: SymbolScope::Dynamic,
356 weak: false,
357 section: SymbolSection::Section(text),
358 flags: SymbolFlags::None,
359 });
360 object.write().expect("write COFF fixture")
361 }
362
363 fn coff_fixture_without_symbols() -> Vec<u8> {
364 let mut object =
365 WriteObject::new(BinaryFormat::Coff, Architecture::X86_64, Endianness::Little);
366 let text = object.section_id(StandardSection::Text);
367 object.append_section_data(text, &[0x90, 0xc3], 1);
368 object.write().expect("write symbol-free COFF fixture")
369 }
370
371 fn coff_zero_sized_alias_fixture() -> Vec<u8> {
372 let mut object =
373 WriteObject::new(BinaryFormat::Coff, Architecture::X86_64, Endianness::Little);
374 let text = object.section_id(StandardSection::Text);
375 let offset = object.append_section_data(text, &[0x90, 0xc3], 1);
376 for (name, size) in [(b"implementation".as_slice(), 2), (b"alias", 0)] {
377 object.add_symbol(Symbol {
378 name: name.to_vec(),
379 value: offset,
380 size,
381 kind: SymbolKind::Text,
382 scope: SymbolScope::Dynamic,
383 weak: false,
384 section: SymbolSection::Section(text),
385 flags: SymbolFlags::None,
386 });
387 }
388 object.write().expect("write zero-sized COFF alias fixture")
389 }
390
391 fn coff_undefined_import_fixture() -> Vec<u8> {
392 let mut object =
393 WriteObject::new(BinaryFormat::Coff, Architecture::X86_64, Endianness::Little);
394 object.add_symbol(Symbol {
395 name: b"external_call".to_vec(),
396 value: 0,
397 size: 0,
398 kind: SymbolKind::Text,
399 scope: SymbolScope::Dynamic,
400 weak: false,
401 section: SymbolSection::Undefined,
402 flags: SymbolFlags::None,
403 });
404 object.write().expect("write COFF undefined import fixture")
405 }
406
407 #[test]
408 fn sorted_pdb_line_addresses_are_sliced_to_each_symbol_range() {
409 let frames = [(2, "before"), (10, "start"), (12, "inside"), (15, "after")];
410 let start = frames.partition_point(|(address, _)| *address < 10);
411 let matched: Vec<_> = frames[start..]
412 .iter()
413 .take_while(|(address, _)| *address < 15)
414 .map(|(_, label)| *label)
415 .collect();
416
417 assert_eq!(matched, ["start", "inside"]);
418 }
419
420 #[test]
421 fn parses_a_coff_function_without_executing_it() {
422 let ir = PeCoffBackend
423 .parse(&coff_fixture())
424 .expect("parse COFF fixture");
425 assert_eq!(ir.format, ArtifactFormat::PeCoff);
426 assert_eq!(ir.symbols.len(), 1, "{ir:#?}");
427 assert!(ir.capabilities.symbols);
428 assert_eq!(ir.symbols[0].name.as_deref(), Some("render"));
429 assert_eq!(ir.symbols[0].code, vec![0x90, 0xc3]);
430 assert_eq!(
431 ir.symbols[0]
432 .normalized
433 .as_ref()
434 .map(|value| value.version.as_str()),
435 Some(PE_COFF_NORMALIZATION_VERSION)
436 );
437 }
438
439 #[test]
446 fn an_inferred_text_region_can_be_joined_to_debug_information() {
447 let bytes = coff_fixture_without_symbols();
448 let file = object::File::parse(bytes.as_slice()).expect("parse symbol-free COFF fixture");
449 let mut ir = ArtifactIr::empty(ArtifactFormat::PeCoff, &bytes);
450 let ranges = infer_text_regions(&file, &mut ir).expect("infer the text region");
451
452 assert_eq!(ranges.len(), ir.symbols.len());
453 assert_eq!(ranges[0].fingerprint, ir.symbols[0].fingerprint);
454 assert!(ranges[0].end > ranges[0].start);
455 }
456
457 #[test]
459 fn a_relative_address_without_a_load_address_is_itself() {
460 let bytes = coff_fixture();
461 let file = object::File::parse(bytes.as_slice()).expect("parse COFF fixture");
462
463 assert_eq!(symbol_address_of(&file, 0x20), 0x20);
464 }
465
466 #[test]
467 fn symbol_free_coff_keeps_an_explicitly_inferred_text_region() {
468 let ir = PeCoffBackend
469 .parse(&coff_fixture_without_symbols())
470 .expect("parse symbol-free COFF fixture");
471 assert_eq!(ir.symbols.len(), 1, "{ir:#?}");
472 assert!(ir.capabilities.symbols);
473 assert!(ir.symbols[0].size_inferred);
474 assert_eq!(ir.symbols[0].name, None);
475 assert_eq!(ir.symbols[0].code, vec![0x90, 0xc3]);
476 }
477
478 #[test]
479 fn zero_sized_coff_alias_is_retained_without_claiming_implementation_bytes() {
480 let ir = PeCoffBackend
481 .parse(&coff_zero_sized_alias_fixture())
482 .expect("parse zero-sized COFF alias fixture");
483 let alias = ir
484 .symbols
485 .iter()
486 .find(|symbol| symbol.name.as_deref() == Some("alias"))
487 .expect("alias record");
488 let implementation = ir
489 .symbols
490 .iter()
491 .find(|symbol| symbol.name.as_deref() == Some("implementation"))
492 .expect("implementation record");
493 assert!(alias.size_inferred);
494 assert_eq!(alias.size, 0);
495 assert!(alias.code.is_empty());
496 assert_eq!(implementation.code, vec![0x90, 0xc3]);
497 }
498
499 #[test]
500 fn records_undefined_coff_symbols_as_imports() {
501 let ir = PeCoffBackend
502 .parse(&coff_undefined_import_fixture())
503 .expect("parse COFF undefined import fixture");
504 assert_eq!(ir.imports.len(), 1, "{ir:#?}");
505 assert_eq!(ir.imports[0].name.as_deref(), Some("external_call"));
506 assert_eq!(ir.imports[0].kind, ArtifactImportKind::Function);
507 }
508
509 #[test]
510 fn other_bytes_do_not_claim_the_backend() {
511 assert!(!PeCoffBackend.detects(b"not an object"));
512 assert!(matches!(
513 PeCoffBackend.parse(b"not an object"),
514 Err(ArtifactError::WrongFormat { .. })
515 ));
516 }
517
518 #[test]
519 fn a_pdb_is_not_guessed_for_a_coff_object() {
520 let error = PeCoffBackend
521 .parse_with_pdb(&coff_fixture(), Some(b"not a pdb"))
522 .expect_err("COFF objects cannot have an external PDB companion");
523 assert!(error.to_string().contains("COFF object"));
524 }
525
526 #[test]
527 fn pdb_identity_requires_guid_and_accepts_a_newer_age() {
528 assert!(pdb_identity_matches([1; 16], 3, [1; 16], 3));
529 assert!(pdb_identity_matches([1; 16], 4, [1; 16], 3));
530 assert!(!pdb_identity_matches([2; 16], 4, [1; 16], 3));
531 assert!(!pdb_identity_matches([1; 16], 2, [1; 16], 3));
532 }
533
534 proptest::proptest! {
535 #[test]
536 fn arbitrary_bytes_never_panic(bytes in proptest::collection::vec(any::<u8>(), 0..4096)) {
537 let _ = PeCoffBackend.parse(&bytes);
538 }
539 }
540}