Skip to main content

linux_abi_scan/
lib.rs

1//! Static inspection of a built x86_64 Linux executable or shared object
2//! against a target's glibc and kernel floors: **tier 1** of the testing
3//! tiers.
4//!
5//! | Tier | How | Reliability |
6//! | --- | --- | --- |
7//! | 1. Inspect (this crate) | Read the ELF file; nothing runs | Exact for symbols, versions, and ELF notes; heuristic for syscalls in code |
8//! | 2. Simulate | Run tests under a seccomp filter or in a container (`kernel-abi-tools`) | Catches what actually executes; syscall availability and `madvise` only |
9//! | 3. Qualify | Run on a real host at the target's minimums (e.g. SLES 12 SP5) | The only qualification |
10//!
11//! A scan reports what the file *could* do, not what it *does*: it cannot see
12//! syscalls made inside the C library, numbers computed at run time,
13//! libraries loaded with `dlopen`, new flags on old syscalls, or whether a
14//! program falls back when a call fails. Findings carry a [`Confidence`].
15//!
16//! The kernel data is the same that the seccomp profiles are built from
17//! (`kernel_abi_tools::syscalls`, `madvise_advice`, and a preset's
18//! backports), so tiers 1 and 2 cannot disagree about what a kernel has.
19//!
20//! # Example
21//!
22//! Scan a file against the CentOS 7 preset (glibc 2.17, Linux 3.10). The bytes
23//! here are a minimal x86_64 ELF file with no imports and no code; use
24//! [`scan_file`] for a file on disk.
25//!
26//! ```
27//! use linux_abi_scan::{scan, ScanTarget, Severity};
28//! use linux_targets::{Request, Selection};
29//!
30//! let selection = Selection::resolve(&Request {
31//!     distro: Some("centos-7"),
32//!     ..Request::default()
33//! })?;
34//! let target = ScanTarget::from_selection(&selection);
35//!
36//! // ELF header (64 bytes) and one empty section header (64 bytes).
37//! let mut elf = vec![0u8; 128];
38//! elf[..7].copy_from_slice(b"\x7fELF\x02\x01\x01"); // ELF64, little-endian
39//! elf[16] = 2; // executable
40//! elf[18] = 62; // x86_64
41//! elf[40] = 64; // section headers at offset 64
42//! elf[58] = 64; // section header size
43//! elf[60] = 1; // one section header
44//!
45//! let report = scan(&elf, &target)?;
46//! assert_eq!(report.count(Severity::Error), 0);
47//! assert_eq!(report.exit_code(false), 0);
48//! print!("{}", report.to_text(true));
49//!
50//! // Input that is not an x86_64 ELF file is an error, not a report.
51//! assert!(scan(b"#!/bin/sh\n", &target).is_err());
52//! # Ok::<(), String>(())
53//! ```
54//!
55//! A report without errors means only that the scan found nothing: it is not
56//! evidence that the file runs on the target. Confirm with tiers 2 and 3.
57
58#![warn(missing_docs)]
59
60pub mod elf;
61
62use kernel_abi_tools::{
63    madvise_accepts, madvise_advice, present_syscalls, syscalls, KernelVersion, Target,
64};
65use linux_targets::{GlibcVersion, Selection};
66use std::collections::{BTreeMap, BTreeSet};
67use std::fmt::Write as _;
68use std::path::Path;
69
70/// What to check against. `None` skips that side's checks (host glibc or
71/// host kernel), and the report says so.
72pub struct ScanTarget {
73    /// The glibc floor: no `GLIBC_*` version newer than this may be required.
74    pub glibc: Option<GlibcVersion>,
75    /// The kernel to check syscalls and `madvise` advice against, with a
76    /// preset's backports.
77    pub kernel: Option<Target>,
78    /// How the target is named in the report, e.g. a preset's description.
79    pub label: String,
80}
81
82impl ScanTarget {
83    /// The floors of a `linux_targets` selection; a preset's kernel includes
84    /// its sourced backports.
85    pub fn from_selection(sel: &Selection) -> Self {
86        let kernel = match (&sel.distro, sel.kernel) {
87            (Some(d), _) => Some(Target::distro(d)),
88            (None, Some(k)) => Some(Target::kernel(k)),
89            (None, None) => None,
90        };
91        ScanTarget {
92            glibc: sel.glibc,
93            kernel,
94            label: sel.describe(),
95        }
96    }
97}
98
99/// How a finding was established.
100#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
101pub enum Confidence {
102    /// Read from ELF metadata the loader itself uses.
103    Exact,
104    /// Recovered from machine code by pattern matching.
105    Heuristic,
106}
107
108/// How serious a finding is, from most to least serious.
109#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
110pub enum Severity {
111    /// The target does not provide something the file requires.
112    Error,
113    /// Possibly a problem: optional use, or a heuristic finding.
114    Warning,
115    /// Compatible, or context.
116    Info,
117}
118
119/// One thing the scan established about the file.
120#[derive(Clone, Debug, PartialEq, Eq)]
121pub struct Finding {
122    /// How serious it is.
123    pub severity: Severity,
124    /// How it was established: from loader metadata or from machine code.
125    pub confidence: Confidence,
126    /// `glibc-version`, `glibc-wrapper`, `syscall-call`, `syscall-insn`,
127    /// `madvise`, `abi-tag`, `relr`, or `scope`.
128    pub check: &'static str,
129    /// What the finding is about: a symbol version, a function, a syscall, an
130    /// advice value, or a note.
131    pub subject: String,
132    /// Explanation for people, including why it matters on the target.
133    pub detail: String,
134    /// Code address of the call site, for code findings.
135    pub address: Option<u64>,
136}
137
138/// The result of scanning one file.
139#[derive(Debug)]
140pub struct Report {
141    /// The target's label (see [`ScanTarget::label`]).
142    pub target: String,
143    /// Every finding, sorted by severity, confidence, check, address and subject.
144    pub findings: Vec<Finding>,
145}
146
147impl Report {
148    /// The number of findings with this severity.
149    pub fn count(&self, severity: Severity) -> usize {
150        self.findings
151            .iter()
152            .filter(|f| f.severity == severity)
153            .count()
154    }
155
156    /// 0 when nothing fails; 1 when an error (or, with `strict`, a warning)
157    /// was found.
158    pub fn exit_code(&self, strict: bool) -> i32 {
159        let failing = self.count(Severity::Error)
160            + if strict {
161                self.count(Severity::Warning)
162            } else {
163                0
164            };
165        i32::from(failing > 0)
166    }
167
168    /// Human-readable report; `verbose` includes informational findings.
169    pub fn to_text(&self, verbose: bool) -> String {
170        let mut out = format!("target: {}\n", self.target);
171        for f in &self.findings {
172            if f.severity == Severity::Info && !verbose {
173                continue;
174            }
175            let sev = match f.severity {
176                Severity::Error => "ERROR",
177                Severity::Warning => "WARN",
178                Severity::Info => "info",
179            };
180            let conf = f.confidence.as_str();
181            let at = f.address.map_or(String::new(), |a| format!(" at {a:#x}"));
182            writeln!(
183                out,
184                "{sev:5} {conf:9} {:13} {}{at}: {}",
185                f.check, f.subject, f.detail
186            )
187            .unwrap();
188        }
189        writeln!(
190            out,
191            "{} errors, {} warnings, {} info. Static inspection is tier 1: confirm with the \
192             seccomp/container simulation (tier 2) and on a real target host (tier 3).",
193            self.count(Severity::Error),
194            self.count(Severity::Warning),
195            self.count(Severity::Info)
196        )
197        .unwrap();
198        out
199    }
200
201    /// Stable TSV with a header, for tools.
202    pub fn to_tsv(&self) -> String {
203        let mut out = String::from("severity\tconfidence\tcheck\tsubject\taddress\tdetail\n");
204        for f in &self.findings {
205            writeln!(
206                out,
207                "{}\t{}\t{}\t{}\t{}\t{}",
208                f.severity.as_str(),
209                f.confidence.as_str(),
210                f.check,
211                f.subject,
212                f.address.map_or(String::new(), |a| format!("{a:#x}")),
213                f.detail.replace(['\t', '\n'], " ")
214            )
215            .unwrap();
216        }
217        out
218    }
219}
220
221impl Severity {
222    /// Lower-case name: `error`, `warning` or `info`, as in the TSV report.
223    pub fn as_str(self) -> &'static str {
224        match self {
225            Severity::Error => "error",
226            Severity::Warning => "warning",
227            Severity::Info => "info",
228        }
229    }
230}
231
232impl Confidence {
233    /// Lower-case name: `exact` or `heuristic`, as in the TSV report.
234    pub fn as_str(self) -> &'static str {
235        match self {
236            Confidence::Exact => "exact",
237            Confidence::Heuristic => "heuristic",
238        }
239    }
240}
241
242/// glibc functions whose syscall has a different name.
243const WRAPPER_ALIASES: &[(&str, &str)] = &[
244    ("fstatat", "newfstatat"),
245    ("fstatat64", "newfstatat"),
246    ("posix_fadvise", "fadvise64"),
247    ("posix_fadvise64", "fadvise64"),
248    ("prlimit", "prlimit64"),
249    ("pread", "pread64"),
250    ("pwrite", "pwrite64"),
251    ("preadv64", "preadv"),
252    ("pwritev64", "pwritev"),
253    ("preadv64v2", "preadv2"),
254    ("pwritev64v2", "pwritev2"),
255    ("fallocate64", "fallocate"),
256];
257
258/// Syscalls that Rust's standard library calls with a fallback when the
259/// kernel answers `ENOSYS`. A finding for one of these is still reported,
260/// because the scan cannot tell whether the call came from std.
261const RUST_STD_FALLBACKS: &[&str] = &[
262    "statx",
263    "getrandom",
264    "copy_file_range",
265    "pidfd_open",
266    "pidfd_send_signal",
267    "pidfd_getfd",
268    "clone3",
269    "futex_waitv",
270];
271
272/// How far back from a call site to look for the constant argument.
273const LOOKBACK: usize = 64;
274
275/// Scans the ELF file at `path`; see [`scan`].
276///
277/// # Errors
278///
279/// The file cannot be read, or [`scan`] rejects its contents. The message
280/// starts with the path.
281pub fn scan_file(path: &Path, target: &ScanTarget) -> Result<Report, String> {
282    let bytes = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?;
283    scan(&bytes, target).map_err(|e| format!("{}: {e}", path.display()))
284}
285
286/// Scans an x86_64 ELF64 executable or shared object against `target`: its
287/// `GLIBC_*` requirements against the glibc floor, and its imported syscall
288/// wrappers, constant `syscall()` numbers, `syscall` instructions, `madvise`
289/// advice, ABI note and `DT_RELR` use against the kernel. A side whose floor is
290/// `None` is skipped, with a `scope` finding saying so.
291///
292/// # Errors
293///
294/// The bytes are not an x86_64 ELF64 little-endian file, have no section
295/// headers, or are malformed (see [`elf::Elf::parse`]).
296pub fn scan(bytes: &[u8], target: &ScanTarget) -> Result<Report, String> {
297    let elf = elf::Elf::parse(bytes)?;
298    let mut findings = Vec::new();
299    glibc_checks(&elf, target, &mut findings);
300    kernel_checks(&elf, target, &mut findings);
301    findings.sort_by(|a, b| {
302        (a.severity, a.confidence, a.check, a.address, &a.subject).cmp(&(
303            b.severity,
304            b.confidence,
305            b.check,
306            b.address,
307            &b.subject,
308        ))
309    });
310    Ok(Report {
311        target: target.label.clone(),
312        findings,
313    })
314}
315
316fn glibc_version(v: &str) -> Option<GlibcVersion> {
317    GlibcVersion::parse(v.strip_prefix("GLIBC_")?).ok()
318}
319
320fn glibc_checks(elf: &elf::Elf, target: &ScanTarget, out: &mut Vec<Finding>) {
321    let Some(floor) = target.glibc else {
322        out.push(scope(
323            "glibc",
324            "no glibc floor selected; glibc checks skipped (host glibc)",
325        ));
326        return;
327    };
328    let mut seen = BTreeSet::new();
329    for need in &elf.needs {
330        if !seen.insert((need.file.clone(), need.version.clone())) {
331            continue;
332        }
333        let subject = format!("{} {}", need.file, need.version);
334        let (severity, detail) = if need.version == "GLIBC_ABI_DT_RELR" {
335            continue; // reported by the DT_RELR check
336        } else if need.version == "GLIBC_PRIVATE" {
337            (
338                Severity::Warning,
339                "glibc-internal interface; not stable across releases".to_string(),
340            )
341        } else if let Some(v) = glibc_version(&need.version) {
342            if v <= floor {
343                (Severity::Info, format!("within the glibc {floor} floor"))
344            } else if need.weak {
345                (
346                    Severity::Warning,
347                    format!("newer than glibc {floor}, but a weak requirement"),
348                )
349            } else {
350                (
351                    Severity::Error,
352                    format!("needs glibc {v}; the target has {floor}"),
353                )
354            }
355        } else {
356            continue; // GCC_*, GLIBCXX_*, and other libraries' versions
357        };
358        out.push(Finding {
359            severity,
360            confidence: Confidence::Exact,
361            check: "glibc-version",
362            subject,
363            detail,
364            address: None,
365        });
366    }
367    if elf.relr {
368        let ok = floor
369            >= GlibcVersion {
370                major: 2,
371                minor: 36,
372            };
373        out.push(Finding {
374            severity: if ok { Severity::Info } else { Severity::Error },
375            confidence: Confidence::Exact,
376            check: "relr",
377            subject: "DT_RELR".into(),
378            detail: if ok {
379                format!("relative relocations in RELR form; supported from glibc 2.36 (floor {floor})")
380            } else {
381                format!("relative relocations in RELR form need glibc 2.36; the target has {floor} (link without -z pack-relative-relocs)")
382            },
383            address: None,
384        });
385    }
386}
387
388fn kernel_checks(elf: &elf::Elf, target: &ScanTarget, out: &mut Vec<Finding>) {
389    let Some(kernel) = &target.kernel else {
390        out.push(scope(
391            "kernel",
392            "no kernel floor selected; kernel checks skipped (host kernel, not simulated)",
393        ));
394        return;
395    };
396    let all = syscalls();
397    let by_name: BTreeMap<&str, &kernel_abi_tools::Syscall> =
398        all.iter().map(|s| (s.name, s)).collect();
399    let by_nr: BTreeMap<u32, &kernel_abi_tools::Syscall> = all.iter().map(|s| (s.nr, s)).collect();
400    let present: BTreeSet<u32> = present_syscalls(kernel).iter().map(|s| s.nr).collect();
401    let fallback = |name: &str| {
402        if RUST_STD_FALLBACKS.contains(&name) {
403            "; Rust's standard library falls back when it returns ENOSYS (other code may not)"
404        } else {
405            ""
406        }
407    };
408
409    if let Some([a, b, c]) = elf.abi_tag {
410        let tag = KernelVersion { major: a, minor: b };
411        let (severity, detail) = if tag <= kernel.kernel {
412            (
413                Severity::Info,
414                format!(
415                    "declares Linux {a}.{b}.{c} or later; within the {} floor",
416                    kernel.kernel
417                ),
418            )
419        } else {
420            (
421                Severity::Warning,
422                format!(
423                    "declares Linux {a}.{b}.{c} or later, above the {} floor; glibc's loader refuses shared objects whose tag exceeds the running kernel",
424                    kernel.kernel
425                ),
426            )
427        };
428        out.push(Finding {
429            severity,
430            confidence: Confidence::Exact,
431            check: "abi-tag",
432            subject: "NT_GNU_ABI_TAG".into(),
433            detail,
434            address: None,
435        });
436    }
437
438    // Imported glibc wrappers for syscalls.
439    for imp in &elf.imports {
440        let syscall_name = WRAPPER_ALIASES
441            .iter()
442            .find(|(f, _)| *f == imp.name)
443            .map_or(imp.name.as_str(), |(_, s)| s);
444        let Some(sc) = by_name.get(syscall_name) else {
445            continue;
446        };
447        let version = imp
448            .version
449            .as_deref()
450            .map_or(String::new(), |v| format!("@{v}"));
451        let subject = format!("{}{version}", imp.name);
452        let (severity, detail) = if present.contains(&sc.nr) {
453            (
454                Severity::Info,
455                format!(
456                    "syscall {} (Linux {}) is available on {}",
457                    sc.name, sc.first, kernel.label
458                ),
459            )
460        } else if imp.weak {
461            (
462                Severity::Warning,
463                format!(
464                    "weak import of a wrapper for {} (Linux {}), which {} lacks; the program must handle its absence{}",
465                    sc.name, sc.first, kernel.label, fallback(sc.name)
466                ),
467            )
468        } else {
469            (
470                Severity::Error,
471                format!(
472                    "wrapper for {} (Linux {}), which {} lacks: calls fail with ENOSYS",
473                    sc.name, sc.first, kernel.label
474                ),
475            )
476        };
477        out.push(Finding {
478            severity,
479            confidence: Confidence::Exact,
480            check: "glibc-wrapper",
481            subject,
482            detail,
483            address: None,
484        });
485    }
486
487    // Machine code: calls to syscall()/madvise() and raw syscall instructions.
488    let sites = code_sites(elf);
489    let mut unresolved = [0usize; 3];
490    for site in &sites {
491        match site.kind {
492            SiteKind::SyscallFn | SiteKind::SyscallInsn => {
493                let (check, via, slot) = if site.kind == SiteKind::SyscallFn {
494                    ("syscall-call", "syscall()", 0)
495                } else {
496                    ("syscall-insn", "syscall instruction", 1)
497                };
498                let Some(nr) = site.constant else {
499                    unresolved[slot] += 1;
500                    continue;
501                };
502                let (severity, subject, detail) = match by_nr.get(&(nr as u32)) {
503                    Some(sc) if present.contains(&sc.nr) => (
504                        Severity::Info,
505                        sc.name.to_string(),
506                        format!("{via} with number {nr} (Linux {}); available", sc.first),
507                    ),
508                    Some(sc) => (
509                        Severity::Warning,
510                        sc.name.to_string(),
511                        format!(
512                            "{via} with number {nr}: {} (Linux {}) is missing on {}{}",
513                            sc.name,
514                            sc.first,
515                            kernel.label,
516                            fallback(sc.name)
517                        ),
518                    ),
519                    None => (
520                        Severity::Warning,
521                        format!("#{nr}"),
522                        format!("{via} with number {nr}, which no kernel in the data defines"),
523                    ),
524                };
525                out.push(Finding {
526                    severity,
527                    confidence: Confidence::Heuristic,
528                    check,
529                    subject,
530                    detail,
531                    address: Some(site.address),
532                });
533            }
534            SiteKind::Madvise => {
535                let Some(advice) = site.constant else {
536                    unresolved[2] += 1;
537                    continue;
538                };
539                let name = madvise_advice()
540                    .into_iter()
541                    .find(|a| a.value as u64 == advice)
542                    .map_or(format!("advice {advice}"), |a| {
543                        format!("{} ({advice})", a.name)
544                    });
545                if madvise_accepts(kernel.kernel, advice as u32) {
546                    continue; // accepted advice is not worth a line
547                }
548                out.push(Finding {
549                    severity: Severity::Warning,
550                    confidence: Confidence::Heuristic,
551                    check: "madvise",
552                    subject: name,
553                    detail: format!(
554                        "madvise() with advice that {} rejects with EINVAL",
555                        kernel.label
556                    ),
557                    address: Some(site.address),
558                });
559            }
560        }
561    }
562    for (count, what) in [
563        (unresolved[0], "syscall() calls"),
564        (unresolved[1], "syscall instructions"),
565        (unresolved[2], "madvise() calls"),
566    ] {
567        if count > 0 {
568            out.push(scope(
569                "code",
570                &format!("{count} {what} take a value not known statically; only tier 2 or 3 can check them"),
571            ));
572        }
573    }
574}
575
576fn scope(subject: &str, detail: &str) -> Finding {
577    Finding {
578        severity: Severity::Info,
579        confidence: Confidence::Exact,
580        check: "scope",
581        subject: subject.into(),
582        detail: detail.into(),
583        address: None,
584    }
585}
586
587#[derive(Clone, Copy, Debug, PartialEq, Eq)]
588enum SiteKind {
589    SyscallFn,
590    SyscallInsn,
591    Madvise,
592}
593
594#[derive(Debug)]
595struct Site {
596    kind: SiteKind,
597    address: u64,
598    constant: Option<u64>,
599}
600
601fn rel32(b: &[u8], at: usize) -> Option<i64> {
602    Some(i32::from_le_bytes(b.get(at..at + 4)?.try_into().ok()?) as i64)
603}
604
605fn imm32(b: &[u8], at: usize) -> Option<u64> {
606    Some(u32::from_le_bytes(b.get(at..at + 4)?.try_into().ok()?) as u64)
607}
608
609/// Finds the last write of a constant to a register in `window` (the bytes
610/// just before a call site). `mov r32, imm32` is `op imm32`; `mov r64,
611/// imm32` is `48 c7 modrm imm32`; `xor r32, r32` is `31/33 modrm`.
612///
613/// A `syscall` or an indirect `call` clobbers rax and the argument
614/// registers, so the search starts after the last one in the window: a
615/// constant set before it does not belong to this site.
616fn last_constant(window: &[u8], mov_op: u8, modrm: u8) -> Option<u64> {
617    let start = (0..window.len().saturating_sub(1))
618        .rev()
619        .find_map(|k| match (window[k], window[k + 1]) {
620            (0x0f, 0x05) => Some(k + 2),
621            (0xff, 0x15) => Some((k + 6).min(window.len())),
622            _ => None,
623        })
624        .unwrap_or(0);
625    let window = &window[start..];
626    let mut best: Option<(usize, u64)> = None;
627    let mut consider = |end: usize, value: u64| {
628        if best.is_none_or(|(e, _)| end > e) {
629            best = Some((end, value));
630        }
631    };
632    for j in 0..window.len() {
633        if window[j] == mov_op && j + 5 <= window.len() {
634            consider(j + 5, imm32(window, j + 1)?);
635        }
636        if window[j..].starts_with(&[0x48, 0xc7, modrm]) && j + 7 <= window.len() {
637            consider(j + 7, imm32(window, j + 3)?);
638        }
639        if (window[j] == 0x31 || window[j] == 0x33) && window.get(j + 1) == Some(&modrm) {
640            consider(j + 2, 0);
641        }
642    }
643    best.map(|(_, v)| v)
644}
645
646/// Every call to `syscall`/`madvise` through the GOT or a PLT stub, and every
647/// raw `syscall` instruction, with the constant argument when one is found.
648fn code_sites(elf: &elf::Elf) -> Vec<Site> {
649    let slots_of = |name: &str| -> BTreeSet<u64> {
650        elf.imports
651            .iter()
652            .filter(|i| i.name == name)
653            .flat_map(|i| i.slots.iter().copied())
654            .collect()
655    };
656    let syscall_slots = slots_of("syscall");
657    let madvise_slots = slots_of("madvise");
658
659    // PLT stub address -> GOT slot it jumps through.
660    let mut stubs: BTreeMap<u64, u64> = BTreeMap::new();
661    for c in elf.code.iter().filter(|c| c.name.starts_with(".plt")) {
662        let b = c.bytes;
663        for i in 0..b.len() {
664            if b[i..].starts_with(&[0xff, 0x25]) {
665                if let Some(rel) = rel32(b, i + 2) {
666                    let slot = (c.addr as i64 + i as i64 + 6 + rel) as u64;
667                    let mut start = i;
668                    if start >= 1 && b[start - 1] == 0xf2 {
669                        start -= 1; // bnd
670                    }
671                    if start >= 4 && b[start - 4..start] == [0xf3, 0x0f, 0x1e, 0xfa] {
672                        start -= 4; // endbr64
673                    }
674                    stubs.insert(c.addr + start as u64, slot);
675                }
676            }
677        }
678    }
679
680    let mut sites = Vec::new();
681    let mut seen = BTreeSet::new();
682    for c in elf.code.iter().filter(|c| !c.name.starts_with(".plt")) {
683        let b = c.bytes;
684        for i in 0..b.len().saturating_sub(1) {
685            let here = c.addr + i as u64;
686            let slot = match (b[i], b[i + 1]) {
687                (0xe8 | 0xe9, _) => rel32(b, i + 1)
688                    .map(|rel| (here as i64 + 5 + rel) as u64)
689                    .and_then(|t| stubs.get(&t).copied()),
690                (0xff, 0x15 | 0x25) => rel32(b, i + 2).map(|rel| (here as i64 + 6 + rel) as u64),
691                _ => None,
692            };
693            let window = &b[i.saturating_sub(LOOKBACK)..i];
694            let kind = match slot {
695                Some(s) if syscall_slots.contains(&s) => {
696                    Some((SiteKind::SyscallFn, last_constant(window, 0xbf, 0xff)))
697                }
698                Some(s) if madvise_slots.contains(&s) => {
699                    Some((SiteKind::Madvise, last_constant(window, 0xba, 0xd2)))
700                }
701                _ if b[i] == 0x0f && b[i + 1] == 0x05 => {
702                    // A raw syscall needs its number in eax just before it;
703                    // without one the two bytes are likely part of another
704                    // instruction, so they are not counted.
705                    last_constant(window, 0xb8, 0xc0)
706                        .filter(|nr| *nr < 1024)
707                        .map(|nr| (SiteKind::SyscallInsn, Some(nr)))
708                }
709                _ => None,
710            };
711            if let Some((kind, constant)) = kind {
712                if seen.insert(here) {
713                    sites.push(Site {
714                        kind,
715                        address: here,
716                        constant,
717                    });
718                }
719            }
720        }
721    }
722    sites
723}
724
725#[cfg(test)]
726mod tests;