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/// Fold raw `nlist` entries in physical symbol-table order without first
35/// materializing a [`SymbolTable`].
36///
37/// This performs one pass over the `nlist` entries. The accumulator is returned
38/// only after every entry and referenced string-table name has parsed
39/// successfully. If a later entry is malformed, the partially folded
40/// accumulator is dropped and only the parse error is returned. The caller
41/// controls retained memory through the accumulator and need not collect
42/// [`Symbol`] values.
43pub fn fold_symbols<'data, State>(
44    macho: &MachoFile<'data>,
45    mut state: State,
46    mut folder: impl FnMut(&mut State, Symbol<'data>) -> Result<()>,
47) -> Result<State> {
48    let layout = symbol_table_layout(macho)?;
49    fold_nlist_symbols(macho, &layout, &mut state, &mut folder)?;
50    Ok(state)
51}
52
53struct SymbolTableLayout<'data> {
54    string_table: StringTable<'data>,
55    symbol_count: usize,
56    symbol_offset: usize,
57}
58
59fn symbol_table_layout<'data>(macho: &MachoFile<'data>) -> Result<SymbolTableLayout<'data>> {
60    let symtab = macho
61        .find_load_command(|lc| lc.as_symtab().is_some())
62        .and_then(|lc| lc.kind.as_symtab())
63        .ok_or_else(|| Error::format("no LC_SYMTAB load command found"))?;
64
65    let data = macho.bytes();
66
67    // Validate and slice the string table
68    let str_start = symtab.str_offset as usize;
69    let str_end = str_start
70        .checked_add(symtab.str_size as usize)
71        .ok_or_else(|| {
72            Error::bounds(str_start as u64, symtab.str_size as u64, data.len() as u64)
73        })?;
74    if str_end > data.len() {
75        return Err(Error::bounds(
76            str_start as u64,
77            symtab.str_size as u64,
78            data.len() as u64,
79        ));
80    }
81    let string_table = StringTable::new(&data[str_start..str_end]);
82
83    let nsyms = symtab.nsyms as usize;
84    if nsyms > MAX_SYMBOLS {
85        return Err(Error::format(format!(
86            "symbol table claims {nsyms} symbols, which exceeds the limit of {MAX_SYMBOLS}"
87        )));
88    }
89
90    Ok(SymbolTableLayout {
91        string_table,
92        symbol_count: nsyms,
93        symbol_offset: symtab.sym_offset as usize,
94    })
95}
96
97fn fold_nlist_symbols<'data, State>(
98    macho: &MachoFile<'data>,
99    layout: &SymbolTableLayout<'data>,
100    state: &mut State,
101    mut folder: impl FnMut(&mut State, Symbol<'data>) -> Result<()>,
102) -> Result<()> {
103    match macho.bitness() {
104        Bitness::Bits64 => fold_nlist64(
105            macho.bytes(),
106            macho.endian(),
107            layout.symbol_offset,
108            layout.symbol_count,
109            &layout.string_table,
110            state,
111            &mut folder,
112        ),
113        Bitness::Bits32 => fold_nlist32(
114            macho.bytes(),
115            macho.endian(),
116            layout.symbol_offset,
117            layout.symbol_count,
118            &layout.string_table,
119            state,
120            &mut folder,
121        ),
122    }
123}
124
125#[allow(clippy::too_many_arguments)]
126fn fold_nlist64<'data, State>(
127    data: &'data [u8],
128    endian: crate::format::io::Endian,
129    offset: usize,
130    count: usize,
131    string_table: &StringTable<'data>,
132    state: &mut State,
133    folder: &mut impl FnMut(&mut State, Symbol<'data>) -> Result<()>,
134) -> Result<()> {
135    let entry_size = size_of::<RawNlist64>();
136
137    for i in 0..count {
138        let entry_off = offset
139            .checked_add(
140                i.checked_mul(entry_size)
141                    .ok_or_else(|| Error::format(format!("nlist64[{i}]: stride overflows")))?,
142            )
143            .ok_or_else(|| Error::format(format!("nlist64[{i}]: offset overflows")))?;
144        let raw: RawNlist64 = pod::read_pod(data, entry_off)?;
145        let n_strx = endian.interpret_u32(raw.n_strx);
146        let n_desc = endian.interpret_u16(raw.n_desc);
147        let n_value = endian.interpret_u64(raw.n_value);
148
149        let name = string_table.get(n_strx)?;
150
151        folder(
152            state,
153            Symbol {
154                name,
155                sym_type: SymbolType::from_n_type(raw.n_type),
156                external: raw.n_type & N_EXT != 0,
157                private_external: raw.n_type & N_PEXT != 0,
158                section_index: raw.n_sect,
159                desc: n_desc,
160                value: n_value,
161                index: i,
162            },
163        )?;
164    }
165
166    Ok(())
167}
168
169#[allow(clippy::too_many_arguments)]
170fn fold_nlist32<'data, State>(
171    data: &'data [u8],
172    endian: crate::format::io::Endian,
173    offset: usize,
174    count: usize,
175    string_table: &StringTable<'data>,
176    state: &mut State,
177    folder: &mut impl FnMut(&mut State, Symbol<'data>) -> Result<()>,
178) -> Result<()> {
179    let entry_size = size_of::<RawNlist32>();
180
181    for i in 0..count {
182        let entry_off = offset
183            .checked_add(
184                i.checked_mul(entry_size)
185                    .ok_or_else(|| Error::format(format!("nlist32[{i}]: stride overflows")))?,
186            )
187            .ok_or_else(|| Error::format(format!("nlist32[{i}]: offset overflows")))?;
188        let raw: RawNlist32 = pod::read_pod(data, entry_off)?;
189        let n_strx = endian.interpret_u32(raw.n_strx);
190        // Cast i16 to u16 to preserve bit pattern for bitmask interpretation
191        let n_desc = endian.interpret_u16(raw.n_desc as u16);
192        let n_value = endian.interpret_u32(raw.n_value) as u64;
193
194        let name = string_table.get(n_strx)?;
195
196        folder(
197            state,
198            Symbol {
199                name,
200                sym_type: SymbolType::from_n_type(raw.n_type),
201                external: raw.n_type & N_EXT != 0,
202                private_external: raw.n_type & N_PEXT != 0,
203                section_index: raw.n_sect,
204                desc: n_desc,
205                value: n_value,
206                index: i,
207            },
208        )?;
209    }
210
211    Ok(())
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    fn macho_with_symbols(second_name_offset: u32) -> Vec<u8> {
219        let command_size = 24u32;
220        let symbol_offset = 32u32 + command_size;
221        let symbol_count = 2u32;
222        let string_table = b"\0second\0first\0";
223        let string_offset = symbol_offset + symbol_count * size_of::<RawNlist64>() as u32;
224        let mut bytes = Vec::with_capacity(string_offset as usize + string_table.len());
225
226        // mach_header_64
227        bytes.extend_from_slice(&MH_MAGIC_64.to_le_bytes());
228        bytes.extend_from_slice(&CPU_TYPE_X86_64.to_le_bytes());
229        bytes.extend_from_slice(&CPU_SUBTYPE_X86_64_ALL.to_le_bytes());
230        bytes.extend_from_slice(&MH_EXECUTE.to_le_bytes());
231        bytes.extend_from_slice(&1u32.to_le_bytes());
232        bytes.extend_from_slice(&command_size.to_le_bytes());
233        bytes.extend_from_slice(&0u32.to_le_bytes());
234        bytes.extend_from_slice(&0u32.to_le_bytes());
235
236        // symtab_command
237        bytes.extend_from_slice(&LC_SYMTAB.to_le_bytes());
238        bytes.extend_from_slice(&command_size.to_le_bytes());
239        bytes.extend_from_slice(&symbol_offset.to_le_bytes());
240        bytes.extend_from_slice(&symbol_count.to_le_bytes());
241        bytes.extend_from_slice(&string_offset.to_le_bytes());
242        bytes.extend_from_slice(&(string_table.len() as u32).to_le_bytes());
243
244        // File order differs from lexical name order.
245        push_nlist64(&mut bytes, 8, 0x2000);
246        push_nlist64(&mut bytes, second_name_offset, 0x1000);
247        bytes.extend_from_slice(string_table);
248        bytes
249    }
250
251    fn push_nlist64(bytes: &mut Vec<u8>, name_offset: u32, value: u64) {
252        bytes.extend_from_slice(&name_offset.to_le_bytes());
253        bytes.push(N_SECT | N_EXT);
254        bytes.push(1);
255        bytes.extend_from_slice(&0u16.to_le_bytes());
256        bytes.extend_from_slice(&value.to_le_bytes());
257    }
258
259    fn parse_fixture(bytes: &[u8]) -> MachoFile<'_> {
260        crate::format::macho::parse_macho_file(bytes).unwrap()
261    }
262
263    #[test]
264    fn fold_symbols_uses_physical_nlist_order() {
265        let bytes = macho_with_symbols(1);
266        let macho = parse_fixture(&bytes);
267
268        let names = fold_symbols(&macho, Vec::new(), |names, symbol| {
269            names.push((symbol.index, symbol.name, symbol.value));
270            Ok(())
271        })
272        .unwrap();
273
274        assert_eq!(names, [(0, "first", 0x2000), (1, "second", 0x1000)]);
275        let table = parse_symbol_table(&macho).unwrap();
276        assert_eq!(
277            table
278                .symbols()
279                .iter()
280                .map(|symbol| symbol.name)
281                .collect::<Vec<_>>(),
282            ["first", "second"]
283        );
284    }
285
286    #[test]
287    fn fold_symbols_does_not_require_a_symbol_collection() {
288        let bytes = macho_with_symbols(1);
289        let macho = parse_fixture(&bytes);
290
291        let (count, value_sum) = fold_symbols(&macho, (0usize, 0u64), |summary, symbol| {
292            summary.0 += 1;
293            summary.1 += symbol.value;
294            Ok(())
295        })
296        .unwrap();
297
298        assert_eq!(count, 2);
299        assert_eq!(value_sum, 0x3000);
300    }
301
302    #[test]
303    fn malformed_suffix_does_not_return_partially_folded_symbol_state() {
304        let bytes = macho_with_symbols(99);
305        let macho = parse_fixture(&bytes);
306        let mut folder_calls = 0usize;
307
308        let result = fold_symbols(&macho, 0usize, |count, _symbol| {
309            folder_calls += 1;
310            *count += 1;
311            Ok(())
312        });
313
314        assert!(result.is_err());
315        assert_eq!(folder_calls, 1);
316        assert!(parse_symbol_table(&macho).is_err());
317    }
318}