Skip to main content

libc_abi_tools/
provider.rs

1//! glibc link providers: everything needed to link x86_64 Linux programs for one
2//! glibc release, prepared on the build machine.
3//!
4//! A provider holds:
5//! - the release's headers;
6//! - its startup objects (`crt1.o`, `crti.o`, `crtn.o`) and nonshared archives;
7//! - link-only stubs of its shared libraries, with exactly that release's symbols
8//!   and versions, generated from the ABI lists;
9//! - a `libgcc_s` stub with GCC 4.8.5's interface.
10//!
11//! It supplies no glibc runtime: programs run against the target system's glibc.
12//!
13//! # Delivery modes
14//!
15//! [`cache::Mode`] selects how the glibc objects are obtained. Every mode produces
16//! the same layout and the same stubs:
17//!
18//! - **`source`** (default): compiles the recorded sources of the
19//!   `glibc-provider-sources` crate with the host's GCC, offline, and verifies every
20//!   object against the interface recorded from the qualified container build
21//!   ([`build`]). A difference that was not reviewed refuses the provider. It needs
22//!   the `provider-sources` feature (on by default). Without the feature, source
23//!   mode returns an error that names `container`.
24//! - **`container`**: runs the pinned container build that CI runs ([`pins`]),
25//!   with podman or docker and network access.
26//! - **`prebuilt`**: CI-built objects. Planned, after a legal review. Selecting it
27//!   returns an error today.
28//!
29//! # Cache and descriptor
30//!
31//! [`prepare::prepare`] builds a provider into the cache ([`cache`]), or reuses one
32//! whose inputs hash the same. Each provider has a descriptor, `provider.txt`: a
33//! small text file in the `libc-abi-provider-v1` format that names its directories.
34//! [`Provider`] parses and checks it. The descriptor is a trusted local build
35//! configuration, not a signed or redistributable SDK. [`cli`] implements the
36//! `provider` commands shared by `libc-abi` and `cargo libc`.
37//!
38//! The design is in
39//! [PROVIDER-DELIVERY.md](https://github.com/rustcommons/linux-abi-tools/blob/main/docs/PROVIDER-DELIVERY.md),
40//! and the user guide is
41//! [USING-PROVIDERS.md](https://github.com/rustcommons/linux-abi-tools/blob/main/docs/USING-PROVIDERS.md).
42//!
43//! # Example
44//!
45//! ```
46//! use libc_abi_tools::provider::Provider;
47//!
48//! let root = if cfg!(windows) { "C:/p" } else { "/p" };
49//! let text = format!(
50//!     "libc-abi-provider-v1\ntarget\tx86_64-unknown-linux-gnu\nglibc\t2.17\n\
51//!      sysroot\t{root}/sysroot\nstubs\t{root}/stubs\ngcc-startup\t{root}/gcc\n\
52//!      compiler-runtime\t{root}/compiler-runtime\n"
53//! );
54//! let provider = Provider::parse(&text).unwrap();
55//! assert_eq!(provider.release.to_string(), "2.17");
56//! // glibc 2.17 still has a separate libpthread_nonshared.a.
57//! assert_eq!(provider.nonshared_archives().len(), 2);
58//! // Selection is exact: a 2.17 provider never serves 2.22.
59//! assert!(provider.validate_selection("2.22".parse().unwrap()).is_err());
60//! ```
61pub mod build;
62pub mod cache;
63pub mod cli;
64pub mod json;
65pub mod pins;
66pub mod prepare;
67
68use crate::version::Version;
69use std::{
70    collections::BTreeMap,
71    fs,
72    io::Read,
73    path::{Path, PathBuf},
74};
75
76/// The first line of a version-1 provider descriptor.
77pub const FORMAT: &str = "libc-abi-provider-v1";
78/// glibc releases with a qualified provider; selection is exact, without fallback.
79pub const RELEASES: &[&str] = &[
80    "2.17", "2.18", "2.19", "2.20", "2.21", "2.22", "2.23", "2.24", "2.25", "2.26", "2.27", "2.28",
81    "2.29", "2.30", "2.31", "2.32", "2.33", "2.34", "2.35", "2.36", "2.37", "2.38", "2.39", "2.40",
82    "2.41", "2.42", "2.43", "2.44",
83];
84/// The shared libraries a provider has link stubs for, by soname. The stubs
85/// directory also has each one's linker name (`libc.so`, ..., `libld.so` for the
86/// loader) resolving to the same file.
87pub const SONAMES: [&str; 8] = [
88    "libc.so.6",
89    "libm.so.6",
90    "ld-linux-x86-64.so.2",
91    "libresolv.so.2",
92    "libpthread.so.0",
93    "libdl.so.2",
94    "librt.so.1",
95    "libutil.so.1",
96];
97
98/// A parsed provider descriptor. Its paths are absolute and have no `..`
99/// components; [`Provider::validate_files`] checks what they contain.
100#[derive(Clone, Debug)]
101pub struct Provider {
102    /// The glibc release (`glibc` field), one of [`RELEASES`].
103    pub release: Version,
104    /// The release's headers, startup objects, nonshared archives and loader
105    /// (`sysroot` field).
106    pub sysroot: PathBuf,
107    /// The link stubs of [`SONAMES`] and their linker names (`stubs` field).
108    pub stubs: PathBuf,
109    /// The GCC directory with `crtbegin.o` and `crtend.o` (`gcc-startup` field).
110    pub gcc_startup: PathBuf,
111    /// The `libgcc_s` stub, `libgcc_s.so` and `libgcc_s.so.1`
112    /// (`compiler-runtime` field).
113    pub compiler_runtime: PathBuf,
114    /// The descriptor text exactly as read.
115    pub descriptor: String,
116}
117
118impl Provider {
119    /// Reads and parses the descriptor at PATH (see [`Provider::parse`]).
120    ///
121    /// # Errors
122    ///
123    /// When the file cannot be read, or [`Provider::parse`] rejects it.
124    pub fn load(path: &Path) -> Result<Self, String> {
125        let mut text = String::new();
126        fs::File::open(path)
127            .and_then(|f| f.take(16385).read_to_string(&mut text))
128            .map_err(|e| format!("cannot read provider {}: {e}", path.display()))?;
129        Self::parse(&text)
130    }
131
132    /// Parses a version-1 descriptor: the [`FORMAT`] line, then exactly the six
133    /// tab-separated fields `target`, `glibc`, `sysroot`, `stubs`, `gcc-startup`
134    /// and `compiler-runtime`, in any order. It checks only the text; see
135    /// [`Provider::validate_files`] for the files.
136    ///
137    /// # Errors
138    ///
139    /// When the text is over 16 KiB, has CR characters or no final newline, has
140    /// another format line, has an unknown, duplicate, empty or control-character
141    /// field, lacks a field, names a target other than `x86_64-unknown-linux-gnu`
142    /// or a glibc outside [`RELEASES`], or has a path that is relative or contains
143    /// `..`.
144    pub fn parse(text: &str) -> Result<Self, String> {
145        if text.len() > 16384 || text.contains('\r') || !text.ends_with('\n') {
146            return Err("provider must be LF-terminated UTF-8, at most 16 KiB".into());
147        }
148        let mut lines = text.lines();
149        if lines.next() != Some(FORMAT) {
150            return Err("unsupported provider format".into());
151        }
152        let mut fields = BTreeMap::new();
153        for line in lines {
154            let (key, value) = line.split_once('\t').ok_or("invalid provider field")?;
155            if ![
156                "target",
157                "glibc",
158                "sysroot",
159                "stubs",
160                "gcc-startup",
161                "compiler-runtime",
162            ]
163            .contains(&key)
164                || value.is_empty()
165                || value.chars().any(char::is_control)
166                || fields.insert(key, value).is_some()
167            {
168                return Err("unknown, duplicate, empty, or invalid provider field".into());
169            }
170        }
171        if fields.len() != 6
172            || fields.get("target") != Some(&"x86_64-unknown-linux-gnu")
173            || !fields.get("glibc").is_some_and(|v| RELEASES.contains(v))
174        {
175            return Err(format!(
176                "provider v1 requires exactly six fields, x86_64 Linux and glibc {}",
177                RELEASES.join(", ")
178            ));
179        }
180        let path = |key| {
181            let path = PathBuf::from(fields[key]);
182            if !path.is_absolute()
183                || path
184                    .components()
185                    .any(|c| matches!(c, std::path::Component::ParentDir))
186            {
187                return Err(format!(
188                    "provider {key} must be an absolute path without parent components"
189                ));
190            }
191            Ok(path)
192        };
193        Ok(Self {
194            release: fields["glibc"].parse()?,
195            sysroot: path("sysroot")?,
196            stubs: path("stubs")?,
197            gcc_startup: path("gcc-startup")?,
198            compiler_runtime: path("compiler-runtime")?,
199            descriptor: text.into(),
200        })
201    }
202
203    /// Static archives linked into every executable, relative to the sysroot.
204    /// glibc 2.28 merged libpthread_nonshared.a into libc_nonshared.a.
205    pub fn nonshared_archives(&self) -> &'static [&'static str] {
206        let merged = Version {
207            major: 2,
208            minor: 28,
209            patch: 0,
210        };
211        if self.release < merged {
212            &[
213                "usr/lib64/libc_nonshared.a",
214                "usr/lib64/libpthread_nonshared.a",
215            ]
216        } else {
217            &["usr/lib64/libc_nonshared.a"]
218        }
219    }
220
221    /// Checks that the provider is for exactly the REQUESTED glibc release.
222    ///
223    /// # Errors
224    ///
225    /// When REQUESTED differs from [`Provider::release`]; there is no fallback to
226    /// another release.
227    pub fn validate_selection(&self, requested: Version) -> Result<(), String> {
228        if requested != self.release {
229            return Err(format!(
230                "provider targets glibc {}, but --glibc requested {requested}",
231                self.release
232            ));
233        }
234        Ok(())
235    }
236
237    /// Checks the provider's files: each directory exists; the headers
238    /// (`features.h`), startup objects, loader and [nonshared
239    /// archives](Provider::nonshared_archives) are in the sysroot; `crtbegin.o` and
240    /// `crtend.o` are in [`Provider::gcc_startup`]; the `libgcc_s` stub is in
241    /// [`Provider::compiler_runtime`]; and every stub and linker name is in
242    /// [`Provider::stubs`]. Each must resolve, after following symbolic links, to a
243    /// regular file inside its directory, and each linker name to the same file as
244    /// its soname. It does not check file contents.
245    ///
246    /// # Errors
247    ///
248    /// When a directory or file is missing, resolves outside its directory, or a
249    /// linker name resolves to another file than its soname.
250    pub fn validate_files(&self) -> Result<(), String> {
251        for root in [
252            &self.sysroot,
253            &self.stubs,
254            &self.gcc_startup,
255            &self.compiler_runtime,
256        ] {
257            if !root.is_dir() {
258                return Err(format!("provider directory missing: {}", root.display()));
259            }
260        }
261        for (root, names) in [
262            (
263                &self.sysroot,
264                &[
265                    "usr/include/features.h",
266                    "usr/lib64/crt1.o",
267                    "usr/lib64/crti.o",
268                    "usr/lib64/crtn.o",
269                    "lib64/ld-linux-x86-64.so.2",
270                ][..],
271            ),
272            (&self.sysroot, self.nonshared_archives()),
273            (&self.gcc_startup, &["crtbegin.o", "crtend.o"][..]),
274            (
275                &self.compiler_runtime,
276                &["libgcc_s.so", "libgcc_s.so.1"][..],
277            ),
278            (&self.stubs, &SONAMES[..]),
279            (
280                &self.stubs,
281                &[
282                    "libc.so",
283                    "libm.so",
284                    "libld.so",
285                    "libresolv.so",
286                    "libpthread.so",
287                    "libdl.so",
288                    "librt.so",
289                    "libutil.so",
290                ][..],
291            ),
292        ] {
293            let canonical = fs::canonicalize(root).map_err(|e| e.to_string())?;
294            for name in names {
295                let file = fs::canonicalize(root.join(name))
296                    .map_err(|e| format!("missing provider input {name}: {e}"))?;
297                if !file.starts_with(&canonical) || !file.is_file() {
298                    return Err(format!(
299                        "provider input {name} must resolve to a file inside {}",
300                        root.display()
301                    ));
302                }
303            }
304        }
305        for (linker_name, soname) in [
306            "libc.so",
307            "libm.so",
308            "libld.so",
309            "libresolv.so",
310            "libpthread.so",
311            "libdl.so",
312            "librt.so",
313            "libutil.so",
314        ]
315        .iter()
316        .zip(SONAMES)
317        {
318            if fs::canonicalize(self.stubs.join(linker_name)).map_err(|e| e.to_string())?
319                != fs::canonicalize(self.stubs.join(soname)).map_err(|e| e.to_string())?
320            {
321                return Err(format!(
322                    "provider linker name {linker_name} must resolve to {soname}"
323                ));
324            }
325        }
326        Ok(())
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333    fn valid() -> String {
334        let root = if cfg!(windows) { "C:/sdk" } else { "/sdk" };
335        format!("{FORMAT}\ntarget\tx86_64-unknown-linux-gnu\nglibc\t2.17\nsysroot\t{root}\nstubs\t{root}/stubs\ngcc-startup\t{root}/gcc\ncompiler-runtime\t{root}/runtime\n")
336    }
337    #[test]
338    fn rejects_wrong_versions_and_ambiguous_descriptors() {
339        let text = valid();
340        assert!(Provider::parse(&text).is_ok());
341        for bad in [
342            text.replace("2.17", "2.16"),
343            text.replace(FORMAT, "libc-abi-provider-v2"),
344            format!("{text}glibc\t2.17\n"),
345            text.replace("stubs\t", "unknown\t"),
346            text.replace("/stubs", "/../stubs"),
347            text.replace('\n', "\r\n"),
348            text.trim_end().into(),
349            "x".repeat(16385),
350        ] {
351            assert!(Provider::parse(&bad).is_err());
352        }
353        assert!(Provider::parse(&text).unwrap().validate_files().is_err());
354    }
355
356    #[test]
357    fn nonshared_archives_follow_the_release() {
358        for (release, count) in [("2.17", 2), ("2.27", 2), ("2.28", 1), ("2.44", 1)] {
359            let provider = Provider::parse(&valid().replace("2.17", release)).unwrap();
360            let archives = provider.nonshared_archives();
361            assert_eq!(archives.len(), count);
362            assert_eq!(archives[0], "usr/lib64/libc_nonshared.a");
363        }
364    }
365
366    #[test]
367    fn selects_each_supported_release_without_fallback() {
368        for &release in RELEASES {
369            let provider = Provider::parse(&valid().replace("2.17", release)).unwrap();
370            assert_eq!(provider.release, release.parse().unwrap());
371            assert!(provider
372                .validate_selection(release.parse().unwrap())
373                .is_ok());
374            for other in RELEASES.iter().copied().chain(["2.16", "2.22.1", "2.45"]) {
375                if other != release {
376                    assert!(provider.validate_selection(other.parse().unwrap()).is_err());
377                }
378            }
379        }
380        assert!(Provider::parse(&valid())
381            .unwrap()
382            .validate_selection("2.22".parse().unwrap())
383            .is_err());
384        assert!(Provider::parse(&valid().replace("2.17", "2.22"))
385            .unwrap()
386            .validate_selection("2.17".parse().unwrap())
387            .is_err());
388        assert!(Provider::parse(&valid().replace("2.17", "2.22.0")).is_err());
389    }
390}