Skip to main content

libc_abi_tools/provider/
cache.rs

1//! Prepared providers on disk:
2//!
3//! ```text
4//! ROOT/x86_64-unknown-linux-gnu/glibc-2.28/<mode>-<content hash>/
5//!     provider.txt  sysroot/  stubs/  compiler-runtime/  manifest.json
6//! ```
7//!
8//! ROOT is `$CARGO_LIBC_PROVIDER_DIR`, else `$CARGO_HOME/libc-providers`, else
9//! `~/.cargo/libc-providers`. The content hash covers every input of the
10//! preparation, so changed inputs never reuse a stale provider. `manifest.json`
11//! records the mode, origin, tools, and every file's SHA-256 and size.
12use super::{json::Value, Provider, FORMAT, RELEASES, SONAMES};
13use crate::{elf, glibc::Snapshot, libgcc, sha256, stub, version::Version};
14use std::{
15    collections::BTreeSet,
16    env, fmt, fs,
17    path::{Path, PathBuf},
18};
19
20/// The target triple every provider is for.
21pub const TARGET: &str = "x86_64-unknown-linux-gnu";
22/// The `format` value of a version-1 `manifest.json`.
23pub const MANIFEST_FORMAT: &str = "libc-abi-provider-manifest-1";
24
25/// How a provider's glibc objects are obtained.
26#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
27pub enum Mode {
28    /// Compiled locally with the host's GCC from glibc-provider-sources (default).
29    Source,
30    /// The CI-built objects of glibc-provider-binaries (after legal review).
31    Prebuilt,
32    /// Built locally by the pinned container build.
33    Container,
34}
35
36impl Mode {
37    /// Every mode.
38    pub const ALL: [Mode; 3] = [Mode::Source, Mode::Prebuilt, Mode::Container];
39
40    /// The mode's name as used on command lines and in directory names:
41    /// `source`, `prebuilt` or `container`.
42    pub fn name(self) -> &'static str {
43        match self {
44            Mode::Source => "source",
45            Mode::Prebuilt => "prebuilt",
46            Mode::Container => "container",
47        }
48    }
49}
50
51impl fmt::Display for Mode {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        f.write_str(self.name())
54    }
55}
56
57impl std::str::FromStr for Mode {
58    type Err = String;
59    fn from_str(text: &str) -> Result<Self, String> {
60        Mode::ALL
61            .into_iter()
62            .find(|m| m.name() == text)
63            .ok_or_else(|| {
64                format!("unknown provider mode {text:?}; use source, prebuilt or container")
65            })
66    }
67}
68
69/// The default cache root (see the module documentation).
70pub fn default_root() -> Result<PathBuf, String> {
71    let root = if let Some(dir) = env::var_os("CARGO_LIBC_PROVIDER_DIR") {
72        PathBuf::from(dir)
73    } else if let Some(home) = env::var_os("CARGO_HOME") {
74        PathBuf::from(home).join("libc-providers")
75    } else if let Some(home) = env::var_os("HOME") {
76        PathBuf::from(home).join(".cargo/libc-providers")
77    } else {
78        return Err(
79            "set CARGO_LIBC_PROVIDER_DIR, CARGO_HOME or HOME for the provider cache".into(),
80        );
81    };
82    if !root.is_absolute() {
83        return Err(format!(
84            "the provider cache {} must be an absolute path",
85            root.display()
86        ));
87    }
88    Ok(root)
89}
90
91/// Checks a release name against the supported list.
92pub fn release(name: &str) -> Result<&'static str, String> {
93    RELEASES
94        .iter()
95        .copied()
96        .find(|r| *r == name)
97        .ok_or_else(|| {
98            format!(
99                "glibc {name} has no provider; supported: {}",
100                RELEASES.join(", ")
101            )
102        })
103}
104
105/// One prepared provider.
106#[derive(Clone, Debug, PartialEq, Eq)]
107pub struct Entry {
108    /// The glibc release, such as `2.28`.
109    pub release: String,
110    /// How its glibc objects were obtained.
111    pub mode: Mode,
112    /// The content hash of its preparation inputs, in hexadecimal.
113    pub hash: String,
114    /// Its directory: `ROOT/x86_64-unknown-linux-gnu/glibc-RELEASE/MODE-HASH`.
115    pub dir: PathBuf,
116}
117
118impl Entry {
119    /// The path of its descriptor, `provider.txt` (see [`super::Provider`]). The
120    /// file need not exist.
121    pub fn descriptor(&self) -> PathBuf {
122        self.dir.join("provider.txt")
123    }
124
125    /// Reads and parses its `manifest.json`.
126    ///
127    /// # Errors
128    ///
129    /// When the file cannot be read or is not JSON this module's parser accepts.
130    pub fn manifest(&self) -> Result<Value, String> {
131        let path = self.dir.join("manifest.json");
132        let text = fs::read_to_string(&path).map_err(|e| format!("{}: {e}", path.display()))?;
133        super::json::parse(&text).map_err(|e| format!("{}: {e}", path.display()))
134    }
135}
136
137/// The directory of RELEASE's providers.
138pub fn release_dir(root: &Path, release: &str) -> PathBuf {
139    root.join(TARGET).join(format!("glibc-{release}"))
140}
141
142/// Where a provider with this mode and content hash lives.
143pub fn entry(root: &Path, release: &str, mode: Mode, hash: &str) -> Entry {
144    Entry {
145        release: release.into(),
146        mode,
147        hash: hash.into(),
148        dir: release_dir(root, release).join(format!("{mode}-{hash}")),
149    }
150}
151
152/// Every prepared provider under ROOT, by release, mode and hash.
153pub fn list(root: &Path) -> Result<Vec<Entry>, String> {
154    let mut entries = Vec::new();
155    let target = root.join(TARGET);
156    if !target.is_dir() {
157        return Ok(entries);
158    }
159    for &release in RELEASES {
160        let dir = release_dir(root, release);
161        let Ok(read) = fs::read_dir(&dir) else {
162            continue;
163        };
164        for item in read {
165            let item = item.map_err(|e| e.to_string())?;
166            let name = item.file_name().to_string_lossy().into_owned();
167            let Some((mode, hash)) = name.split_once('-') else {
168                continue;
169            };
170            let (Ok(mode), true) = (mode.parse::<Mode>(), hex(hash)) else {
171                continue;
172            };
173            if item.path().join("manifest.json").is_file() {
174                entries.push(Entry {
175                    release: release.into(),
176                    mode,
177                    hash: hash.into(),
178                    dir: item.path(),
179                });
180            }
181        }
182    }
183    entries.sort_by(|a, b| {
184        let key = |e: &Entry| (e.release.parse::<Version>().ok(), e.mode, e.hash.clone());
185        key(a).cmp(&key(b))
186    });
187    Ok(entries)
188}
189
190fn hex(text: &str) -> bool {
191    !text.is_empty() && text.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
192}
193
194/// Text that identifies a preparation's inputs; its hash names the entry.
195pub fn content_hash(inputs: &[(&str, &[u8])]) -> String {
196    let mut text = Vec::new();
197    for (name, bytes) in inputs {
198        text.extend(format!("{name}\t{}\n", bytes.len()).as_bytes());
199        text.extend(*bytes);
200        text.push(b'\n');
201    }
202    sha256::hex(&sha256::sha256(&text))[..16].to_string()
203}
204
205/// Every file and symbolic link under DIR except `provider.txt` and
206/// `manifest.json`, which hold machine-specific paths and the inventory itself.
207pub fn inventory(dir: &Path) -> Result<Value, String> {
208    let mut items = Vec::new();
209    let mut stack = vec![PathBuf::new()];
210    let mut seen = BTreeSet::new();
211    while let Some(relative) = stack.pop() {
212        let mut names: Vec<_> = fs::read_dir(dir.join(&relative))
213            .map_err(|e| format!("{}: {e}", dir.join(&relative).display()))?
214            .map(|e| e.map(|e| e.file_name()))
215            .collect::<Result<_, _>>()
216            .map_err(|e| e.to_string())?;
217        names.sort();
218        for name in names {
219            let path = relative.join(&name);
220            let full = dir.join(&path);
221            let text = path
222                .to_str()
223                .ok_or("provider paths must be UTF-8")?
224                .replace('\\', "/");
225            if relative.as_os_str().is_empty()
226                && matches!(text.as_str(), "provider.txt" | "manifest.json")
227            {
228                continue;
229            }
230            let meta = fs::symlink_metadata(&full).map_err(|e| e.to_string())?;
231            if meta.file_type().is_symlink() {
232                let link = fs::read_link(&full).map_err(|e| e.to_string())?;
233                let link = link.to_str().ok_or("link targets must be UTF-8")?;
234                if Path::new(link).is_absolute() || link.contains("..") {
235                    return Err(format!("{text}: links must stay in their directory"));
236                }
237                seen.insert(text.clone());
238                items.push(Value::Object(vec![
239                    ("path".into(), Value::String(text)),
240                    ("link".into(), Value::String(link.into())),
241                ]));
242            } else if meta.is_dir() {
243                stack.push(path);
244            } else if meta.is_file() {
245                let bytes = fs::read(&full).map_err(|e| format!("{}: {e}", full.display()))?;
246                seen.insert(text.clone());
247                items.push(Value::Object(vec![
248                    ("path".into(), Value::String(text)),
249                    (
250                        "sha256".into(),
251                        Value::String(sha256::hex(&sha256::sha256(&bytes))),
252                    ),
253                    ("size".into(), Value::Integer(bytes.len() as u64)),
254                ]));
255            } else {
256                return Err(format!("{text}: not a file, directory or link"));
257            }
258        }
259    }
260    items.sort_by(|a, b| a.str("path").ok().cmp(&b.str("path").ok()));
261    Ok(Value::Array(items))
262}
263
264/// Writes `BASE.S` and `BASE.map` into DIR and links `SONAME` from them with the
265/// host's GNU as and ld, as the qualified pipeline does.
266pub fn link_stub(
267    tools: &super::build::Tools,
268    dir: &Path,
269    base: &str,
270    soname: &str,
271    sources: &stub::Sources,
272) -> Result<(), String> {
273    let (assembly, map, object) = (
274        dir.join(format!("{base}.S")),
275        dir.join(format!("{base}.map")),
276        dir.join(format!("{base}.o")),
277    );
278    fs::write(&assembly, &sources.assembly).map_err(|e| e.to_string())?;
279    fs::write(&map, &sources.versions).map_err(|e| e.to_string())?;
280    let text = |p: &Path| p.to_string_lossy().into_owned();
281    super::build::run(
282        &tools.assembler,
283        &["--64".into(), text(&assembly), "-o".into(), text(&object)],
284        dir,
285    )?;
286    super::build::run(
287        &tools.linker,
288        &[
289            "-shared".into(),
290            "--build-id=none".into(),
291            "-soname".into(),
292            soname.into(),
293            format!("--version-script={}", text(&map)),
294            text(&object),
295            "-o".into(),
296            text(&dir.join(soname)),
297        ],
298        dir,
299    )?;
300    fs::remove_file(&object).map_err(|e| e.to_string())
301}
302
303/// A relative symbolic link NAME -> TARGET inside DIR.
304pub fn symlink(dir: &Path, target: &str, name: &str) -> Result<(), String> {
305    #[cfg(unix)]
306    {
307        std::os::unix::fs::symlink(target, dir.join(name))
308            .map_err(|e| format!("{}: {e}", dir.join(name).display()))
309    }
310    #[cfg(not(unix))]
311    {
312        let _ = (dir, target, name);
313        Err("providers are prepared on Linux only".into())
314    }
315}
316
317/// Writes the eight glibc stubs (and their `lib*.so` names) into DIR.
318pub fn write_glibc_stubs(
319    tools: &super::build::Tools,
320    dir: &Path,
321    stubs: &[(&str, stub::Sources)],
322) -> Result<(), String> {
323    fs::create_dir_all(dir).map_err(|e| e.to_string())?;
324    for (library, sources) in stubs {
325        let soname = stub::glibc_soname(library).ok_or("unknown library")?;
326        link_stub(tools, dir, library, soname, sources)?;
327        symlink(dir, soname, &format!("lib{library}.so"))?;
328    }
329    Ok(())
330}
331
332/// Writes the `libgcc_s` stub (GCC 4.8.5's interface) into DIR.
333pub fn write_libgcc_stub(tools: &super::build::Tools, dir: &Path) -> Result<(), String> {
334    fs::create_dir_all(dir).map_err(|e| e.to_string())?;
335    link_stub(tools, dir, "libgcc_s", libgcc::SONAME, &libgcc::stub()?)?;
336    symlink(dir, libgcc::SONAME, "libgcc_s.so")
337}
338
339/// Rechecks ENTRY: every file against the manifest, the descriptor, the eight
340/// stubs against the ABI lists, and the `libgcc_s` stub against GCC 4.8.5's
341/// interface. Returns the descriptor.
342pub fn verify(entry: &Entry, snapshot: &Snapshot) -> Result<Provider, String> {
343    let manifest = entry.manifest()?;
344    if manifest.str("format")? != MANIFEST_FORMAT
345        || manifest.str("glibc")? != entry.release
346        || manifest.str("mode")? != entry.mode.name()
347        || manifest.str("hash")? != entry.hash
348    {
349        return Err(format!(
350            "{}: manifest does not describe this entry",
351            entry.dir.display()
352        ));
353    }
354    let recorded = manifest.get("files").ok_or("manifest lacks files")?;
355    let actual = inventory(&entry.dir)?;
356    if *recorded != actual {
357        let names = |v: &Value| -> BTreeSet<String> {
358            match v {
359                Value::Array(items) => items.iter().map(|i| format!("{i:?}")).collect(),
360                _ => BTreeSet::new(),
361            }
362        };
363        let changed: Vec<String> = names(recorded)
364            .symmetric_difference(&names(&actual))
365            .take(3)
366            .cloned()
367            .collect();
368        return Err(format!(
369            "{}: files differ from manifest.json: {changed:?}",
370            entry.dir.display()
371        ));
372    }
373    let provider = Provider::load(&entry.descriptor())?;
374    provider.validate_selection(entry.release.parse()?)?;
375    provider.validate_files()?;
376    for path in [
377        &provider.sysroot,
378        &provider.stubs,
379        &provider.compiler_runtime,
380    ] {
381        if !path.starts_with(&entry.dir) {
382            return Err(format!(
383                "{} must point inside {}",
384                path.display(),
385                entry.dir.display()
386            ));
387        }
388    }
389    if snapshot.release.to_string() != entry.release {
390        return Err("the ABI snapshot is for another release".into());
391    }
392    for (library, soname) in crate::glibc::LIBRARIES
393        .iter()
394        .map(|l| (*l, stub::glibc_soname(l).expect("known")))
395    {
396        debug_assert!(SONAMES.contains(&soname));
397        let metadata = elf::Metadata::load(&provider.stubs.join(soname))?;
398        stub::render_glibc(snapshot, library, &metadata)
399            .map_err(|e| format!("stub {soname}: {e}"))?;
400    }
401    let runtime = elf::Metadata::load(&provider.compiler_runtime.join(libgcc::SONAME))?;
402    let key = |s: &elf::Symbol| {
403        (
404            s.name.clone(),
405            s.version.clone(),
406            s.default_version,
407            s.symbol_type,
408            s.size * u64::from(s.symbol_type != 2),
409        )
410    };
411    let exported: BTreeSet<_> = runtime
412        .symbols
413        .iter()
414        .filter(|s| s.section != 0 && s.version.as_deref().is_some_and(|v| v.starts_with("GCC_")))
415        .map(key)
416        .collect();
417    let expected: BTreeSet<_> = libgcc::stub()?.selected.symbols.iter().map(key).collect();
418    if exported != expected || runtime.soname != libgcc::SONAME {
419        return Err(format!(
420            "{}: the libgcc_s stub does not have GCC {}'s interface",
421            provider.compiler_runtime.display(),
422            libgcc::GCC_RELEASE
423        ));
424    }
425    Ok(provider)
426}
427
428/// Copies DIR to a new TARGET, keeping symbolic links as links.
429pub fn copy_tree(dir: &Path, target: &Path) -> Result<(), String> {
430    fs::create_dir(target).map_err(|e| format!("cannot create new {}: {e}", target.display()))?;
431    for item in fs::read_dir(dir).map_err(|e| e.to_string())? {
432        let item = item.map_err(|e| e.to_string())?;
433        let (from, to) = (item.path(), target.join(item.file_name()));
434        let kind = fs::symlink_metadata(&from)
435            .map_err(|e| e.to_string())?
436            .file_type();
437        if kind.is_symlink() {
438            let link = fs::read_link(&from).map_err(|e| e.to_string())?;
439            let parent = to.parent().expect("inside target");
440            let name = to
441                .file_name()
442                .expect("named")
443                .to_string_lossy()
444                .into_owned();
445            symlink(
446                parent,
447                link.to_str().ok_or("link targets must be UTF-8")?,
448                &name,
449            )?;
450        } else if kind.is_dir() {
451            copy_tree(&from, &to)?;
452        } else {
453            fs::copy(&from, &to).map_err(|e| format!("{}: {e}", from.display()))?;
454        }
455    }
456    Ok(())
457}
458
459/// Removes a directory tree, making read-only directories writable first.
460pub fn remove_tree(dir: &Path) -> Result<(), String> {
461    if !dir.exists() {
462        return Ok(());
463    }
464    if fs::remove_dir_all(dir).is_ok() {
465        return Ok(());
466    }
467    let mut stack = vec![dir.to_path_buf()];
468    while let Some(path) = stack.pop() {
469        if let Ok(meta) = fs::symlink_metadata(&path) {
470            if meta.is_dir() {
471                let mut permissions = meta.permissions();
472                #[allow(clippy::permissions_set_readonly_false)]
473                permissions.set_readonly(false);
474                let _ = fs::set_permissions(&path, permissions);
475                if let Ok(read) = fs::read_dir(&path) {
476                    stack.extend(read.flatten().map(|e| e.path()));
477                }
478            }
479        }
480    }
481    fs::remove_dir_all(dir).map_err(|e| format!("cannot remove {}: {e}", dir.display()))
482}
483
484/// Copies ENTRY into TO (`TO/glibc-X.Y/<mode>-<hash>`) for another machine.
485pub fn export(entry: &Entry, to: &Path) -> Result<PathBuf, String> {
486    let target = to
487        .join(format!("glibc-{}", entry.release))
488        .join(entry.dir.file_name().expect("named"));
489    fs::create_dir_all(target.parent().expect("has parent")).map_err(|e| e.to_string())?;
490    copy_tree(&entry.dir, &target)?;
491    // The descriptor's paths are this machine's; import writes new ones.
492    fs::remove_file(target.join("provider.txt")).map_err(|e| e.to_string())?;
493    Ok(target)
494}
495
496/// Imports every exported provider under FROM into ROOT, after checking each
497/// file against its manifest. GCC_STARTUP is this machine's GCC directory.
498pub fn import(from: &Path, root: &Path, gcc_startup: &Path) -> Result<Vec<Entry>, String> {
499    let mut imported = Vec::new();
500    for release_item in fs::read_dir(from).map_err(|e| format!("{}: {e}", from.display()))? {
501        let release_path = release_item.map_err(|e| e.to_string())?.path();
502        let Some(release_name) = release_path
503            .file_name()
504            .and_then(|n| n.to_str())
505            .and_then(|n| n.strip_prefix("glibc-"))
506            .map(String::from)
507        else {
508            continue;
509        };
510        let release_name = release(&release_name)?;
511        for item in fs::read_dir(&release_path).map_err(|e| e.to_string())? {
512            let source = item.map_err(|e| e.to_string())?.path();
513            let name = source
514                .file_name()
515                .and_then(|n| n.to_str())
516                .ok_or("entry names must be UTF-8")?
517                .to_string();
518            let Some((mode, hash)) = name.split_once('-') else {
519                continue;
520            };
521            let mode: Mode = mode.parse()?;
522            if !hex(hash) {
523                return Err(format!("{}: not a provider entry", source.display()));
524            }
525            let staged = Entry {
526                release: release_name.into(),
527                mode,
528                hash: hash.into(),
529                dir: source.clone(),
530            };
531            let manifest = staged.manifest()?;
532            if manifest.get("files") != Some(&inventory(&source)?) {
533                return Err(format!(
534                    "{}: files differ from manifest.json",
535                    source.display()
536                ));
537            }
538            let target = entry(root, release_name, mode, hash);
539            if target.dir.exists() {
540                imported.push(target);
541                continue;
542            }
543            let parent = target.dir.parent().expect("has parent");
544            fs::create_dir_all(parent).map_err(|e| e.to_string())?;
545            let temporary = parent.join(format!(".tmp-import-{}-{name}", std::process::id()));
546            remove_tree(&temporary)?;
547            copy_tree(&source, &temporary)?;
548            // The descriptor names the final directory.
549            let text = descriptor_text(&target.dir, release_name, gcc_startup)?;
550            fs::write(temporary.join("provider.txt"), text).map_err(|e| e.to_string())?;
551            fs::rename(&temporary, &target.dir).map_err(|e| e.to_string())?;
552            imported.push(target);
553        }
554    }
555    Ok(imported)
556}
557
558/// The text of a descriptor for an entry directory DIR.
559pub fn descriptor_text(dir: &Path, release: &str, gcc_startup: &Path) -> Result<String, String> {
560    let mut text = format!("{FORMAT}\ntarget\t{TARGET}\nglibc\t{release}\n");
561    for (key, path) in [
562        ("sysroot", dir.join("sysroot")),
563        ("stubs", dir.join("stubs")),
564        ("gcc-startup", gcc_startup.to_path_buf()),
565        ("compiler-runtime", dir.join("compiler-runtime")),
566    ] {
567        let path = path.to_str().ok_or("provider paths must be UTF-8")?;
568        text.push_str(&format!("{key}\t{path}\n"));
569    }
570    Provider::parse(&text)?;
571    Ok(text)
572}
573
574#[cfg(test)]
575mod tests {
576    use super::*;
577
578    #[test]
579    fn names_entries_by_release_mode_and_hash() {
580        let root = Path::new("/cache");
581        let e = entry(root, "2.28", Mode::Source, "0123456789abcdef");
582        assert_eq!(
583            e.dir,
584            Path::new("/cache/x86_64-unknown-linux-gnu/glibc-2.28/source-0123456789abcdef")
585        );
586        assert_eq!(e.descriptor(), e.dir.join("provider.txt"));
587        assert_eq!("container".parse::<Mode>().unwrap(), Mode::Container);
588        assert!("binary".parse::<Mode>().is_err());
589        assert!(release("2.16").is_err());
590        assert_eq!(release("2.44").unwrap(), "2.44");
591    }
592
593    #[test]
594    fn hashes_every_input() {
595        let a = content_hash(&[("x", b"1"), ("y", b"2")]);
596        assert_eq!(a.len(), 16);
597        assert_ne!(a, content_hash(&[("x", b"1"), ("y", b"3")]));
598        // Boundaries between inputs are part of the hash.
599        assert_ne!(
600            content_hash(&[("x", b"12"), ("y", b"")]),
601            content_hash(&[("x", b"1"), ("y", b"2")])
602        );
603    }
604
605    #[test]
606    fn inventories_files_and_rejects_changes() {
607        let dir = env::temp_dir().join(format!("lat-cache-test-{}", std::process::id()));
608        let _ = remove_tree(&dir);
609        fs::create_dir_all(dir.join("stubs")).unwrap();
610        fs::write(dir.join("stubs/a"), b"abc").unwrap();
611        fs::write(dir.join("provider.txt"), b"machine specific").unwrap();
612        let first = inventory(&dir).unwrap();
613        let text = first.to_text();
614        assert!(text.contains("\"stubs/a\""), "{text}");
615        assert!(text.contains(&sha256::hex(&sha256::sha256(b"abc"))));
616        assert!(!text.contains("provider.txt"));
617        fs::write(dir.join("provider.txt"), b"another machine").unwrap();
618        assert_eq!(inventory(&dir).unwrap(), first);
619        fs::write(dir.join("stubs/a"), b"abd").unwrap();
620        assert_ne!(inventory(&dir).unwrap(), first);
621        remove_tree(&dir).unwrap();
622    }
623}