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
16//! use isr::{
17//!     download::pdb::CodeView,
18//!     cache::{IsrCache, JsonCodec},
19//! };
20//!
21//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
22//! # std::env::set_current_dir("../..")?;
23//! // Create a new cache instance.
24//! let cache = IsrCache::<JsonCodec>::new("cache")?;
25//!
26//! // Use the CodeView information of the Windows 10.0.18362.356 kernel.
27//! let codeview = CodeView {
28//!     path: String::from("ntkrnlmp.pdb"),
29//!     guid: String::from("ce7ffb00c20b87500211456b3e905c471"),
30//! };
31//!
32//! // Fetch and create (or get existing) the entry.
33//! let entry = cache.entry_from_codeview(codeview)?;
34//!
35//! // Get the profile from the entry.
36//! let profile = entry.profile()?;
37//! # Ok(())
38//! # }
39//! ```
40//!
41//! Example of loading a profile based on a Linux kernel banner:
42//!
43//! ```rust
44//! use isr::cache::{IsrCache, JsonCodec};
45//!
46//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
47//! # std::env::set_current_dir("../..")?;
48//! // Create a new cache instance.
49//! let cache = IsrCache::<JsonCodec>::new("cache")?;
50//!
51//! // Use the Linux banner of the Ubuntu 6.8.0-40.40~22.04.3-generic kernel.
52//! let banner = "Linux version 6.8.0-40-generic \
53//!               (buildd@lcy02-amd64-078) \
54//!               (x86_64-linux-gnu-gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04) \
55//!               12.3.0, GNU ld (GNU Binutils for Ubuntu) 2.38) \
56//!               #40~22.04.3-Ubuntu SMP PREEMPT_DYNAMIC \
57//!               Tue Jul 30 17:30:19 UTC 2 \
58//!               (Ubuntu 6.8.0-40.40~22.04.3-generic 6.8.12)";
59//!
60//! // Fetch and create (or get existing) the entry.
61//! // Note that the download of Linux debug symbols may take a while.
62//! let entry = cache.entry_from_linux_banner(banner)?;
63//!
64//! // Get the profile from the entry.
65//! let profile = entry.profile()?;
66//! # Ok(())
67//! # }
68//! ```
69//!
70//! Consult the [`vmi`] crate for more information on how to download debug
71//! symbols for introspected VMs.
72//!
73//! [`isr`]: ../isr/index.html
74//! [`vmi`]: ../vmi/index.html
75
76mod codec;
77mod error;
78
79use std::{
80    fs::File,
81    path::{Path, PathBuf},
82};
83
84pub use isr_core::Profile;
85pub use isr_dl_linux::{
86    LinuxBanner, LinuxVersionSignature, UbuntuDownloader, UbuntuVersionSignature,
87};
88pub use isr_dl_pdb::{CodeView, PdbDownloader};
89use memmap2::Mmap;
90
91pub use self::{
92    codec::{BincodeCodec, Codec, JsonCodec, MsgpackCodec},
93    error::Error,
94};
95
96/// An entry in the [`IsrCache`].
97pub struct Entry<C>
98where
99    C: Codec,
100{
101    /// The path to the profile.
102    profile_path: PathBuf,
103
104    /// The raw profile data.
105    data: Mmap,
106
107    /// The codec used to encode and decode the profile.
108    _codec: std::marker::PhantomData<C>,
109}
110
111impl<C> Entry<C>
112where
113    C: Codec,
114{
115    /// Creates a new entry from the profile path.
116    pub fn new(profile_path: PathBuf) -> Result<Self, Error> {
117        let data = unsafe { Mmap::map(&File::open(&profile_path)?)? };
118        Ok(Self {
119            profile_path,
120            data,
121            _codec: std::marker::PhantomData,
122        })
123    }
124
125    /// Returns the path to the profile.
126    pub fn profile_path(&self) -> &Path {
127        &self.profile_path
128    }
129
130    /// Returns the raw profile data.
131    pub fn data(&self) -> &[u8] {
132        &self.data
133    }
134
135    /// Decodes the profile from the entry.
136    pub fn profile(&self) -> Result<Profile<'_>, C::DecodeError> {
137        C::decode(&self.data)
138    }
139}
140
141/// A cache for OS kernel profiles.
142///
143/// Manages the download and extraction of necessary debug symbols.
144/// Uses a [`Codec`] to encode and decode profiles. The default codec is
145/// [`JsonCodec`].
146pub struct IsrCache<C = JsonCodec>
147where
148    C: Codec,
149{
150    /// The directory where cached profiles are stored.
151    directory: PathBuf,
152
153    /// The codec used to encode and decode profiles.
154    _codec: std::marker::PhantomData<C>,
155}
156
157impl<C> IsrCache<C>
158where
159    C: Codec,
160{
161    /// Creates a new `IsrCache` instance, initializing it with the provided
162    /// directory. If the directory doesn't exist, it attempts to create it.
163    pub fn new(directory: impl Into<PathBuf>) -> Result<Self, Error> {
164        let directory = directory.into();
165        std::fs::create_dir_all(&directory)?;
166
167        Ok(Self {
168            directory,
169            _codec: std::marker::PhantomData,
170        })
171    }
172
173    /// Creates or retrieves a cached profile from a [`CodeView`] debug
174    /// information structure.
175    ///
176    /// If a profile for the given `CodeView` information already exists in
177    /// the cache, its path is returned. Otherwise, the necessary PDB file is
178    /// downloaded, the profile is generated and stored in the cache, and its
179    /// path is returned.
180    #[cfg(feature = "pdb")]
181    pub fn entry_from_codeview(&self, codeview: CodeView) -> Result<Entry<C>, Error> {
182        let path = Path::new(&codeview.path);
183
184        // <cache>/windows/ntkrnlmp.pdb/3844dbb920174967be7aa4a2c20430fa2
185        let destination = self
186            .directory
187            .join("windows")
188            .join(path)
189            .join(&codeview.guid);
190
191        std::fs::create_dir_all(&destination)?;
192
193        // <cache>/windows/ntkrnlmp.pdb/3844dbb920174967be7aa4a2c20430fa2/ntkrnlmp.pdb
194        let pdb_path = destination.join(path);
195        if !pdb_path.exists() {
196            PdbDownloader::new(codeview.clone())
197                .with_output(&pdb_path)
198                .download()?;
199        }
200
201        // <cache>/windows/ntkrnlmp.pdb/3844dbb920174967be7aa4a2c20430fa2/profile<.ext>
202        let profile_path = destination.join("profile").with_extension(C::EXTENSION);
203
204        match File::create_new(&profile_path) {
205            Ok(profile_file) => {
206                let pdb_file = File::open(&pdb_path)?;
207                isr_pdb::create_profile(pdb_file, |profile| C::encode(profile_file, profile))?;
208            }
209            Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
210                tracing::info!(?profile_path, "profile already exists");
211            }
212            Err(err) => return Err(err.into()),
213        }
214
215        Entry::new(profile_path)
216    }
217
218    /// Creates or retrieves a cached profile from a PE file.
219    ///
220    /// Extracts the [`CodeView`] debug information from the PE file and
221    /// delegates to [`entry_from_codeview`].
222    ///
223    /// [`entry_from_codeview`]: Self::entry_from_codeview
224    #[cfg(feature = "pdb")]
225    pub fn entry_from_pe(&self, path: impl AsRef<Path>) -> Result<Entry<C>, Error> {
226        self.entry_from_codeview(CodeView::from_path(path).map_err(isr_dl_pdb::Error::from)?)
227    }
228
229    /// Creates or retrieves a cached profile based on a Linux kernel banner.
230    ///
231    /// Parses the banner to determine the kernel version and downloads the
232    /// necessary debug symbols and system map if not present in the cache.
233    /// Generates and stores the profile, returning its path.
234    #[cfg(feature = "linux")]
235    pub fn entry_from_linux_banner(&self, linux_banner: &str) -> Result<Entry<C>, Error> {
236        let banner = match LinuxBanner::parse(linux_banner) {
237            Some(banner) => banner,
238            None => return Err(Error::InvalidBanner),
239        };
240
241        let destination_path = match banner.version_signature {
242            Some(LinuxVersionSignature::Ubuntu(version_signature)) => {
243                self.download_from_ubuntu_version_signature(version_signature)?
244            }
245            _ => return Err(Error::InvalidBanner),
246        };
247
248        let profile_path = destination_path
249            .join("profile")
250            .with_extension(C::EXTENSION);
251
252        match File::create_new(&profile_path) {
253            Ok(profile_file) => {
254                let kernel_file = File::open(destination_path.join("vmlinux-dbgsym"))?;
255                let systemmap_file = File::open(destination_path.join("System.map"))?;
256                isr_dwarf::create_profile(kernel_file, systemmap_file, |profile| {
257                    C::encode(profile_file, profile)
258                })?;
259            }
260            Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
261                tracing::info!(?profile_path, "profile already exists");
262            }
263            Err(err) => return Err(err.into()),
264        }
265
266        Entry::new(profile_path)
267    }
268
269    /// Downloads and extracts the required debug symbols from the Ubuntu
270    /// repositories based on the Ubuntu version signature in the Linux banner.
271    ///
272    /// Returns the path to the directory containing the downloaded and
273    /// extracted files.
274    #[cfg(feature = "linux")]
275    fn download_from_ubuntu_version_signature(
276        &self,
277        version_signature: UbuntuVersionSignature,
278    ) -> Result<PathBuf, isr_dl_linux::Error> {
279        let UbuntuVersionSignature {
280            release,
281            revision,
282            kernel_flavour,
283            ..
284        } = version_signature;
285
286        // <cache>/ubuntu
287        let downloader = UbuntuDownloader::new(&release, &revision, &kernel_flavour)
288            .with_output_directory(self.directory.join("ubuntu"));
289
290        // <cache>/ubuntu/6.8.0-40.40~22.04.3-generic
291        let destination_path = downloader.destination_path();
292
293        // Download only what's necessary.
294
295        // <cache>/ubuntu/6.8.0-40.40~22.04.3-generic/linux-image.deb
296        // <cache>/ubuntu/6.8.0-40.40~22.04.3-generic/vmlinuz
297        let downloader = match destination_path.join("linux-image.deb").exists() {
298            false => downloader
299                .download_linux_image_as("linux-image.deb")
300                .extract_linux_image_as("vmlinuz"),
301            true => {
302                tracing::info!("linux-image.deb already exists");
303                downloader
304            }
305        };
306
307        // <cache>/ubuntu/6.8.0-40.40~22.04.3-generic/linux-image-dbgsym.deb
308        // <cache>/ubuntu/6.8.0-40.40~22.04.3-generic/vmlinux-dbgsym
309        let downloader = match destination_path.join("linux-image-dbgsym.deb").exists() {
310            false => downloader
311                .download_linux_image_dbgsym_as("linux-image-dbgsym.deb")
312                .extract_linux_image_dbgsym_as("vmlinux-dbgsym"),
313            true => {
314                tracing::info!("linux-image-dbgsym.deb already exists");
315                downloader
316            }
317        };
318
319        // <cache>/ubuntu/6.8.0-40.40~22.04.3-generic/linux-modules.deb
320        // <cache>/ubuntu/6.8.0-40.40~22.04.3-generic/System.map
321        let downloader = match destination_path.join("linux-modules.deb").exists() {
322            false => downloader
323                .download_linux_modules_as("linux-modules.deb")
324                .extract_systemmap_as("System.map"),
325            true => {
326                tracing::info!("linux-modules.deb already exists");
327                downloader
328            }
329        };
330
331        match downloader.skip_existing().download() {
332            Ok(_paths) => (),
333            // UbuntuDownloader::download() returns Err(InvalidOptions) if
334            // there's nothing to download.
335            Err(isr_dl_linux::ubuntu::Error::InvalidOptions) => {
336                tracing::info!("nothing to download");
337            }
338            Err(err) => return Err(err.into()),
339        }
340
341        Ok(destination_path)
342    }
343}