linux_memutils/
agesa.rs

1// SPDX-FileCopyrightText: Benedikt Vollmerhaus <benedikt@vollmerhaus.org>
2// SPDX-License-Identifier: MIT
3/*!
4Utilities for finding the [AGESA] version in physical memory (on AMD Zen).
5
6# AGESA
7
8AGESA is a procedure library by AMD embedded into the UEFI firmware of AMD
9platforms up to and including Zen 5. It performs _Platform Initialization_,
10so it is responsible for CPU startup, memory training, IO (including PCIe)
11configuration, and more.
12
13Because of AGESA's importance for stability _and security_, one may want to
14inspect its version, ideally from user space on a running system. Alas, the
15Linux kernel does not provide a straightforward interface for this; however,
16AGESA's version marker is generally located somewhere in extended memory and
17can thus be obtained via a brute-force search as implemented by this module.
18
19<div class="warning">
20
21  [Per coreboot], there are two documented iterations of AGESA:
22
23  * **v5** (or [Arch2008]) for CPU families before Zen (< `17h`)
24  * **v9** for Zen and later (>= `17h`)
25
26  This module supports both, but **v5** was not yet comprehensively tested.
27
28</div>
29
30[AGESA]: https://en.wikipedia.org/wiki/AGESA
31[Per coreboot]: https://doc.coreboot.org/soc/amd/family17h.html#introduction
32[Arch2008]: https://www.amd.com/content/dam/amd/en/documents/processor-tech-docs/specifications/44065_Arch2008.pdf
33*/
34use crate::iomem::{MemoryRegion, MemoryRegionType, parse_proc_iomem};
35use crate::reader::SkippingBufReader;
36use std::collections::VecDeque;
37use std::fs::File;
38use std::io::{Read, Seek};
39use std::{io, str};
40use thiserror::Error;
41
42/// An AGESA version found in physical memory.
43#[derive(PartialEq)]
44pub struct FoundVersion {
45    /// The complete version string (may include trailing whitespace).
46    pub agesa_version: String,
47    /// The absolute start address of this version in physical memory.
48    pub absolute_address: usize,
49    /// The memory region this version is located in.
50    pub surrounding_region: MemoryRegion,
51}
52
53impl FoundVersion {
54    /// Return this version's offset within its surrounding memory region.
55    #[must_use]
56    pub fn offset_in_region(&self) -> usize {
57        self.absolute_address - self.surrounding_region.start_address
58    }
59}
60
61#[derive(Error, Debug)]
62pub enum SearchError {
63    #[error("could not open `/dev/mem`")]
64    DevMemUnopenable(#[source] io::Error),
65
66    #[error("could not read memory map from `/proc/iomem`")]
67    IomemUnreadable(#[source] io::Error),
68
69    #[error("could not read byte in physical memory from `/dev/mem`")]
70    ByteUnreadable(#[source] io::Error),
71}
72
73pub type SearchResult = Result<Option<FoundVersion>, SearchError>;
74
75/// Search for the AGESA version within all `Reserved` memory regions.
76///
77/// # Errors
78///
79/// This function will return an error if no memory map could be obtained.
80/// It will also return errors for reading from physical memory according
81/// to [`find_agesa_version_in_memory_region`].
82pub fn find_agesa_version() -> SearchResult {
83    let possible_regions =
84        get_reserved_regions_in_extended_memory().map_err(SearchError::IomemUnreadable)?;
85
86    for region in possible_regions {
87        log::info!("Searching memory region: {region}");
88        let maybe_found_version = find_agesa_version_in_memory_region(region)?;
89        if maybe_found_version.is_some() {
90            return Ok(maybe_found_version);
91        }
92    }
93
94    Ok(None)
95}
96
97/// Search for the AGESA version within the given memory region.
98///
99/// # Errors
100///
101/// This function will return an error if `/dev/mem` could not be opened
102/// or an unexpected read error occurred during the search.
103pub fn find_agesa_version_in_memory_region(region: MemoryRegion) -> SearchResult {
104    let file = File::open("/dev/mem").map_err(SearchError::DevMemUnopenable)?;
105    let buf_reader = SkippingBufReader::new(file, region.start_address, Some(region.end_address));
106
107    if let Some((agesa_version, absolute_address)) = find_agesa_version_in_reader(buf_reader)? {
108        return Ok(Some(FoundVersion {
109            agesa_version,
110            absolute_address,
111            surrounding_region: region,
112        }));
113    }
114
115    Ok(None)
116}
117
118/// The possible states of an ongoing search.
119enum SearchState {
120    Searching,
121    SignatureFound,
122    VersionStartFound,
123}
124
125/// The signature indicating the start of an AGESA v9 version in memory.
126const SIGNATURE_V9: &[u8] = b"AGESA!V";
127const SIGNATURE_LENGTH: usize = SIGNATURE_V9.len();
128
129/// The signature indicating the start of an AGESA v5 version in memory.
130///
131/// Per the Arch2008 spec, this is should be `!!AGESA `; however, on the
132/// [Kaveri platform] I tested, it is `!!!AGESA` immediately followed by
133/// the version string. The specified prefix should work for both cases.
134///
135/// [Kaveri platform]: https://github.com/fishbaoz/KaveriPI/blob/master/AGESA/AMD.h#L260
136const SIGNATURE_V5: &[u8; SIGNATURE_LENGTH] = b"!!AGESA";
137
138fn find_agesa_version_in_reader<R: Read + Seek>(
139    mut buf_reader: SkippingBufReader<R>,
140) -> Result<Option<(String, usize)>, SearchError> {
141    let mut version_string = Vec::new();
142
143    let mut search_state = SearchState::Searching;
144    let mut search_window = VecDeque::with_capacity(SIGNATURE_LENGTH);
145
146    for b in (&mut buf_reader).bytes() {
147        let byte = b.map_err(SearchError::ByteUnreadable)?;
148
149        match search_state {
150            SearchState::Searching => {
151                if search_window.len() == SIGNATURE_LENGTH {
152                    search_window.pop_front();
153                }
154                search_window.push_back(byte);
155
156                if search_window.eq(&SIGNATURE_V9) {
157                    // AGESA!V9␀CezannePI-FP6 1.0.1.1␀
158                    //       ^
159                    search_state = SearchState::SignatureFound;
160                } else if search_window.eq(&SIGNATURE_V5) {
161                    // !!!AGESAKaveriPI        V1.1.0.7    ␀
162                    //        ^
163                    // For AGESA v5, the version string starts right after
164                    // the signature, so there is no null byte to skip to
165                    search_state = SearchState::VersionStartFound;
166                }
167            }
168            SearchState::SignatureFound => {
169                if byte == b'\0' {
170                    // AGESA!V9␀CezannePI-FP6 1.0.1.1␀
171                    //         ^
172                    search_state = SearchState::VersionStartFound;
173                }
174            }
175            SearchState::VersionStartFound if byte == b'\0' => {
176                // AGESA!V9␀CezannePI-FP6 1.0.1.1␀
177                //                               ^
178                let trimmed_version = version_string.trim_ascii_start();
179                let absolute_address = buf_reader.position_in_file() - trimmed_version.len() - 1;
180                return Ok(Some((
181                    str::from_utf8(trimmed_version).unwrap().into(),
182                    absolute_address,
183                )));
184            }
185            SearchState::VersionStartFound => {
186                version_string.push(byte);
187            }
188        }
189    }
190
191    Ok(None)
192}
193
194/// The start address of extended memory.
195const EXTENDED_MEM_START: usize = 0x0000_0000_0010_0000;
196
197/// Find and return all `Reserved` regions in [extended memory] (> 1 MiB).
198///
199/// Testing on a few machines showed that at least one `Reserved` region in
200/// extended memory reliably contains the AGESA version – usually the first
201/// one at that. Even `Usable` regions may occasionally include it, but the
202/// initial (generally small) `Reserved` regions are much faster to search.
203///
204/// [extended memory]: https://wiki.osdev.org/Memory_Map_(x86)#Extended_Memory_(%3E_1_MiB)
205///
206/// # Errors
207///
208/// This function will return an error if `/proc/iomem` could not be read.
209pub fn get_reserved_regions_in_extended_memory() -> io::Result<Vec<MemoryRegion>> {
210    let all_regions = parse_proc_iomem()?;
211    let reserved_high_mem_regions: Vec<MemoryRegion> = all_regions
212        .into_iter()
213        .filter(|r| r.region_type == MemoryRegionType::Reserved)
214        .filter(|r| r.start_address >= EXTENDED_MEM_START)
215        .collect();
216
217    Ok(reserved_high_mem_regions)
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    mod found_version {
225        use super::*;
226
227        #[test]
228        fn offset_in_region_returns_expected_offset() {
229            let version = FoundVersion {
230                agesa_version: "CezannePI-FP6 1.0.1.1".into(),
231                absolute_address: 20,
232                surrounding_region: MemoryRegion {
233                    start_address: 5,
234                    end_address: 100,
235                    region_type: MemoryRegionType::Reserved,
236                },
237            };
238            assert_eq!(version.offset_in_region(), 15);
239        }
240    }
241
242    mod find_agesa_version_in_reader {
243        use super::*;
244        use indoc::formatdoc;
245        use rstest::rstest;
246        use std::io::Cursor;
247
248        #[rstest]
249        #[case::agesa_v9_signature(
250            "AGESA!V9\0CezannePI-FP6 1.0.1.1\0",
251            "CezannePI-FP6 1.0.1.1",
252            37
253        )]
254        #[case::agesa_v5_signature_arch2008(
255            "!!AGESA KaveriPI        V1.1.0.7    \0",
256            "KaveriPI        V1.1.0.7    ",
257            36
258        )]
259        #[case::agesa_v5_signature_alternative(
260            "!!!AGESAKaveriPI        V1.1.0.7    \0",
261            "KaveriPI        V1.1.0.7    ",
262            36
263        )]
264        fn returns_expected_version_string_and_absolute_address(
265            #[case] version_in_memory: String,
266            #[case] expected_version_string: String,
267            #[case] expected_absolute_address: usize,
268        ) {
269            let file = Cursor::new(formatdoc! {"
270                PreceedingUnrelated\0Bytes%p
271                {version_in_memory}
272                \0SubsequentUnrelatedBytes\0
273            "});
274            let buf_reader = SkippingBufReader::new(file, 0, None);
275
276            let result = find_agesa_version_in_reader(buf_reader).unwrap();
277            assert_eq!(
278                result,
279                Some((expected_version_string, expected_absolute_address))
280            );
281        }
282
283        #[test]
284        fn returns_none_if_no_agesa_signature_is_present() {
285            let file = Cursor::new(b"AESA!V9\0CezannePI-FP6 1.0.1.1\0");
286            let buf_reader = SkippingBufReader::new(file, 0, None);
287
288            let result = find_agesa_version_in_reader(buf_reader).unwrap();
289            assert_eq!(result, None);
290        }
291
292        #[test]
293        fn returns_none_if_version_string_does_not_end() {
294            let file = Cursor::new(b"AGESA!V9\0CezannePI-FP6 1.0.1.1");
295            let buf_reader = SkippingBufReader::new(file, 0, None);
296
297            let result = find_agesa_version_in_reader(buf_reader).unwrap();
298            assert_eq!(result, None);
299        }
300    }
301}