apple_codesign/
macho_universal.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use {
6    goblin::mach::{
7        fat::{FatArch, FAT_MAGIC, SIZEOF_FAT_ARCH, SIZEOF_FAT_HEADER},
8        Mach,
9    },
10    scroll::{IOwrite, Pwrite},
11    std::io::Write,
12    thiserror::Error,
13};
14
15#[derive(Debug, Error)]
16pub enum UniversalMachOError {
17    #[error("I/O error: {0}")]
18    Io(#[from] std::io::Error),
19
20    #[error("mach-o parse error: {0}")]
21    Goblin(#[from] goblin::error::Error),
22
23    #[error("scroll error: {0}")]
24    Scroll(#[from] scroll::Error),
25}
26
27/// Interface for constructing a universal Mach-O binary.
28#[derive(Clone, Default)]
29pub struct UniversalBinaryBuilder {
30    binaries: Vec<Vec<u8>>,
31}
32
33impl UniversalBinaryBuilder {
34    pub fn add_binary(&mut self, data: impl AsRef<[u8]>) -> Result<usize, UniversalMachOError> {
35        let data = data.as_ref();
36
37        match Mach::parse(data)? {
38            Mach::Binary(_) => {
39                self.binaries.push(data.to_vec());
40                Ok(1)
41            }
42            Mach::Fat(multiarch) => {
43                for arch in multiarch.iter_arches() {
44                    let arch = arch?;
45
46                    let data =
47                        &data[arch.offset as usize..arch.offset as usize + arch.size as usize];
48                    self.binaries.push(data.to_vec());
49                }
50
51                Ok(multiarch.narches)
52            }
53        }
54    }
55
56    /// Write a universal Mach-O to the given writer.
57    pub fn write(&self, writer: &mut impl Write) -> Result<(), UniversalMachOError> {
58        create_universal_macho(writer, self.binaries.iter().map(|x| x.as_slice()))
59    }
60}
61
62/// Create a universal mach-o binary from existing mach-o binaries.
63///
64/// The binaries will be parsed as Mach-O.
65///
66/// Because the size of the individual Mach-O binaries must be written into a
67/// header, all content is buffered internally.
68pub fn create_universal_macho<'a>(
69    writer: &mut impl Write,
70    binaries: impl Iterator<Item = &'a [u8]>,
71) -> Result<(), UniversalMachOError> {
72    // Binaries are aligned on page boundaries. x86-64 appears to use
73    // 4k. aarch64 16k. It really doesn't appear to matter unless you want
74    // to minimize binary size, so we always use 16k.
75    const ALIGN_VALUE: u32 = 14;
76    let align: u32 = 2u32.pow(ALIGN_VALUE);
77
78    let mut records = vec![];
79
80    let mut offset: u32 = align;
81
82    for binary in binaries {
83        let macho = goblin::mach::MachO::parse(binary, 0)?;
84
85        // This will be 0 for the 1st binary.
86        let pad_bytes = match offset % align {
87            0 => 0,
88            x => align - x,
89        };
90
91        offset += pad_bytes;
92
93        let arch = FatArch {
94            cputype: macho.header.cputype,
95            cpusubtype: macho.header.cpusubtype,
96            offset,
97            size: binary.len() as u32,
98            align: ALIGN_VALUE,
99        };
100
101        offset += arch.size;
102
103        records.push((arch, pad_bytes as usize, binary));
104    }
105
106    // Fat header is the magic plus the number of records.
107    writer.iowrite_with(FAT_MAGIC, scroll::BE)?;
108    writer.iowrite_with(records.len() as u32, scroll::BE)?;
109
110    for (fat_arch, _, _) in &records {
111        let mut buffer = [0u8; SIZEOF_FAT_ARCH];
112        buffer.pwrite_with(fat_arch, 0, scroll::BE)?;
113        writer.write_all(&buffer)?;
114    }
115
116    // Pad NULL until first mach-o binary.
117    let current_offset = SIZEOF_FAT_HEADER + records.len() * SIZEOF_FAT_ARCH;
118    writer.write_all(&b"\0".repeat(align as usize - current_offset % align as usize))?;
119
120    // This input would be nonsensical. Let's not even support it.
121    assert!(current_offset <= align as usize, "too many mach-o entries");
122
123    for (_, pad_bytes, macho_data) in records {
124        writer.write_all(&b"\0".repeat(pad_bytes))?;
125        writer.write_all(macho_data)?;
126    }
127
128    Ok(())
129}