Skip to main content

warp_runtime/
macho.rs

1//! Just enough of 64-bit Mach-O (macOS executables) to carry a program so the executable still signs cleanly:
2//! codesign wants every byte of the file inside a segment, its signature last. The carried program goes at the end
3//! of the last segment (__LINKEDIT), whose size grows to cover it; the old signature is dropped first. Anything that
4//! is not a 64-bit little-endian Mach-O (Linux's ELF) passes unchanged.
5use std::ops::Range;
6
7/// Enough of an executable's start to hold its header and load commands
8pub const MAX_HEADER_BYTES: usize = 1 << 16;
9const MAGIC_64: u32 = 0xfeed_facf;
10const HEADER_BYTES: usize = 32;
11const COMMAND_COUNT_AT: usize = 16;
12const COMMANDS_SIZE_AT: usize = 20;
13const SEGMENT_64: u32 = 0x19;
14const CODE_SIGNATURE: u32 = 0x1d;
15/// segment_command_64: vmsize, fileoff, filesize follow the name and vmaddr
16const SEGMENT_VMSIZE_AT: usize = 32;
17const SEGMENT_FILEOFF_AT: usize = 40;
18const SEGMENT_FILESIZE_AT: usize = 48;
19/// linkedit_data_command: dataoff after cmd and cmdsize
20const DATA_OFFSET_AT: usize = 8;
21/// a segment's memory size is whole pages: arm64's 16 KB, a multiple of x86-64's 4 KB
22const PAGE_BYTES: u64 = 0x4000;
23
24fn u32_at(bytes: &[u8], at: usize) -> Option<u32> {
25	Some(u32::from_le_bytes(bytes.get(at..at + 4)?.try_into().ok()?))
26}
27
28fn u64_at(bytes: &[u8], at: usize) -> Option<u64> {
29	Some(u64::from_le_bytes(bytes.get(at..at + 8)?.try_into().ok()?))
30}
31
32fn set_u32(bytes: &mut [u8], at: usize, value: u32) {
33	bytes[at..at + 4].copy_from_slice(&value.to_le_bytes());
34}
35
36fn set_u64(bytes: &mut [u8], at: usize, value: u64) {
37	bytes[at..at + 8].copy_from_slice(&value.to_le_bytes());
38}
39
40/// The load commands of a 64-bit Mach-O: (command, where it lies); none for any other file
41fn load_commands(bytes: &[u8]) -> Vec<(u32, Range<usize>)> {
42	let mut commands = Vec::new();
43	if u32_at(bytes, 0) != Some(MAGIC_64) {
44		return commands;
45	}
46	let count = u32_at(bytes, COMMAND_COUNT_AT).unwrap_or(0);
47	let mut at = HEADER_BYTES;
48	for _ in 0..count {
49		let (Some(command), Some(size)) = (u32_at(bytes, at), u32_at(bytes, at + 4)) else {
50			break;
51		};
52		commands.push((command, at..at + size as usize));
53		at += size as usize;
54	}
55	commands
56}
57
58/// Where the code signature starts, if the executable is a signed Mach-O
59pub fn signature_offset(bytes: &[u8]) -> Option<usize> {
60	let (_, signature) = load_commands(bytes).into_iter().find(|(command, _)| *command == CODE_SIGNATURE)?;
61	u32_at(bytes, signature.start + DATA_OFFSET_AT).map(|offset| offset as usize)
62}
63
64/// The executable without its code signature: the signature's bytes and its load command removed
65pub fn unsigned(bytes: &[u8]) -> Vec<u8> {
66	let mut image = bytes.to_vec();
67	let Some((_, signature)) = load_commands(bytes).into_iter().find(|(command, _)| *command == CODE_SIGNATURE) else {
68		return image;
69	};
70	let offset = signature_offset(bytes).expect("a signature command names its offset");
71	image.truncate(offset);
72	let commands_end = HEADER_BYTES + u32_at(bytes, COMMANDS_SIZE_AT).unwrap_or(0) as usize;
73	let removed = signature.len();
74	image.copy_within(signature.end..commands_end, signature.start);
75	image[commands_end - removed..commands_end].fill(0);
76	set_u32(&mut image, COMMAND_COUNT_AT, u32_at(bytes, COMMAND_COUNT_AT).unwrap_or(1) - 1);
77	set_u32(&mut image, COMMANDS_SIZE_AT, (commands_end - HEADER_BYTES - removed) as u32);
78	extend_last_segment_to_end(&mut image);
79	image
80}
81
82/// Make the segment that ends the file (__LINKEDIT) reach the end of the file: what was appended becomes its content
83pub fn extend_last_segment_to_end(image: &mut [u8]) {
84	let last_segment = load_commands(image)
85		.into_iter()
86		.filter(|(command, _)| *command == SEGMENT_64)
87		.max_by_key(|(_, segment)| u64_at(image, segment.start + SEGMENT_FILEOFF_AT));
88	let Some((_, segment)) = last_segment else { return };
89	let file_offset = u64_at(image, segment.start + SEGMENT_FILEOFF_AT).unwrap_or(0);
90	let file_size = image.len() as u64 - file_offset;
91	set_u64(image, segment.start + SEGMENT_FILESIZE_AT, file_size);
92	set_u64(image, segment.start + SEGMENT_VMSIZE_AT, file_size.next_multiple_of(PAGE_BYTES));
93}