Skip to main content

libc_abi_tools/
glibc.rs

1//! glibc abilist parsing and collection, derived from Zig's glibc tools.
2//! Copyright (c) Zig contributors; Copyright (c) 2026 rustcommons contributors.
3//! SPDX-License-Identifier: MIT
4
5use crate::version::Version;
6use std::{
7    collections::BTreeMap,
8    fmt::Write as _,
9    fs,
10    io::Read,
11    path::{Path, PathBuf},
12    process::Command,
13};
14
15/// The only target triple this module's ABI lists describe.
16pub const TARGET: &str = "x86_64-unknown-linux-gnu";
17/// Same library selection as the pinned upstream glibc consolidator.
18pub const LIBRARIES: [&str; 8] = ["m", "c", "ld", "resolv", "pthread", "dl", "rt", "util"];
19const ABI_PATH: &str = "sysdeps/unix/sysv/linux/x86_64/64";
20const MAX_FILE_BYTES: u64 = 10 * 1024 * 1024;
21
22/// What an ABI list entry defines.
23#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
24pub enum Kind {
25    /// A function (`F` in an ABI list).
26    Function,
27    /// A data object (`D`) of the given size in bytes.
28    Object(u16),
29    /// A thread-local variable (`T`) of the given size in bytes.
30    Tls(u16),
31}
32
33/// One versioned definition from a glibc ABI list.
34#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
35pub struct Symbol {
36    /// The symbol name.
37    pub name: String,
38    /// The numeric part of its `GLIBC_*` version node, such as 2.2.5.
39    pub version: Version,
40    /// Whether it is a function, object or TLS variable, with the size of data.
41    pub kind: Kind,
42}
43
44/// Read both pre-2.23 grouped lists and modern per-line versioned lists.
45/// Absolute version-node markers (`A`) and GCC_* entries are not glibc symbols.
46/// The result is sorted by name and version.
47///
48/// # Errors
49///
50/// Fails, naming the line, on an unknown version namespace or symbol kind, a
51/// malformed name or size, or the same name and version listed with two kinds.
52pub fn parse_abilist(text: &str) -> Result<Vec<Symbol>, String> {
53    let mut group: Option<&str> = None;
54    let mut records = BTreeMap::new();
55    for (index, line) in text.lines().enumerate() {
56        let fail = |message: &str| format!("line {}: {message}", index + 1);
57        let fields: Vec<_> = line.split_ascii_whitespace().collect();
58        if fields.is_empty() {
59            continue;
60        }
61        let indented = line.starts_with([' ', '\t']);
62        if !indented && fields.len() == 1 {
63            validate_namespace(fields[0]).map_err(|e| fail(&e))?;
64            group = Some(fields[0]);
65            continue;
66        }
67        let (namespace, values) = if indented {
68            (
69                group.ok_or_else(|| fail("symbol before a version group"))?,
70                &fields[..],
71            )
72        } else {
73            // A modern row cannot silently inherit a previous grouped version.
74            group = None;
75            (fields[0], &fields[1..])
76        };
77        validate_namespace(namespace).map_err(|e| fail(&e))?;
78        if values.len() < 2 {
79            return Err(fail("expected a symbol name and kind"));
80        }
81        let name = values[0];
82        if !name.bytes().all(|b| b.is_ascii_graphic()) {
83            return Err(fail(
84                "symbol names must contain printable ASCII without spaces",
85            ));
86        }
87        let size = || -> Result<u16, String> {
88            if values.len() != 3 {
89                return Err(fail("data/TLS symbols require exactly one size"));
90            }
91            let raw = values[2];
92            let (digits, radix) = raw.strip_prefix("0x").map_or((raw, 10), |s| (s, 16));
93            if digits.is_empty()
94                || !digits.bytes().all(|b| {
95                    if radix == 16 {
96                        b.is_ascii_hexdigit()
97                    } else {
98                        b.is_ascii_digit()
99                    }
100                })
101            {
102                return Err(fail("invalid symbol size"));
103            }
104            u16::from_str_radix(digits, radix).map_err(|_| fail("symbol size exceeds u16"))
105        };
106        let kind = match values[1] {
107            "F" if values.len() == 2 => Kind::Function,
108            "D" => Kind::Object(size()?),
109            "T" => Kind::Tls(size()?),
110            "A" if values.len() == 2 => continue,
111            _ => return Err(fail("unsupported symbol kind or extra fields")),
112        };
113        if namespace.starts_with("GCC_") {
114            continue;
115        }
116        let version: Version = namespace[6..].parse().map_err(|e: String| fail(&e))?;
117        let key = (name.to_owned(), version);
118        if let Some(previous) = records.insert(key, kind) {
119            if previous != kind {
120                return Err(fail("conflicting duplicate symbol/version"));
121            }
122        }
123    }
124    Ok(records
125        .into_iter()
126        .map(|((name, version), kind)| Symbol {
127            name,
128            version,
129            kind,
130        })
131        .collect())
132}
133
134fn validate_namespace(text: &str) -> Result<(), String> {
135    let numeric = text
136        .strip_prefix("GLIBC_")
137        .or_else(|| text.strip_prefix("GCC_"))
138        .ok_or_else(|| format!("unsupported version namespace {text:?}"))?;
139    numeric.parse::<Version>().map(|_| ())
140}
141
142/// The public ABI of one exact glibc release: every tracked library's list.
143#[derive(Clone, Debug, Eq, PartialEq)]
144pub struct Snapshot {
145    /// The glibc release, such as 2.17.
146    pub release: Version,
147    /// Each library in [`LIBRARIES`] by short name (`c`, `m`, ...), with its
148    /// definitions sorted by name and version.
149    pub libraries: BTreeMap<String, Vec<Symbol>>,
150}
151
152impl Snapshot {
153    /// Load one exact release from a data directory laid out like
154    /// glibc-abilists' `data/`: `<root>/<release>/<path in glibc's tree>`.
155    /// Missing libraries fail instead of silently producing an incomplete ABI.
156    ///
157    /// # Errors
158    ///
159    /// Fails if `release` is not a glibc release from 2.17, or a library's list
160    /// is missing, too large or malformed.
161    pub fn load(root: &Path, release: Version) -> Result<Self, String> {
162        release.validate_release()?;
163        let release_dir = root.join(release.to_string());
164        let (snapshot, _) = load_source(&release_dir, release)?;
165        Ok(snapshot)
166    }
167
168    /// Load one exact release from the glibc ABI lists compiled into the
169    /// `glibc-abilists` crate (the default `bundled-glibc-data` feature).
170    ///
171    /// # Errors
172    ///
173    /// Fails if `release` is not one of [`bundled_releases`] or its data is
174    /// malformed.
175    #[cfg(feature = "bundled-glibc-data")]
176    pub fn bundled(release: Version) -> Result<Self, String> {
177        release.validate_release()?;
178        let (snapshot, _) = load_with(release, |relative| bundled_file(release, relative))?;
179        Ok(snapshot)
180    }
181
182    /// Stable TSV ordered by library, symbol name, and numeric symbol version.
183    /// A query with no matches returns the header alone.
184    pub fn to_tsv(&self, symbol: Option<&str>) -> String {
185        let mut out = String::from("release\ttarget\tlibrary\tsymbol\tversion\tkind\tsize\n");
186        for (library, symbols) in &self.libraries {
187            for entry in symbols {
188                if symbol.is_some_and(|name| name != entry.name) {
189                    continue;
190                }
191                let (kind, size) = match entry.kind {
192                    Kind::Function => ("F", String::from("-")),
193                    Kind::Object(size) => ("D", size.to_string()),
194                    Kind::Tls(size) => ("T", size.to_string()),
195                };
196                writeln!(
197                    out,
198                    "{}\t{TARGET}\t{library}\t{}\tGLIBC_{}\t{kind}\t{size}",
199                    self.release, entry.name, entry.version
200                )
201                .unwrap();
202            }
203        }
204        out
205    }
206}
207
208fn read_inside(root: &Path, relative: &Path) -> Result<Vec<u8>, String> {
209    let path = root.join(relative);
210    let actual = path
211        .canonicalize()
212        .map_err(|e| format!("{}: {e}", path.display()))?;
213    if !actual.starts_with(root) {
214        return Err(format!(
215            "{} resolves outside the source tree",
216            path.display()
217        ));
218    }
219    if !actual.is_file() {
220        return Err(format!("{} is not a regular file", path.display()));
221    }
222    let mut bytes = Vec::new();
223    fs::File::open(&actual)
224        .map_err(|e| e.to_string())?
225        .take(MAX_FILE_BYTES + 1)
226        .read_to_end(&mut bytes)
227        .map_err(|e| e.to_string())?;
228    if bytes.len() as u64 > MAX_FILE_BYTES {
229        return Err(format!("{} exceeds the 10 MiB input limit", path.display()));
230    }
231    Ok(bytes)
232}
233
234type SourceFiles = Vec<(PathBuf, Vec<u8>)>;
235
236fn load_source(source: &Path, release: Version) -> Result<(Snapshot, SourceFiles), String> {
237    let source = source
238        .canonicalize()
239        .map_err(|e| format!("{}: {e}", source.display()))?;
240    load_with(release, |relative| read_inside(&source, relative))
241}
242
243/// The path of a library's ABI list in glibc's tree (and in a data
244/// directory's release subdirectory).
245pub fn abilist_path(release: Version, library: &str) -> PathBuf {
246    let mut path = PathBuf::from(ABI_PATH);
247    if release.minor <= 19 {
248        path.push("nptl");
249    }
250    path.join(if library == "ld" {
251        "ld.abilist".to_owned()
252    } else {
253        format!("lib{library}.abilist")
254    })
255}
256
257fn load_with(
258    release: Version,
259    read: impl Fn(&Path) -> Result<Vec<u8>, String>,
260) -> Result<(Snapshot, SourceFiles), String> {
261    let mut libraries = BTreeMap::new();
262    let mut files = Vec::new();
263    for library in LIBRARIES {
264        let relative = abilist_path(release, library);
265        let bytes = read(&relative)?;
266        let text =
267            std::str::from_utf8(&bytes).map_err(|e| format!("{}: {e}", relative.display()))?;
268        let records = parse_abilist(text).map_err(|e| format!("{}: {e}", relative.display()))?;
269        if records.iter().any(|record| record.version > release) {
270            return Err(format!(
271                "{} contains a symbol newer than release {release}",
272                relative.display()
273            ));
274        }
275        libraries.insert(library.to_owned(), records);
276        files.push((relative, bytes));
277    }
278    Ok((Snapshot { release, libraries }, files))
279}
280
281fn git(source: &Path, args: &[&str]) -> Result<String, String> {
282    let output = Command::new("git")
283        .arg("-C")
284        .arg(source)
285        .args(args)
286        .output()
287        .map_err(|e| format!("cannot run git: {e}"))?;
288    if !output.status.success() {
289        return Err(format!(
290            "git {} failed: {}",
291            args.join(" "),
292            String::from_utf8_lossy(&output.stderr).trim()
293        ));
294    }
295    String::from_utf8(output.stdout)
296        .map(|s| s.trim().to_owned())
297        .map_err(|e| e.to_string())
298}
299
300/// The bundled file at `relative` for `release`.
301#[cfg(feature = "bundled-glibc-data")]
302fn bundled_file(release: Version, relative: &Path) -> Result<Vec<u8>, String> {
303    let version = release.to_string();
304    let path = relative.to_string_lossy().replace('\\', "/");
305    glibc_abilists::abilists()
306        .iter()
307        .find(|a| a.release == version && a.path == path)
308        .map(|a| a.text.as_bytes().to_vec())
309        .ok_or_else(|| format!("glibc-abilists has no {path} for {release}"))
310}
311
312/// Releases available in the bundled glibc-abilists data.
313#[cfg(feature = "bundled-glibc-data")]
314pub fn bundled_releases() -> Result<Vec<Version>, String> {
315    glibc_abilists::releases()
316        .iter()
317        .map(|r| r.version.parse())
318        .collect()
319}
320
321/// Writes the bundled ABI lists into `output` in the data-directory layout
322/// that [`Snapshot::load`] reads, for tools that need files on disk. The
323/// directory must not exist yet. Returns the number of files written.
324#[cfg(feature = "bundled-glibc-data")]
325pub fn export_bundled(output: &Path) -> Result<usize, String> {
326    fs::create_dir(output)
327        .map_err(|e| format!("cannot create new directory {}: {e}", output.display()))?;
328    let mut count = 0;
329    for a in glibc_abilists::abilists() {
330        let path = output.join(a.release).join(a.path);
331        if let Some(parent) = path.parent() {
332            fs::create_dir_all(parent).map_err(|e| e.to_string())?;
333        }
334        fs::write(&path, a.text).map_err(|e| format!("{}: {e}", path.display()))?;
335        count += 1;
336    }
337    let mut provenance = String::from(
338        "# ABI lists exported from the glibc-abilists crate; see its data/releases.tsv\n\
339         # and data/files.tsv for each tag's commit, signature, and file hashes.\n\
340         # License: LGPL-2.1-or-later (the GNU C Library's).\n\
341         release\ttag\tcommit\n",
342    );
343    for r in glibc_abilists::releases() {
344        writeln!(provenance, "{}\t{}\t{}", r.version, r.tag, r.commit).unwrap();
345    }
346    fs::write(output.join("SOURCE.tsv"), provenance).map_err(|e| e.to_string())?;
347    // The LGPL travels with every redistributed copy of the lists.
348    fs::write(output.join("COPYING.LIB"), glibc_abilists::LICENSE_TEXT)
349        .map_err(|e| e.to_string())?;
350    Ok(count)
351}
352
353/// Collect the eight x86_64 ABI lists from a clean, exactly tagged glibc checkout.
354/// The new release directory must not already exist. All inputs are read and
355/// parsed before creating output; no download or build is performed.
356pub fn collect(source: &Path, output: &Path) -> Result<PathBuf, String> {
357    let tag = git(
358        source,
359        &["describe", "--tags", "--exact-match", "--match", "glibc-*"],
360    )?;
361    let release: Version = tag
362        .strip_prefix("glibc-")
363        .ok_or("expected an official glibc release tag")?
364        .parse()?;
365    release.validate_release()?;
366    if tag != format!("glibc-{release}") {
367        return Err("expected an exact glibc-MAJOR.MINOR release tag".into());
368    }
369    if !git(source, &["status", "--porcelain", "--untracked-files=all"])?.is_empty() {
370        return Err("source checkout must be clean before collection".into());
371    }
372    let commit = git(source, &["rev-parse", "HEAD"])?;
373    let (_, mut files) = load_source(source, release)?;
374    let source = source.canonicalize().map_err(|e| e.to_string())?;
375    // Preserve the source tree's own license text alongside the collected data.
376    for name in ["COPYING", "COPYING.LIB"] {
377        files.push((PathBuf::from(name), read_inside(&source, Path::new(name))?));
378    }
379    let destination = output.join(release.to_string());
380    fs::create_dir_all(output).map_err(|e| e.to_string())?;
381    fs::create_dir(&destination)
382        .map_err(|e| format!("cannot create new release {}: {e}", destination.display()))?;
383    for (relative, bytes) in files {
384        let path = destination.join(relative);
385        if let Some(parent) = path.parent() {
386            fs::create_dir_all(parent).map_err(|e| e.to_string())?;
387        }
388        fs::write(&path, bytes).map_err(|e| format!("{}: {e}", path.display()))?;
389    }
390    fs::write(
391        destination.join("SOURCE.txt"),
392        format!("tag={tag}\ncommit={commit}\ntarget={TARGET}\n"),
393    )
394    .map_err(|e| e.to_string())?;
395    Ok(destination)
396}