Skip to main content

macho_objc/
resolve.rs

1use std::collections::HashMap;
2
3use crate::error::{Error, Result};
4use crate::format::io::endian::Endian;
5use crate::format::io::pod;
6use crate::model::addr::{ThinFileOffset, Va};
7use crate::model::load_command::LoadCommand;
8use crate::model::macho_file::MachoFile;
9#[cfg(feature = "fixups")]
10use macho_dyld::chained::{ChainedFixups, parse_chained_fixups};
11#[cfg(feature = "fixups")]
12use macho_dyld::types::FixupKind;
13
14/// Resolves pointers in ObjC metadata, handling chained fixups.
15///
16/// For arm64e binaries, raw pointer values in metadata sections are chained
17/// fixup entries, not actual addresses. This resolver maps file offsets to
18/// their resolved targets.
19pub struct ObjCResolver<'data> {
20    macho: &'data MachoFile<'data>,
21    /// Map from file offset -> resolved fixup
22    fixup_map: HashMap<u64, ResolvedFixup>,
23    image_base: u64,
24    endian: Endian,
25}
26
27/// Provenance for one pointer-valued Objective-C metadata field.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum ObjCPointerProvenance {
30    /// The file stores an ordinary pointer and no fixup record covers it.
31    Direct,
32    /// A chained rebase resolves the pointer within the image.
33    ChainedRebase,
34    /// A chained bind names an external symbol.
35    ChainedBind {
36        /// Imported symbol name.
37        import_name: String,
38    },
39    /// A legacy rebase opcode covers the pointer.
40    LegacyRebase,
41    /// A legacy bind opcode names an external symbol.
42    LegacyBind {
43        /// Imported symbol name.
44        import_name: String,
45    },
46}
47
48#[derive(Debug, Clone)]
49#[cfg_attr(not(feature = "fixups"), allow(dead_code))]
50enum ResolvedFixup {
51    ChainedRebase(u64),
52    ChainedBind { import_name: String },
53    LegacyRebase,
54    LegacyBind { import_name: String },
55}
56
57impl<'data> ObjCResolver<'data> {
58    /// Performs new.
59    pub fn new(macho: &'data MachoFile<'data>) -> Result<Self> {
60        let image_base = macho.image_base().0;
61        let endian = macho.endian();
62
63        // Presence is decided by the load command, not by whether its payload
64        // happened to parse. Damaged chained metadata must reject rather than
65        // being misclassified as an image that uses legacy fixups.
66        #[cfg(feature = "fixups")]
67        let fixup_map = if macho
68            .find_load_command(|command| matches!(command, LoadCommand::DyldChainedFixups(_)))
69            .is_some()
70        {
71            let fixups = parse_chained_fixups(macho)?;
72            build_fixup_map(macho, &fixups)?
73        } else {
74            build_legacy_fixup_map(macho)?
75        };
76        #[cfg(not(feature = "fixups"))]
77        let fixup_map = {
78            if macho
79                .find_load_command(|command| {
80                    matches!(
81                        command,
82                        LoadCommand::DyldChainedFixups(_)
83                            | LoadCommand::DyldInfo(_)
84                            | LoadCommand::DyldInfoOnly(_)
85                    )
86                })
87                .is_some()
88            {
89                return Err(Error::unsupported(
90                    "Objective-C fixup decoding requires the `fixups` feature",
91                ));
92            }
93            HashMap::new()
94        };
95
96        Ok(Self {
97            macho,
98            fixup_map,
99            image_base,
100            endian,
101        })
102    }
103
104    /// Read a pointer at a file offset, resolving chained fixups.
105    /// Returns the resolved VA target, or None if it's an external bind.
106    pub fn read_pointer_at_offset(&self, file_offset: u64) -> Result<Option<Va>> {
107        // Check fixup map first
108        if let Some(fixup) = self.fixup_map.get(&file_offset) {
109            return match fixup {
110                ResolvedFixup::ChainedRebase(target) => self
111                    .image_base
112                    .checked_add(*target)
113                    .map(Va)
114                    .map(Some)
115                    .ok_or_else(|| Error::address("chained rebase target overflows")),
116                ResolvedFixup::LegacyRebase => {
117                    // The linker already wrote the correct un-slid VA.
118                    let raw = pod::read_pod::<u64>(self.macho.bytes(), file_offset as usize)
119                        .map(|v| self.endian.interpret_u64(v))?;
120                    if raw == 0 {
121                        Ok(None)
122                    } else {
123                        Ok(Some(Va(raw)))
124                    }
125                }
126                ResolvedFixup::ChainedBind { .. } | ResolvedFixup::LegacyBind { .. } => Ok(None),
127            };
128        }
129
130        // No fixup — read raw pointer value
131        let raw = pod::read_pod::<u64>(self.macho.bytes(), file_offset as usize)
132            .map(|v| self.endian.interpret_u64(v))?;
133
134        if raw == 0 {
135            Ok(None)
136        } else {
137            Ok(Some(Va(raw)))
138        }
139    }
140
141    /// Read a pointer at a VA, resolving chained fixups.
142    pub fn read_pointer_at_va(&self, va: Va) -> Result<Option<Va>> {
143        let offset = self.macho.address_map().va_to_thin_offset(va)?;
144        self.read_pointer_at_offset(offset.0)
145    }
146
147    /// Read a C string at a VA.
148    pub fn read_cstring(&self, va: Va) -> Result<&'data str> {
149        if va.0 == 0 {
150            return Err(Error::address("null pointer"));
151        }
152        let offset = self.macho.address_map().va_to_thin_offset(va)?;
153        let data = self.macho.bytes();
154        let start = offset.as_usize();
155        if start >= data.len() {
156            return Err(Error::bounds(start as u64, 1, data.len() as u64));
157        }
158        let slice = &data[start..];
159        let end = slice.iter().position(|&b| b == 0).unwrap_or(slice.len());
160        std::str::from_utf8(&slice[..end])
161            .map_err(|e| Error::format(format!("invalid UTF-8 at VA {va}: {e}")))
162    }
163
164    /// Get the file offset for a VA.
165    pub fn va_to_offset(&self, va: Va) -> Result<ThinFileOffset> {
166        Ok(self.macho.address_map().va_to_thin_offset(va)?)
167    }
168
169    /// Get the import name if a fixup at this file offset is a bind.
170    pub fn bind_name_at_offset(&self, file_offset: u64) -> Option<&str> {
171        match self.fixup_map.get(&file_offset) {
172            Some(
173                ResolvedFixup::ChainedBind { import_name }
174                | ResolvedFixup::LegacyBind { import_name },
175            ) => Some(import_name),
176            _ => None,
177        }
178    }
179
180    /// Reports how a pointer field is represented without resolving or
181    /// discarding its fixup source.
182    pub fn pointer_provenance_at_offset(&self, file_offset: u64) -> ObjCPointerProvenance {
183        match self.fixup_map.get(&file_offset) {
184            None => ObjCPointerProvenance::Direct,
185            Some(ResolvedFixup::ChainedRebase(_)) => ObjCPointerProvenance::ChainedRebase,
186            Some(ResolvedFixup::ChainedBind { import_name }) => {
187                ObjCPointerProvenance::ChainedBind {
188                    import_name: import_name.clone(),
189                }
190            }
191            Some(ResolvedFixup::LegacyRebase) => ObjCPointerProvenance::LegacyRebase,
192            Some(ResolvedFixup::LegacyBind { import_name }) => ObjCPointerProvenance::LegacyBind {
193                import_name: import_name.clone(),
194            },
195        }
196    }
197
198    /// Performs macho.
199    pub fn macho(&self) -> &MachoFile<'data> {
200        self.macho
201    }
202
203    /// Performs image_base.
204    pub fn image_base(&self) -> u64 {
205        self.image_base
206    }
207
208    /// Performs endian.
209    pub fn endian(&self) -> Endian {
210        self.endian
211    }
212}
213
214#[cfg(feature = "fixups")]
215fn build_fixup_map(
216    macho: &MachoFile<'_>,
217    fixups: &ChainedFixups<'_>,
218) -> Result<HashMap<u64, ResolvedFixup>> {
219    let mut map = HashMap::new();
220
221    for fixup in &fixups.fixups {
222        // Convert segment_index + segment_offset to file offset
223        let seg = macho.segments().get(fixup.segment_index).ok_or_else(|| {
224            Error::format(format!(
225                "chained fixup references absent segment {}",
226                fixup.segment_index
227            ))
228        })?;
229        let file_offset = seg
230            .file_offset()
231            .0
232            .checked_add(fixup.segment_offset)
233            .ok_or_else(|| Error::address("chained fixup file offset overflows"))?;
234
235        match &fixup.kind {
236            FixupKind::Rebase { target } | FixupKind::AuthRebase { target, .. } => {
237                map.insert(file_offset, ResolvedFixup::ChainedRebase(*target));
238            }
239            FixupKind::Bind { import_index, .. } | FixupKind::AuthBind { import_index, .. } => {
240                let name = fixups
241                    .imports
242                    .get(*import_index as usize)
243                    .map(|import| import.name.to_string())
244                    .ok_or_else(|| {
245                        Error::format(format!(
246                            "chained bind references absent import {import_index}"
247                        ))
248                    })?;
249                map.insert(
250                    file_offset,
251                    ResolvedFixup::ChainedBind { import_name: name },
252                );
253            }
254            _ => {
255                return Err(Error::unsupported(
256                    "chained fixup kind is not supported by Objective-C decoding",
257                ));
258            }
259        }
260    }
261
262    Ok(map)
263}
264
265#[cfg(feature = "fixups")]
266fn build_legacy_fixup_map(macho: &MachoFile<'_>) -> Result<HashMap<u64, ResolvedFixup>> {
267    let mut map = HashMap::new();
268
269    // Import bind entries — these give us symbol names at specific offsets
270    let (regular, weak, lazy) = macho_dyld::bind::parse_bind_entries(macho)?;
271    for entry in regular.iter().chain(weak.iter()).chain(lazy.iter()) {
272        let seg = macho.segments().get(entry.segment_index).ok_or_else(|| {
273            Error::format(format!(
274                "legacy bind references absent segment {}",
275                entry.segment_index
276            ))
277        })?;
278        let file_offset = seg
279            .file_offset()
280            .0
281            .checked_add(entry.segment_offset)
282            .ok_or_else(|| Error::address("legacy bind file offset overflows"))?;
283        map.insert(
284            file_offset,
285            ResolvedFixup::LegacyBind {
286                import_name: entry.symbol_name.to_string(),
287            },
288        );
289    }
290
291    // Rebase entries — these tell us which pointers contain relocated addresses
292    for entry in macho_dyld::rebase::parse_rebase_entries(macho)? {
293        let seg = macho.segments().get(entry.segment_index).ok_or_else(|| {
294            Error::format(format!(
295                "legacy rebase references absent segment {}",
296                entry.segment_index
297            ))
298        })?;
299        let file_offset = seg
300            .file_offset()
301            .0
302            .checked_add(entry.segment_offset)
303            .ok_or_else(|| Error::address("legacy rebase file offset overflows"))?;
304        // The linker already wrote the correct un-slid VA. Do not overwrite
305        // a bind if damaged metadata describes both at one location.
306        map.entry(file_offset)
307            .or_insert(ResolvedFixup::LegacyRebase);
308    }
309
310    Ok(map)
311}