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}