Skip to main content

rustc_codegen_ssa/back/
symbol_edit.rs

1//! Binary-level symbol editing for staticlib post-processing.
2//!
3//! - **Hide**: sets STV_HIDDEN (ELF) or N_PEXT (Mach-O) on non-exported symbols.
4//! - **Rename**: appends a vendor-specific suffix to non-exported symbol names by
5//!   rebuilding the string table.
6
7use std::borrow::Cow;
8use std::mem;
9
10use object::read::coff::{CoffHeader, ImageSymbol as _};
11use object::read::elf::{SectionHeader as _, Sym as _};
12use object::read::macho::Nlist;
13use object::{Endianness, elf, macho, pe};
14use rustc_data_structures::fx::{FxHashMap, FxHashSet};
15use rustc_middle::middle::exported_symbols::SymbolExportKind;
16
17use super::symbol_export::undecorate_coff_symbol;
18
19struct Patch {
20    offset: usize,
21    value: u8,
22}
23
24struct RenameEntry {
25    name_field_offset: usize,
26    name: String,
27}
28
29pub(super) fn apply_edits<'a>(
30    data: &'a [u8],
31    exported: &FxHashSet<String>,
32    hide: bool,
33    rename: Option<&(FxHashSet<String>, &str)>,
34) -> Cow<'a, [u8]> {
35    let result = match object::File::parse(data).ok() {
36        Some(object::File::Elf64(_)) => elf_edit_impl::<elf::FileHeader64<Endianness>>(
37            data,
38            exported,
39            hide,
40            rename,
41            const { builtin # offset_of(elf::Sym64<Endianness>, st_other) }mem::offset_of!(elf::Sym64<Endianness>, st_other),
42        ),
43        Some(object::File::Elf32(_)) => elf_edit_impl::<elf::FileHeader32<Endianness>>(
44            data,
45            exported,
46            hide,
47            rename,
48            const { builtin # offset_of(elf::Sym32<Endianness>, st_other) }mem::offset_of!(elf::Sym32<Endianness>, st_other),
49        ),
50        Some(object::File::MachO64(_)) => macho_edit_impl::<macho::MachHeader64<Endianness>>(
51            data,
52            exported,
53            hide,
54            rename,
55            const { builtin # offset_of(macho::Nlist64<Endianness>, n_type) }mem::offset_of!(macho::Nlist64<Endianness>, n_type),
56        ),
57        Some(object::File::MachO32(_)) => macho_edit_impl::<macho::MachHeader32<Endianness>>(
58            data,
59            exported,
60            hide,
61            rename,
62            const { builtin # offset_of(macho::Nlist32<Endianness>, n_type) }mem::offset_of!(macho::Nlist32<Endianness>, n_type),
63        ),
64        Some(object::File::Coff(f)) => coff_edit_impl(data, f.coff_header(), hide, rename),
65        Some(object::File::CoffBig(f)) => coff_edit_impl(data, f.coff_header(), hide, rename),
66        _ => None,
67    };
68    match result {
69        Some(v) => Cow::Owned(v),
70        None => Cow::Borrowed(data),
71    }
72}
73
74pub(super) fn collect_internal_names(
75    data: &[u8],
76    exported: &FxHashSet<String>,
77    out: &mut FxHashSet<String>,
78) {
79    let Ok(file) = object::File::parse(data) else { return };
80    match file {
81        object::File::Elf64(_) => {
82            elf_collect_impl::<elf::FileHeader64<Endianness>>(data, exported, out)
83        }
84        object::File::Elf32(_) => {
85            elf_collect_impl::<elf::FileHeader32<Endianness>>(data, exported, out)
86        }
87        object::File::MachO64(_) => {
88            macho_collect_impl::<macho::MachHeader64<Endianness>>(data, exported, out)
89        }
90        object::File::MachO32(_) => {
91            macho_collect_impl::<macho::MachHeader32<Endianness>>(data, exported, out)
92        }
93        object::File::Coff(f) => coff_collect_impl(data, f.coff_header(), exported, out),
94        object::File::CoffBig(f) => coff_collect_impl(data, f.coff_header(), exported, out),
95        _ => {}
96    }
97}
98
99/// The `SymbolExportKind` of a COFF symbol, so `#`-prefixed `Arm64EC` text symbols are told
100/// apart from data symbols during undecoration.
101fn coff_symbol_kind(sym: &impl object::read::coff::ImageSymbol) -> SymbolExportKind {
102    if sym.derived_type() == pe::IMAGE_SYM_DTYPE_FUNCTION {
103        SymbolExportKind::Text
104    } else {
105        SymbolExportKind::Data
106    }
107}
108
109fn elf_collect_impl<Elf: object::read::elf::FileHeader<Endian = Endianness>>(
110    data: &[u8],
111    exported: &FxHashSet<String>,
112    out: &mut FxHashSet<String>,
113) where
114    u64: From<Elf::Word>,
115{
116    let Ok(header) = Elf::parse(data) else { return };
117    let Ok(endian) = header.endian() else { return };
118    let Ok(sections) = header.sections(endian, data) else { return };
119    let Ok(symtab) = sections.symbols(endian, data, elf::SHT_SYMTAB) else { return };
120    let strings = symtab.strings();
121
122    for sym in symtab.iter() {
123        let binding = sym.st_bind();
124        if binding != elf::STB_GLOBAL && binding != elf::STB_WEAK {
125            continue;
126        }
127        if sym.is_undefined(endian) {
128            continue;
129        }
130        let Ok(name_bytes) = sym.name(endian, strings) else { continue };
131        let Ok(name) = str::from_utf8(name_bytes) else { continue };
132        if !exported.contains(name) {
133            out.insert(name.to_string());
134        }
135    }
136}
137
138fn macho_collect_impl<Mach: object::read::macho::MachHeader<Endian = Endianness>>(
139    data: &[u8],
140    exported: &FxHashSet<String>,
141    out: &mut FxHashSet<String>,
142) {
143    let Ok(header) = Mach::parse(data, 0) else { return };
144    let Ok(endian) = header.endian() else { return };
145    let Ok(mut commands) = header.load_commands(endian, data, 0) else { return };
146
147    let symtab_cmd = loop {
148        let Ok(Some(cmd)) = commands.next() else { return };
149        if let Ok(Some(st)) = cmd.symtab() {
150            break st;
151        }
152    };
153    let Ok(symtab) = symtab_cmd.symbols::<Mach, _>(endian, data) else { return };
154    let strings = symtab.strings();
155
156    for nlist in symtab.iter() {
157        if nlist.is_stab() {
158            continue;
159        }
160        if nlist.is_undefined() {
161            continue;
162        }
163        if nlist.n_type() & macho::N_EXT == 0 {
164            continue;
165        }
166        let Ok(name_bytes) = nlist.name(endian, strings) else { continue };
167        let Ok(name) = str::from_utf8(name_bytes) else { continue };
168        let name = name.strip_prefix('_').unwrap_or(name);
169        if !exported.contains(name) {
170            out.insert(name.to_string());
171        }
172    }
173}
174
175// ---------------------------------------------------------------------------
176// ELF: single-pass collection + apply
177// ---------------------------------------------------------------------------
178
179fn elf_edit_impl<Elf: object::read::elf::FileHeader<Endian = Endianness>>(
180    data: &[u8],
181    exported: &FxHashSet<String>,
182    hide: bool,
183    rename: Option<&(FxHashSet<String>, &str)>,
184    st_other_offset: usize,
185) -> Option<Vec<u8>>
186where
187    u64: From<Elf::Word>,
188{
189    let header = Elf::parse(data).ok()?;
190    let endian = header.endian().ok()?;
191    let sections = header.sections(endian, data).ok()?;
192    let symtab = sections.symbols(endian, data, elf::SHT_SYMTAB).ok()?;
193    let data_ptr = data.as_ptr() as usize;
194    let strings = symtab.strings();
195
196    let mut patches = Vec::new();
197    let mut renames = Vec::new();
198
199    for sym in symtab.iter() {
200        let binding = sym.st_bind();
201        if binding != elf::STB_GLOBAL && binding != elf::STB_WEAK {
202            continue;
203        }
204        let Ok(name_bytes) = sym.name(endian, strings) else { continue };
205        let Ok(name) = str::from_utf8(name_bytes) else { continue };
206
207        let sym_addr = sym as *const Elf::Sym as usize;
208
209        if hide && !sym.is_undefined(endian) && !exported.contains(name) {
210            let offset = sym_addr - data_ptr + st_other_offset;
211            let new_vis = (sym.st_other() & !0x03) | elf::STV_HIDDEN;
212            patches.push(Patch { offset, value: new_vis });
213        }
214        if let Some((rename_set, _)) = rename {
215            if rename_set.contains(name) {
216                renames.push(RenameEntry {
217                    name_field_offset: sym_addr - data_ptr,
218                    name: name.to_string(),
219                });
220            }
221        }
222    }
223
224    if patches.is_empty() && renames.is_empty() {
225        return None;
226    }
227
228    let mut result = data.to_vec();
229    for p in &patches {
230        result[p.offset] = p.value;
231    }
232
233    if !renames.is_empty() {
234        let suffix = rename.unwrap().1;
235        if let Some(renamed) =
236            elf_rebuild_strtab::<Elf>(&result, &renames, suffix, &sections, header, endian)
237        {
238            result = renamed;
239        }
240    }
241
242    Some(result)
243}
244
245fn elf_rebuild_strtab<Elf: object::read::elf::FileHeader<Endian = Endianness>>(
246    data: &[u8],
247    renames: &[RenameEntry],
248    suffix: &str,
249    sections: &object::read::elf::SectionTable<'_, Elf>,
250    header: &Elf,
251    endian: Endianness,
252) -> Option<Vec<u8>>
253where
254    u64: From<Elf::Word>,
255{
256    let mut strtab_si: Option<usize> = None;
257    for section in sections.iter() {
258        if section.sh_type(endian) == elf::SHT_SYMTAB {
259            strtab_si = Some(section.sh_link(endian) as usize);
260            break;
261        }
262    }
263    let strtab_si = strtab_si?;
264
265    let e_shoff = u64::from(header.e_shoff(endian)) as usize;
266    let e_shentsize = mem::size_of::<Elf::SectionHeader>();
267    let e_shnum = sections.len();
268
269    let strtab_section = sections.section(object::SectionIndex(strtab_si)).ok()?;
270    let old_strtab_offset = u64::from(strtab_section.sh_offset(endian)) as usize;
271    let old_strtab_size = u64::from(strtab_section.sh_size(endian)) as usize;
272    let old_strtab = data.get(old_strtab_offset..old_strtab_offset + old_strtab_size)?;
273
274    let (new_strtab, rename_map) = build_renamed_strtab(old_strtab, renames, suffix);
275
276    let is_64 = mem::size_of::<Elf::Word>() == 8;
277    let new_strtab_file_off = data.len();
278    let new_strtab_size = new_strtab.len();
279    let new_e_shoff_raw = new_strtab_file_off + new_strtab_size;
280    let new_e_shoff = (new_e_shoff_raw + 7) & !7;
281    let padding = new_e_shoff - new_e_shoff_raw;
282    let section_headers_size = e_shentsize * e_shnum;
283
284    let result_size = new_e_shoff + section_headers_size;
285    let mut result = Vec::with_capacity(result_size);
286    result.extend_from_slice(data);
287    result.extend_from_slice(&new_strtab);
288    result.resize(result.len() + padding, 0);
289    let sh_data = data.get(e_shoff..e_shoff + section_headers_size)?;
290    result.extend_from_slice(sh_data);
291
292    if is_64 {
293        write_u64_at(
294            &mut result,
295            const { builtin # offset_of(elf::FileHeader64<Endianness>, e_shoff) }mem::offset_of!(elf::FileHeader64<Endianness>, e_shoff),
296            new_e_shoff as u64,
297            endian,
298        );
299    } else {
300        write_u32_at(
301            &mut result,
302            const { builtin # offset_of(elf::FileHeader32<Endianness>, e_shoff) }mem::offset_of!(elf::FileHeader32<Endianness>, e_shoff),
303            new_e_shoff as u32,
304            endian,
305        );
306    }
307
308    let new_strtab_shdr_offset = new_e_shoff + strtab_si * e_shentsize;
309
310    if is_64 {
311        let sh_offset_field = const { builtin # offset_of(elf::SectionHeader64<Endianness>, sh_offset) }mem::offset_of!(elf::SectionHeader64<Endianness>, sh_offset);
312        let sh_size_field = const { builtin # offset_of(elf::SectionHeader64<Endianness>, sh_size) }mem::offset_of!(elf::SectionHeader64<Endianness>, sh_size);
313        write_u64_at(
314            &mut result,
315            new_strtab_shdr_offset + sh_offset_field,
316            new_strtab_file_off as u64,
317            endian,
318        );
319        write_u64_at(
320            &mut result,
321            new_strtab_shdr_offset + sh_size_field,
322            new_strtab_size as u64,
323            endian,
324        );
325    } else {
326        let sh_offset_field = const { builtin # offset_of(elf::SectionHeader32<Endianness>, sh_offset) }mem::offset_of!(elf::SectionHeader32<Endianness>, sh_offset);
327        let sh_size_field = const { builtin # offset_of(elf::SectionHeader32<Endianness>, sh_size) }mem::offset_of!(elf::SectionHeader32<Endianness>, sh_size);
328        write_u32_at(
329            &mut result,
330            new_strtab_shdr_offset + sh_offset_field,
331            new_strtab_file_off as u32,
332            endian,
333        );
334        write_u32_at(
335            &mut result,
336            new_strtab_shdr_offset + sh_size_field,
337            new_strtab_size as u32,
338            endian,
339        );
340    }
341
342    for entry in renames {
343        let new_st_name = rename_map[&entry.name];
344        write_u32_at(&mut result, entry.name_field_offset, new_st_name, endian);
345    }
346
347    Some(result)
348}
349
350// ---------------------------------------------------------------------------
351// Mach-O: single-pass collection + apply
352// ---------------------------------------------------------------------------
353
354fn macho_edit_impl<Mach: object::read::macho::MachHeader<Endian = Endianness>>(
355    data: &[u8],
356    exported: &FxHashSet<String>,
357    hide: bool,
358    rename: Option<&(FxHashSet<String>, &str)>,
359    n_type_offset: usize,
360) -> Option<Vec<u8>> {
361    let header = Mach::parse(data, 0).ok()?;
362    let endian = header.endian().ok()?;
363    let mut commands = header.load_commands(endian, data, 0).ok()?;
364
365    let (symtab_cmd, symtab_cmd_offset) = loop {
366        let cmd = commands.next().ok()??;
367        if let Some(st) = cmd.symtab().ok().flatten() {
368            break (st, cmd.raw_data().as_ptr() as usize - data.as_ptr() as usize);
369        }
370    };
371
372    let symtab: object::read::macho::SymbolTable<'_, Mach, &_> =
373        symtab_cmd.symbols(endian, data).ok()?;
374    let data_ptr = data.as_ptr() as usize;
375    let strings = symtab.strings();
376
377    let mut patches = Vec::new();
378    let mut renames = Vec::new();
379
380    for nlist in symtab.iter() {
381        if nlist.is_stab() {
382            continue;
383        }
384        if nlist.n_type() & macho::N_EXT == 0 {
385            continue;
386        }
387        let Ok(name_bytes) = nlist.name(endian, strings) else { continue };
388        let Ok(raw_name) = str::from_utf8(name_bytes) else { continue };
389        let name = raw_name.strip_prefix('_').unwrap_or(raw_name);
390
391        let nlist_addr = nlist as *const Mach::Nlist as usize;
392
393        if hide && !nlist.is_undefined() && !exported.contains(name) {
394            let offset = nlist_addr - data_ptr + n_type_offset;
395            patches.push(Patch { offset, value: nlist.n_type() | macho::N_PEXT });
396        }
397        if let Some((rename_set, _)) = rename {
398            if rename_set.contains(name) {
399                renames.push(RenameEntry {
400                    name_field_offset: nlist_addr - data_ptr,
401                    name: raw_name.to_string(),
402                });
403            }
404        }
405    }
406
407    if patches.is_empty() && renames.is_empty() {
408        return None;
409    }
410
411    let mut result = data.to_vec();
412    for p in &patches {
413        result[p.offset] = p.value;
414    }
415
416    if !renames.is_empty() {
417        let suffix = rename.unwrap().1;
418        if let Some(renamed) =
419            macho_rebuild_strtab(&result, &renames, suffix, &symtab_cmd, symtab_cmd_offset, endian)
420        {
421            result = renamed;
422        }
423    }
424
425    Some(result)
426}
427
428fn macho_rebuild_strtab(
429    data: &[u8],
430    renames: &[RenameEntry],
431    suffix: &str,
432    symtab_cmd: &macho::SymtabCommand<Endianness>,
433    symtab_cmd_offset: usize,
434    endian: Endianness,
435) -> Option<Vec<u8>> {
436    let old_stroff = symtab_cmd.stroff.get(endian) as usize;
437    let old_strsize = symtab_cmd.strsize.get(endian) as usize;
438    let old_strtab = data.get(old_stroff..old_stroff + old_strsize)?;
439
440    let (new_strtab, rename_map) = build_renamed_strtab(old_strtab, renames, suffix);
441
442    let new_strtab_file_off = data.len();
443    let new_strtab_size = new_strtab.len();
444
445    let mut result = Vec::with_capacity(data.len() + new_strtab_size);
446    result.extend_from_slice(data);
447    result.extend_from_slice(&new_strtab);
448
449    let stroff_off = const { builtin # offset_of(macho::SymtabCommand<Endianness>, stroff) }mem::offset_of!(macho::SymtabCommand<Endianness>, stroff);
450    let strsize_off = const { builtin # offset_of(macho::SymtabCommand<Endianness>, strsize) }mem::offset_of!(macho::SymtabCommand<Endianness>, strsize);
451    write_u32_at(&mut result, symtab_cmd_offset + stroff_off, new_strtab_file_off as u32, endian);
452    write_u32_at(&mut result, symtab_cmd_offset + strsize_off, new_strtab_size as u32, endian);
453
454    for entry in renames {
455        let new_strx = rename_map[&entry.name];
456        write_u32_at(&mut result, entry.name_field_offset, new_strx, endian);
457    }
458
459    Some(result)
460}
461
462// ---------------------------------------------------------------------------
463// COFF: single-pass collection + apply
464// ---------------------------------------------------------------------------
465
466fn coff_collect_impl<'data, Coff: CoffHeader>(
467    data: &'data [u8],
468    header: &'data Coff,
469    exported: &FxHashSet<String>,
470    out: &mut FxHashSet<String>,
471) {
472    let Ok(symbols) = header.symbols(data) else { return };
473    let strings = symbols.strings();
474    let machine = header.machine();
475
476    for (_index, sym) in symbols.iter() {
477        let sclass = sym.storage_class();
478        if sclass != pe::IMAGE_SYM_CLASS_EXTERNAL && sclass != pe::IMAGE_SYM_CLASS_WEAK_EXTERNAL {
479            continue;
480        }
481        if sym.section_number() <= 0 {
482            continue;
483        }
484        let Ok(name_bytes) = sym.name(strings) else { continue };
485        let Ok(name) = str::from_utf8(name_bytes) else { continue };
486        let export_name = undecorate_coff_symbol(name, machine, coff_symbol_kind(sym));
487        if !exported.contains(export_name) {
488            out.insert(name.to_string());
489        }
490    }
491}
492
493fn coff_edit_impl<'data, Coff: CoffHeader>(
494    data: &'data [u8],
495    header: &'data Coff,
496    hide: bool,
497    rename: Option<&(FxHashSet<String>, &str)>,
498) -> Option<Vec<u8>> {
499    // COFF has no visibility concept, so hiding is unsupported; the caller
500    // has already warned and does not request it.
501    if !!hide { ::core::panicking::panic("assertion failed: !hide") };assert!(!hide);
502
503    let pointer_to_symbol_table = header.pointer_to_symbol_table() as usize;
504    let number_of_symbols = header.number_of_symbols() as usize;
505    // ImageSymbol is 18 bytes; ImageSymbolEx (bigobj) is 20.
506    let sym_size = mem::size_of::<Coff::ImageSymbolBytes>();
507
508    let symbol_bytes_offset = pointer_to_symbol_table;
509    let strtab_base = pointer_to_symbol_table + number_of_symbols * sym_size;
510    if strtab_base > data.len() {
511        return None;
512    }
513
514    if data.len() < strtab_base + 4 {
515        return None;
516    }
517    let old_len =
518        u32::from_le_bytes(data[strtab_base..strtab_base + 4].try_into().unwrap()) as usize;
519    if strtab_base + old_len > data.len() {
520        return None;
521    }
522
523    let Ok(symbols) = header.symbols(data) else { return None };
524    let strings = symbols.strings();
525
526    let mut renames = Vec::new();
527    for (index, sym) in symbols.iter() {
528        let sclass = sym.storage_class();
529        if sclass != pe::IMAGE_SYM_CLASS_EXTERNAL && sclass != pe::IMAGE_SYM_CLASS_WEAK_EXTERNAL {
530            continue;
531        }
532        let Ok(name_bytes) = sym.name(strings) else { continue };
533        let Ok(name) = str::from_utf8(name_bytes) else { continue };
534        if rename.is_some_and(|(rename_set, _)| rename_set.contains(name)) {
535            renames.push(RenameEntry {
536                name_field_offset: symbol_bytes_offset + index.0 * sym_size,
537                name: name.to_string(),
538            });
539        }
540    }
541    if renames.is_empty() {
542        return None;
543    }
544    let suffix = rename.unwrap().1;
545
546    let mut new_strtab = Vec::new();
547    let mut map: FxHashMap<String, u32> = FxHashMap::default();
548    let mut sorted_names: Vec<&str> = renames.iter().map(|r| r.name.as_str()).collect();
549    sorted_names.sort();
550    sorted_names.dedup();
551    for name in &sorted_names {
552        let rel_offset = (old_len + new_strtab.len()) as u32;
553        new_strtab.extend_from_slice(name.as_bytes());
554        new_strtab.extend_from_slice(suffix.as_bytes());
555        new_strtab.push(0);
556        map.insert(name.to_string(), rel_offset);
557    }
558
559    let mut result = data.to_vec();
560    let new_len = (old_len + new_strtab.len()) as u32;
561    result[strtab_base..strtab_base + 4].copy_from_slice(&new_len.to_le_bytes());
562    result.extend_from_slice(&new_strtab);
563
564    // Long names are stored as all-zero name[0..4] plus the string table
565    // offset in name[4..8].
566    for r in &renames {
567        let rel = map[&r.name];
568        let field = &mut result[r.name_field_offset..r.name_field_offset + 8];
569        field[..4].fill(0);
570        field[4..].copy_from_slice(&rel.to_le_bytes());
571    }
572
573    Some(result)
574}
575
576// ---------------------------------------------------------------------------
577// Shared helpers
578// ---------------------------------------------------------------------------
579
580fn build_renamed_strtab(
581    old_strtab: &[u8],
582    renames: &[RenameEntry],
583    suffix: &str,
584) -> (Vec<u8>, FxHashMap<String, u32>) {
585    let mut new_strtab = old_strtab.to_vec();
586    let mut rename_map: FxHashMap<String, u32> = FxHashMap::default();
587
588    let mut sorted_names: Vec<&str> = renames.iter().map(|r| r.name.as_str()).collect();
589    sorted_names.sort();
590    sorted_names.dedup();
591
592    for name in &sorted_names {
593        let new_offset = new_strtab.len() as u32;
594        new_strtab.extend_from_slice(name.as_bytes());
595        new_strtab.extend_from_slice(suffix.as_bytes());
596        new_strtab.push(0);
597        rename_map.insert(name.to_string(), new_offset);
598    }
599
600    (new_strtab, rename_map)
601}
602
603fn write_u32_at(buf: &mut [u8], offset: usize, value: u32, endian: Endianness) {
604    let bytes = match endian {
605        Endianness::Little => value.to_le_bytes(),
606        Endianness::Big => value.to_be_bytes(),
607    };
608    buf[offset..offset + 4].copy_from_slice(&bytes);
609}
610
611fn write_u64_at(buf: &mut [u8], offset: usize, value: u64, endian: Endianness) {
612    let bytes = match endian {
613        Endianness::Little => value.to_le_bytes(),
614        Endianness::Big => value.to_be_bytes(),
615    };
616    buf[offset..offset + 8].copy_from_slice(&bytes);
617}