Skip to main content

macho_core/format/
symbols.rs

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
22/// Performs parse_symbol_table.
23pub 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
34/// Parse selected `nlist` entries without walking or materializing the complete
35/// symbol table.
36///
37/// Results preserve the requested index order, including duplicate indices.
38/// The symbol- and string-table layouts and every requested entry are fully
39/// bounds checked. An out-of-range index rejects the whole request.
40pub 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
58/// Fold raw `nlist` entries in physical symbol-table order without first
59/// materializing a [`SymbolTable`].
60///
61/// This performs one pass over the `nlist` entries. The accumulator is returned
62/// only after every entry and referenced string-table name has parsed
63/// successfully. If a later entry is malformed, the partially folded
64/// accumulator is dropped and only the parse error is returned. The caller
65/// controls retained memory through the accumulator and need not collect
66/// [`Symbol`] values.
67pub 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    // Validate and slice the string table
92    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        // Cast i16 to u16 to preserve bit pattern for bitmask interpretation
265        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        // mach_header_64
301        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        // symtab_command
311        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        // File order differs from lexical name order.
319        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}