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