Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,9 @@ But this tool is an unified opensource approach to manipulate UEFI settings on a

### For OCP hardware

* Extract HiiDB. Linux exposes no file that holds it, and `/sys/firmware/efi/systab` does not list its address, so the OCP way is to read the physical address and length from the `HiiDB-1b838190-4625-4ead-abc9-cd5e6af18fe0` efivar and then read that memory from `/dev/mem`.
* If the firmware publishes a copy of the HiiDB as the vendor ACPI table `HIID`, it is read from `/sys/firmware/acpi/tables/HIID` instead (skip the 36-byte ACPI header), without `/dev/mem`. This is the way where `/dev/mem` cannot reach the database (e.g. aarch64, whose EFI runtime memory is `nomap`) or the database is too large for one efivarfs read. When the table is missing or unreadable, the reason goes to stderr and the OCP way is used.
* Extract HiiDB. Linux exposes no file that holds it, so the standard way is to follow the EFI System Table: `/sys/firmware/efi/config_table` holds the address of its `EFI_CONFIGURATION_TABLE` array, the entry for `gEfiHiiDatabaseProtocolGuid` points at the exported database, and every step is read from `/dev/mem`. The firmware must install that entry (`PcdHiiOsRuntimeSupport`, TRUE by default in edk2), and not every kernel creates `config_table`. The length is not recorded, so it is found by walking the package lists up to the zero padding after them.
* If that fails, the reason goes to stderr and the vendor ACPI table `HIID` is tried: if the firmware publishes a copy of the HiiDB there, it is read from `/sys/firmware/acpi/tables/HIID` (skip the 36-byte ACPI header), without `/dev/mem`. This is the way where `/dev/mem` cannot reach the database (e.g. aarch64, whose EFI runtime memory is `nomap`) or the database is too large for one efivarfs read.
* If that fails too, the reason goes to stderr and the OCP way is used: read the physical address and length from the `HiiDB-1b838190-4625-4ead-abc9-cd5e6af18fe0` efivar and then read that memory from `/dev/mem`.
* Library (`hii`) in Rust partially implements the UEFI Hii specification.
* It parses the HiiDB op-codes into a DOM tree like representation which can be read by machines and humans.
* It can ask questions about UEFI settings from HiiDB and get their answers.
Expand Down Expand Up @@ -214,7 +215,7 @@ uefisettings hii dump-efivars -f hiidb.bin efivars
uefisettings hii list-questions --efivars efivars
```

`extract-db` reads the HiiDB from the `HIID` ACPI table or, failing that, from /dev/mem; either needs root (the table is root-only, /dev/mem needs CAP_SYS_RAWIO). `dump-efivars -f` reads
`extract-db` reads the HiiDB from the EFI configuration table, failing that from the `HIID` ACPI table, and failing that through the OCP efivar and /dev/mem; all need root (the table is root-only, /dev/mem needs CAP_SYS_RAWIO). `dump-efivars -f` reads
only efivarfs, whose files every user can read, so run it unprivileged: as root, it would leave a
directory that only root can read.

Expand Down
188 changes: 183 additions & 5 deletions src/lib/hii/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,33 @@ pub const HIIDB_ACPI_PATH: &str = "/sys/firmware/acpi/tables/HIID";
// prefixes the HII database bytes in the HIID table.
const ACPI_HEADER_LEN: usize = 36;

// Linux prints the physical address of the EFI System Table's ConfigurationTable
// array here (an EFI_CONFIGURATION_TABLE[]). Not every kernel creates it (e.g.
// NVIDIA aarch64 does not).
pub const EFI_CONFIG_TABLE_PATH: &str = "/sys/firmware/efi/config_table";

// gEfiHiiDatabaseProtocolGuid, EF9FC172-A1B2-4693-B327-6D32FC416042, in memory
// byte order (the first three fields little-endian). HiiDatabaseDxe installs
// the exported database under it when PcdHiiOsRuntimeSupport is TRUE.
const HII_DATABASE_GUID: [u8; 16] = [
0x72, 0xc1, 0x9f, 0xef, 0xb2, 0xa1, 0x93, 0x46, 0xb3, 0x27, 0x6d, 0x32, 0xfc, 0x41, 0x60, 0x42,
];

// A 64-bit EFI_CONFIGURATION_TABLE entry: VendorGuid (16 bytes) + VendorTable
// pointer (8 bytes).
const CONFIG_TABLE_ENTRY_LEN: usize = 24;

// The entry count lives in the EFI System Table, which Linux does not expose, so
// stop after this many entries (firmware has a few dozen).
const MAX_CONFIG_TABLE_ENTRIES: u64 = 256;

// EFI_HII_PACKAGE_LIST_HEADER: PackageListGuid (16 bytes) + PackageLength (u32,
// which includes this header).
const PACKAGE_LIST_HEADER_LEN: usize = 20;

// Upper bound for the database size found by walking its package lists.
const MAX_HII_DB_LEN: usize = 64 * 1024 * 1024;

const DEV_MEM_PATH: &str = "/dev/mem";

/// Below this address (1 MiB), x86 read() of /dev/mem may return zeros.
Expand Down Expand Up @@ -114,11 +141,22 @@ impl HiiDBEFIVar {
}

pub fn extract_db() -> Result<Vec<u8>> {
// Prefer the vendor ACPI table that carries the HII database content
// directly (read from sysfs, no /dev/mem); fall back to the OCP variable +
// /dev/mem when it is absent (e.g. genuine OCP hardware). The fallback
// reason is printed to stderr so a failure here is not masked by the OCP
// error below.
// Prefer the spec-standard route (the HII database configuration table
// found through the EFI System Table), then the vendor ACPI table that
// carries the database content directly (read from sysfs, no /dev/mem), and
// fall back to the OCP variable + /dev/mem. Each fallback reason is printed
// to stderr so a failure here is not masked by the OCP error below.
match extract_db_efi_config_table() {
Ok(bytes) => {
debug!("HII database source: EFI configuration table ({EFI_CONFIG_TABLE_PATH})");
return Ok(bytes);
}
Err(e) => eprintln!(
"[uefisettings] could not use the EFI configuration table \
({EFI_CONFIG_TABLE_PATH}): {e:#}; falling back to the HII ACPI table"
),
}

match extract_db_acpi() {
Ok(bytes) => return Ok(bytes),
Err(e) => eprintln!(
Expand All @@ -143,6 +181,86 @@ pub fn extract_db() -> Result<Vec<u8>> {
read_physical_memory(db_info.address, db_info.length.try_into()?)
}

/// Reads the HII database through the EFI configuration table: the
/// ConfigurationTable address from EFI_CONFIG_TABLE_PATH, the entry for
/// gEfiHiiDatabaseProtocolGuid in it, then the database that entry points to.
/// Every step reads /dev/mem.
fn extract_db_efi_config_table() -> Result<Vec<u8>> {
let text = std::fs::read_to_string(EFI_CONFIG_TABLE_PATH)
.context(format!("Failed to read {EFI_CONFIG_TABLE_PATH}"))?;
let table_address = parse_hex_address(&text)?;

let mem_file =
File::open(DEV_MEM_PATH).with_context(|| format!("Failed to open {DEV_MEM_PATH}"))?;
let read = |address: u64, length: usize| {
read_physical_memory_at(&mem_file, address, length, FileExt::read_exact_at)
};

let mut db_address = None;
for index in 0..MAX_CONFIG_TABLE_ENTRIES {
let entry_address = table_address + index * CONFIG_TABLE_ENTRY_LEN as u64;
let entry = read(entry_address, CONFIG_TABLE_ENTRY_LEN)?;
if let Some(address) = parse_hii_config_table_entry(&entry) {
db_address = Some(address);
break;
}
}
let db_address = db_address.with_context(|| {
format!(
"No gEfiHiiDatabaseProtocolGuid entry in the first {MAX_CONFIG_TABLE_ENTRIES} \
configuration table entries (is PcdHiiOsRuntimeSupport FALSE?)"
)
})?;

let length = hii_db_length(|offset, len| read(db_address + offset as u64, len))?;
read(db_address, length)
}

/// Parses the address in `text`, as Linux prints it ("0x7fc2d198").
fn parse_hex_address(text: &str) -> Result<u64> {
let text = text.trim();
let digits = text.strip_prefix("0x").unwrap_or(text);
u64::from_str_radix(digits, 16).with_context(|| format!("Not a hexadecimal address: {text:?}"))
}

/// Returns the VendorTable pointer of `entry` (an EFI_CONFIGURATION_TABLE) when
/// it is the gEfiHiiDatabaseProtocolGuid entry with a non-NULL pointer.
fn parse_hii_config_table_entry(entry: &[u8]) -> Option<u64> {
let (guid, pointer) = entry.split_at(16);
if guid != HII_DATABASE_GUID {
return None;
}
let address = u64::from_le_bytes(pointer.try_into().ok()?);
(address != 0).then_some(address)
}

/// Returns the size of the exported database, which the configuration table does
/// not record: HiiDatabaseDxe over-allocates the buffer by 25% and zero-fills it
/// (HiiGetDatabaseInfo), so the database is the run of package lists, walked by
/// their PackageLength, that ends at the first zero PackageLength. `read(offset,
/// length)` returns database bytes.
fn hii_db_length(read: impl Fn(usize, usize) -> Result<Vec<u8>>) -> Result<usize> {
let mut total = 0;
loop {
let header = read(total, PACKAGE_LIST_HEADER_LEN)?;
let length = u32::from_le_bytes(header[16..20].try_into()?) as usize;
if length == 0 {
break;
}
if length < PACKAGE_LIST_HEADER_LEN {
bail!("HII package list at offset 0x{total:x} has a bad PackageLength: {length}");
}
total += length;
if total > MAX_HII_DB_LEN {
bail!("HII database is larger than {MAX_HII_DB_LEN} bytes");
}
}
if total == 0 {
bail!("HII database holds no package list");
}
Ok(total)
}

/// Reads the HII database from the vendor ACPI table (HIIDB_ACPI_PATH) whose
/// body IS the database content: a 36-byte ACPI header followed by the
/// database bytes, so the header is dropped and the rest returned.
Expand Down Expand Up @@ -623,4 +741,64 @@ mod tests {
assert_eq!(root_errno(&err), Some(errno), "{err:#}");
}
}

#[test]
fn parses_config_table_address() {
assert_eq!(parse_hex_address("0x7fc2d198\n").unwrap(), 0x7fc2d198);
assert_eq!(parse_hex_address("7fc2d198").unwrap(), 0x7fc2d198);
assert!(parse_hex_address("none").is_err());
}

#[test]
fn finds_only_the_hii_database_config_table_entry() {
let mut entry = [0u8; CONFIG_TABLE_ENTRY_LEN];
entry[..16].copy_from_slice(&HII_DATABASE_GUID);
entry[16..].copy_from_slice(&0x7fc0_6018u64.to_le_bytes());
assert_eq!(parse_hii_config_table_entry(&entry), Some(0x7fc0_6018));

entry[16..].fill(0);
assert_eq!(parse_hii_config_table_entry(&entry), None, "NULL pointer");

entry[16..].copy_from_slice(&0x7fc0_6018u64.to_le_bytes());
entry[0] ^= 1;
assert_eq!(parse_hii_config_table_entry(&entry), None, "other GUID");
}

/// A package list of `length` bytes: a header whose PackageLength is `length`.
fn package_list(length: u32) -> Vec<u8> {
let mut list = vec![0xaa; length as usize];
list[16..20].copy_from_slice(&length.to_le_bytes());
list
}

#[test]
fn hii_db_length_stops_at_zero_padding() {
let mut db = package_list(40);
db.extend(package_list(24));
db.extend([0u8; 30]); // the 25% over-allocation, zero-filled
let read = |offset: usize, len: usize| Ok(db[offset..offset + len].to_vec());
assert_eq!(hii_db_length(read).unwrap(), 64);
}

#[test]
fn hii_db_length_rejects_bad_databases() {
let empty = [0u8; 32];
let read = |offset: usize, len: usize| Ok(empty[offset..offset + len].to_vec());
assert!(hii_db_length(read).is_err(), "no package list");

let mut short = package_list(20);
short[16..20].copy_from_slice(&19u32.to_le_bytes());
let read = |offset: usize, len: usize| Ok(short[offset..offset + len].to_vec());
assert!(
hii_db_length(read).is_err(),
"PackageLength below the header"
);

// Every list claims 16 MiB and the data never ends.
let endless = package_list(20);
let mut header = endless[..PACKAGE_LIST_HEADER_LEN].to_vec();
header[16..20].copy_from_slice(&(16u32 << 20).to_le_bytes());
let read = |_: usize, _: usize| Ok(header.clone());
assert!(hii_db_length(read).is_err(), "over the size cap");
}
}