Skip to main content

onelf_format/
verdef.rs

1//! Dynamic-section facts of a shared object: the versions it defines and
2//! the sonames it needs.
3//!
4//! A versioned library lists the versions it defines in its
5//! `SHT_GNU_verdef` section. Two copies of the same soname can be ordered
6//! by those sets: under symbol versioning a newer build only ever adds
7//! versions, so the copy whose set contains the other's can stand in for
8//! it. That ordering is what lets a host library and a bundled one be
9//! compared without running either.
10//!
11//! Only the sections involved are read, so a large library costs a few
12//! small reads rather than a whole-file parse. Every offset and count is
13//! checked against the file before it is used, since the host's libraries
14//! are not the package's to trust.
15
16use std::collections::BTreeSet;
17use std::fs::File;
18use std::io::{self, Read, Seek, SeekFrom};
19use std::path::Path;
20
21/// The versions a shared object defines, excluding the base version that
22/// only names the object itself.
23pub type VersionSet = BTreeSet<String>;
24
25/// Which copy of a soname the version comparison chose.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Choice {
28    /// The host's copy defines every version the bundled one does, and
29    /// more.
30    Host,
31    /// The bundled copy defines everything the host's does.
32    Bundle,
33    /// Each copy defines a version the other lacks. Reported so the
34    /// publisher can see it; the bundled copy is used.
35    Incomparable,
36}
37
38/// Order two version sets. Equal sets and two empty sets choose the
39/// bundle, since nothing about the host copy is known to be better.
40pub fn compare(bundle: &VersionSet, host: &VersionSet) -> Choice {
41    if host.is_subset(bundle) {
42        Choice::Bundle
43    } else if bundle.is_subset(host) {
44        Choice::Host
45    } else {
46        Choice::Incomparable
47    }
48}
49
50const SHT_DYNAMIC: u32 = 6;
51const SHT_GNU_VERDEF: u32 = 0x6fff_fffd;
52const VER_FLG_BASE: u16 = 0x1;
53const VERDEF_ENTRY: usize = 20;
54const VERDAUX_ENTRY: usize = 8;
55const DT_NULL: u64 = 0;
56const DT_NEEDED: u64 = 1;
57
58/// The section header table is bounded so a corrupt count cannot drive a
59/// large allocation; a real table is a few kilobytes.
60const MAX_SHDR_TABLE: usize = 1 << 20;
61/// A version definition or dynamic section is a few hundred bytes; a
62/// string table can be large in a library with many symbols.
63const MAX_SECTION: usize = 1 << 20;
64const MAX_STRTAB: usize = 64 << 20;
65
66fn invalid(msg: &str) -> io::Error {
67    io::Error::new(io::ErrorKind::InvalidData, msg)
68}
69
70/// Read the version definitions of the shared object at `path`.
71///
72/// An object without a version definition section yields an empty set. A
73/// file that is not an ELF object, or whose tables point outside the file,
74/// is an error rather than an empty set, so the caller can tell "defines
75/// nothing" from "cannot be read".
76pub fn read(path: &Path) -> io::Result<VersionSet> {
77    let mut object = Object::open(path)?;
78    let Some((defs, strings)) = object.section_with_strings(SHT_GNU_VERDEF)? else {
79        return Ok(VersionSet::new());
80    };
81    parse_verdef(&defs, &strings)
82}
83
84/// Read the sonames the shared object at `path` needs, in table order.
85/// An object without a dynamic section needs nothing.
86pub fn read_needed(path: &Path) -> io::Result<Vec<String>> {
87    let mut object = Object::open(path)?;
88    let Some((dynamic, strings)) = object.section_with_strings(SHT_DYNAMIC)? else {
89        return Ok(Vec::new());
90    };
91    parse_needed(&dynamic, &strings, object.layout.class64)
92}
93
94/// An opened ELF object with its section header table loaded.
95struct Object {
96    file: File,
97    file_len: u64,
98    layout: Layout,
99    table: Vec<u8>,
100}
101
102impl Object {
103    fn open(path: &Path) -> io::Result<Self> {
104        let mut file = File::open(path)?;
105        let file_len = file.metadata()?.len();
106        let mut header = [0u8; 64];
107        let n = file.read(&mut header)?;
108        let layout = Layout::parse(&header[..n])?;
109
110        let table_len = layout
111            .shnum
112            .checked_mul(layout.shentsize)
113            .ok_or_else(|| invalid("section header table overflows"))?;
114        if table_len > MAX_SHDR_TABLE {
115            return Err(invalid("section header table is implausibly large"));
116        }
117        let table = read_at(&mut file, file_len, layout.shoff, table_len)?;
118        Ok(Object {
119            file,
120            file_len,
121            layout,
122            table,
123        })
124    }
125
126    /// The first section of type `kind` and the string table it links to,
127    /// or `None` when the object has no such section.
128    fn section_with_strings(&mut self, kind: u32) -> io::Result<Option<(Vec<u8>, Vec<u8>)>> {
129        let Some(section) = self.layout.find_section(&self.table, kind)? else {
130            return Ok(None);
131        };
132        if section.size > MAX_SECTION {
133            return Err(invalid("section is implausibly large"));
134        }
135        let strtab = self.layout.section(&self.table, section.link)?;
136        if strtab.size > MAX_STRTAB {
137            return Err(invalid("string table is implausibly large"));
138        }
139        let body = read_at(&mut self.file, self.file_len, section.offset, section.size)?;
140        let strings = read_at(&mut self.file, self.file_len, strtab.offset, strtab.size)?;
141        Ok(Some((body, strings)))
142    }
143}
144
145/// Read `len` bytes at `offset`, refusing before the read if the range does
146/// not fit the file.
147fn read_at(file: &mut File, file_len: u64, offset: u64, len: usize) -> io::Result<Vec<u8>> {
148    let end = offset
149        .checked_add(len as u64)
150        .ok_or_else(|| invalid("section range overflows"))?;
151    if end > file_len {
152        return Err(invalid("section lies outside the file"));
153    }
154    file.seek(SeekFrom::Start(offset))?;
155    let mut buf = vec![0u8; len];
156    file.read_exact(&mut buf)?;
157    Ok(buf)
158}
159
160struct Section {
161    offset: u64,
162    size: usize,
163    link: usize,
164}
165
166/// Where the section header table sits and how its entries are laid out,
167/// for the ELF class of the file.
168struct Layout {
169    class64: bool,
170    shoff: u64,
171    shentsize: usize,
172    shnum: usize,
173}
174
175impl Layout {
176    fn parse(header: &[u8]) -> io::Result<Self> {
177        if header.len() < 64 || header[0..4] != *b"\x7fELF" {
178            return Err(invalid("not an ELF object"));
179        }
180        if header[5] != 1 {
181            return Err(invalid("big-endian ELF is not supported"));
182        }
183        let layout = match header[4] {
184            2 => Layout {
185                class64: true,
186                shoff: u64::from_le_bytes(header[40..48].try_into().unwrap()),
187                shentsize: u16::from_le_bytes(header[58..60].try_into().unwrap()) as usize,
188                shnum: u16::from_le_bytes(header[60..62].try_into().unwrap()) as usize,
189            },
190            1 => Layout {
191                class64: false,
192                shoff: u32::from_le_bytes(header[32..36].try_into().unwrap()) as u64,
193                shentsize: u16::from_le_bytes(header[46..48].try_into().unwrap()) as usize,
194                shnum: u16::from_le_bytes(header[48..50].try_into().unwrap()) as usize,
195            },
196            _ => return Err(invalid("unknown ELF class")),
197        };
198        let min_entry = if layout.class64 { 64 } else { 40 };
199        if layout.shnum > 0 && layout.shentsize < min_entry {
200            return Err(invalid("section header entries are too small"));
201        }
202        Ok(layout)
203    }
204
205    fn section(&self, table: &[u8], index: usize) -> io::Result<Section> {
206        let start = index
207            .checked_mul(self.shentsize)
208            .ok_or_else(|| invalid("section index overflows"))?;
209        let entry = table
210            .get(start..start + self.shentsize)
211            .ok_or_else(|| invalid("section index outside the table"))?;
212        let (offset, size, link) = if self.class64 {
213            (
214                u64::from_le_bytes(entry[24..32].try_into().unwrap()),
215                u64::from_le_bytes(entry[32..40].try_into().unwrap()),
216                u32::from_le_bytes(entry[40..44].try_into().unwrap()),
217            )
218        } else {
219            (
220                u32::from_le_bytes(entry[16..20].try_into().unwrap()) as u64,
221                u32::from_le_bytes(entry[20..24].try_into().unwrap()) as u64,
222                u32::from_le_bytes(entry[24..28].try_into().unwrap()),
223            )
224        };
225        let size = usize::try_from(size).map_err(|_| invalid("section size overflows"))?;
226        Ok(Section {
227            offset,
228            size,
229            link: link as usize,
230        })
231    }
232
233    fn find_section(&self, table: &[u8], kind: u32) -> io::Result<Option<Section>> {
234        for i in 0..self.shnum {
235            let start = i * self.shentsize;
236            let entry = &table[start..start + self.shentsize];
237            let sh_type = u32::from_le_bytes(entry[4..8].try_into().unwrap());
238            if sh_type == kind {
239                return self.section(table, i).map(Some);
240            }
241        }
242        Ok(None)
243    }
244}
245
246/// Walk the `Elf_Verdef` chain in `defs`, taking each definition's first
247/// `Elf_Verdaux` as its name.
248fn parse_verdef(defs: &[u8], strings: &[u8]) -> io::Result<VersionSet> {
249    let mut out = VersionSet::new();
250    if defs.is_empty() {
251        return Ok(out);
252    }
253    let mut pos = 0usize;
254    // A chain cannot legitimately have more links than fit in the section;
255    // a corrupt one that never reaches zero is cut here.
256    for _ in 0..=defs.len() / VERDEF_ENTRY {
257        let entry = defs
258            .get(pos..pos + VERDEF_ENTRY)
259            .ok_or_else(|| invalid("version definition outside its section"))?;
260        let flags = u16::from_le_bytes(entry[2..4].try_into().unwrap());
261        let aux = u32::from_le_bytes(entry[12..16].try_into().unwrap()) as usize;
262        let next = u32::from_le_bytes(entry[16..20].try_into().unwrap()) as usize;
263
264        if flags & VER_FLG_BASE == 0 {
265            let aux_at = pos
266                .checked_add(aux)
267                .ok_or_else(|| invalid("version auxiliary offset overflows"))?;
268            let vda = defs
269                .get(aux_at..aux_at + VERDAUX_ENTRY)
270                .ok_or_else(|| invalid("version name entry outside its section"))?;
271            let name_at = u32::from_le_bytes(vda[0..4].try_into().unwrap()) as usize;
272            out.insert(string_at(strings, name_at)?);
273        }
274
275        if next == 0 {
276            return Ok(out);
277        }
278        pos = pos
279            .checked_add(next)
280            .ok_or_else(|| invalid("version definition chain overflows"))?;
281    }
282    Err(invalid("version definition chain does not terminate"))
283}
284
285/// Collect the `DT_NEEDED` entries of a dynamic section.
286fn parse_needed(dynamic: &[u8], strings: &[u8], class64: bool) -> io::Result<Vec<String>> {
287    let entry_len = if class64 { 16 } else { 8 };
288    let mut out = Vec::new();
289    for entry in dynamic.chunks_exact(entry_len) {
290        let (tag, value) = if class64 {
291            (
292                u64::from_le_bytes(entry[0..8].try_into().unwrap()),
293                u64::from_le_bytes(entry[8..16].try_into().unwrap()),
294            )
295        } else {
296            (
297                u32::from_le_bytes(entry[0..4].try_into().unwrap()) as u64,
298                u32::from_le_bytes(entry[4..8].try_into().unwrap()) as u64,
299            )
300        };
301        match tag {
302            DT_NULL => break,
303            DT_NEEDED => {
304                let at = usize::try_from(value).map_err(|_| invalid("needed name overflows"))?;
305                out.push(string_at(strings, at)?);
306            }
307            _ => {}
308        }
309    }
310    Ok(out)
311}
312
313fn string_at(strings: &[u8], at: usize) -> io::Result<String> {
314    let rest = strings
315        .get(at..)
316        .ok_or_else(|| invalid("name outside the string table"))?;
317    let end = rest
318        .iter()
319        .position(|&b| b == 0)
320        .ok_or_else(|| invalid("name is not terminated"))?;
321    std::str::from_utf8(&rest[..end])
322        .map(String::from)
323        .map_err(|_| invalid("name is not UTF-8"))
324}
325
326/// Builders for synthetic shared objects, shared with the resolver's tests.
327#[cfg(test)]
328pub(crate) mod fixture {
329    use super::*;
330
331    /// A 64-bit ELF with a string table, a version definition section built
332    /// from `defs` (each `(flags, name)`), and a dynamic section naming
333    /// `needed`. Either list may be empty, in which case that section is
334    /// still emitted but holds nothing.
335    pub(crate) fn shared_object(defs: &[(u16, &str)], needed: &[&str]) -> Vec<u8> {
336        shared_object_with_interp(defs, needed, None)
337    }
338
339    /// [`shared_object`] with a `PT_INTERP` program header naming `interp`,
340    /// the way a libc names the loader it belongs with.
341    pub(crate) fn shared_object_with_interp(
342        defs: &[(u16, &str)],
343        needed: &[&str],
344        interp: Option<&str>,
345    ) -> Vec<u8> {
346        let mut strings: Vec<u8> = vec![0];
347        let mut intern = |s: &str| {
348            let at = strings.len() as u32;
349            strings.extend_from_slice(s.as_bytes());
350            strings.push(0);
351            at
352        };
353        let name_offsets: Vec<u32> = defs.iter().map(|(_, n)| intern(n)).collect();
354        let needed_offsets: Vec<u32> = needed.iter().map(|n| intern(n)).collect();
355
356        let mut verdef = Vec::new();
357        for (i, (flags, _)) in defs.iter().enumerate() {
358            let last = i + 1 == defs.len();
359            verdef.extend_from_slice(&1u16.to_le_bytes());
360            verdef.extend_from_slice(&flags.to_le_bytes());
361            verdef.extend_from_slice(&(i as u16).to_le_bytes());
362            verdef.extend_from_slice(&1u16.to_le_bytes());
363            verdef.extend_from_slice(&0u32.to_le_bytes());
364            verdef.extend_from_slice(&(VERDEF_ENTRY as u32).to_le_bytes());
365            let next = if last {
366                0
367            } else {
368                (VERDEF_ENTRY + VERDAUX_ENTRY) as u32
369            };
370            verdef.extend_from_slice(&next.to_le_bytes());
371            verdef.extend_from_slice(&name_offsets[i].to_le_bytes());
372            verdef.extend_from_slice(&0u32.to_le_bytes());
373        }
374
375        let mut dynamic = Vec::new();
376        for off in needed_offsets {
377            dynamic.extend_from_slice(&DT_NEEDED.to_le_bytes());
378            dynamic.extend_from_slice(&(off as u64).to_le_bytes());
379        }
380        dynamic.extend_from_slice(&DT_NULL.to_le_bytes());
381        dynamic.extend_from_slice(&0u64.to_le_bytes());
382
383        let mut elf = build_elf(&strings, &verdef, &dynamic);
384        if let Some(interp) = interp {
385            // One program header, placed after the section headers and
386            // before the section bodies is not possible without moving
387            // them, so it goes at the end with the string it names.
388            let phoff = elf.len();
389            let interp_at = phoff + 56;
390            elf.resize(interp_at + interp.len() + 1, 0);
391            elf[32..40].copy_from_slice(&(phoff as u64).to_le_bytes());
392            elf[54..56].copy_from_slice(&56u16.to_le_bytes());
393            elf[56..58].copy_from_slice(&1u16.to_le_bytes());
394            elf[phoff..phoff + 4].copy_from_slice(&3u32.to_le_bytes());
395            elf[phoff + 8..phoff + 16].copy_from_slice(&(interp_at as u64).to_le_bytes());
396            elf[phoff + 32..phoff + 40].copy_from_slice(&((interp.len() + 1) as u64).to_le_bytes());
397            elf[interp_at..interp_at + interp.len()].copy_from_slice(interp.as_bytes());
398        }
399        elf
400    }
401
402    pub(crate) fn build_elf(strings: &[u8], verdef: &[u8], dynamic: &[u8]) -> Vec<u8> {
403        let shoff = 64usize;
404        let shentsize = 64usize;
405        let shnum = 4usize;
406        let strings_at = shoff + shentsize * shnum;
407        let verdef_at = strings_at + strings.len();
408        let dynamic_at = verdef_at + verdef.len();
409        let mut v = vec![0u8; dynamic_at + dynamic.len()];
410        v[0..4].copy_from_slice(b"\x7fELF");
411        v[4] = 2;
412        v[5] = 1;
413        v[16..18].copy_from_slice(&3u16.to_le_bytes()); // ET_DYN
414        v[40..48].copy_from_slice(&(shoff as u64).to_le_bytes());
415        v[58..60].copy_from_slice(&(shentsize as u16).to_le_bytes());
416        v[60..62].copy_from_slice(&(shnum as u16).to_le_bytes());
417        let mut shdr = |index: usize, sh_type: u32, offset: usize, size: usize, link: u32| {
418            let at = shoff + index * shentsize;
419            v[at + 4..at + 8].copy_from_slice(&sh_type.to_le_bytes());
420            v[at + 24..at + 32].copy_from_slice(&(offset as u64).to_le_bytes());
421            v[at + 32..at + 40].copy_from_slice(&(size as u64).to_le_bytes());
422            v[at + 40..at + 44].copy_from_slice(&link.to_le_bytes());
423        };
424        shdr(1, 3, strings_at, strings.len(), 0);
425        shdr(2, SHT_GNU_VERDEF, verdef_at, verdef.len(), 1);
426        shdr(3, SHT_DYNAMIC, dynamic_at, dynamic.len(), 1);
427        v[strings_at..strings_at + strings.len()].copy_from_slice(strings);
428        v[verdef_at..verdef_at + verdef.len()].copy_from_slice(verdef);
429        v[dynamic_at..dynamic_at + dynamic.len()].copy_from_slice(dynamic);
430        v
431    }
432
433    /// A fresh directory under the system temp dir for one test.
434    pub(crate) fn temp_dir(tag: &str) -> std::path::PathBuf {
435        let dir = std::env::temp_dir().join(format!(
436            "onelf-{tag}-{}-{}",
437            std::process::id(),
438            std::time::SystemTime::now()
439                .duration_since(std::time::UNIX_EPOCH)
440                .unwrap()
441                .as_nanos()
442        ));
443        std::fs::create_dir_all(&dir).unwrap();
444        dir
445    }
446}
447
448#[cfg(test)]
449mod tests {
450    use super::fixture::{build_elf, shared_object, temp_dir};
451    use super::*;
452
453    fn set(names: &[&str]) -> VersionSet {
454        names.iter().map(|s| s.to_string()).collect()
455    }
456
457    fn write_temp(name: &str, bytes: &[u8]) -> std::path::PathBuf {
458        let path = temp_dir("verdef").join(name);
459        std::fs::write(&path, bytes).unwrap();
460        path
461    }
462
463    #[test]
464    fn compare_orders_by_superset() {
465        let old = set(&["GLIBC_2.2.5", "GLIBC_2.17"]);
466        let new = set(&["GLIBC_2.2.5", "GLIBC_2.17", "GLIBC_2.34"]);
467        assert_eq!(compare(&old, &new), Choice::Host);
468        assert_eq!(compare(&new, &old), Choice::Bundle);
469        assert_eq!(compare(&new, &new), Choice::Bundle);
470        assert_eq!(compare(&set(&[]), &set(&[])), Choice::Bundle);
471        // An unversioned host copy never displaces a versioned bundle.
472        assert_eq!(compare(&old, &set(&[])), Choice::Bundle);
473        // An unversioned bundle against a versioned host reads as the host
474        // satisfying every version the bundle demands, which is none.
475        assert_eq!(compare(&set(&[]), &old), Choice::Host);
476        let fork = set(&["GLIBC_2.2.5", "VENDOR_1.0"]);
477        assert_eq!(compare(&old, &fork), Choice::Incomparable);
478    }
479
480    #[test]
481    fn reads_definitions_and_skips_the_base_version() {
482        let elf = shared_object(
483            &[
484                (VER_FLG_BASE, "libfoo.so.1"),
485                (0, "FOO_1.0"),
486                (0, "FOO_1.2"),
487            ],
488            &[],
489        );
490        let path = write_temp("libfoo.so.1", &elf);
491        assert_eq!(read(&path).unwrap(), set(&["FOO_1.0", "FOO_1.2"]));
492    }
493
494    #[test]
495    fn reads_needed_sonames_in_order() {
496        let elf = shared_object(&[], &["libc.so.6", "libm.so.6"]);
497        let path = write_temp("libbar.so.1", &elf);
498        assert_eq!(read_needed(&path).unwrap(), ["libc.so.6", "libm.so.6"]);
499        assert!(read(&path).unwrap().is_empty());
500    }
501
502    #[test]
503    fn no_sections_means_nothing_defined_or_needed() {
504        let mut v = vec![0u8; 64];
505        v[0..4].copy_from_slice(b"\x7fELF");
506        v[4] = 2;
507        v[5] = 1;
508        let path = write_temp("plain.so", &v);
509        assert!(read(&path).unwrap().is_empty());
510        assert!(read_needed(&path).unwrap().is_empty());
511    }
512
513    #[test]
514    fn not_an_elf_is_an_error() {
515        let path = write_temp("text", b"#!/bin/sh\n");
516        assert!(read(&path).is_err());
517        assert!(read_needed(&path).is_err());
518    }
519
520    #[test]
521    fn truncated_section_is_an_error_not_a_panic() {
522        let elf = shared_object(&[(0, "FOO_1.0")], &["libc.so.6"]);
523        let path = write_temp("cut.so", &elf[..elf.len() - 4]);
524        assert!(read_needed(&path).is_err());
525        for cut in 0..elf.len() {
526            let p = write_temp("cut.so", &elf[..cut]);
527            let _ = read(&p);
528            let _ = read_needed(&p);
529        }
530    }
531
532    #[test]
533    fn name_past_the_string_table_is_an_error() {
534        let strings = b"\0FOO_1.0\0";
535        let mut verdef = Vec::new();
536        verdef.extend_from_slice(&1u16.to_le_bytes());
537        verdef.extend_from_slice(&0u16.to_le_bytes());
538        verdef.extend_from_slice(&0u16.to_le_bytes());
539        verdef.extend_from_slice(&1u16.to_le_bytes());
540        verdef.extend_from_slice(&0u32.to_le_bytes());
541        verdef.extend_from_slice(&(VERDEF_ENTRY as u32).to_le_bytes());
542        verdef.extend_from_slice(&0u32.to_le_bytes());
543        verdef.extend_from_slice(&9999u32.to_le_bytes());
544        verdef.extend_from_slice(&0u32.to_le_bytes());
545        let mut dynamic = Vec::new();
546        dynamic.extend_from_slice(&DT_NEEDED.to_le_bytes());
547        dynamic.extend_from_slice(&9999u64.to_le_bytes());
548        let path = write_temp("bad.so", &build_elf(strings, &verdef, &dynamic));
549        assert!(read(&path).is_err());
550        assert!(read_needed(&path).is_err());
551    }
552
553    #[test]
554    fn a_chain_that_leaves_the_section_is_an_error() {
555        let mut elf = shared_object(&[(0, "FOO_1.0"), (0, "FOO_1.1")], &[]);
556        // The dynamic section holds only its terminating entry.
557        let dynamic_len = 16;
558        let verdef_at = elf.len() - dynamic_len - 2 * (VERDEF_ENTRY + VERDAUX_ENTRY);
559        elf[verdef_at + 16..verdef_at + 20].copy_from_slice(&1000u32.to_le_bytes());
560        let path = write_temp("runaway.so", &elf);
561        assert!(read(&path).is_err());
562
563        // A chain ending early is fine: only the entries reached count.
564        let mut elf = shared_object(&[(0, "FOO_1.0"), (0, "FOO_1.1")], &[]);
565        elf[verdef_at + 16..verdef_at + 20].copy_from_slice(&0u32.to_le_bytes());
566        let path = write_temp("short.so", &elf);
567        assert_eq!(read(&path).unwrap(), set(&["FOO_1.0"]));
568    }
569
570    #[test]
571    fn reads_the_host_libc_when_present() {
572        let candidates = [
573            "/lib64/libc.so.6",
574            "/usr/lib64/libc.so.6",
575            "/usr/lib/libc.so.6",
576            "/lib/x86_64-linux-gnu/libc.so.6",
577            "/lib/aarch64-linux-gnu/libc.so.6",
578        ];
579        let Some(libc) = candidates.iter().find(|p| Path::new(p).is_file()) else {
580            return;
581        };
582        let set = read(Path::new(libc)).unwrap();
583        assert!(set.iter().any(|v| v.starts_with("GLIBC_2.")), "{set:?}");
584        assert!(!set.iter().any(|v| v == "libc.so.6"));
585        let needed = read_needed(Path::new(libc)).unwrap();
586        assert!(
587            needed.iter().any(|n| n.starts_with("ld-linux")),
588            "{needed:?}"
589        );
590    }
591}