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