1use std::fs;
12use std::fs::File;
13use std::io::Read;
14use std::io::Write;
15use std::os::unix::ffi::OsStrExt;
16use std::os::unix::fs::MetadataExt;
17use std::path::Path;
18use std::path::PathBuf;
19
20use goblin::elf::Elf;
21use goblin::elf::header;
22use goblin::elf::program_header;
23use goblin::elf::section_header;
24use iced_x86::Decoder;
25use iced_x86::DecoderOptions;
26use iced_x86::Instruction;
27use iced_x86::Mnemonic;
28use serde::Deserialize;
29use serde::Serialize;
30use uuid::Uuid;
31
32use crate::Context;
33use crate::Error;
34
35const CACHE_SCHEMA_VERSION: u32 = 2;
36
37#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
39pub struct InstructionSite {
40 pub offset: u64,
42 pub instruction: String,
44 pub length: u8,
46}
47
48#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
50pub struct ModificationTime {
51 pub seconds: i64,
52 pub nanoseconds: i64,
53}
54
55#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
57pub struct InstructionMap {
58 pub schema_version: u32,
59 pub binary: PathBuf,
60 pub file_length: u64,
61 pub modified: ModificationTime,
62 pub sites: Vec<InstructionSite>,
63}
64
65#[derive(Debug, Clone, Copy, Eq, PartialEq)]
67pub enum CacheStatus {
68 Hit,
69 Miss,
70}
71
72#[derive(Debug, Clone, Eq, PartialEq)]
74pub struct InstructionMapResult {
75 pub map: InstructionMap,
76 pub cache_status: CacheStatus,
77 pub cache_path: PathBuf,
78}
79
80#[derive(Debug, Clone, Eq, PartialEq)]
81struct FileIdentity {
82 binary: PathBuf,
83 file_length: u64,
84 modified: ModificationTime,
85}
86
87#[derive(Debug, Clone, Copy)]
88struct ExecutableRange {
89 file_offset: u64,
90 address: u64,
91 size: u64,
92}
93
94pub fn default_cache_dir() -> PathBuf {
96 dirs::cache_dir()
97 .map_or_else(|| PathBuf::from("/tmp/hermit"), |dir| dir.join("hermit"))
98 .join("instruction-maps")
99}
100
101pub fn load_or_generate(
103 binary: impl AsRef<Path>,
104 cache_dir: impl AsRef<Path>,
105) -> Result<InstructionMapResult, Error> {
106 let requested = binary.as_ref();
107 let canonical = fs::canonicalize(requested)
108 .with_context(|| format!("failed to resolve binary {}", requested.display()))?;
109 let mut file = File::open(&canonical)
110 .with_context(|| format!("failed to open binary {}", canonical.display()))?;
111 let before = file
112 .metadata()
113 .with_context(|| format!("failed to stat binary {}", canonical.display()))?;
114 if !before.is_file() {
115 return Err(Error::msg(format!(
116 "instruction map input is not a regular file: {}",
117 canonical.display()
118 )));
119 }
120
121 let identity = FileIdentity::new(canonical, &before);
122 let cache_dir = cache_dir.as_ref();
123 let cache_path = cache_dir.join(format!("{}.json", cache_key(&identity)));
124 if let Some(map) = read_valid_cache(&cache_path, &identity) {
125 return Ok(InstructionMapResult {
126 map,
127 cache_status: CacheStatus::Hit,
128 cache_path,
129 });
130 }
131
132 let mut bytes = Vec::new();
133 file.read_to_end(&mut bytes)
134 .with_context(|| format!("failed to read binary {}", identity.binary.display()))?;
135 let after = file
136 .metadata()
137 .with_context(|| format!("failed to restat binary {}", identity.binary.display()))?;
138 if before.len() != after.len()
139 || before.mtime() != after.mtime()
140 || before.mtime_nsec() != after.mtime_nsec()
141 {
142 return Err(Error::msg(format!(
143 "binary changed while generating instruction map: {}",
144 identity.binary.display()
145 )));
146 }
147
148 let sites = scan_elf(&bytes)
149 .with_context(|| format!("failed to scan binary {}", identity.binary.display()))?;
150 let map = InstructionMap {
151 schema_version: CACHE_SCHEMA_VERSION,
152 binary: identity.binary.clone(),
153 file_length: identity.file_length,
154 modified: identity.modified.clone(),
155 sites,
156 };
157 write_cache(&cache_path, &map)?;
158
159 Ok(InstructionMapResult {
160 map,
161 cache_status: CacheStatus::Miss,
162 cache_path,
163 })
164}
165
166impl FileIdentity {
167 fn new(binary: PathBuf, metadata: &fs::Metadata) -> Self {
168 Self {
169 binary,
170 file_length: metadata.len(),
171 modified: ModificationTime {
172 seconds: metadata.mtime(),
173 nanoseconds: metadata.mtime_nsec(),
174 },
175 }
176 }
177}
178
179fn cache_key(identity: &FileIdentity) -> String {
180 let mut input = Vec::new();
181 input.extend_from_slice(&CACHE_SCHEMA_VERSION.to_le_bytes());
182 input.extend_from_slice(identity.binary.as_os_str().as_bytes());
183 input.push(0);
184 input.extend_from_slice(&identity.file_length.to_le_bytes());
185 input.extend_from_slice(&identity.modified.seconds.to_le_bytes());
186 input.extend_from_slice(&identity.modified.nanoseconds.to_le_bytes());
187 Uuid::new_v5(&Uuid::NAMESPACE_URL, &input)
188 .simple()
189 .to_string()
190}
191
192fn read_valid_cache(cache_path: &Path, identity: &FileIdentity) -> Option<InstructionMap> {
193 let map: InstructionMap = serde_json::from_reader(File::open(cache_path).ok()?).ok()?;
194 (map.schema_version == CACHE_SCHEMA_VERSION
195 && map.binary == identity.binary
196 && map.file_length == identity.file_length
197 && map.modified == identity.modified)
198 .then_some(map)
199}
200
201fn write_cache(cache_path: &Path, map: &InstructionMap) -> Result<(), Error> {
202 let cache_dir = cache_path
203 .parent()
204 .ok_or_else(|| Error::msg("instruction map cache path has no parent"))?;
205 fs::create_dir_all(cache_dir).with_context(|| {
206 format!(
207 "failed to create instruction map cache directory {}",
208 cache_dir.display()
209 )
210 })?;
211
212 let mut temporary = tempfile::Builder::new()
213 .prefix(".instruction-map-")
214 .tempfile_in(cache_dir)
215 .with_context(|| {
216 format!(
217 "failed to create temporary instruction map in {}",
218 cache_dir.display()
219 )
220 })?;
221 serde_json::to_writer(&mut temporary, map)?;
222 temporary.write_all(b"\n")?;
223 temporary.as_file().sync_all()?;
224 temporary.persist(cache_path).with_context(|| {
225 format!(
226 "failed to persist instruction map cache {}",
227 cache_path.display()
228 )
229 })?;
230 Ok(())
231}
232
233fn scan_elf(bytes: &[u8]) -> Result<Vec<InstructionSite>, Error> {
234 let elf = Elf::parse(bytes).context("input is not a valid ELF file")?;
235 if !elf.little_endian {
236 return Err(Error::msg("big-endian x86 ELF files are not supported"));
237 }
238 let bitness = match elf.header.e_machine {
239 header::EM_386 => 32,
240 header::EM_X86_64 => 64,
241 machine => {
242 return Err(Error::msg(format!(
243 "unsupported ELF machine {machine}; expected x86 or x86-64"
244 )));
245 }
246 };
247
248 let mut sites = Vec::new();
249 for range in executable_ranges(&elf) {
250 let start = usize::try_from(range.file_offset)
251 .context("executable range offset does not fit in memory")?;
252 let size =
253 usize::try_from(range.size).context("executable range size does not fit in memory")?;
254 let end = start
255 .checked_add(size)
256 .ok_or_else(|| Error::msg("executable range overflows the file offset space"))?;
257 let code = bytes.get(start..end).ok_or_else(|| {
258 Error::msg(format!(
259 "executable range {:#x}..{:#x} lies outside the ELF file",
260 range.file_offset,
261 range.file_offset.saturating_add(range.size)
262 ))
263 })?;
264 scan_range(code, range, bitness, &mut sites)?;
265 }
266 sites.sort_unstable();
267 sites.dedup();
268 Ok(sites)
269}
270
271fn executable_ranges(elf: &Elf<'_>) -> Vec<ExecutableRange> {
272 let mut ranges = elf
273 .section_headers
274 .iter()
275 .filter(|section| {
276 section.sh_flags & u64::from(section_header::SHF_EXECINSTR) != 0
277 && section.sh_type != section_header::SHT_NOBITS
278 && section.sh_size != 0
279 })
280 .map(|section| ExecutableRange {
281 file_offset: section.sh_offset,
282 address: section.sh_addr,
283 size: section.sh_size,
284 })
285 .collect::<Vec<_>>();
286
287 if ranges.is_empty() {
288 ranges.extend(
289 elf.program_headers
290 .iter()
291 .filter(|segment| {
292 segment.p_type == program_header::PT_LOAD
293 && segment.p_flags & program_header::PF_X != 0
294 && segment.p_filesz != 0
295 })
296 .map(|segment| ExecutableRange {
297 file_offset: segment.p_offset,
298 address: segment.p_vaddr,
299 size: segment.p_filesz,
300 }),
301 );
302 }
303
304 ranges.sort_unstable_by_key(|range| (range.file_offset, range.address, range.size));
305 ranges
306}
307
308fn scan_range(
309 bytes: &[u8],
310 range: ExecutableRange,
311 bitness: u32,
312 sites: &mut Vec<InstructionSite>,
313) -> Result<(), Error> {
314 let mut decoder = Decoder::with_ip(bitness, bytes, range.address, DecoderOptions::NONE);
315 while decoder.can_decode() {
316 let instruction = decoder.decode();
317 let Some(name) = nondeterministic_instruction(&instruction) else {
318 continue;
319 };
320 let relative_offset = instruction
321 .ip()
322 .checked_sub(range.address)
323 .ok_or_else(|| Error::msg("decoded instruction precedes its executable range"))?;
324 let offset = range
325 .file_offset
326 .checked_add(relative_offset)
327 .ok_or_else(|| Error::msg("decoded instruction file offset overflowed"))?;
328 sites.push(InstructionSite {
329 offset,
330 instruction: name.to_string(),
331 length: u8::try_from(instruction.len())
332 .context("decoded x86 instruction length does not fit in u8")?,
333 });
334 }
335 Ok(())
336}
337
338fn nondeterministic_instruction(instruction: &Instruction) -> Option<&'static str> {
340 match instruction.mnemonic() {
341 Mnemonic::Syscall => Some("syscall"),
342 Mnemonic::Cpuid => Some("cpuid"),
343 Mnemonic::Rdrand => Some("rdrand"),
344 Mnemonic::Rdtsc => Some("rdtsc"),
345 Mnemonic::Rdtscp => Some("rdtscp"),
346 Mnemonic::Rdseed => Some("rdseed"),
347 Mnemonic::Sysenter => Some("sysenter"),
348 Mnemonic::Xbegin => Some("xbegin"),
349 Mnemonic::Xend => Some("xend"),
350 Mnemonic::Int if instruction.immediate8() == 0x80 => Some("int80"),
351 _ => None,
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 const CODE_OFFSET: usize = 64;
360 const CODE_ADDRESS: u64 = 0x0040_0000;
361 const SECTION_HEADER_SIZE: usize = 64;
362
363 fn write_u16(bytes: &mut [u8], offset: usize, value: u16) {
364 bytes[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
365 }
366
367 fn write_u32(bytes: &mut [u8], offset: usize, value: u32) {
368 bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
369 }
370
371 fn write_u64(bytes: &mut [u8], offset: usize, value: u64) {
372 bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
373 }
374
375 fn elf_with_executable_section(code: &[u8]) -> Vec<u8> {
376 let section_table = (CODE_OFFSET + code.len() + 7) & !7;
377 let mut bytes = vec![0; section_table + 2 * SECTION_HEADER_SIZE];
378 bytes[..4].copy_from_slice(b"\x7fELF");
379 bytes[4] = 2;
380 bytes[5] = 1;
381 bytes[6] = 1;
382 write_u16(&mut bytes, 16, header::ET_EXEC);
383 write_u16(&mut bytes, 18, header::EM_X86_64);
384 write_u32(&mut bytes, 20, 1);
385 write_u64(&mut bytes, 24, CODE_ADDRESS);
386 write_u64(&mut bytes, 40, section_table as u64);
387 write_u16(&mut bytes, 52, 64);
388 write_u16(&mut bytes, 58, SECTION_HEADER_SIZE as u16);
389 write_u16(&mut bytes, 60, 2);
390 bytes[CODE_OFFSET..CODE_OFFSET + code.len()].copy_from_slice(code);
391
392 let text = section_table + SECTION_HEADER_SIZE;
393 write_u32(&mut bytes, text + 4, section_header::SHT_PROGBITS);
394 write_u64(
395 &mut bytes,
396 text + 8,
397 u64::from(section_header::SHF_ALLOC | section_header::SHF_EXECINSTR),
398 );
399 write_u64(&mut bytes, text + 16, CODE_ADDRESS);
400 write_u64(&mut bytes, text + 24, CODE_OFFSET as u64);
401 write_u64(&mut bytes, text + 32, code.len() as u64);
402 write_u64(&mut bytes, text + 48, 1);
403 bytes
404 }
405
406 fn all_target_instructions() -> Vec<u8> {
407 vec![
408 0x0f, 0x05, 0x0f, 0xa2, 0x48, 0x0f, 0xc7, 0xf0, 0x0f, 0x31, 0x48, 0x0f, 0xc7, 0xf8,
409 0x0f, 0x01, 0xf9, 0x0f, 0x34, 0xcd, 0x80, 0xc7, 0xf8, 0, 0, 0, 0, 0x0f, 0x01, 0xd5,
410 ]
411 }
412
413 #[test]
414 fn scans_every_requested_instruction() {
415 let sites = scan_elf(&elf_with_executable_section(&all_target_instructions())).unwrap();
416 assert_eq!(
417 sites,
418 vec![
419 InstructionSite {
420 offset: 64,
421 instruction: "syscall".into(),
422 length: 2
423 },
424 InstructionSite {
425 offset: 66,
426 instruction: "cpuid".into(),
427 length: 2
428 },
429 InstructionSite {
430 offset: 68,
431 instruction: "rdrand".into(),
432 length: 4
433 },
434 InstructionSite {
435 offset: 72,
436 instruction: "rdtsc".into(),
437 length: 2
438 },
439 InstructionSite {
440 offset: 74,
441 instruction: "rdseed".into(),
442 length: 4
443 },
444 InstructionSite {
445 offset: 78,
446 instruction: "rdtscp".into(),
447 length: 3
448 },
449 InstructionSite {
450 offset: 81,
451 instruction: "sysenter".into(),
452 length: 2
453 },
454 InstructionSite {
455 offset: 83,
456 instruction: "int80".into(),
457 length: 2
458 },
459 InstructionSite {
460 offset: 85,
461 instruction: "xbegin".into(),
462 length: 6
463 },
464 InstructionSite {
465 offset: 91,
466 instruction: "xend".into(),
467 length: 3
468 },
469 ]
470 );
471 }
472
473 #[test]
474 fn cache_hit_skips_regeneration_and_identity_changes_miss() {
475 let temp = tempfile::tempdir().unwrap();
476 let binary = temp.path().join("fixture");
477 let cache = temp.path().join("cache");
478 fs::write(
479 &binary,
480 elf_with_executable_section(&all_target_instructions()),
481 )
482 .unwrap();
483
484 let first = load_or_generate(&binary, &cache).unwrap();
485 assert_eq!(first.cache_status, CacheStatus::Miss);
486 assert!(first.cache_path.is_file());
487
488 let second = load_or_generate(&binary, &cache).unwrap();
489 assert_eq!(second.cache_status, CacheStatus::Hit);
490 assert_eq!(second.map, first.map);
491 assert_eq!(second.cache_path, first.cache_path);
492
493 let mut changed = fs::read(&binary).unwrap();
494 changed.push(0);
495 fs::write(&binary, changed).unwrap();
496 let third = load_or_generate(&binary, &cache).unwrap();
497 assert_eq!(third.cache_status, CacheStatus::Miss);
498 assert_ne!(third.cache_path, first.cache_path);
499 }
500
501 #[test]
502 fn stale_schema_cache_is_regenerated() {
503 let temp = tempfile::tempdir().unwrap();
504 let binary = temp.path().join("fixture");
505 let cache = temp.path().join("cache");
506 fs::write(
507 &binary,
508 elf_with_executable_section(&all_target_instructions()),
509 )
510 .unwrap();
511
512 let canonical = fs::canonicalize(&binary).unwrap();
513 let metadata = fs::metadata(&canonical).unwrap();
514 let identity = FileIdentity::new(canonical.clone(), &metadata);
515 let cache_path = cache.join(format!("{}.json", cache_key(&identity)));
516 let stale = InstructionMap {
517 schema_version: CACHE_SCHEMA_VERSION - 1,
518 binary: canonical,
519 file_length: identity.file_length,
520 modified: identity.modified,
521 sites: Vec::new(),
522 };
523 write_cache(&cache_path, &stale).unwrap();
524
525 let regenerated = load_or_generate(&binary, &cache).unwrap();
526 assert_eq!(regenerated.cache_status, CacheStatus::Miss);
527 assert_eq!(regenerated.map.schema_version, CACHE_SCHEMA_VERSION);
528 assert!(
529 regenerated
530 .map
531 .sites
532 .iter()
533 .any(|site| site.instruction == "rdtscp")
534 );
535 }
536
537 struct FourGibBoundary {
539 base: *mut u8,
540 }
541
542 impl FourGibBoundary {
543 const PAGE: usize = 4096;
544
545 fn map() -> Self {
550 for multiple in 1..=64usize {
551 let base = (multiple << 32) - Self::PAGE;
552 let mapped = unsafe {
555 libc::mmap(
556 base as *mut libc::c_void,
557 2 * Self::PAGE,
558 libc::PROT_READ | libc::PROT_WRITE,
559 libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_FIXED_NOREPLACE,
560 -1,
561 0,
562 )
563 };
564 if mapped == libc::MAP_FAILED {
565 continue;
566 }
567 if mapped as usize == base {
568 return Self {
569 base: mapped.cast(),
570 };
571 }
572 unsafe { libc::munmap(mapped, 2 * Self::PAGE) };
574 }
575 panic!("no 4 GiB-aligned address below 256 GiB was free to map");
576 }
577
578 fn bytes(&mut self) -> &mut [u8] {
580 unsafe { std::slice::from_raw_parts_mut(self.base, 2 * Self::PAGE) }
583 }
584 }
585
586 impl Drop for FourGibBoundary {
587 fn drop(&mut self) {
588 unsafe { libc::munmap(self.base.cast(), 2 * Self::PAGE) };
590 }
591 }
592
593 #[test]
607 fn decodes_instructions_that_end_at_or_cross_a_4gib_host_address() {
608 const NOP: u8 = 0x90;
609 const CPUID: [u8; 2] = [0x0f, 0xa2];
610 const RDTSC: [u8; 2] = [0x0f, 0x31];
611 const MOV_RIP_RELATIVE: [u8; 7] = [0x48, 0x8b, 0x05, 0x10, 0x00, 0x00, 0x00];
613 let boundary = FourGibBoundary::PAGE;
614 let mut mapping = FourGibBoundary::map();
615 let bytes = mapping.bytes();
616 assert_eq!(bytes[boundary..].as_ptr() as usize & 0xffff_ffff, 0);
617
618 for (instruction, start, name) in [
621 (CPUID, boundary - 2, "cpuid"),
622 (RDTSC, boundary - 1, "rdtsc"),
623 ] {
624 bytes.fill(NOP);
625 bytes[start..start + instruction.len()].copy_from_slice(&instruction);
626 let window = boundary - 16..boundary + 16;
627 let range = ExecutableRange {
628 file_offset: 0x1000,
629 address: CODE_ADDRESS,
630 size: window.len() as u64,
631 };
632 let mut sites = Vec::new();
633 scan_range(&bytes[window.clone()], range, 64, &mut sites).unwrap();
634 assert_eq!(
635 sites,
636 vec![InstructionSite {
637 offset: range.file_offset + (start - window.start) as u64,
638 instruction: name.into(),
639 length: 2,
640 }]
641 );
642 }
643
644 bytes.fill(NOP);
647 let start = boundary - 3;
648 bytes[start..start + MOV_RIP_RELATIVE.len()].copy_from_slice(&MOV_RIP_RELATIVE);
649 let mut decoder = Decoder::with_ip(64, &bytes[start..], CODE_ADDRESS, DecoderOptions::NONE);
650 let instruction = decoder.decode();
651 assert_eq!(instruction.mnemonic(), Mnemonic::Mov);
652 assert_eq!(instruction.len(), MOV_RIP_RELATIVE.len());
653 assert_eq!(
654 instruction.next_ip(),
655 CODE_ADDRESS + MOV_RIP_RELATIVE.len() as u64
656 );
657 assert_eq!(decoder.position(), MOV_RIP_RELATIVE.len());
658 }
659}