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