1use crate::error::{Error, Result};
2use crate::format::constants::*;
3use crate::format::io::pod::{self, RawNlist32, RawNlist64};
4use crate::model::ext::MachoExt;
5use crate::model::header::Bitness;
6use crate::model::macho_file::MachoFile;
7use crate::model::symbol::{StringTable, Symbol, SymbolTable, SymbolType};
8
9const MAX_SYMBOLS: usize = 10_000_000;
10
11impl<'data> MachoExt<'data> for SymbolTable<'data> {
12 type Error = Error;
13
14 fn parse<'mf>(macho: &'mf MachoFile<'data>) -> Result<Self>
15 where
16 'data: 'mf,
17 {
18 parse_symbol_table(macho)
19 }
20}
21
22pub fn parse_symbol_table<'data>(macho: &MachoFile<'data>) -> Result<SymbolTable<'data>> {
24 let layout = symbol_table_layout(macho)?;
25 let mut symbols = Vec::new();
26 fold_nlist_symbols(macho, &layout, &mut symbols, |symbols, symbol| {
27 symbols.push(symbol);
28 Ok(())
29 })?;
30
31 Ok(SymbolTable::new(symbols, layout.string_table))
32}
33
34pub fn parse_symbols_at<'data>(
41 macho: &MachoFile<'data>,
42 indices: &[usize],
43) -> Result<Vec<Symbol<'data>>> {
44 let layout = symbol_table_layout(macho)?;
45 let mut symbols = Vec::with_capacity(indices.len());
46 for &index in indices {
47 if index >= layout.symbol_count {
48 return Err(Error::format(format!(
49 "symbol index {index} exceeds table count {}",
50 layout.symbol_count
51 )));
52 }
53 symbols.push(parse_nlist_symbol(macho, &layout, index)?);
54 }
55 Ok(symbols)
56}
57
58pub fn fold_symbols<'data, State>(
68 macho: &MachoFile<'data>,
69 mut state: State,
70 mut folder: impl FnMut(&mut State, Symbol<'data>) -> Result<()>,
71) -> Result<State> {
72 let layout = symbol_table_layout(macho)?;
73 fold_nlist_symbols(macho, &layout, &mut state, &mut folder)?;
74 Ok(state)
75}
76
77struct SymbolTableLayout<'data> {
78 string_table: StringTable<'data>,
79 symbol_count: usize,
80 symbol_offset: usize,
81}
82
83fn symbol_table_layout<'data>(macho: &MachoFile<'data>) -> Result<SymbolTableLayout<'data>> {
84 let symtab = macho
85 .find_load_command(|lc| lc.as_symtab().is_some())
86 .and_then(|lc| lc.kind.as_symtab())
87 .ok_or_else(|| Error::format("no LC_SYMTAB load command found"))?;
88
89 let data = macho.bytes();
90
91 let str_start = symtab.str_offset as usize;
93 let str_end = str_start
94 .checked_add(symtab.str_size as usize)
95 .ok_or_else(|| {
96 Error::bounds(str_start as u64, symtab.str_size as u64, data.len() as u64)
97 })?;
98 if str_end > data.len() {
99 return Err(Error::bounds(
100 str_start as u64,
101 symtab.str_size as u64,
102 data.len() as u64,
103 ));
104 }
105 let string_table = StringTable::new(&data[str_start..str_end]);
106
107 let nsyms = symtab.nsyms as usize;
108 if nsyms > MAX_SYMBOLS {
109 return Err(Error::format(format!(
110 "symbol table claims {nsyms} symbols, which exceeds the limit of {MAX_SYMBOLS}"
111 )));
112 }
113
114 Ok(SymbolTableLayout {
115 string_table,
116 symbol_count: nsyms,
117 symbol_offset: symtab.sym_offset as usize,
118 })
119}
120
121fn fold_nlist_symbols<'data, State>(
122 macho: &MachoFile<'data>,
123 layout: &SymbolTableLayout<'data>,
124 state: &mut State,
125 mut folder: impl FnMut(&mut State, Symbol<'data>) -> Result<()>,
126) -> Result<()> {
127 match macho.bitness() {
128 Bitness::Bits64 => fold_nlist64(
129 macho.bytes(),
130 macho.endian(),
131 layout.symbol_offset,
132 layout.symbol_count,
133 &layout.string_table,
134 state,
135 &mut folder,
136 ),
137 Bitness::Bits32 => fold_nlist32(
138 macho.bytes(),
139 macho.endian(),
140 layout.symbol_offset,
141 layout.symbol_count,
142 &layout.string_table,
143 state,
144 &mut folder,
145 ),
146 }
147}
148
149fn parse_nlist_symbol<'data>(
150 macho: &MachoFile<'data>,
151 layout: &SymbolTableLayout<'data>,
152 index: usize,
153) -> Result<Symbol<'data>> {
154 match macho.bitness() {
155 Bitness::Bits64 => {
156 let entry_size = size_of::<RawNlist64>();
157 let entry_off = nlist_offset(layout.symbol_offset, index, entry_size, "nlist64")?;
158 let raw: RawNlist64 = pod::read_pod(macho.bytes(), entry_off)?;
159 let endian = macho.endian();
160 Ok(Symbol {
161 name: layout.string_table.get(endian.interpret_u32(raw.n_strx))?,
162 sym_type: SymbolType::from_n_type(raw.n_type),
163 external: raw.n_type & N_EXT != 0,
164 private_external: raw.n_type & N_PEXT != 0,
165 section_index: raw.n_sect,
166 desc: endian.interpret_u16(raw.n_desc),
167 value: endian.interpret_u64(raw.n_value),
168 index,
169 })
170 }
171 Bitness::Bits32 => {
172 let entry_size = size_of::<RawNlist32>();
173 let entry_off = nlist_offset(layout.symbol_offset, index, entry_size, "nlist32")?;
174 let raw: RawNlist32 = pod::read_pod(macho.bytes(), entry_off)?;
175 let endian = macho.endian();
176 Ok(Symbol {
177 name: layout.string_table.get(endian.interpret_u32(raw.n_strx))?,
178 sym_type: SymbolType::from_n_type(raw.n_type),
179 external: raw.n_type & N_EXT != 0,
180 private_external: raw.n_type & N_PEXT != 0,
181 section_index: raw.n_sect,
182 desc: endian.interpret_u16(raw.n_desc as u16),
183 value: u64::from(endian.interpret_u32(raw.n_value)),
184 index,
185 })
186 }
187 }
188}
189
190fn nlist_offset(base: usize, index: usize, entry_size: usize, kind: &str) -> Result<usize> {
191 base.checked_add(
192 index
193 .checked_mul(entry_size)
194 .ok_or_else(|| Error::format(format!("{kind}[{index}]: stride overflows")))?,
195 )
196 .ok_or_else(|| Error::format(format!("{kind}[{index}]: offset overflows")))
197}
198
199#[allow(clippy::too_many_arguments)]
200fn fold_nlist64<'data, State>(
201 data: &'data [u8],
202 endian: crate::format::io::Endian,
203 offset: usize,
204 count: usize,
205 string_table: &StringTable<'data>,
206 state: &mut State,
207 folder: &mut impl FnMut(&mut State, Symbol<'data>) -> Result<()>,
208) -> Result<()> {
209 let entry_size = size_of::<RawNlist64>();
210
211 for i in 0..count {
212 let entry_off = offset
213 .checked_add(
214 i.checked_mul(entry_size)
215 .ok_or_else(|| Error::format(format!("nlist64[{i}]: stride overflows")))?,
216 )
217 .ok_or_else(|| Error::format(format!("nlist64[{i}]: offset overflows")))?;
218 let raw: RawNlist64 = pod::read_pod(data, entry_off)?;
219 let n_strx = endian.interpret_u32(raw.n_strx);
220 let n_desc = endian.interpret_u16(raw.n_desc);
221 let n_value = endian.interpret_u64(raw.n_value);
222
223 let name = string_table.get(n_strx)?;
224
225 folder(
226 state,
227 Symbol {
228 name,
229 sym_type: SymbolType::from_n_type(raw.n_type),
230 external: raw.n_type & N_EXT != 0,
231 private_external: raw.n_type & N_PEXT != 0,
232 section_index: raw.n_sect,
233 desc: n_desc,
234 value: n_value,
235 index: i,
236 },
237 )?;
238 }
239
240 Ok(())
241}
242
243#[allow(clippy::too_many_arguments)]
244fn fold_nlist32<'data, State>(
245 data: &'data [u8],
246 endian: crate::format::io::Endian,
247 offset: usize,
248 count: usize,
249 string_table: &StringTable<'data>,
250 state: &mut State,
251 folder: &mut impl FnMut(&mut State, Symbol<'data>) -> Result<()>,
252) -> Result<()> {
253 let entry_size = size_of::<RawNlist32>();
254
255 for i in 0..count {
256 let entry_off = offset
257 .checked_add(
258 i.checked_mul(entry_size)
259 .ok_or_else(|| Error::format(format!("nlist32[{i}]: stride overflows")))?,
260 )
261 .ok_or_else(|| Error::format(format!("nlist32[{i}]: offset overflows")))?;
262 let raw: RawNlist32 = pod::read_pod(data, entry_off)?;
263 let n_strx = endian.interpret_u32(raw.n_strx);
264 let n_desc = endian.interpret_u16(raw.n_desc as u16);
266 let n_value = endian.interpret_u32(raw.n_value) as u64;
267
268 let name = string_table.get(n_strx)?;
269
270 folder(
271 state,
272 Symbol {
273 name,
274 sym_type: SymbolType::from_n_type(raw.n_type),
275 external: raw.n_type & N_EXT != 0,
276 private_external: raw.n_type & N_PEXT != 0,
277 section_index: raw.n_sect,
278 desc: n_desc,
279 value: n_value,
280 index: i,
281 },
282 )?;
283 }
284
285 Ok(())
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 fn macho_with_symbols(second_name_offset: u32) -> Vec<u8> {
293 let command_size = 24u32;
294 let symbol_offset = 32u32 + command_size;
295 let symbol_count = 2u32;
296 let string_table = b"\0second\0first\0";
297 let string_offset = symbol_offset + symbol_count * size_of::<RawNlist64>() as u32;
298 let mut bytes = Vec::with_capacity(string_offset as usize + string_table.len());
299
300 bytes.extend_from_slice(&MH_MAGIC_64.to_le_bytes());
302 bytes.extend_from_slice(&CPU_TYPE_X86_64.to_le_bytes());
303 bytes.extend_from_slice(&CPU_SUBTYPE_X86_64_ALL.to_le_bytes());
304 bytes.extend_from_slice(&MH_EXECUTE.to_le_bytes());
305 bytes.extend_from_slice(&1u32.to_le_bytes());
306 bytes.extend_from_slice(&command_size.to_le_bytes());
307 bytes.extend_from_slice(&0u32.to_le_bytes());
308 bytes.extend_from_slice(&0u32.to_le_bytes());
309
310 bytes.extend_from_slice(&LC_SYMTAB.to_le_bytes());
312 bytes.extend_from_slice(&command_size.to_le_bytes());
313 bytes.extend_from_slice(&symbol_offset.to_le_bytes());
314 bytes.extend_from_slice(&symbol_count.to_le_bytes());
315 bytes.extend_from_slice(&string_offset.to_le_bytes());
316 bytes.extend_from_slice(&(string_table.len() as u32).to_le_bytes());
317
318 push_nlist64(&mut bytes, 8, 0x2000);
320 push_nlist64(&mut bytes, second_name_offset, 0x1000);
321 bytes.extend_from_slice(string_table);
322 bytes
323 }
324
325 fn push_nlist64(bytes: &mut Vec<u8>, name_offset: u32, value: u64) {
326 bytes.extend_from_slice(&name_offset.to_le_bytes());
327 bytes.push(N_SECT | N_EXT);
328 bytes.push(1);
329 bytes.extend_from_slice(&0u16.to_le_bytes());
330 bytes.extend_from_slice(&value.to_le_bytes());
331 }
332
333 fn parse_fixture(bytes: &[u8]) -> MachoFile<'_> {
334 crate::format::macho::parse_macho_file(bytes).unwrap()
335 }
336
337 #[test]
338 fn fold_symbols_uses_physical_nlist_order() {
339 let bytes = macho_with_symbols(1);
340 let macho = parse_fixture(&bytes);
341
342 let names = fold_symbols(&macho, Vec::new(), |names, symbol| {
343 names.push((symbol.index, symbol.name, symbol.value));
344 Ok(())
345 })
346 .unwrap();
347
348 assert_eq!(names, [(0, "first", 0x2000), (1, "second", 0x1000)]);
349 let table = parse_symbol_table(&macho).unwrap();
350 assert_eq!(
351 table
352 .symbols()
353 .iter()
354 .map(|symbol| symbol.name)
355 .collect::<Vec<_>>(),
356 ["first", "second"]
357 );
358 }
359
360 #[test]
361 fn fold_symbols_does_not_require_a_symbol_collection() {
362 let bytes = macho_with_symbols(1);
363 let macho = parse_fixture(&bytes);
364
365 let (count, value_sum) = fold_symbols(&macho, (0usize, 0u64), |summary, symbol| {
366 summary.0 += 1;
367 summary.1 += symbol.value;
368 Ok(())
369 })
370 .unwrap();
371
372 assert_eq!(count, 2);
373 assert_eq!(value_sum, 0x3000);
374 }
375
376 #[test]
377 fn selected_symbols_preserve_request_order_and_skip_unrequested_rows() {
378 let bytes = macho_with_symbols(99);
379 let macho = parse_fixture(&bytes);
380
381 let symbols = parse_symbols_at(&macho, &[0, 0]).unwrap();
382 assert_eq!(
383 symbols
384 .iter()
385 .map(|symbol| (symbol.index, symbol.name, symbol.value))
386 .collect::<Vec<_>>(),
387 [(0, "first", 0x2000), (0, "first", 0x2000)]
388 );
389 assert!(parse_symbols_at(&macho, &[1]).is_err());
390 assert!(parse_symbols_at(&macho, &[2]).is_err());
391 }
392
393 #[test]
394 fn malformed_suffix_does_not_return_partially_folded_symbol_state() {
395 let bytes = macho_with_symbols(99);
396 let macho = parse_fixture(&bytes);
397 let mut folder_calls = 0usize;
398
399 let result = fold_symbols(&macho, 0usize, |count, _symbol| {
400 folder_calls += 1;
401 *count += 1;
402 Ok(())
403 });
404
405 assert!(result.is_err());
406 assert_eq!(folder_calls, 1);
407 assert!(parse_symbol_table(&macho).is_err());
408 }
409}