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}