Skip to main content

intel_crashlog/source/pmt/
sysfs.rs

1// Copyright (C) 2026 Intel Corporation
2// SPDX-License-Identifier: MIT
3
4use super::PmtDeviceId;
5use super::bdf::PciBdf;
6use crate::CrashLog;
7use crate::error::Error;
8use crate::metadata::Time;
9use crate::region::Region;
10use crate::source::{Capabilities, Capability};
11use std::collections::BTreeSet;
12#[cfg(feature = "control_commands")]
13use std::io::Write;
14use std::path::{Path, PathBuf};
15
16pub(super) struct PmtSysFs {
17    root: PathBuf,
18}
19
20pub(super) struct PmtSysFsEndpoint {
21    path: PathBuf,
22}
23
24impl Default for PmtSysFs {
25    fn default() -> Self {
26        Self::new(Path::new("/"))
27    }
28}
29
30impl PmtSysFs {
31    pub fn new(path: &Path) -> Self {
32        Self {
33            root: path.to_owned(),
34        }
35    }
36
37    fn class_path(&self) -> PathBuf {
38        let mut path = self.root.clone();
39        path.push("sys");
40        path.push("class");
41        path.push("intel_pmt");
42        path
43    }
44
45    fn pci_dev_path(&self) -> PathBuf {
46        let mut path = self.root.clone();
47        path.push("sys");
48        path.push("bus");
49        path.push("pci");
50        path.push("devices");
51        path
52    }
53
54    fn discover_from_class(&self) -> Vec<PmtDeviceId> {
55        let path = self.class_path();
56
57        std::fs::read_dir(&path)
58            .inspect_err(|err| log::warn!("Cannot read {}: {err}", path.display()))
59            .ok()
60            .map(|read_dir| {
61                read_dir
62                    .filter_map(|entry| entry.ok())
63                    .filter_map(|entry| {
64                        let name = entry.file_name();
65                        let entry_name = name.to_str().unwrap_or_default();
66
67                        if entry_name.trim_end_matches(char::is_numeric) == "crashlog" {
68                            Some(PmtDeviceId::Name(entry_name.to_string()))
69                        } else {
70                            None
71                        }
72                    })
73                    .collect()
74            })
75            .unwrap_or_default()
76    }
77
78    fn has_crashlog_vsec(path: &Path) -> bool {
79        match std::fs::read_dir(path) {
80            Err(err) => log::warn!("Cannot read {}: {err}", path.display()),
81            Ok(entries) => {
82                for entry in entries {
83                    let Ok(entry) = entry else { continue };
84                    let filename = entry.file_name();
85                    let Some(filename) = filename.to_str() else {
86                        continue;
87                    };
88                    if filename.trim_end_matches(char::is_numeric) == "intel_vsec.crashlog." {
89                        return true;
90                    }
91                }
92            }
93        }
94
95        false
96    }
97
98    fn discover_from_pci(&self) -> Vec<PmtDeviceId> {
99        let path = self.pci_dev_path();
100        let mut devices = BTreeSet::default();
101
102        match std::fs::read_dir(&path) {
103            Err(err) => log::warn!("Cannot read {}: {err}", path.display()),
104            Ok(entries) => {
105                for entry in entries {
106                    let Ok(entry) = entry else {
107                        continue;
108                    };
109                    let filename = entry.file_name();
110                    let Some(filename) = filename.to_str() else {
111                        continue;
112                    };
113                    let Ok(bdf) = filename.parse() else {
114                        continue;
115                    };
116
117                    if Self::has_crashlog_vsec(&entry.path()) {
118                        devices.insert(PmtDeviceId::Bdf(bdf));
119                    }
120                }
121            }
122        }
123
124        devices.into_iter().collect()
125    }
126
127    pub fn discover(&self) -> Vec<PmtDeviceId> {
128        let mut devices = self.discover_from_class();
129        devices.extend(self.discover_from_pci());
130        devices
131    }
132
133    pub fn get_all_endpoints(&self) -> Vec<PmtSysFsEndpoint> {
134        self.discover()
135            .into_iter()
136            .filter(|dev| !dev.is_alias())
137            .flat_map(|devid| self.get_endpoints(&devid))
138            .collect()
139    }
140
141    fn get_endpoints_from_vsec(path: &Path) -> Vec<PmtSysFsEndpoint> {
142        let mut pmt_path = path.to_owned();
143        pmt_path.push("intel_pmt");
144        let mut endpoints = Vec::default();
145
146        match std::fs::read_dir(&pmt_path) {
147            Err(err) => log::warn!("Cannot read {}: {err}", path.display()),
148            Ok(entries) => {
149                for entry in entries {
150                    let Ok(entry) = entry else {
151                        continue;
152                    };
153                    let filename = entry.file_name();
154                    let Some(filename) = filename.to_str() else {
155                        continue;
156                    };
157                    if filename.trim_end_matches(char::is_numeric) == "crashlog"
158                        && let Some(endpoint) = PmtSysFsEndpoint::new(&entry.path())
159                    {
160                        endpoints.push(endpoint);
161                    }
162                }
163            }
164        }
165
166        endpoints
167    }
168
169    fn get_endpoints_from_bdf(&self, bdf: &PciBdf) -> Vec<PmtSysFsEndpoint> {
170        let mut endpoints = Vec::default();
171        let mut path = self.pci_dev_path();
172        path.push(bdf.to_string());
173
174        match std::fs::read_dir(&path) {
175            Err(err) => log::error!("Cannot read {}: {err}", path.display()),
176            Ok(entries) => {
177                for entry in entries {
178                    let Ok(entry) = entry else {
179                        continue;
180                    };
181                    let filename = entry.file_name();
182                    let Some(filename) = filename.to_str() else {
183                        continue;
184                    };
185                    if filename.trim_end_matches(char::is_numeric) == "intel_vsec.crashlog." {
186                        endpoints.extend(Self::get_endpoints_from_vsec(&entry.path()));
187                    }
188                }
189            }
190        }
191
192        endpoints
193    }
194
195    pub fn get_endpoints(&self, id: &PmtDeviceId) -> Vec<PmtSysFsEndpoint> {
196        match id {
197            PmtDeviceId::Name(name) => {
198                let mut path = self.class_path();
199                path.push(name);
200
201                let mut endpoints = Vec::new();
202                if let Some(endpoint) = PmtSysFsEndpoint::new(&path) {
203                    endpoints.push(endpoint);
204                }
205                endpoints
206            }
207            PmtDeviceId::Bdf(bdf) => self.get_endpoints_from_bdf(bdf),
208        }
209    }
210
211    pub fn extract(&self, dev: &PmtDeviceId) -> Result<Vec<CrashLog>, Error> {
212        let mut crashlogs = Vec::new();
213
214        for endpoint in self.get_endpoints(dev) {
215            match endpoint.extract() {
216                Ok(mut crashlog) => {
217                    crashlog.metadata.time = Time::now();
218                    crashlogs.push(crashlog);
219                }
220                Err(Error::EmptyRegion) => (),
221                Err(err) => return Err(err),
222            }
223        }
224
225        Ok(crashlogs)
226    }
227
228    pub fn capabilities(&self, dev: &PmtDeviceId) -> Capabilities {
229        let mut capabilities = Capabilities::new();
230
231        for endpoint in self.get_endpoints(dev) {
232            capabilities.append(&mut endpoint.capabilities());
233        }
234
235        capabilities
236    }
237}
238
239impl PmtSysFsEndpoint {
240    pub fn new(path: &Path) -> Option<Self> {
241        if path.is_dir() {
242            Some(Self {
243                path: path.to_owned(),
244            })
245        } else {
246            log::error!("{} is not a valid PMT Crash Log endpoint", path.display());
247            None
248        }
249    }
250
251    #[cfg(feature = "control_commands")]
252    pub fn trigger(&self) -> Result<(), Error> {
253        self.write_command("trigger", b"1")
254    }
255
256    #[cfg(feature = "control_commands")]
257    pub fn rearm(&self) -> Result<(), Error> {
258        self.write_command("rearm", b"1")
259    }
260
261    #[cfg(feature = "control_commands")]
262    pub fn clear(&self) -> Result<(), Error> {
263        self.write_command("clear", b"1")
264    }
265
266    #[cfg(feature = "control_commands")]
267    pub fn enable_disable(&self, enable: bool) -> Result<(), Error> {
268        self.write_command("enable", if enable { b"1" } else { b"0" })
269    }
270
271    #[cfg(feature = "control_commands")]
272    fn write_command(&self, entry: &str, value: &[u8]) -> Result<(), Error> {
273        let path = self.path.join(entry);
274
275        let mut file = std::fs::OpenOptions::new()
276            .write(true)
277            .truncate(true)
278            .open(&path)
279            .map_err(Error::IOError)
280            .inspect_err(|err| log::info!("Failed to open {}: {err}", path.display()))?;
281
282        file.write_all(value)
283            .map_err(Self::to_control_error)
284            .inspect_err(|err| log::info!("Failed to write to {}: {err}", path.display()))?;
285
286        Ok(())
287    }
288
289    #[cfg(feature = "control_commands")]
290    fn to_control_error(err: std::io::Error) -> Error {
291        log::debug!("Error reported by the PmtSysFs: {err}");
292        match err.kind() {
293            std::io::ErrorKind::AlreadyExists => Error::CrashLogPending,
294            std::io::ErrorKind::ResourceBusy => Error::SourceBusy,
295            std::io::ErrorKind::InvalidInput => Error::ControlCommandRejected,
296            _ => Error::IOError(err),
297        }
298    }
299
300    pub fn capabilities(&self) -> Capabilities {
301        let mut capabilities: Capabilities = Capabilities::new();
302
303        if self.path.join("crashlog").exists() {
304            capabilities.insert(Capability::Extract);
305        }
306
307        if self.path.join("trigger").exists() {
308            capabilities.insert(Capability::Trigger);
309        }
310
311        if self.path.join("enable").exists() {
312            capabilities.insert(Capability::EnableDisable);
313        }
314
315        if self.path.join("clear").exists() {
316            capabilities.insert(Capability::Clear);
317        }
318
319        if self.path.join("rearm").exists() {
320            capabilities.insert(Capability::Rearm);
321        }
322
323        capabilities
324    }
325
326    pub fn extract(&self) -> Result<CrashLog, Error> {
327        CrashLog::from_regions(vec![
328            std::fs::read(self.path.join("crashlog"))
329                .map_err(Error::IOError)
330                .and_then(|region| Region::from_slice(&region))
331                .inspect(|_| log::info!("Extracted valid record from {}", self.path.display()))
332                .inspect_err(|err| log::error!("{}: {err}", self.path.display()))?,
333        ])
334    }
335}
336
337impl CrashLog {
338    /// Reads the Crash Log reported through all Intel PMT devices from the Linux sysfs
339    pub fn from_pmt_sysfs() -> Result<Self, Error> {
340        let regions: Vec<Region> = PmtSysFs::default()
341            .get_all_endpoints()
342            .into_iter()
343            .filter_map(|dev| dev.extract().ok())
344            .flat_map(|crashlog| crashlog.regions)
345            .collect();
346
347        let mut crashlog = CrashLog::from_regions(regions)?;
348        crashlog.metadata.time = Time::now();
349        Ok(crashlog)
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356    use tempfile;
357
358    #[test]
359    fn discovery() {
360        let root = tempfile::tempdir().unwrap();
361
362        let sysfs = PmtSysFs::new(root.path());
363        assert!(sysfs.discover().is_empty());
364
365        let mut class_path = root.path().to_owned();
366        class_path.push("sys");
367        class_path.push("class");
368        class_path.push("intel_pmt");
369        std::fs::create_dir_all(&class_path).unwrap();
370
371        for dev in 0..2 {
372            let mut dev_path = class_path.clone();
373            dev_path.push(format!("crashlog{}", dev));
374            std::fs::create_dir(dev_path).unwrap();
375        }
376
377        let mut dummy_dev_path = class_path.clone();
378        dummy_dev_path.push("foo");
379        std::fs::create_dir(dummy_dev_path).unwrap();
380
381        assert_eq!(sysfs.discover_from_class().len(), 2);
382    }
383
384    #[test]
385    fn discovery_pci() {
386        let root = tempfile::tempdir().unwrap();
387
388        let sysfs = PmtSysFs::new(root.path());
389        assert!(sysfs.discover().is_empty());
390
391        let mut pci_root_path = root.path().to_owned();
392        pci_root_path.push("sys");
393        pci_root_path.push("bus");
394        pci_root_path.push("pci");
395        pci_root_path.push("devices");
396        std::fs::create_dir_all(&pci_root_path).unwrap();
397
398        // Create PCI BDF directories with intel_vsec.crashlog markers
399        let bdfs = vec!["0000:00:1f.5", "0000:00:0e.0"];
400
401        for bdf in &bdfs {
402            let mut bdf_dir = pci_root_path.clone();
403            bdf_dir.push(bdf);
404            std::fs::create_dir(&bdf_dir).unwrap();
405
406            let mut vsec_marker = bdf_dir.clone();
407            vsec_marker.push("intel_vsec.crashlog.42");
408            std::fs::create_dir(&vsec_marker).unwrap();
409
410            let mut vsec_marker = bdf_dir.clone();
411            vsec_marker.push("intel_vsec.crashlog.1337");
412            std::fs::create_dir(&vsec_marker).unwrap();
413        }
414
415        let devices = sysfs.discover();
416        assert_eq!(devices.len(), 2);
417
418        assert!(devices.contains(&PmtDeviceId::Bdf(PciBdf::new(0, 0, 0x1f, 5))));
419        assert!(devices.contains(&PmtDeviceId::Bdf(PciBdf::new(0, 0, 0x0e, 0))));
420    }
421
422    #[test]
423    fn extract() {
424        let root = tempfile::tempdir().unwrap();
425
426        let sysfs = PmtSysFs::new(root.path());
427        assert!(sysfs.discover().is_empty());
428
429        let mut dev_path = root.path().to_owned();
430        dev_path.push("sys");
431        dev_path.push("class");
432        dev_path.push("intel_pmt");
433        dev_path.push("crashlog0");
434        std::fs::create_dir_all(&dev_path).unwrap();
435
436        let data = std::fs::read("tests/samples/dummy_crashlog_agent_rev1.crashlog").unwrap();
437        let crashlog = CrashLog::from_slice(&data).unwrap();
438
439        let mut path = dev_path.to_owned();
440        path.push("crashlog");
441        std::fs::write(path, crashlog.regions[0].to_bytes()).unwrap();
442
443        let devices = sysfs.discover();
444        let dev = sysfs.get_endpoints(&devices[0]);
445
446        let extracted_crashlog = dev[0].extract().unwrap();
447        assert_eq!(crashlog.to_bytes(), extracted_crashlog.to_bytes());
448    }
449
450    #[test]
451    fn trigger() {
452        let root = tempfile::tempdir().unwrap();
453
454        let dev = PmtSysFsEndpoint::new(root.path()).unwrap();
455        assert!(matches!(dev.trigger(), Err(Error::IOError(_))));
456
457        let mut path = root.path().to_owned();
458        path.push("trigger");
459        std::fs::write(&path, b"0").unwrap();
460
461        dev.trigger().unwrap();
462        assert_eq!(&std::fs::read_to_string(&path).unwrap(), "1");
463    }
464
465    #[test]
466    fn rearm() {
467        let root = tempfile::tempdir().unwrap();
468
469        let dev = PmtSysFsEndpoint::new(root.path()).unwrap();
470        assert!(matches!(dev.rearm(), Err(Error::IOError(_))));
471
472        let mut path = root.path().to_owned();
473        path.push("rearm");
474        std::fs::write(&path, b"0").unwrap();
475
476        dev.rearm().unwrap();
477        assert_eq!(&std::fs::read_to_string(&path).unwrap(), "1");
478    }
479
480    #[test]
481    fn enable_disable() {
482        let root = tempfile::tempdir().unwrap();
483
484        let dev = PmtSysFsEndpoint::new(root.path()).unwrap();
485        assert!(matches!(dev.enable_disable(true), Err(Error::IOError(_))));
486
487        let mut path = root.path().to_owned();
488        path.push("enable");
489        std::fs::write(&path, b"0").unwrap();
490
491        dev.enable_disable(true).unwrap();
492        assert_eq!(&std::fs::read_to_string(&path).unwrap(), "1");
493
494        dev.enable_disable(false).unwrap();
495        assert_eq!(&std::fs::read_to_string(&path).unwrap(), "0");
496    }
497
498    #[test]
499    fn capabilities() {
500        let root = tempfile::tempdir().unwrap();
501
502        let sysfs = PmtSysFs::new(root.path());
503        assert!(sysfs.discover().is_empty());
504
505        let mut dev_path = root.path().to_owned();
506        dev_path.push("sys");
507        dev_path.push("class");
508        dev_path.push("intel_pmt");
509        dev_path.push("crashlog0");
510        std::fs::create_dir_all(&dev_path).unwrap();
511
512        let mut cl_path = dev_path.to_owned();
513        cl_path.push("crashlog");
514        std::fs::create_dir(&cl_path).unwrap();
515
516        let mut trigger_path = dev_path.to_owned();
517        trigger_path.push("trigger");
518        std::fs::create_dir(&trigger_path).unwrap();
519
520        let mut rearm_path = dev_path.to_owned();
521        rearm_path.push("rearm");
522        std::fs::create_dir(&rearm_path).unwrap();
523
524        let devices = sysfs.discover();
525        let dev = sysfs.get_endpoints(&devices[0]);
526        let dev_capabilities = dev[0].capabilities();
527
528        assert_eq!(dev_capabilities.len(), 3);
529        assert!(dev_capabilities.contains(&Capability::Extract));
530        assert!(dev_capabilities.contains(&Capability::Trigger));
531        assert!(dev_capabilities.contains(&Capability::Rearm));
532    }
533
534    #[test]
535    fn control_commands() {
536        let root = tempfile::tempdir().unwrap();
537
538        let sysfs = PmtSysFs::new(root.path());
539        assert!(sysfs.discover().is_empty());
540
541        let mut dev_path = root.path().to_owned();
542        dev_path.push("sys");
543        dev_path.push("class");
544        dev_path.push("intel_pmt");
545        dev_path.push("crashlog0");
546        std::fs::create_dir_all(&dev_path).unwrap();
547
548        let mut enable_path = dev_path.to_owned();
549        enable_path.push("enable");
550        std::fs::write(&enable_path, b"0").unwrap();
551
552        let mut trigger_path = dev_path.to_owned();
553        trigger_path.push("trigger");
554        std::fs::write(&trigger_path, b"0").unwrap();
555
556        let devices = sysfs.discover();
557        let dev = sysfs.get_endpoints(&devices[0]);
558
559        dev[0].trigger().unwrap();
560        assert_eq!(&std::fs::read_to_string(&trigger_path).unwrap(), "1");
561
562        dev[0].enable_disable(true).unwrap();
563        assert_eq!(&std::fs::read_to_string(&enable_path).unwrap(), "1");
564
565        dev[0].enable_disable(false).unwrap();
566        assert_eq!(&std::fs::read_to_string(&enable_path).unwrap(), "0");
567    }
568}