1use std::collections::BTreeSet;
7use std::sync::atomic::{AtomicU64, Ordering};
8
9type FixupPair = (u64, u64);
10type FixupTables = (Vec<FixupPair>, Vec<FixupPair>);
11
12#[derive(Debug, Clone)]
14pub enum FixupStatus {
15 Absent,
17 Applied { pairs: usize },
20 Skipped { reason: String },
25}
26
27pub struct Pe {
29 data: Vec<u8>,
30 pub image_base: u64,
31 sections: Vec<Section>,
32 fixup_posts: Vec<(u64, u64)>,
45 fixup_rel32: Vec<(u64, u64)>,
51 fixup_status: FixupStatus,
53 id: u64,
61}
62
63fn next_pe_id() -> u64 {
64 static NEXT_ID: AtomicU64 = AtomicU64::new(1);
65 NEXT_ID.fetch_add(1, Ordering::Relaxed)
66}
67
68#[derive(Clone, Debug)]
69pub struct Section {
70 pub name: String,
71 pub virtual_address: u32, pub virtual_size: u32,
73 pub raw_offset: u32,
74 pub raw_size: u32,
75 pub characteristics: u32,
79}
80
81pub mod section_flags {
84 pub const CODE: u32 = 0x0000_0020;
86 pub const DISCARDABLE: u32 = 0x0200_0000;
88 pub const EXECUTE: u32 = 0x2000_0000;
90 pub const READ: u32 = 0x4000_0000;
92 pub const WRITE: u32 = 0x8000_0000;
94}
95
96impl Section {
97 pub fn is_executable(&self) -> bool {
100 self.characteristics & (section_flags::EXECUTE | section_flags::CODE) != 0
101 }
102}
103
104fn u16(d: &[u8], o: usize) -> u16 {
105 u16::from_le_bytes([d[o], d[o + 1]])
106}
107fn u32(d: &[u8], o: usize) -> u32 {
108 u32::from_le_bytes([d[o], d[o + 1], d[o + 2], d[o + 3]])
109}
110
111impl Pe {
112 pub fn load(path: &str) -> Result<Pe, String> {
113 let data = std::fs::read(path).map_err(|e| format!("read {path}: {e}"))?;
114 Pe::parse(data)
115 }
116
117 pub fn parse(data: Vec<u8>) -> Result<Pe, String> {
118 let sz = data.len();
119 if sz < 0x40 || &data[0..2] != b"MZ" {
120 return Err(format!(
121 "not an MZ image: first 2 bytes are {:?} (expected b\"MZ\"), file size = {sz} bytes",
122 &data[..data.len().min(2)]
123 ));
124 }
125 let e_lfanew = u32(&data, 0x3c) as usize;
126 if data.len() >= e_lfanew + 2 && &data[e_lfanew..e_lfanew + 2] == b"LE" {
127 return parse_le(data, e_lfanew);
128 }
129 if data.len() < e_lfanew + 24 || &data[e_lfanew..e_lfanew + 4] != b"PE\0\0" {
130 let there = data
135 .get(e_lfanew..e_lfanew + 4)
136 .map(|s| format!("{:02x?}", s))
137 .unwrap_or_else(|| "<past EOF>".into());
138 return Err(format!(
139 "bad PE signature: e_lfanew = {e_lfanew:#x}, bytes there = [{there}] (expected b\"PE\\0\\0\"); file size = {sz} bytes"
140 ));
141 }
142 let coff = e_lfanew + 4;
143 let machine = u16(&data, coff);
144 if machine != 0x014c {
145 return Err(format!(
146 "unsupported machine {machine:#06x} (expected i386 = 0x014c); this crate only decodes 32-bit x86"
147 ));
148 }
149 let num_sections = u16(&data, coff + 2) as usize;
150 let opt_size = u16(&data, coff + 16) as usize;
151 let opt = coff + 20;
152 let magic = u16(&data, opt);
153 if magic != 0x10b {
154 return Err(format!(
155 "unsupported optional-header magic {magic:#06x} (expected PE32 = 0x10b); PE32+ (64-bit) and ROM images are not handled"
156 ));
157 }
158 let image_base = u32(&data, opt + 28) as u64;
159
160 let sec_table = opt + opt_size;
161 let mut sections = Vec::with_capacity(num_sections);
162 for i in 0..num_sections {
163 let s = sec_table + i * 40;
164 if s + 40 > data.len() {
165 return Err(format!(
166 "section table runs past EOF: section #{i} would start at file offset {s:#x} \
167 (e_lfanew={e_lfanew:#x} + opt_size={opt_size:#x} + {i}*40), \
168 but file size is only {sz} bytes — corrupt header or wrong format"
169 ));
170 }
171 let raw_name = &data[s..s + 8];
172 let end = raw_name.iter().position(|&b| b == 0).unwrap_or(8);
173 sections.push(Section {
174 name: String::from_utf8_lossy(&raw_name[..end]).into_owned(),
175 virtual_size: u32(&data, s + 8),
176 virtual_address: u32(&data, s + 12),
177 raw_size: u32(&data, s + 16),
178 raw_offset: u32(&data, s + 20),
179 characteristics: u32(&data, s + 36),
180 });
181 }
182 Ok(Pe {
183 data,
184 image_base,
185 sections,
186 fixup_posts: Vec::new(),
187 fixup_rel32: Vec::new(),
188 fixup_status: FixupStatus::Absent,
189 id: next_pe_id(),
190 })
191 }
192
193 pub fn id(&self) -> u64 {
195 self.id
196 }
197
198 pub fn fixup_post_images(&self, pres: &BTreeSet<u64>) -> BTreeSet<u64> {
204 let mut out = BTreeSet::new();
205 if self.fixup_posts.is_empty() || pres.is_empty() {
206 return out;
207 }
208 for &(pre, post) in &self.fixup_posts {
209 if pres.contains(&pre) {
210 out.insert(post);
211 }
212 }
213 out
214 }
215
216 pub fn fixup_post_image(&self, pre: u64) -> Option<u64> {
225 self.fixup_posts
226 .iter()
227 .find_map(|&(p, post)| (p == pre).then_some(post))
228 }
229
230 pub fn fixup_count(&self) -> usize {
233 self.fixup_posts.len()
234 }
235
236 pub fn fixup_pairs(&self) -> &[(u64, u64)] {
240 &self.fixup_posts
241 }
242
243 pub fn fixup_rel32_pairs(&self) -> &[(u64, u64)] {
245 &self.fixup_rel32
246 }
247
248 pub fn fixup_rel32_target(&self, loc: u64) -> Option<u64> {
251 self.fixup_rel32
252 .iter()
253 .find_map(|&(l, t)| (l == loc).then_some(t))
254 }
255
256 pub fn fixup_status(&self) -> &FixupStatus {
258 &self.fixup_status
259 }
260
261 pub fn section_of(&self, va: u64) -> Option<&Section> {
263 let rva = va.checked_sub(self.image_base)? as u32;
264 self.sections.iter().find(|s| {
265 let span = s.virtual_size.max(s.raw_size);
268 rva >= s.virtual_address && rva < s.virtual_address + span
269 })
270 }
271
272 pub fn read(&self, va: u64, len: usize) -> Option<&[u8]> {
276 let sec = self.section_of(va)?;
277 let rva = (va - self.image_base) as u32;
278 let off_in_sec = rva - sec.virtual_address;
279 if off_in_sec >= sec.raw_size {
280 return None; }
282 let file_off = (sec.raw_offset + off_in_sec) as usize;
283 let avail = (sec.raw_size - off_in_sec) as usize;
284 let take = len.min(avail).min(self.data.len().saturating_sub(file_off));
285 Some(&self.data[file_off..file_off + take])
286 }
287
288 pub fn sections(&self) -> &[Section] {
290 &self.sections
291 }
292
293 pub fn is_executable_va(&self, va: u64) -> bool {
294 self.section_of(va).is_some()
306 }
307
308 pub fn is_executable_section_va(&self, va: u64) -> bool {
315 self.section_of(va)
316 .map(|s| s.is_executable())
317 .unwrap_or(false)
318 }
319
320 pub fn describe_sections(&self, va: u64) -> String {
325 let mut s = String::new();
326 let mut best: Option<(&Section, u64)> = None;
327 for sec in &self.sections {
328 let start = self.image_base + sec.virtual_address as u64;
329 let span = sec.virtual_size.max(sec.raw_size) as u64;
330 let end = start + span;
331 s.push_str(&format!(
332 " {name:<8} [0x{start:08x}..0x{end:08x}) ({span} bytes){flag}\n",
333 name = sec.name,
334 start = start,
335 end = end,
336 span = span,
337 flag = if sec.is_executable() { " code" } else { "" },
338 ));
339 let d = if va < start {
340 start - va
341 } else {
342 va.saturating_sub(end)
343 };
344 if best.map(|(_, bd)| d < bd).unwrap_or(true) {
345 best = Some((sec, d));
346 }
347 }
348 if let Some((sec, d)) = best {
349 s.push_str(&format!(
350 " closest section to {va:#010x}: {name} (distance {d:#x} bytes) — \
351 did you mean 0x{start:08x}?",
352 name = sec.name,
353 start = self.image_base + sec.virtual_address as u64,
354 ));
355 }
356 s
357 }
358
359 pub fn image_range(&self) -> (u64, u64) {
361 let end = self
362 .sections
363 .iter()
364 .map(|s| s.virtual_address as u64 + s.virtual_size.max(s.raw_size) as u64)
365 .max()
366 .unwrap_or(0);
367 (self.image_base, self.image_base + end)
368 }
369
370 pub fn referenced_addrs(&self) -> std::collections::HashSet<u32> {
383 let (lo, hi) = self.image_range();
384 let mut out: std::collections::HashSet<u32> = std::collections::HashSet::new();
385 for sec in &self.sections {
386 let start = sec.raw_offset as usize;
387 let end = (start + sec.raw_size as usize).min(self.data.len());
388 if start >= end {
389 continue;
390 }
391 let blob = &self.data[start..end];
392 let sec_va = self.image_base + sec.virtual_address as u64;
393 if sec.name == ".text" || !sec.is_executable() {
400 for i in 0..blob.len().saturating_sub(4) {
401 if blob[i] == 0xEB && i + 2 <= blob.len() {
408 let rel = blob[i + 1] as i8 as i64;
409 let t = (sec_va + i as u64 + 2).wrapping_add(rel as u64);
410 if (lo..hi).contains(&t) {
411 out.insert(t as u32);
412 }
413 }
414 let (op, rel_at) = match blob[i] {
415 0xE8 | 0xE9 => (blob[i], i + 1), 0x0F if i + 6 <= blob.len() && (0x80..=0x8F).contains(&blob[i + 1]) => {
417 (blob[i + 1], i + 2) }
419 _ => continue,
420 };
421 let _ = op;
422 if rel_at + 4 > blob.len() {
423 continue;
424 }
425 let rel = i32::from_le_bytes([
426 blob[rel_at],
427 blob[rel_at + 1],
428 blob[rel_at + 2],
429 blob[rel_at + 3],
430 ]) as i64;
431 let t = (sec_va + (rel_at as u64 + 4)).wrapping_add(rel as u64);
432 if (lo..hi).contains(&t) {
433 out.insert(t as u32);
434 }
435 }
436 }
437 for w in blob.windows(4) {
439 let v = u32::from_le_bytes([w[0], w[1], w[2], w[3]]) as u64;
440 if (lo..hi).contains(&v) {
441 out.insert(v as u32);
442 }
443 }
444 }
445 if !self.fixup_posts.is_empty() {
456 let pres: BTreeSet<u64> = out.iter().map(|&v| v as u64).collect();
457 for post in self.fixup_post_images(&pres) {
458 if (lo..hi).contains(&post) {
459 out.insert(post as u32);
460 }
461 }
462 }
463 for &(_, t) in &self.fixup_rel32 {
467 if (lo..hi).contains(&t) {
468 out.insert(t as u32);
469 }
470 }
471 out
472 }
473}
474
475fn parse_le(data: Vec<u8>, le_off: usize) -> Result<Pe, String> {
496 let sz = data.len();
497 let need = le_off + 0xAC;
498 if sz < need + 4 {
499 return Err(format!(
500 "truncated LE header: need {need:#x} bytes for header, file size = {sz} bytes"
501 ));
502 }
503 let at32 = |o: usize| -> Result<u32, String> {
504 if o + 4 > sz {
505 return Err(format!("LE header read past EOF at {o:#x}"));
506 }
507 Ok(u32::from_le_bytes([
508 data[o],
509 data[o + 1],
510 data[o + 2],
511 data[o + 3],
512 ]))
513 };
514 let at16 = |o: usize| -> Result<u16, String> {
515 if o + 2 > sz {
516 return Err(format!("LE header read past EOF at {o:#x}"));
517 }
518 Ok(u16::from_le_bytes([data[o], data[o + 1]]))
519 };
520 let cpu = at16(le_off + 8)?;
521 if cpu != 2 {
522 return Err(format!(
523 "unsupported LE cpu {cpu:#x} (expected 386 = 0x2); only 32-bit x86 LE is handled"
524 ));
525 }
526 let numpages = at32(le_off + 0x14)? as usize;
527 let pagesize = at32(le_off + 0x28)? as usize;
528 let lastpagesize = at32(le_off + 0x2C)? as usize;
529 let objtab_rel = at32(le_off + 0x40)? as usize;
530 let objcnt = at32(le_off + 0x44)? as usize;
531 let pagemap_rel = at32(le_off + 0x48)? as usize;
532 let itermap = at32(le_off + 0x4C)?;
533 let datapage_abs = at32(le_off + 0x80)? as usize;
534 if !(512..=16384).contains(&pagesize) {
535 return Err(format!("implausible LE pagesize {pagesize}"));
536 }
537 if numpages == 0 || numpages > 100_000 {
538 return Err(format!("implausible LE page count {numpages}"));
539 }
540 if objcnt == 0 || objcnt > 100 {
541 return Err(format!("implausible LE object count {objcnt}"));
542 }
543 if itermap != 0 {
544 return Err(format!(
545 "unsupported LE iterated-data map at {itermap:#x}: iterated pages not yet supported"
546 ));
547 }
548 let objtab = le_off.checked_add(objtab_rel).ok_or("LE objtab overflow")?;
549 let pagemap = le_off
550 .checked_add(pagemap_rel)
551 .ok_or("LE pagemap overflow")?;
552 if objtab + objcnt * 24 > sz {
553 return Err(format!(
554 "LE object table runs past EOF: objtab={objtab:#x} + {objcnt}*24, file size {sz:#x}"
555 ));
556 }
557 if pagemap + numpages * 4 > sz {
558 return Err(format!(
559 "LE page map runs past EOF: pagemap={pagemap:#x} + {numpages}*4, file size {sz:#x}"
560 ));
561 }
562 if datapage_abs >= sz {
563 return Err(format!(
564 "LE data pages offset {datapage_abs:#x} past EOF (file size {sz:#x})"
565 ));
566 }
567 let mut phys_of_logical: Vec<(u32, u8)> = Vec::with_capacity(numpages);
571 for i in 0..numpages {
572 let o = pagemap + i * 4;
573 let b0 = data[o] as u32;
574 let b1 = data[o + 1] as u32;
575 let b2 = data[o + 2] as u32;
576 let flags = data[o + 3];
577 let page = (b0 << 16) | (b1 << 8) | b2;
578 if flags != 0 {
579 return Err(format!(
580 "unsupported LE page {i} flags={flags:#x} (page={page}): only flag-0 legal pages are supported; iterated/zero pages TODO"
581 ));
582 }
583 if page == 0 || page as usize > numpages {
584 return Err(format!(
585 "LE page map entry {i} has out-of-range physical page {page} (numpages={numpages})"
586 ));
587 }
588 phys_of_logical.push((page, flags));
589 }
590 let mut new_data: Vec<u8> = Vec::new();
591 let mut sections: Vec<Section> = Vec::with_capacity(objcnt);
592 let mut objs: Vec<(u32, usize, usize, usize)> = Vec::with_capacity(objcnt);
595 let mut exec_seen = 0usize;
596 let mut data_seen = 0usize;
597 for oi in 0..objcnt {
598 let o = objtab + oi * 24;
599 let vsize = u32::from_le_bytes([data[o], data[o + 1], data[o + 2], data[o + 3]]) as usize;
600 let base = u32::from_le_bytes([data[o + 4], data[o + 5], data[o + 6], data[o + 7]]);
601 let flags = u32::from_le_bytes([data[o + 8], data[o + 9], data[o + 10], data[o + 11]]);
602 let page_idx =
603 u32::from_le_bytes([data[o + 12], data[o + 13], data[o + 14], data[o + 15]]) as usize;
604 let page_cnt =
605 u32::from_le_bytes([data[o + 16], data[o + 17], data[o + 18], data[o + 19]]) as usize;
606 if page_idx == 0 || page_idx - 1 + page_cnt > numpages {
607 return Err(format!(
608 "LE object {oi} page range [{page_idx}..{}) out of 1..={numpages}",
609 page_idx + page_cnt
610 ));
611 }
612 objs.push((base, vsize, page_idx, page_cnt));
613 let start = new_data.len();
614 for j in 0..page_cnt {
615 let (phys, _) = phys_of_logical[page_idx - 1 + j];
616 let is_last_phys = phys as usize == numpages;
617 let plen = if is_last_phys { lastpagesize } else { pagesize };
618 let foff = datapage_abs + (phys as usize - 1) * pagesize;
619 if foff + plen > sz {
620 return Err(format!(
621 "LE object {oi} page {j} (phys {phys}) file range [{foff:#x}..{:#x}) past EOF {sz:#x}",
622 foff + plen
623 ));
624 }
625 new_data.extend_from_slice(&data[foff..foff + plen]);
626 }
627 let file_len = new_data.len() - start;
628 let raw_size = vsize.min(file_len);
629 new_data.truncate(start + raw_size);
632 let is_exec = flags & 0x4 != 0;
633 let (name, characteristics) = if is_exec {
634 let n = if exec_seen == 0 {
635 ".text".to_string()
636 } else {
637 format!(".text{exec_seen}")
638 };
639 exec_seen += 1;
640 (
641 n,
642 section_flags::CODE | section_flags::EXECUTE | section_flags::READ,
643 )
644 } else {
645 let n = if data_seen == 0 {
646 ".data".to_string()
647 } else {
648 format!(".data{data_seen}")
649 };
650 data_seen += 1;
651 (n, section_flags::READ | section_flags::WRITE)
652 };
653 sections.push(Section {
654 name,
655 virtual_address: base,
656 virtual_size: vsize as u32,
657 raw_offset: start as u32,
658 raw_size: raw_size as u32,
659 characteristics,
660 });
661 }
662 let (fixup_posts, fixup_rel32, fixup_status) = match collect_fixups(
663 &data, le_off, &objs, numpages, pagesize, §ions, &new_data,
664 ) {
665 Ok((pairs, rel32)) => {
666 let n = pairs.len();
667 (pairs, rel32, FixupStatus::Applied { pairs: n })
668 }
669 Err(e) if e.starts_with(NOVEL_FIXUP_PREFIX) => {
670 (Vec::new(), Vec::new(), FixupStatus::Skipped { reason: e })
671 }
672 Err(e) => return Err(e),
673 };
674 Ok(Pe {
675 data: new_data,
676 image_base: 0,
677 sections,
678 fixup_posts,
679 fixup_rel32,
680 fixup_status,
681 id: next_pe_id(),
682 })
683}
684
685const NOVEL_FIXUP_PREFIX: &str = "NOVEL-FIXUP-RECORD: ";
693
694#[allow(clippy::too_many_arguments)]
707fn collect_fixups(
708 data: &[u8],
709 le_off: usize,
710 objs: &[(u32, usize, usize, usize)],
711 numpages: usize,
712 pagesize: usize,
713 sections: &[Section],
714 image: &[u8],
715) -> Result<FixupTables, String> {
716 let pre_of = |va: u64| -> Option<u32> {
717 for s in sections {
718 let lo = s.virtual_address as u64;
719 let hi = lo + s.virtual_size as u64;
720 if (lo..hi).contains(&va) {
721 let off = s.raw_offset as u64 + (va - lo);
722 if off + 4 <= s.raw_offset as u64 + s.raw_size as u64 {
723 let o = off as usize;
724 return Some(u32::from_le_bytes([
725 image[o],
726 image[o + 1],
727 image[o + 2],
728 image[o + 3],
729 ]));
730 }
731 return Some(0);
733 }
734 }
735 None
736 };
737 let at32 = |o: usize| -> Result<u32, String> {
738 if o + 4 > data.len() {
739 return Err(format!("LE fixup header read past EOF at {o:#x}"));
740 }
741 Ok(u32::from_le_bytes([
742 data[o],
743 data[o + 1],
744 data[o + 2],
745 data[o + 3],
746 ]))
747 };
748 let fpt_field = le_off
749 .checked_add(0x68)
750 .ok_or("LE fixup page-table field overflow")?;
751 let frt_field = le_off
752 .checked_add(0x6C)
753 .ok_or("LE fixup record-table field overflow")?;
754 let fpt = le_off
755 .checked_add(at32(fpt_field)? as usize)
756 .ok_or("LE fixup page-table offset overflow")?;
757 let frt = le_off
758 .checked_add(at32(frt_field)? as usize)
759 .ok_or("LE fixup record-table offset overflow")?;
760 if fpt == le_off && frt == le_off {
761 return Ok((Vec::new(), Vec::new()));
763 }
764 let tab_len = (numpages + 1)
765 .checked_mul(4)
766 .ok_or("LE fixup page-table size overflow")?;
767 if fpt.checked_add(tab_len) != Some(frt) {
770 return Err(format!(
771 "LE fixup tables not adjacent: pagetab={fpt:#x} + {tab_len:#x} != rectab={frt:#x}"
772 ));
773 }
774 if frt > data.len() {
775 return Err(format!(
776 "LE fixup record table {frt:#x} past EOF (file size {:#x})",
777 data.len()
778 ));
779 }
780 let mut bounds: Vec<(usize, usize)> = Vec::with_capacity(numpages);
781 let mut prev = 0usize;
782 let mut first = true;
783 for i in 0..=numpages {
784 let e = at32(fpt + i * 4)? as usize;
785 if !first && e < prev {
786 return Err(format!(
787 "LE fixup page table not monotonic at page {i}: {e:#x} < {prev:#x}"
788 ));
789 }
790 first = false;
791 prev = e;
792 if i < numpages {
793 bounds.push((e, 0));
794 } else if frt.checked_add(e).map_or(true, |end| end > data.len()) {
795 return Err(format!(
796 "LE fixup record table end {e:#x} past EOF (file size {:#x})",
797 data.len()
798 ));
799 }
800 if i > 0 {
801 bounds[i - 1].1 = e;
802 }
803 }
804 let mut out: Vec<(u64, u64)> = Vec::new();
805 let mut out_rel32: Vec<(u64, u64)> = Vec::new();
806 for (pi, (start, end)) in bounds.iter().enumerate() {
807 let lp = pi + 1; let (obase, _, opage, _) = objs
809 .iter()
810 .find(|(_, _, p, c)| lp >= *p && lp < p + c)
811 .ok_or(format!("LE fixup page {lp} belongs to no object"))?;
812 let lin_base = (*obase as usize)
813 .checked_add((lp - opage) * pagesize)
814 .ok_or(format!("LE fixup page {lp} linear base overflow"))?;
815 let mut p = frt
816 .checked_add(*start)
817 .ok_or("LE fixup record offset overflow")?;
818 let pend = frt
819 .checked_add(*end)
820 .ok_or("LE fixup record end overflow")?;
821 while p < pend {
822 let rec_off = p - frt;
823 let st = *data.get(p).ok_or(format!(
824 "LE fixup record {rec_off:#x} (page {lp}) truncated: missing srctype"
825 ))?;
826 let fl = *data.get(p + 1).ok_or(format!(
827 "LE fixup record {rec_off:#x} (page {lp}) truncated: missing tgtflags"
828 ))?;
829 p += 2;
830 let mut srcs: Vec<u16> = Vec::new();
831 if st & 0x20 != 0 {
832 let cnt = *data.get(p).ok_or(format!(
833 "LE fixup record {rec_off:#x} (page {lp}) truncated: missing source count"
834 ))?;
835 p += 1;
836 for _ in 0..cnt {
837 if p + 2 > pend || p + 2 > data.len() {
838 return Err(format!(
839 "LE fixup record {rec_off:#x} (page {lp}) source runs past page end"
840 ));
841 }
842 srcs.push(u16::from_le_bytes([data[p], data[p + 1]]));
843 p += 2;
844 }
845 } else {
846 if p + 2 > pend || p + 2 > data.len() {
847 return Err(format!(
848 "LE fixup record {rec_off:#x} (page {lp}) source runs past page end"
849 ));
850 }
851 srcs.push(u16::from_le_bytes([data[p], data[p + 1]]));
852 p += 2;
853 }
854 let low4 = st & 0x0F;
855 let is_sel = (st == 0x02 || st == 0x22) && fl == 0x00;
856 let is_off = (low4 == 7 || low4 == 8) && (st & 0xD0) == 0 && (fl == 0x00 || fl == 0x10);
857 if !is_sel && !is_off {
858 return Err(format!(
863 "{NOVEL_FIXUP_PREFIX}record {rec_off:#x} (page {lp}): srctype=0x{st:02X} tgtflags=0x{fl:02X}"
864 ));
865 }
866 if p + 1 > pend || p + 1 > data.len() {
867 return Err(format!(
868 "LE fixup record {rec_off:#x} (page {lp}) truncated: missing target object"
869 ));
870 }
871 let obj = data[p] as usize;
872 p += 1;
873 if obj < 1 || obj > objs.len() {
874 return Err(format!(
875 "LE fixup record {rec_off:#x} (page {lp}) has bad target object {obj}"
876 ));
877 }
878 let wide = fl & 0x10 != 0;
879 let toff: u32 = if is_sel {
880 0
881 } else if wide {
882 if p + 4 > pend || p + 4 > data.len() {
883 return Err(format!(
884 "LE fixup record {rec_off:#x} (page {lp}) truncated: missing wide target offset"
885 ));
886 }
887 let v = u32::from_le_bytes([data[p], data[p + 1], data[p + 2], data[p + 3]]);
888 p += 4;
889 v
890 } else {
891 if p + 2 > pend || p + 2 > data.len() {
892 return Err(format!(
893 "LE fixup record {rec_off:#x} (page {lp}) truncated: missing narrow target offset"
894 ));
895 }
896 let v = u16::from_le_bytes([data[p], data[p + 1]]) as u32;
897 p += 2;
898 v
899 };
900 let target = objs[obj - 1].0.wrapping_add(toff);
901 for s in srcs {
902 let loc = if (s as usize) < pagesize {
906 lin_base.checked_add(s as usize)
907 } else if s >= 0xFFFD {
908 lin_base
909 .checked_add(s as usize)
910 .and_then(|l| l.checked_sub(0x1_0000))
911 } else {
912 None
913 };
914 let loc = loc.ok_or(format!(
915 "LE fixup record {rec_off:#x} (page {lp}) bad source offset {s:#x} (pagesize {pagesize:#x})"
916 ))?;
917 let pre = pre_of(loc as u64).ok_or(format!(
918 "LE fixup record {rec_off:#x} (page {lp}) location {loc:#x} outside image"
919 ))?;
920 if is_sel {
921 continue; }
923 if low4 == 7 {
924 out.push((pre as u64, target as u64));
925 } else if low4 == 8 {
926 out_rel32.push((loc as u64, target as u64));
929 }
930 }
931 }
932 if p != pend {
933 return Err(format!(
934 "LE fixup page {lp} consumed to {:#x}, expected end {:#x}",
935 p - frt,
936 pend - frt
937 ));
938 }
939 }
940 Ok((out, out_rel32))
941}
942
943#[cfg(test)]
944mod tests {
945 use super::*;
946
947 fn build_fixup_le() -> Vec<u8> {
953 let le_off = 0x40usize;
954 let pagesize = 0x1000usize;
955 let mut data = vec![0u8; 0x1400];
956 data[0..2].copy_from_slice(b"MZ");
957 data[0x3c..0x40].copy_from_slice(&(le_off as u32).to_le_bytes());
958 data[le_off..le_off + 2].copy_from_slice(b"LE");
959 data[le_off + 8..le_off + 10].copy_from_slice(&2u16.to_le_bytes()); data[le_off + 0x14..le_off + 0x18].copy_from_slice(&1u32.to_le_bytes()); data[le_off + 0x28..le_off + 0x2C].copy_from_slice(&(pagesize as u32).to_le_bytes()); data[le_off + 0x2C..le_off + 0x30].copy_from_slice(&(pagesize as u32).to_le_bytes()); data[le_off + 0x40..le_off + 0x44].copy_from_slice(&0x1C0u32.to_le_bytes()); data[le_off + 0x44..le_off + 0x48].copy_from_slice(&1u32.to_le_bytes()); data[le_off + 0x48..le_off + 0x4C].copy_from_slice(&0x1D8u32.to_le_bytes()); data[le_off + 0x4C..le_off + 0x50].copy_from_slice(&0u32.to_le_bytes()); data[le_off + 0x68..le_off + 0x6C].copy_from_slice(&0x2C0u32.to_le_bytes()); data[le_off + 0x6C..le_off + 0x70].copy_from_slice(&0x2C8u32.to_le_bytes()); data[le_off + 0x80..le_off + 0x84].copy_from_slice(&0x400u32.to_le_bytes()); let o = 0x200usize;
972 data[o..o + 4].copy_from_slice(&0x1000u32.to_le_bytes());
973 data[o + 4..o + 8].copy_from_slice(&0x10000u32.to_le_bytes());
974 data[o + 8..o + 12].copy_from_slice(&0x4u32.to_le_bytes());
975 data[o + 12..o + 16].copy_from_slice(&1u32.to_le_bytes());
976 data[o + 16..o + 20].copy_from_slice(&1u32.to_le_bytes());
977 data[0x218..0x21C].copy_from_slice(&[0x00, 0x00, 0x01, 0x00]);
979 data[0x300..0x304].copy_from_slice(&0u32.to_le_bytes());
981 data[0x304..0x308].copy_from_slice(&26u32.to_le_bytes());
982 data[0x308..0x30F].copy_from_slice(&[0x07, 0x00, 0x10, 0x00, 0x01, 0x00, 0x05]);
985 data[0x30F..0x316].copy_from_slice(&[0x08, 0x00, 0x21, 0x00, 0x01, 0x00, 0x01]);
988 data[0x316..0x322].copy_from_slice(&[
992 0x27, 0x10, 0x02, 0x30, 0x00, 0x34, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00,
993 ]);
994 let pg = 0x400usize;
996 data[pg..pg + 6].copy_from_slice(&[0xA1, 0x00, 0x04, 0x01, 0x00, 0xC3]);
997 data[pg + 0x10..pg + 0x14].copy_from_slice(&0x10400u32.to_le_bytes());
998 data[pg + 0x20..pg + 0x25].copy_from_slice(&[0xE8, 0x00, 0x00, 0x00, 0x00]);
999 data[pg + 0x30..pg + 0x34].copy_from_slice(&0x10410u32.to_le_bytes());
1000 data[pg + 0x34..pg + 0x38].copy_from_slice(&0x10420u32.to_le_bytes());
1001 data
1002 }
1003
1004 #[test]
1005 fn le_fixup_07_pairs_absorbed() {
1006 let pe = Pe::parse(build_fixup_le()).expect("parse synthetic LE");
1007 assert_eq!(pe.fixup_count(), 3, "one narrow 07 + two listed 07 dwords");
1008 let got = pe.fixup_post_images(&[0x10400].into_iter().collect());
1009 assert_eq!(got, [0x10500].into_iter().collect());
1010 let got = pe.fixup_post_images(&[0x10410, 0x10420].into_iter().collect());
1011 assert_eq!(got, [0x10600].into_iter().collect());
1012 let got = pe.fixup_post_images(&[0x99999].into_iter().collect());
1014 assert!(got.is_empty());
1015 }
1016
1017 #[test]
1018 fn le_fixup_post_global_passes_spec() {
1019 use crate::{analyze, Spec};
1020 let pe = Pe::parse(build_fixup_le()).expect("parse synthetic LE");
1021 let cg = analyze(&pe, 0x10000).expect("analyze synthetic fn");
1022 assert!(cg.globals.contains(&0x10400), "{cg:?}");
1023 Spec::new(0x10000)
1025 .globals(&[0x10400])
1026 .check(&pe)
1027 .expect("pre global");
1028 Spec::new(0x10000)
1030 .globals(&[0x10500])
1031 .check(&pe)
1032 .expect("post global");
1033 assert!(Spec::new(0x10000).globals(&[0x10999]).check(&pe).is_err());
1035 }
1036
1037 #[test]
1038 fn le_fixup_post_image_single_lookup() {
1039 let pe = Pe::parse(build_fixup_le()).expect("parse synthetic LE");
1040 assert_eq!(pe.fixup_post_image(0x10400), Some(0x10500));
1041 assert_eq!(pe.fixup_post_image(0x10410), Some(0x10600));
1042 assert_eq!(pe.fixup_post_image(0x10420), Some(0x10600));
1043 assert_eq!(pe.fixup_post_image(0x99999), None);
1045 }
1046
1047 #[test]
1048 fn le_referenced_addrs_include_fixup_post_images() {
1049 let pe = Pe::parse(build_fixup_le()).expect("parse synthetic LE");
1050 let refs = pe.referenced_addrs();
1051 assert!(
1054 refs.contains(&0x10400),
1055 "pre operand ref missing: {refs:x?}"
1056 );
1057 assert!(refs.contains(&0x10410), "pre stored dword ref missing");
1058 assert!(refs.contains(&0x10420), "pre stored dword ref missing");
1059 assert!(refs.contains(&0x10500), "post image of 0x10400 missing");
1063 assert!(
1064 refs.contains(&0x10600),
1065 "post image of 0x10410/0x10420 missing"
1066 );
1067 assert!(
1069 !refs.contains(&0x10999),
1070 "garbage address must stay unreferenced"
1071 );
1072 }
1073
1074 #[test]
1075 fn le_fixup_unknown_record_skips_loudly() {
1076 use super::FixupStatus;
1077 let mut data = build_fixup_le();
1078 data[0x308] = 0x09; let pe = Pe::parse(data).expect("novel fixup kind must skip, not fail");
1082 assert_eq!(pe.fixup_count(), 0);
1083 match pe.fixup_status() {
1084 FixupStatus::Skipped { reason } => {
1085 assert!(reason.contains("srctype=0x09"), "{reason}")
1086 }
1087 other => panic!("expected Skipped, got {other:?}"),
1088 }
1089 }
1090
1091 #[test]
1092 fn le_fixup_tables_must_be_adjacent() {
1093 let mut data = build_fixup_le();
1094 data[0x40 + 0x68..0x40 + 0x6C].copy_from_slice(&0x2C4u32.to_le_bytes());
1095 match Pe::parse(data) {
1096 Ok(_) => panic!("moved page table must fail"),
1097 Err(err) => assert!(err.contains("not adjacent"), "{err}"),
1098 }
1099 }
1100}