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
14pub struct ObjCResolver<'data> {
20 macho: &'data MachoFile<'data>,
21 fixup_map: HashMap<u64, ResolvedFixup>,
23 image_base: u64,
24 endian: Endian,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum ObjCPointerProvenance {
30 Direct,
32 ChainedRebase,
34 ChainedBind {
36 import_name: String,
38 },
39 LegacyRebase,
41 LegacyBind {
43 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 pub fn new(macho: &'data MachoFile<'data>) -> Result<Self> {
60 let image_base = macho.image_base().0;
61 let endian = macho.endian();
62
63 #[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 pub fn read_pointer_at_offset(&self, file_offset: u64) -> Result<Option<Va>> {
107 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 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 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 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 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 pub fn va_to_offset(&self, va: Va) -> Result<ThinFileOffset> {
166 Ok(self.macho.address_map().va_to_thin_offset(va)?)
167 }
168
169 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 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 pub fn macho(&self) -> &MachoFile<'data> {
200 self.macho
201 }
202
203 pub fn image_base(&self) -> u64 {
205 self.image_base
206 }
207
208 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 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 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 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 map.entry(file_offset)
307 .or_insert(ResolvedFixup::LegacyRebase);
308 }
309
310 Ok(map)
311}