Skip to main content

libc_abi_tools/
stub.rs

1//! Experimental evidence-driven x86_64 link-only assembly, separate from collection.
2//! No code, initialized data, IFUNC implementations, or TLS initialization is copied.
3use crate::{
4    elf,
5    glibc::{Kind, Snapshot},
6    version::Version,
7};
8use std::{
9    collections::{BTreeMap, BTreeSet},
10    fmt::Write as _,
11};
12
13/// The inputs for assembling and linking one link-only stub library.
14#[derive(Debug, Eq, PartialEq)]
15pub struct Sources {
16    /// GNU assembler source defining every selected symbol; function bodies trap.
17    pub assembly: String,
18    /// The linker version script giving each symbol its version node.
19    pub versions: String,
20    /// The definitions the stub reproduces, sorted.
21    pub selected: elf::Metadata,
22}
23
24/// Join an exact snapshot with all public numeric GLIBC definitions in one DSO.
25/// Reject mismatched membership/kind/size; private and unversioned definitions
26/// are intentionally outside the abilist contract.
27pub fn render_glibc(
28    snapshot: &Snapshot,
29    library: &str,
30    reference: &elf::Metadata,
31) -> Result<Sources, String> {
32    snapshot.release.validate_release()?;
33    let soname = glibc_soname(library).ok_or("unsupported glibc library identity")?;
34    if reference.soname != soname {
35        return Err(format!("expected SONAME {soname}"));
36    }
37    let expected = snapshot
38        .libraries
39        .get(library)
40        .ok_or("missing library ABI metadata")?;
41    let public = elf::Metadata {
42        soname: reference.soname.clone(),
43        symbols: reference
44            .symbols
45            .iter()
46            .filter(|s| {
47                matches!(s.visibility, 0 | 3)
48                    && s.version
49                        .as_deref()
50                        .and_then(|v| v.strip_prefix("GLIBC_"))
51                        .is_some_and(|v| v.parse::<Version>().is_ok())
52            })
53            .cloned()
54            .collect(),
55    };
56    if public.symbols.len() != expected.len() {
57        return Err("ELF/snapshot public version membership differs".into());
58    }
59    let mut actual = BTreeMap::new();
60    for s in &public.symbols {
61        if actual
62            .insert((s.name.as_str(), s.version.as_deref()), s)
63            .is_some()
64        {
65            return Err("duplicate ELF public definition".into());
66        }
67    }
68    let mut expected_keys = BTreeSet::new();
69    for entry in expected {
70        if !expected_keys.insert((entry.name.as_str(), entry.version)) {
71            return Err("duplicate ABI definition".into());
72        }
73        let version = format!("GLIBC_{}", entry.version);
74        let s = actual
75            .get(&(entry.name.as_str(), Some(version.as_str())))
76            .ok_or_else(|| format!("{}@{version}: missing ELF definition", entry.name))?;
77        let matches = match entry.kind {
78            Kind::Function => matches!(s.symbol_type, 2 | 10),
79            Kind::Object(size) => s.symbol_type == 1 && s.size == u64::from(size),
80            Kind::Tls(size) => s.symbol_type == 6 && s.size == u64::from(size),
81        };
82        if !matches {
83            return Err(format!(
84                "{}@{version}: ELF/snapshot kind or size differs",
85                entry.name
86            ));
87        }
88    }
89    let names: BTreeSet<_> = expected.iter().map(|s| s.name.as_str()).collect();
90    render(&public, &names.into_iter().collect::<Vec<_>>())
91}
92
93/// The SONAME of each glibc library, in [`crate::glibc::LIBRARIES`]' names.
94pub fn glibc_soname(library: &str) -> Option<&'static str> {
95    Some(match library {
96        "c" => "libc.so.6",
97        "m" => "libm.so.6",
98        "ld" => "ld-linux-x86-64.so.2",
99        "resolv" => "libresolv.so.2",
100        "pthread" => "libpthread.so.0",
101        "dl" => "libdl.so.2",
102        "rt" => "librt.so.1",
103        "util" => "libutil.so.1",
104        _ => return None,
105    })
106}
107
108/// The stub evidence of all eight libraries of a release, from a directory with
109/// the real libraries (an installed `lib64`), as one [`elf::Metadata::to_tsv`]
110/// table: exactly the public definitions [`render_glibc`] uses, each library
111/// checked against the snapshot first. [`glibc_from_table`] renders stubs from it
112/// without the libraries.
113pub fn glibc_table(snapshot: &Snapshot, lib64: &std::path::Path) -> Result<String, String> {
114    let mut table = String::new();
115    for library in crate::glibc::LIBRARIES {
116        let soname = glibc_soname(library).expect("known library");
117        let reference = elf::Metadata::load(&lib64.join(soname))?;
118        let text = render_glibc(snapshot, library, &reference)
119            .map_err(|e| format!("{soname}: {e}"))?
120            .selected
121            .to_tsv();
122        let rows = if table.is_empty() {
123            &text[..]
124        } else {
125            text.split_once('\n').map_or("", |(_, rows)| rows)
126        };
127        table.push_str(rows);
128    }
129    Ok(table)
130}
131
132/// Stubs for all eight libraries from a [`glibc_table`], by library name.
133pub fn glibc_from_table(
134    snapshot: &Snapshot,
135    table: &str,
136) -> Result<Vec<(&'static str, Sources)>, String> {
137    let libraries = elf::Metadata::from_tsv(table)?;
138    if libraries.len() != crate::glibc::LIBRARIES.len() {
139        return Err("the library table must have exactly the eight glibc libraries".into());
140    }
141    crate::glibc::LIBRARIES
142        .iter()
143        .map(|&library| {
144            let soname = glibc_soname(library).expect("known library");
145            let reference = libraries
146                .iter()
147                .find(|m| m.soname == soname)
148                .ok_or_else(|| format!("the library table lacks {soname}"))?;
149            let sources =
150                render_glibc(snapshot, library, reference).map_err(|e| format!("{soname}: {e}"))?;
151            Ok((library, sources))
152        })
153        .collect()
154}
155
156fn identifier(name: &str) -> bool {
157    let mut bytes = name.bytes();
158    bytes
159        .next()
160        .is_some_and(|b| b.is_ascii_alphabetic() || b == b'_')
161        && bytes.all(|b| b.is_ascii_alphanumeric() || b == b'_')
162        && !name.starts_with("__abi_stub_")
163        && name.len() <= 1024
164}
165
166struct DataRange {
167    start: u64,
168    end: u64,
169    alignment: u64,
170    section: u16,
171    kind: u8,
172}
173
174fn data_layout(
175    symbols: &[elf::Symbol],
176) -> Result<(Vec<DataRange>, BTreeMap<usize, usize>), String> {
177    let mut by_section = BTreeMap::<(u16, u8), Vec<usize>>::new();
178    for (index, s) in symbols
179        .iter()
180        .enumerate()
181        .filter(|(_, s)| matches!(s.symbol_type, 1 | 6))
182    {
183        let alignment = s.section_alignment.max(1);
184        if alignment > 4096 || !alignment.is_power_of_two() {
185            return Err("unsupported data/TLS section alignment".into());
186        }
187        if s.size == 0 || s.size > 16 * 1024 * 1024 {
188            return Err("unsupported zero or oversized data/TLS symbol".into());
189        }
190        s.value
191            .checked_add(s.size)
192            .ok_or("ELF data range overflow")?;
193        by_section
194            .entry((s.section, s.symbol_type))
195            .or_default()
196            .push(index);
197    }
198    let mut groups: Vec<DataRange> = Vec::new();
199    let mut membership = BTreeMap::new();
200    for ((section, kind), mut indexes) in by_section {
201        indexes.sort_by_key(|&index| (symbols[index].value, symbols[index].size));
202        let mut active: Option<usize> = None;
203        for index in indexes {
204            let s = &symbols[index];
205            let end = s.value + s.size; // checked above
206            let group = match active {
207                Some(group) if s.value < groups[group].end => {
208                    groups[group].end = groups[group].end.max(end);
209                    groups[group].alignment =
210                        groups[group].alignment.max(s.section_alignment.max(1));
211                    group
212                }
213                _ => {
214                    let group = groups.len();
215                    groups.push(DataRange {
216                        start: s.value,
217                        end,
218                        alignment: s.section_alignment.max(1),
219                        section,
220                        kind,
221                    });
222                    active = Some(group);
223                    group
224                }
225            };
226            if groups[group].end - groups[group].start > 16 * 1024 * 1024 {
227                return Err("data/TLS backing range exceeds 16 MiB".into());
228            }
229            membership.insert(index, group);
230        }
231    }
232    Ok((groups, membership))
233}
234
235/// Emit every observed version of each requested name. GNU version parents are
236/// not reconstructed. Matching physical ranges share storage without asserting
237/// a semantic alias relationship. Overlapping data/TLS views retain relative
238/// offsets in a common backing range; incompatible/code overlaps are rejected.
239pub fn render(reference: &elf::Metadata, names: &[&str]) -> Result<Sources, String> {
240    render_versions(reference, names, "GLIBC_", false)
241}
242
243/// [`render`] for any numeric version family, such as libgcc_s's `GCC_3.0`.
244/// Each version keeps its observed spelling (`GCC_4.0.0`); GLIBC versions must
245/// be canonical. With `chained`, each version node inherits the previous one,
246/// as GCC's version nodes do.
247pub fn render_versions(
248    reference: &elf::Metadata,
249    names: &[&str],
250    prefix: &str,
251    chained: bool,
252) -> Result<Sources, String> {
253    if names.is_empty() || names.len() > 10000 || reference.symbols.len() > 100000 {
254        return Err("stub requires 1..10000 names and at most 100000 ELF definitions".into());
255    }
256    let requested: BTreeSet<_> = names.iter().copied().collect();
257    if requested.len() != names.len() || requested.iter().any(|name| !identifier(name)) {
258        return Err("stub names must be unique C identifiers outside the reserved prefix".into());
259    }
260    let mut selected = elf::Metadata {
261        soname: reference.soname.clone(),
262        symbols: reference
263            .symbols
264            .iter()
265            .filter(|symbol| requested.contains(symbol.name.as_str()))
266            .cloned()
267            .collect(),
268    };
269    selected.symbols.sort();
270    let observed: BTreeSet<_> = selected.symbols.iter().map(|s| s.name.as_str()).collect();
271    if observed != requested {
272        return Err("requested stub symbol is absent from ELF evidence".into());
273    }
274    let mut keys = BTreeSet::new();
275    let mut defaults = BTreeSet::new();
276    let mut nodes = BTreeMap::<Version, BTreeSet<String>>::new();
277    let mut spellings = BTreeMap::<Version, String>::new();
278    let mut assembly = String::from("# Experimental link-only library; function bodies trap.\n");
279    let mut storage = BTreeMap::new();
280    let mut ranges = BTreeMap::<u16, BTreeMap<u64, u64>>::new();
281    let mut allocated = 0u64;
282    let (data_ranges, data_membership) = data_layout(&selected.symbols)?;
283    let mut emitted_data = BTreeSet::new();
284    for (index, symbol) in selected.symbols.iter().enumerate() {
285        if !matches!(symbol.binding, 1 | 2)
286            || !matches!(symbol.visibility, 0 | 3)
287            || !matches!(symbol.symbol_type, 1 | 2 | 6 | 10)
288            || symbol.section == 0
289            || symbol.section >= 0xff00
290        {
291            return Err(format!(
292                "{}: unsupported binding, visibility, type, or section",
293                symbol.name
294            ));
295        }
296        let alignment = symbol.section_alignment.max(1);
297        if alignment > 4096 || !alignment.is_power_of_two() {
298            return Err(format!("{}: unsupported alignment", symbol.name));
299        }
300        let version = symbol
301            .version
302            .as_deref()
303            .ok_or("unversioned stub definitions unsupported")?;
304        let numeric: Version = version
305            .strip_prefix(prefix)
306            .ok_or_else(|| format!("stub requires numeric {prefix} versions"))?
307            .parse()?;
308        // Reject spellings that cannot be reproduced by the numeric version type.
309        if prefix == "GLIBC_" && format!("GLIBC_{numeric}") != version {
310            return Err("noncanonical GLIBC version".into());
311        }
312        if *spellings
313            .entry(numeric)
314            .or_insert_with(|| version.to_string())
315            != version
316        {
317            return Err(format!("{version}: one version spelled two ways"));
318        }
319        let default = symbol
320            .default_version
321            .ok_or("missing default-version evidence")?;
322        if !keys.insert((&symbol.name, version)) || (default && !defaults.insert(&symbol.name)) {
323            return Err("duplicate definition or multiple default stub versions".into());
324        }
325        nodes
326            .entry(numeric)
327            .or_default()
328            .insert(symbol.name.clone());
329        let key = (
330            symbol.section,
331            symbol.value,
332            symbol.symbol_type,
333            symbol.size,
334        );
335        let emitted_size = if matches!(symbol.symbol_type, 2 | 10) {
336            2
337        } else {
338            symbol.size
339        };
340        if emitted_size == 0 || emitted_size > 16 * 1024 * 1024 {
341            return Err(format!(
342                "{}: unsupported zero or oversized storage",
343                symbol.name
344            ));
345        }
346        let old_storage = if symbol.size == 0 {
347            None
348        } else {
349            storage.get(&key).copied()
350        };
351        let location = if let Some(&group) = data_membership.get(&index) {
352            let range = &data_ranges[group];
353            if emitted_data.insert(group) {
354                let bytes = range.end - range.start;
355                let location = ranges.entry(range.section).or_default();
356                let overlaps_before = location
357                    .range(..=range.start)
358                    .next_back()
359                    .is_some_and(|(_, &end)| end > range.start);
360                let overlaps_after = location
361                    .range(range.start..)
362                    .next()
363                    .is_some_and(|(&start, _)| start < range.end);
364                if overlaps_before || overlaps_after {
365                    return Err("incompatible overlapping storage types".into());
366                }
367                location.insert(range.start, range.end);
368                allocated = allocated
369                    .checked_add(bytes + range.alignment - 1)
370                    .ok_or("stub allocation overflow")?;
371                if allocated > 64 * 1024 * 1024 {
372                    return Err("stub storage budget exceeds 64 MiB".into());
373                }
374                let section = if range.kind == 6 {
375                    ".section .tbss,\"awT\",@nobits"
376                } else {
377                    ".section .bss,\"aw\",@nobits"
378                };
379                writeln!(
380                    assembly,
381                    "{section}\n.balign {}\n.Ldata{group}:\n  .zero {bytes}",
382                    range.alignment
383                )
384                .expect("String write");
385            }
386            format!(".Ldata{group}+{}", symbol.value - range.start)
387        } else {
388            let slot = if let Some(slot) = old_storage {
389                slot
390            } else {
391                // Check original ranges, excluding identical ranges handled above.
392                let end = symbol
393                    .value
394                    .checked_add(symbol.size)
395                    .ok_or("ELF symbol range overflow")?;
396                let location = ranges.entry(symbol.section).or_default();
397                let overlaps_before = location
398                    .range(..=symbol.value)
399                    .next_back()
400                    .is_some_and(|(_, &old_end)| old_end > symbol.value);
401                let overlaps_after = location
402                    .range(symbol.value..)
403                    .next()
404                    .is_some_and(|(&start, _)| start < end);
405                if symbol.size != 0 && (overlaps_before || overlaps_after) {
406                    return Err(format!(
407                    "{}@{version}: partially overlapping symbol ranges unsupported at {}:{:#x}+{}",
408                    symbol.name, symbol.section, symbol.value, symbol.size
409                ));
410                }
411                if symbol.size != 0 {
412                    location.insert(symbol.value, end);
413                }
414                allocated = allocated
415                    .checked_add(emitted_size + alignment - 1)
416                    .ok_or("stub allocation overflow")?;
417                if allocated > 64 * 1024 * 1024 {
418                    return Err("stub storage budget exceeds 64 MiB".into());
419                }
420                let slot = index;
421                if symbol.size != 0 {
422                    storage.insert(key, slot);
423                }
424                let section = match symbol.symbol_type {
425                    1 => ".section .bss,\"aw\",@nobits",
426                    6 => ".section .tbss,\"awT\",@nobits",
427                    _ => ".text",
428                };
429                writeln!(assembly, "{section}\n.balign {alignment}\n.Lstorage{slot}:")
430                    .expect("String write");
431                if matches!(symbol.symbol_type, 2 | 10) {
432                    assembly.push_str("  ud2\n");
433                } else {
434                    writeln!(assembly, "  .zero {emitted_size}").expect("String write");
435                }
436                slot
437            };
438            format!(".Lstorage{slot}")
439        };
440        let private = format!("__abi_stub_{index}");
441        let binding = if symbol.binding == 2 {
442            ".weak"
443        } else {
444            ".globl"
445        };
446        let kind = match symbol.symbol_type {
447            1 => "object",
448            6 => "tls_object",
449            10 => "gnu_indirect_function",
450            _ => "function",
451        };
452        writeln!(assembly, "{binding} {private}\n.set {private},{location}\n.type {private},@{kind}\n.size {private},{emitted_size}").expect("String write");
453        if symbol.visibility == 3 {
454            writeln!(assembly, ".protected {private}").expect("String write");
455        }
456        let separator = if default { "@@" } else { "@" };
457        writeln!(
458            assembly,
459            ".symver {private},{}{separator}{version}",
460            symbol.name
461        )
462        .expect("String write");
463    }
464    assembly.push_str(".section .note.GNU-stack,\"\",@progbits\n");
465    let mut versions = String::new();
466    let mut previous: Option<&str> = None;
467    for (index, (version, names)) in nodes.iter().enumerate() {
468        let spelled = &spellings[version];
469        writeln!(versions, "{spelled} {{\n  global:").expect("String write");
470        for name in names {
471            writeln!(versions, "    {name};").expect("String write");
472        }
473        if index == 0 {
474            versions.push_str("  local: *;\n");
475        }
476        match previous {
477            Some(parent) if chained => writeln!(versions, "}} {parent};").expect("String write"),
478            _ => versions.push_str("};\n"),
479        }
480        previous = Some(spelled);
481    }
482    Ok(Sources {
483        assembly,
484        versions,
485        selected,
486    })
487}