Skip to main content

isr_cache/
lib.rs

1//! # Opinionated cache for OS kernel profiles
2//!
3//! This crate provides a caching mechanism for profiles generated and used by
4//! the [`isr`] crate family. It offers several features to streamline the process
5//! of accessing and managing symbol information, including methods for
6//! downloading necessary debug symbols for Windows (PDB files) and Linux
7//! (DWARF debug info and system map).
8//!
9//! ## Usage
10//!
11//! The main component of this crate is the [`IsrCache`] struct.
12//!
13//! Example of loading a profile from a PDB file using the CodeView information:
14//!
15//! ```rust,no_run
16//! use isr_cache::{CodeView, IsrCache};
17//!
18//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
19//! // Create a new cache instance.
20//! let cache = IsrCache::new("cache")?;
21//!
22//! // Use the CodeView information of the Windows 10.0.18362.356 kernel.
23//! let codeview = CodeView {
24//!     name: String::from("ntkrnlmp.pdb"),
25//!     guid: String::from("ce7ffb00c20b87500211456b3e905c47"),
26//!     age: 1,
27//! };
28//!
29//! // Fetch and create (or get existing) the entry.
30//! let entry = cache.entry_from_codeview(codeview)?;
31//!
32//! // Get the profile from the entry.
33//! let profile = entry.profile()?;
34//! # Ok(())
35//! # }
36//! ```
37//!
38//! Example of loading a profile based on a Linux kernel banner:
39//!
40//! ```rust,no_run
41//! use isr_cache::IsrCache;
42//!
43//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
44//! // Create a new cache instance.
45//! let cache = IsrCache::new("cache")?;
46//!
47//! // Use the Linux banner of the Ubuntu 6.8.0-40.40~22.04.3-generic kernel.
48//! let banner = "Linux version 6.8.0-40-generic \
49//!               (buildd@lcy02-amd64-078) \
50//!               (x86_64-linux-gnu-gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04) \
51//!               12.3.0, GNU ld (GNU Binutils for Ubuntu) 2.38) \
52//!               #40~22.04.3-Ubuntu SMP PREEMPT_DYNAMIC \
53//!               Tue Jul 30 17:30:19 UTC 2 \
54//!               (Ubuntu 6.8.0-40.40~22.04.3-generic 6.8.12)";
55//!
56//! // Fetch and create (or get existing) the entry.
57//! // Note that the download of Linux debug symbols may take a while.
58//! let entry = cache.entry_from_linux_banner(banner)?;
59//!
60//! // Get the profile from the entry.
61//! let profile = entry.profile()?;
62//! # Ok(())
63//! # }
64//! ```
65//!
66//! Consult the [`vmi`] crate for more information on how to download debug
67//! symbols for introspected VMs.
68//!
69//! [`isr`]: ../isr/index.html
70//! [`vmi`]: ../vmi/index.html
71
72mod error;
73
74#[cfg(any(feature = "linux", feature = "windows"))]
75use std::cell::OnceCell;
76use std::{
77    fs::File,
78    io::{BufWriter, Write},
79    path::{Path, PathBuf},
80    sync::Arc,
81};
82
83pub use isr_core::Profile;
84pub use isr_dl::{ProgressContext, ProgressEvent, ProgressFn, ProgressWriter};
85#[cfg(feature = "linux")]
86pub use isr_dl_linux::{
87    ArtifactPolicy, FilenamePolicy, LinuxBanner, LinuxVersionSignature, UbuntuSymbolDownloader,
88    UbuntuSymbolPaths, UbuntuSymbolRequest, UbuntuVersionSignature,
89};
90#[cfg(feature = "windows")]
91pub use isr_dl_windows::{CodeView, ImageSignature, SymbolDownloader, SymbolRequest};
92use memmap2::Mmap;
93use rkyv::ser::{Serializer, allocator::Arena, writer::IoWriter};
94
95pub use self::error::Error;
96
97/// File extension of cached rkyv-serialized [`Profile`]s.
98pub const PROFILE_FILE_EXTENSION: &str = "isr";
99
100/// An entry in the [`IsrCache`].
101pub struct Entry {
102    /// The path to the profile.
103    profile_path: PathBuf,
104
105    /// The raw profile data.
106    data: Mmap,
107}
108
109impl Entry {
110    /// Creates a new entry from the profile path.
111    pub fn new(profile_path: PathBuf) -> Result<Self, Error> {
112        let data = unsafe { Mmap::map(&File::open(&profile_path)?)? };
113        Ok(Self { profile_path, data })
114    }
115
116    /// Returns the path to the profile.
117    pub fn profile_path(&self) -> &Path {
118        &self.profile_path
119    }
120
121    /// Decodes the profile from the entry.
122    pub fn profile(&self) -> Result<Profile<'_>, Error> {
123        let archived = rkyv::access::<_, rkyv::rancor::Error>(&self.data)?;
124        Ok(Profile::from_archived(archived))
125    }
126
127    /// Decodes the profile without validating the archived bytes.
128    ///
129    /// # Safety
130    ///
131    /// The caller must guarantee that `self.data` is a valid rkyv archive of
132    /// [`isr_core::schema::Profile`].
133    pub unsafe fn profile_unchecked(&self) -> Result<Profile<'_>, Error> {
134        let archived = unsafe { rkyv::access_unchecked(&self.data) };
135        Ok(Profile::from_archived(archived))
136    }
137
138    /// Deserializes the profile and re-serializes it as JSON.
139    #[cfg(feature = "json")]
140    pub fn to_json(&self) -> Result<serde_json::Value, Error> {
141        let archived =
142            rkyv::access::<isr_core::schema::ArchivedProfile, rkyv::rancor::Error>(&self.data)?;
143        let deserialized =
144            rkyv::deserialize::<isr_core::schema::Profile, rkyv::rancor::Error>(archived)?;
145
146        Ok(serde_json::to_value(&deserialized)?)
147    }
148
149    /// Encodes a profile.
150    #[allow(unused)]
151    fn encode(writer: impl Write, profile: &isr_core::schema::Profile) -> Result<(), Error> {
152        let writer = BufWriter::new(writer);
153        let mut writer = IoWriter::new(writer);
154        let mut arena = Arena::new();
155
156        let mut serializer = Serializer::new(&mut writer, arena.acquire(), ());
157        rkyv::api::serialize_using::<_, rkyv::rancor::Error>(profile, &mut serializer)?;
158
159        Ok(())
160    }
161}
162
163/// A cache for OS kernel profiles.
164///
165/// Manages the download and extraction of necessary debug symbols.
166pub struct IsrCache {
167    #[cfg(feature = "linux")]
168    ubuntu_downloader: OnceCell<UbuntuSymbolDownloader>,
169
170    #[cfg(feature = "windows")]
171    symbol_downloader: OnceCell<SymbolDownloader>,
172
173    /// The directory where cached profiles are stored.
174    #[allow(unused)]
175    output_directory: PathBuf,
176
177    /// Optional progress callback for download and extraction operations.
178    #[allow(unused)]
179    progress: Option<ProgressFn>,
180
181    /// If true, the cache will not attempt to download any files and will only
182    /// use existing cached profiles.
183    #[allow(unused)]
184    offline: bool,
185}
186
187impl IsrCache {
188    /// Creates a new `IsrCache` instance, initializing it with the provided
189    /// directory. If the directory doesn't exist, it attempts to create it.
190    pub fn new(output_directory: impl Into<PathBuf>) -> Result<Self, Error> {
191        let output_directory = output_directory.into();
192        std::fs::create_dir_all(&output_directory)?;
193
194        Ok(Self {
195            #[cfg(feature = "linux")]
196            ubuntu_downloader: OnceCell::new(),
197            #[cfg(feature = "windows")]
198            symbol_downloader: OnceCell::new(),
199
200            output_directory,
201            progress: None,
202            offline: false,
203        })
204    }
205
206    /// Sets a progress callback for download and extraction operations.
207    pub fn with_progress(self, f: impl Fn(ProgressEvent<'_>) + Send + Sync + 'static) -> Self {
208        Self {
209            progress: Some(Arc::new(f)),
210            ..self
211        }
212    }
213
214    /// Enables or disables offline mode.
215    ///
216    /// In offline mode the cache only uses already-downloaded artifacts and
217    /// never reaches out to the network.
218    pub fn with_offline(self, offline: bool) -> Self {
219        Self { offline, ..self }
220    }
221
222    /// Overrides the default [`UbuntuSymbolDownloader`].
223    #[cfg(feature = "linux")]
224    pub fn with_ubuntu_downloader(self, ubuntu_downloader: UbuntuSymbolDownloader) -> Self {
225        Self {
226            ubuntu_downloader: OnceCell::from(ubuntu_downloader),
227            ..self
228        }
229    }
230
231    /// Returns the [`UbuntuSymbolDownloader`], lazily initializing it.
232    #[cfg(feature = "linux")]
233    pub fn ubuntu_downloader(&self) -> &UbuntuSymbolDownloader {
234        self.ubuntu_downloader.get_or_init(|| {
235            UbuntuSymbolDownloader::builder()
236                .output_directory(self.output_directory.join("ubuntu"))
237                .maybe_progress(self.progress.clone())
238                .build()
239        })
240    }
241
242    /// Overrides the default [`SymbolDownloader`].
243    #[cfg(feature = "windows")]
244    pub fn with_symbol_downloader(self, symbol_downloader: SymbolDownloader) -> Self {
245        Self {
246            symbol_downloader: OnceCell::from(symbol_downloader),
247            ..self
248        }
249    }
250
251    /// Returns the [`SymbolDownloader`], lazily initializing it.
252    #[cfg(feature = "windows")]
253    pub fn symbol_downloader(&self) -> &SymbolDownloader {
254        self.symbol_downloader.get_or_init(|| {
255            SymbolDownloader::builder()
256                .output_directory(self.output_directory.join("windows"))
257                .maybe_progress(self.progress.clone())
258                .build()
259        })
260    }
261
262    /// Creates or retrieves a cached profile based on a Linux kernel banner.
263    ///
264    /// Parses the banner to determine the kernel version and downloads the
265    /// necessary debug symbols and system map if not present in the cache.
266    /// Generates and stores the profile, returning its path.
267    #[cfg(feature = "linux")]
268    pub fn entry_from_linux_banner(&self, linux_banner: &str) -> Result<Entry, Error> {
269        let banner = linux_banner
270            .parse::<LinuxBanner>()
271            .map_err(isr_dl::Error::from)?;
272
273        let output_paths = match banner.version_signature {
274            Some(LinuxVersionSignature::Ubuntu(version_signature)) => {
275                self.download_from_ubuntu_version_signature(version_signature)?
276            }
277            _ => {
278                // Create a synthetic downloader error.
279                return Err(Error::Downloader(isr_dl::Error::Other(Box::new(
280                    isr_dl_linux::DownloaderError::InvalidBanner,
281                ))));
282            }
283        };
284
285        let output_directory = output_paths.output_directory;
286        let profile_path = output_directory
287            .join("profile")
288            .with_extension(PROFILE_FILE_EXTENSION);
289
290        with_part_file(profile_path, |profile_file| {
291            let kernel_file = File::open(output_directory.join("vmlinux-dbgsym"))?;
292            let systemmap_file = File::open(output_directory.join("System.map"))?;
293            isr_dwarf::create_profile(kernel_file, systemmap_file, |profile| {
294                Entry::encode(profile_file, profile)
295            })?;
296
297            Ok(())
298        })
299    }
300
301    /// Downloads and extracts the kernel image, debug symbols, and
302    /// `System.map` for the given Ubuntu version signature.
303    ///
304    /// Returns an [`UbuntuSymbolPaths`] with the output directory and the
305    /// per-artifact paths.
306    #[cfg(feature = "linux")]
307    pub fn download_from_ubuntu_version_signature(
308        &self,
309        version_signature: UbuntuVersionSignature,
310    ) -> Result<UbuntuSymbolPaths, Error> {
311        let request = UbuntuSymbolRequest::builder()
312            .version_signature(version_signature)
313            .linux_image(
314                ArtifactPolicy::builder()
315                    // <cache>/ubuntu/6.8.0-40.40~22.04.3-generic/linux-image.deb
316                    .deb(FilenamePolicy::custom("linux-image.deb"))
317                    // <cache>/ubuntu/6.8.0-40.40~22.04.3-generic/vmlinuz
318                    .extract(FilenamePolicy::custom("vmlinuz"))
319                    .build(),
320            )
321            .linux_image_dbgsym(
322                ArtifactPolicy::builder()
323                    // <cache>/ubuntu/6.8.0-40.40~22.04.3-generic/linux-image-dbgsym.deb
324                    .deb(FilenamePolicy::custom("linux-image-dbgsym.deb"))
325                    // <cache>/ubuntu/6.8.0-40.40~22.04.3-generic/vmlinux-dbgsym
326                    .extract(FilenamePolicy::custom("vmlinux-dbgsym"))
327                    .build(),
328            )
329            .linux_modules(
330                ArtifactPolicy::builder()
331                    // <cache>/ubuntu/6.8.0-40.40~22.04.3-generic/linux-modules.deb
332                    .deb(FilenamePolicy::custom("linux-modules.deb"))
333                    // <cache>/ubuntu/6.8.0-40.40~22.04.3-generic/System.map
334                    .extract(FilenamePolicy::custom("System.map"))
335                    .build(),
336            )
337            .build();
338
339        if let Some(paths) = self.ubuntu_downloader().lookup(&request) {
340            return Ok(paths);
341        }
342
343        if self.offline {
344            return Err(Error::Downloader(isr_dl::Error::ArtifactNotFound));
345        }
346
347        let paths = self.ubuntu_downloader().download(request)?;
348
349        Ok(paths)
350    }
351
352    /// Creates or retrieves a cached profile from a [`CodeView`] debug
353    /// information structure.
354    ///
355    /// If a profile for the given `CodeView` information already exists in
356    /// the cache, its path is returned. Otherwise, the necessary PDB file is
357    /// downloaded, the profile is generated and stored in the cache, and its
358    /// path is returned.
359    #[cfg(feature = "windows")]
360    pub fn entry_from_codeview(&self, codeview: CodeView) -> Result<Entry, Error> {
361        // <cache>/windows/ntkrnlmp.pdb/ce7ffb00c20b87500211456b3e905c471
362        let output_directory = self
363            .output_directory
364            .join("windows")
365            .join(codeview.subdirectory());
366
367        // <cache>/windows/ntkrnlmp.pdb/ce7ffb00c20b87500211456b3e905c471/ntkrnlmp.pdb
368        let pdb_path = self.download_from_codeview(codeview)?;
369
370        // <cache>/windows/ntkrnlmp.pdb/ce7ffb00c20b87500211456b3e905c471/profile.isr
371        let profile_path = output_directory
372            .join("profile")
373            .with_extension(PROFILE_FILE_EXTENSION);
374
375        with_part_file(profile_path, |profile_file| {
376            let pdb_file = File::open(&pdb_path)?;
377            isr_pdb::create_profile(pdb_file, |profile| Entry::encode(profile_file, profile))?;
378
379            Ok(())
380        })
381    }
382
383    /// Creates or retrieves a cached profile from a PE file.
384    ///
385    /// Extracts the [`CodeView`] debug information from the PE file and
386    /// delegates to [`entry_from_codeview`].
387    ///
388    /// [`entry_from_codeview`]: Self::entry_from_codeview
389    #[cfg(feature = "windows")]
390    pub fn entry_from_pe(&self, path: impl AsRef<Path>) -> Result<Entry, Error> {
391        self.entry_from_codeview(CodeView::from_path(path).map_err(isr_dl::Error::from)?)
392    }
393
394    /// Downloads or retrieves a cached PDB from its [`CodeView`] record.
395    #[cfg(feature = "windows")]
396    pub fn download_from_codeview(&self, codeview: CodeView) -> Result<PathBuf, Error> {
397        let request = codeview.into();
398
399        if let Some(pdb_path) = self.symbol_downloader().lookup(&request) {
400            tracing::debug!(path = %pdb_path.display(), "found cached PE image");
401            return Ok(pdb_path);
402        }
403
404        if self.offline {
405            return Err(Error::Downloader(isr_dl::Error::ArtifactNotFound));
406        }
407
408        // <cache>/windows/ntkrnlmp.pdb/ce7ffb00c20b87500211456b3e905c471/ntkrnlmp.pdb
409        let pdb_path = self.symbol_downloader().download(request)?;
410
411        Ok(pdb_path)
412    }
413
414    /// Downloads or retrieves a cached PE binary from its [`ImageSignature`].
415    ///
416    /// PE binaries are cached at:
417    /// `<cache>/windows/<name>/<timestamp><size_of_image>/<name>`
418    ///
419    /// Returns the path to the cached binary.
420    #[cfg(feature = "windows")]
421    pub fn download_from_image_signature(
422        &self,
423        image_signature: ImageSignature,
424    ) -> Result<PathBuf, Error> {
425        let request = image_signature.into();
426
427        if let Some(image_path) = self.symbol_downloader().lookup(&request) {
428            tracing::debug!(path = %image_path.display(), "found cached PE image");
429            return Ok(image_path);
430        }
431
432        if self.offline {
433            return Err(Error::Downloader(isr_dl::Error::ArtifactNotFound));
434        }
435
436        // <cache>/windows/ntoskrnl.exe/7D02613E1047000/ntoskrnl.exe
437        let image_path = self.symbol_downloader().download(request)?;
438
439        Ok(image_path)
440    }
441}
442
443/// Runs `f` against a sibling `.part` file, then renames it over `dest`.
444#[allow(unused)]
445fn with_part_file<F>(profile_path: PathBuf, f: F) -> Result<Entry, Error>
446where
447    F: FnOnce(File) -> Result<(), Error>,
448{
449    if profile_path.exists() {
450        tracing::debug!(
451            profile_path = %profile_path.display(),
452            "profile already exists"
453        );
454
455        return Entry::new(profile_path);
456    }
457
458    let tmp = profile_path.with_added_extension("part");
459    let file = File::create(&tmp)?;
460    f(file)?;
461    std::fs::rename(&tmp, &profile_path)?;
462
463    Entry::new(profile_path)
464}