Git · dadoum.zone
Repositories
Help
Report an Issue
apple-crates
Code
Commits
Branches
Tags
Search
Tree:
a74924e
Branches
Tags
main
apple-crates
apple-codesign-quick
src
macho
thin.rs
Refactor and unify a lot of things for another project and implement airdrop support
Dadoum
commited
a74924e
at 2026-10-25 22:15:58
thin.rs
Blame
History
Raw
use super::{align_to, checked_add, ensure_len}; use crate::error::{CodeSignError, Result}; use byteorder::{ByteOrder, LittleEndian}; use std::path::Path; pub(super) const MH_MAGIC: u32 = 0xfeed_face; pub(super) const MH_MAGIC_64: u32 = 0xfeed_facf; pub(super) const MH_EXECUTE: u32 = 0x2; pub(super) const LC_SEGMENT: u32 = 0x1; pub(super) const LC_SEGMENT_64: u32 = 0x19; pub(super) const LC_CODE_SIGNATURE: u32 = 0x1d; pub(super) const LINKEDIT_DATA_COMMAND_LEN: usize = 16; const SECTION_TYPE: u32 = 0xff; const S_ZEROFILL: u32 = 0x1; const S_THREAD_LOCAL_ZEROFILL: u32 = 0x11; // __LINKEDIT's vmsize stays a multiple of arm64's 16K pages. pub(super) const SEGMENT_PAGE_SIZE: u64 = 16 * 1024; pub(super) const CODE_SIGNATURE_ALIGNMENT: usize = 16; /// Field offsets of segment_command or segment_command_64, and of their sections. #[derive(Debug)] pub(super) struct SegmentLayout { pub command_len: usize, word_len: usize, vmsize: usize, fileoff: usize, filesize: usize, nsects: usize, section_len: usize, section_size: usize, section_offset: usize, section_flags: usize, } pub(super) const SEGMENT_LAYOUT: SegmentLayout = SegmentLayout { command_len: 56, word_len: 4, vmsize: 28, fileoff: 32, filesize: 36, nsects: 48, section_len: 68, section_size: 36, section_offset: 40, section_flags: 56, }; pub(super) const SEGMENT_64_LAYOUT: SegmentLayout = SegmentLayout { command_len: 72, word_len: 8, vmsize: 32, fileoff: 40, filesize: 48, nsects: 64, section_len: 80, section_size: 40, section_offset: 48, section_flags: 64, }; impl SegmentLayout { fn read_word(&self, data: &[u8], offset: usize) -> u64 { if self.word_len == 8 { LittleEndian::read_u64(&data[offset..]) } else { LittleEndian::read_u32(&data[offset..]).into() } } fn write_word(&self, path: &Path, data: &mut [u8], offset: usize, value: u64) -> Result<()> { if self.word_len == 8 { LittleEndian::write_u64(&mut data[offset..], value); } else { let value = u32::try_from(value) .map_err(|_| CodeSignError::macho(path, "a 32-bit segment outgrew 4 GiB"))?; LittleEndian::write_u32(&mut data[offset..], value); } Ok(()) } } #[derive(Clone, Copy, Debug)] pub(super) struct Segment { command: usize, layout: &'static SegmentLayout, } impl Segment { fn file_range(&self, data: &[u8]) -> (u64, u64) { ( self.layout .read_word(data, self.command + self.layout.fileoff), self.layout .read_word(data, self.command + self.layout.filesize), ) } } /// What signing needs from a thin Mach-O's header and load commands. #[derive(Debug)] pub(super) struct MachOHeader { file_type: u32, commands_end: usize, code_signature_command: Option<usize>, linkedit_segment: Option<Segment>, /// __TEXT's file offset and size, which codesign writes as execSegBase and execSegLimit. pub text_segment: (u64, u64), /// Where the furthest segment ends in the file. segments_end: u64, /// The first file offset holding section contents, which is where header padding ends. first_content_offset: Option<u64>, } impl MachOHeader { pub(super) fn parse(path: &Path, data: &[u8]) -> Result<Self> { let header_len = match data.get(..4).map_or(0, LittleEndian::read_u32) { MH_MAGIC => 28, MH_MAGIC_64 => 32, magic => { return Err(CodeSignError::macho( path, format!("unsupported Mach-O magic {magic:#010x}"), )); } }; ensure_len(path, data, header_len, "Mach-O header")?; let command_count = LittleEndian::read_u32(&data[16..]); let commands_len = LittleEndian::read_u32(&data[20..]) as usize; let commands_end = checked_add(path, header_len, commands_len, "load commands")?; ensure_len(path, data, commands_end, "load commands")?; let mut header = MachOHeader { file_type: LittleEndian::read_u32(&data[12..]), commands_end, code_signature_command: None, linkedit_segment: None, text_segment: (0, 0), segments_end: 0, first_content_offset: None, }; let mut offset = header_len; for _ in 0..command_count { ensure_len(path, &data[..commands_end], offset + 8, "load command")?; let command = LittleEndian::read_u32(&data[offset..]); let command_len = LittleEndian::read_u32(&data[offset + 4..]) as usize; let command_end = checked_add(path, offset, command_len, "load command")?; if command_len < 8 || command_end > commands_end { return Err(CodeSignError::macho( path, "a load command overflows sizeofcmds", )); } match command { LC_SEGMENT => { header.parse_segment(path, data, offset, command_len, &SEGMENT_LAYOUT)? } LC_SEGMENT_64 => { header.parse_segment(path, data, offset, command_len, &SEGMENT_64_LAYOUT)? } LC_CODE_SIGNATURE if command_len >= LINKEDIT_DATA_COMMAND_LEN => { header.code_signature_command = Some(offset) } LC_CODE_SIGNATURE => { return Err(CodeSignError::macho(path, "LC_CODE_SIGNATURE is too small")); } _ => {} } offset = command_end; } Ok(header) } fn parse_segment( &mut self, path: &Path, data: &[u8], offset: usize, command_len: usize, layout: &'static SegmentLayout, ) -> Result<()> { if command_len < layout.command_len { return Err(CodeSignError::macho(path, "a segment command is too small")); } let segment = Segment { command: offset, layout, }; let (fileoff, filesize) = segment.file_range(data); let segment_end = fileoff .checked_add(filesize) .ok_or_else(|| CodeSignError::macho(path, "a segment ends beyond 2^64"))?; // Segments without file contents (__PAGEZERO and the like) don't count, nothing of theirs is lost. if filesize != 0 { self.segments_end = self.segments_end.max(segment_end); } match segment_name(&data[offset + 8..offset + 24]) { b"__TEXT" => self.text_segment = (fileoff, filesize), b"__LINKEDIT" => self.linkedit_segment = Some(segment), _ => {} } let section_count = LittleEndian::read_u32(&data[offset + layout.nsects..]) as usize; let sections = offset + layout.command_len; let sections_end = section_count .checked_mul(layout.section_len) .and_then(|sections_len| sections.checked_add(sections_len)); if sections_end.is_none_or(|sections_end| sections_end > offset + command_len) { return Err(CodeSignError::macho( path, "sections overflow their segment command", )); } if section_count == 0 && fileoff != 0 && filesize != 0 { self.record_content(fileoff); } for index in 0..section_count { let section = sections + index * layout.section_len; let size = layout.read_word(data, section + layout.section_size); let file_offset = LittleEndian::read_u32(&data[section + layout.section_offset..]); let section_type = LittleEndian::read_u32(&data[section + layout.section_flags..]) & SECTION_TYPE; if size != 0 && !matches!(section_type, S_ZEROFILL | S_THREAD_LOCAL_ZEROFILL) { self.record_content(file_offset.into()); } } Ok(()) } fn record_content(&mut self, file_offset: u64) { if file_offset != 0 { self.first_content_offset = Some( self.first_content_offset .map_or(file_offset, |first| first.min(file_offset)), ); } } pub(super) fn is_executable(&self) -> bool { self.file_type == MH_EXECUTE } /// The bytes of the existing code signature, if there is one. pub(super) fn code_signature<'d>( &self, path: &Path, data: &'d [u8], ) -> Result<Option<&'d [u8]>> { match self .code_signature_command .map(|command| code_signature_location(data, command)) { Some((offset, size)) if size != 0 => data .get(offset..checked_add(path, offset, size, "code signature")?) .map(Some) .ok_or_else(|| CodeSignError::macho(path, "the code signature is out of bounds")), _ => Ok(None), } } } fn code_signature_location(data: &[u8], command: usize) -> (usize, usize) { ( LittleEndian::read_u32(&data[command + 8..]) as usize, LittleEndian::read_u32(&data[command + 12..]) as usize, ) } fn segment_name(bytes: &[u8]) -> &[u8] { let end = bytes .iter() .position(|byte| *byte == 0) .unwrap_or(bytes.len()); &bytes[..end] } /// A thin Mach-O being signed. pub(super) struct ThinMachO<'a> { path: &'a Path, pub data: Vec<u8>, pub header: MachOHeader, } impl<'a> ThinMachO<'a> { pub(super) fn parse(path: &'a Path, data: Vec<u8>) -> Result<Self> { let header = MachOHeader::parse(path, &data)?; Ok(ThinMachO { path, data, header }) } /// Where the signature goes, and so where the signed code ends: over the old one, or after __LINKEDIT. pub(super) fn code_signature_offset(&self) -> Result<usize> { match self.code_signature_location() { Some((offset, size)) if size != 0 => Ok(offset), _ => Ok(align_to(self.linkedit_end()?, CODE_SIGNATURE_ALIGNMENT)), } } /// Make room for a `len` bytes signature at `code_signature_offset`, growing __LINKEDIT if needed. pub(super) fn reserve_code_signature(&mut self, len: usize) -> Result<usize> { let command = match self.header.code_signature_command { Some(command) => command, None => self.install_code_signature_command(len)?, }; let (offset, size) = code_signature_location(&self.data, command); let old_end = checked_add(self.path, offset, size, "code signature")?; if size != 0 && len <= size { // The old signature's space is big enough, and write_code_signature overwrites all of it. ensure_len(self.path, &self.data, old_end, "code signature")?; Ok(offset) } else { let (start, old_end) = match size { 0 => { let linkedit_end = self.linkedit_end()?; ( align_to(linkedit_end, CODE_SIGNATURE_ALIGNMENT), linkedit_end, ) } // A signature that starts past the end of the file would make us zero-fill up to it. _ if old_end >= self.data.len() && offset <= self.data.len() => (offset, old_end), _ => { return Err(CodeSignError::macho( self.path, "cannot grow a code signature that is not at the end of the Mach-O", )); } }; let kept_len = start.min(old_end); if kept_len < self.header.commands_end { return Err(CodeSignError::macho( self.path, "the code signature would overwrite the load commands", )); } self.grow_linkedit(start + len - old_end)?; self.data.truncate(kept_len); self.data.resize(start + len, 0); for (field, value) in [(8, start), (12, len)] { let value = u32::try_from(value).map_err(|_| { CodeSignError::macho(self.path, "the code signature ends beyond 4 GiB") })?; LittleEndian::write_u32(&mut self.data[command + field..], value); } Ok(start) } } pub(super) fn write_code_signature(&mut self, signature: &[u8]) -> Result<()> { let (offset, size) = self .code_signature_location() .ok_or_else(|| CodeSignError::macho(self.path, "missing LC_CODE_SIGNATURE"))?; if signature.len() > size { return Err(CodeSignError::macho( self.path, "the encoded signature outgrew its reservation", )); } let end = checked_add(self.path, offset, size, "code signature")?; ensure_len(self.path, &self.data, end, "code signature")?; let (signature_space, padding) = self.data[offset..end].split_at_mut(signature.len()); signature_space.copy_from_slice(signature); padding.fill(0); Ok(()) } fn code_signature_location(&self) -> Option<(usize, usize)> { self.header .code_signature_command .map(|command| code_signature_location(&self.data, command)) } /// Only header padding can take a new load command, and it has to be zeroes. fn install_code_signature_command(&mut self, signature_len: usize) -> Result<usize> { let command = self.header.commands_end; let command_end = command + LINKEDIT_DATA_COMMAND_LEN; let padding_end = self .header .first_content_offset .map_or(self.data.len() as u64, |offset| { offset.min(self.data.len() as u64) }); if command_end as u64 > padding_end || self.data[command..command_end] .iter() .any(|byte| *byte != 0) { return Err(CodeSignError::NeedsCodeSignatureAllocation { path: self.path.to_path_buf(), signature_len, }); } let command_count = LittleEndian::read_u32(&self.data[16..]) + 1; let commands_len = LittleEndian::read_u32(&self.data[20..]) .checked_add(LINKEDIT_DATA_COMMAND_LEN as u32) .ok_or_else(|| CodeSignError::macho(self.path, "sizeofcmds overflows"))?; for (offset, value) in [ (16, command_count), (20, commands_len), (command, LC_CODE_SIGNATURE), (command + 4, LINKEDIT_DATA_COMMAND_LEN as u32), (command + 8, 0), (command + 12, 0), ] { LittleEndian::write_u32(&mut self.data[offset..], value); } self.header.commands_end = command_end; self.header.code_signature_command = Some(command); Ok(command) } fn linkedit_segment(&self) -> Result<Segment> { self.header.linkedit_segment.ok_or_else(|| { CodeSignError::macho( self.path, "missing __LINKEDIT segment for the code signature", ) }) } /// The end of __LINKEDIT, where a new code signature goes; anything after it would be lost. fn linkedit_end(&self) -> Result<usize> { let (fileoff, filesize) = self.linkedit_segment()?.file_range(&self.data); let end = fileoff .checked_add(filesize) .filter(|end| *end <= self.data.len() as u64) .ok_or_else(|| { CodeSignError::macho(self.path, "__LINKEDIT extends beyond the Mach-O") })?; if end < self.header.segments_end { return Err(CodeSignError::macho( self.path, "__LINKEDIT is not the last segment of the Mach-O", )); } Ok(end as usize) } fn grow_linkedit(&mut self, growth: usize) -> Result<()> { let Segment { command, layout } = self.linkedit_segment()?; let too_big = || CodeSignError::macho(self.path, "__LINKEDIT outgrew 2^64"); let filesize = layout .read_word(&self.data, command + layout.filesize) .checked_add(growth as u64) .ok_or_else(too_big)?; let vmsize = layout.read_word(&self.data, command + layout.vmsize).max( filesize .checked_next_multiple_of(SEGMENT_PAGE_SIZE) .ok_or_else(too_big)?, ); layout.write_word( self.path, &mut self.data, command + layout.filesize, filesize, )?; layout.write_word(self.path, &mut self.data, command + layout.vmsize, vmsize) } }