Skip to main content

stow_types/
glibc.rs

1//! The glibc floor a bundle member needs to load.
2//!
3//! A `dlopen`'d artifact — proc-macro, dylib, cdylib — inherits the
4//! version-needed requirements of whatever glibc the builder linked it
5//! against. The published index records that floor as `min_glibc` so a
6//! client on an older glibc can refuse the row during resolution instead
7//! of fetching a bundle rustc then cannot load. `GlibcVersion` is the
8//! value on both the D1 artifact record and the signed index row; the
9//! `#[cfg]`-gated helpers below it measure the floor from bytes — from a
10//! single ELF image at publish time, or from every `files/` member of a
11//! stored bundle for backfill.
12
13use std::fmt;
14use std::str::FromStr;
15
16/// A `GLIBC_x.y` release the host's libc must reach for an artifact to
17/// load. Three components because real tags carry one when they need it —
18/// `GLIBC_2.2.5` exists — and `2.2 < 2.2.5` must not hold.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
20pub struct GlibcVersion {
21    /// The major component — `2` for every glibc release in the wild.
22    pub major: u32,
23    /// The minor component.
24    pub minor: u32,
25    /// The patch component, `0` when the tag carries only `major.minor`.
26    pub patch: u32,
27}
28
29impl GlibcVersion {
30    /// Parse one `GLIBC_x.y[.z]` version-needed name — the bytes an ELF
31    /// `SHT_GNU_VERNEED` aux entry names, like `b"GLIBC_2.28"`. Returns
32    /// `None` for anything else: other namespaces (`GCC_3.0`,
33    /// `GLIBCXX_3.4.29`), and glibc's own non-numeric names
34    /// (`GLIBC_PRIVATE`, `GLIBC_ABI_DT_RELR`) that pin an ABI property
35    /// rather than a release.
36    #[must_use]
37    pub fn parse_version_tag(name: &[u8]) -> Option<Self> {
38        let rest = name.strip_prefix(b"GLIBC_")?;
39        let mut numbers = rest.split(|byte| *byte == b'.');
40        let major = parse_digits(numbers.next()?)?;
41        let minor = parse_digits(numbers.next()?)?;
42        let patch = numbers.next().map_or(Some(0), parse_digits)?;
43        numbers.next().is_none().then_some(Self {
44            major,
45            minor,
46            patch,
47        })
48    }
49}
50
51fn parse_digits(bytes: &[u8]) -> Option<u32> {
52    if bytes.is_empty() || !bytes.iter().all(u8::is_ascii_digit) {
53        return None;
54    }
55    let text = std::str::from_utf8(bytes).ok()?;
56    text.parse().ok()
57}
58
59/// `major.minor`, plus `.patch` only when nonzero — `2.28`, `2.2.5`.
60/// The bare two-component rendering is the form every existing release
61/// tag and the `gnu_get_libc_version` string share.
62impl fmt::Display for GlibcVersion {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        if self.patch == 0 {
65            write!(f, "{}.{}", self.major, self.minor)
66        } else {
67            write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
68        }
69    }
70}
71
72/// `"{major}.{minor}[.{patch}]"` — the same shape [`Display`] writes, so
73/// `GlibcVersion::from_str(v.to_string().as_str())` round-trips. Anything
74/// else — empty components, non-digits, a fourth component — fails.
75impl FromStr for GlibcVersion {
76    type Err = GlibcVersionParseError;
77    fn from_str(text: &str) -> Result<Self, Self::Err> {
78        let mut numbers = text.split('.');
79        let parse = |part: Option<&str>| {
80            part.and_then(|part| part.parse::<u32>().ok())
81                .ok_or(GlibcVersionParseError)
82        };
83        let version = Self {
84            major: parse(numbers.next())?,
85            minor: parse(numbers.next())?,
86            patch: numbers.next().map_or(Ok(0), |part| parse(Some(part)))?,
87        };
88        if numbers.next().is_some() {
89            return Err(GlibcVersionParseError);
90        }
91        Ok(version)
92    }
93}
94
95/// `FromStr` could not read the text as `major.minor[.patch]` digits.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct GlibcVersionParseError;
98
99impl fmt::Display for GlibcVersionParseError {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        f.write_str("expected glibc version `major.minor[.patch]`")
102    }
103}
104
105impl std::error::Error for GlibcVersionParseError {}
106
107/// The glibc floor the Linux builder promises every served ELF —
108/// the `manylinux_2_28` / RHEL 8 baseline, `2.28`.
109///
110/// The publish stage refuses to register an artifact whose measured
111/// floor exceeds it, and `stow-admin index backfill-min-glibc`
112/// enqueues a rebuild for every stored row above it. Named once here
113/// so the two checks can never drift on separate literals.
114pub const GLIBC_BASELINE: GlibcVersion = GlibcVersion {
115    major: 2,
116    minor: 28,
117    patch: 0,
118};
119
120/// Wire form is the `Display` string: `"2.28"`, not a two-field object —
121/// the row reads like a version, not a struct.
122impl serde::Serialize for GlibcVersion {
123    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
124        serializer.collect_str(self)
125    }
126}
127
128impl<'de> serde::Deserialize<'de> for GlibcVersion {
129    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
130        let text = String::deserialize(deserializer)?;
131        Self::from_str(&text).map_err(serde::de::Error::custom)
132    }
133}
134
135/// ELF measurement, using `object`'s `SHT_GNU_VERNEED` reader. `zstd` and
136/// `object` both stay off wasm32: the only wasm consumer (stow-edge)
137/// serves index pages and never measures bytes.
138#[cfg(not(target_arch = "wasm32"))]
139mod measure {
140    use object::read::elf::{ElfFile, FileHeader};
141    use std::io::Read as _;
142
143    use super::GlibcVersion;
144    use crate::error::Result;
145    use crate::stow_error;
146
147    /// Highest `GLIBC_x.y` an ELF image's version-needed entries demand.
148    ///
149    /// `None` when the bytes are not an ELF image or carry no glibc
150    /// requirement at all — a static `bytes` blob, JSON, and a `.rlib`
151    /// have no floor, and `None` on a row keeps it servable everywhere.
152    ///
153    /// # Errors
154    ///
155    /// Returns an error when the image parses as ELF but its
156    /// version-needed table is malformed — a corrupt artifact is a loud
157    /// failure, not a silent `None`.
158    pub fn min_glibc_of_elf_bytes(bytes: &[u8]) -> Result<Option<GlibcVersion>> {
159        let Ok(file) = object::File::parse(bytes) else {
160            return Ok(None);
161        };
162        match file {
163            object::File::Elf32(elf) => needed_glibc(&elf),
164            object::File::Elf64(elf) => needed_glibc(&elf),
165            _ => Ok(None),
166        }
167    }
168
169    /// The max over one parsed ELF's version-needed aux names. Every
170    /// entry the iterator yields is required, so the floor is the
171    /// maximum — a `GLIBC_2.28` need under a `libc.so.6` `Verneed` and a
172    /// `GLIBC_2.34` need under `ld.so` both count.
173    fn needed_glibc<Elf: FileHeader>(elf: &ElfFile<'_, Elf>) -> Result<Option<GlibcVersion>> {
174        let endian = elf.endian();
175        let data = elf.data();
176        let sections = elf.elf_section_table();
177        let Some((verneeds, strings_index)) = sections
178            .gnu_verneed(endian, data)
179            .map_err(|error| stow_error!("read ELF verneed section: {error}"))?
180        else {
181            return Ok(None);
182        };
183        let strings = sections
184            .strings(endian, data, strings_index)
185            .map_err(|error| stow_error!("read ELF verneed string table: {error}"))?;
186        let mut floor = None;
187        for verneed in verneeds {
188            let (_verneed, aux_iterator) =
189                verneed.map_err(|error| stow_error!("read ELF verneed entry: {error}"))?;
190            for aux in aux_iterator {
191                let aux = aux.map_err(|error| stow_error!("read ELF vernaux entry: {error}"))?;
192                let name = aux
193                    .name(endian, strings)
194                    .map_err(|error| stow_error!("read ELF vernaux name: {error}"))?;
195                if let Some(version) = GlibcVersion::parse_version_tag(name) {
196                    floor = floor.max(Some(version));
197                }
198            }
199        }
200        Ok(floor)
201    }
202
203    /// Highest `GLIBC_x.y` across a stored bundle's `files/` members.
204    ///
205    /// The same floor publish measures on the outputs themselves, read
206    /// back for rows that predate the field. Bundle members under
207    /// `files/` are zstd-compressed payloads; every other member (the
208    /// manifests, the signature envelopes) is JSON and never an ELF.
209    ///
210    /// # Errors
211    ///
212    /// Returns an error when the tar or a compressed member cannot be
213    /// read — a bundle that cannot be measured cannot be trusted to keep
214    /// its row's floor honest.
215    pub fn min_glibc_of_bundle(bundle_bytes: &[u8]) -> Result<Option<GlibcVersion>> {
216        let mut archive = tar::Archive::new(bundle_bytes);
217        let mut floor = None;
218        for entry in archive
219            .entries()
220            .map_err(|error| stow_error!("read bundle tar: {error}"))?
221        {
222            let mut entry = entry.map_err(|error| stow_error!("read bundle entry: {error}"))?;
223            // Owned so the mutable `read_to_end` borrow below is free.
224            let path = entry.path_bytes().into_owned();
225            if !path.starts_with(b"files/") {
226                continue;
227            }
228            let mut compressed = Vec::new();
229            entry.read_to_end(&mut compressed).map_err(|error| {
230                stow_error!(
231                    "read bundle member {}: {error}",
232                    String::from_utf8_lossy(&path)
233                )
234            })?;
235            let bytes =
236                zstd::stream::decode_all(std::io::Cursor::new(&compressed)).unwrap_or(compressed);
237            floor = floor.max(min_glibc_of_elf_bytes(&bytes)?);
238        }
239        Ok(floor)
240    }
241}
242
243#[cfg(not(target_arch = "wasm32"))]
244pub use measure::{min_glibc_of_bundle, min_glibc_of_elf_bytes};
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn parses_version_tags() {
252        assert_eq!(
253            GlibcVersion::parse_version_tag(b"GLIBC_2.28"),
254            Some(GlibcVersion {
255                major: 2,
256                minor: 28,
257                patch: 0,
258            })
259        );
260        assert_eq!(
261            GlibcVersion::parse_version_tag(b"GLIBC_2.2.5"),
262            Some(GlibcVersion {
263                major: 2,
264                minor: 2,
265                patch: 5,
266            })
267        );
268        assert_eq!(GlibcVersion::parse_version_tag(b"GLIBC_PRIVATE"), None);
269        assert_eq!(GlibcVersion::parse_version_tag(b"GCC_3.0"), None);
270        assert_eq!(GlibcVersion::parse_version_tag(b"GLIBCXX_3.4.29"), None);
271        assert_eq!(GlibcVersion::parse_version_tag(b"GLIBC_2"), None);
272        assert_eq!(GlibcVersion::parse_version_tag(b"GLIBC_2.28.1.9"), None);
273    }
274
275    #[test]
276    fn orders_versions() {
277        let mut versions = [
278            GlibcVersion {
279                major: 2,
280                minor: 28,
281                patch: 0,
282            },
283            GlibcVersion {
284                major: 2,
285                minor: 2,
286                patch: 5,
287            },
288            GlibcVersion {
289                major: 2,
290                minor: 2,
291                patch: 4,
292            },
293            GlibcVersion {
294                major: 2,
295                minor: 35,
296                patch: 0,
297            },
298        ];
299        versions.sort();
300        assert_eq!(
301            versions,
302            [
303                GlibcVersion {
304                    major: 2,
305                    minor: 2,
306                    patch: 4
307                },
308                GlibcVersion {
309                    major: 2,
310                    minor: 2,
311                    patch: 5
312                },
313                GlibcVersion {
314                    major: 2,
315                    minor: 28,
316                    patch: 0
317                },
318                GlibcVersion {
319                    major: 2,
320                    minor: 35,
321                    patch: 0
322                },
323            ]
324        );
325    }
326
327    #[test]
328    fn display_and_from_str_round_trip() {
329        for text in ["2.28", "2.35", "0.0", "2.2.5"] {
330            let version = GlibcVersion::from_str(text).expect("parse");
331            let rendered = version.to_string();
332            let reparsed = GlibcVersion::from_str(&rendered).expect("reparse");
333            assert_eq!(version, reparsed);
334        }
335        assert!(GlibcVersion::from_str("").is_err());
336        assert!(GlibcVersion::from_str("2").is_err());
337        assert!(GlibcVersion::from_str("2.28.x").is_err());
338        assert!(GlibcVersion::from_str("2.28.0.1").is_err());
339    }
340
341    #[test]
342    fn serde_round_trip() {
343        let version = GlibcVersion {
344            major: 2,
345            minor: 28,
346            patch: 0,
347        };
348        let json = serde_json::to_string(&version).expect("serialize");
349        assert_eq!(json, "\"2.28\"");
350        assert_eq!(
351            serde_json::from_str::<GlibcVersion>(&json).expect("deserialize"),
352            version
353        );
354    }
355
356    #[cfg(not(target_arch = "wasm32"))]
357    #[test]
358    fn measures_a_built_so() {
359        // Compile a tiny shared object with the system toolchain rather
360        // than checking in a fixture: whatever `cc` this dev machine
361        // links is exactly the ELF shape the publish stage measures.
362        let dir = tempfile::tempdir().expect("tempdir");
363        let source = dir.path().join("probe.c");
364        // A .so that calls no libc function emits no verneed entry —
365        // call one so the measurement has something to find.
366        std::fs::write(
367            &source,
368            "#include <string.h>\nunsigned long stow_probe(const char *s) { return strlen(s); }",
369        )
370        .expect("write");
371        let output = dir.path().join("probe.so");
372        let status = std::process::Command::new("cc")
373            .args(["-shared", "-o"])
374            .arg(&output)
375            .arg(&source)
376            .status()
377            .expect("run cc");
378        assert!(status.success(), "cc failed to build probe .so");
379        let bytes = std::fs::read(&output).expect("read probe .so");
380        let floor = min_glibc_of_elf_bytes(&bytes).expect("measure");
381        // A working libc link can always be measured; on glibc hosts it
382        // is Some version, on musl/BSD it is None — both honest.
383        if cfg!(all(target_os = "linux", target_env = "gnu")) {
384            assert!(floor.is_some(), "probe .so should need some glibc");
385        }
386    }
387
388    #[cfg(not(target_arch = "wasm32"))]
389    #[test]
390    fn non_elf_bytes_have_no_floor() {
391        assert_eq!(
392            min_glibc_of_elf_bytes(b"not an elf file").expect("measure"),
393            None
394        );
395        assert_eq!(min_glibc_of_elf_bytes(&[]).expect("measure"), None);
396    }
397}