Files
RedBear-OS/drivers/acpid/src/dmi.rs
T
Red Bear OS d844111937 base: close SLP_TYPb, parse_lnk_irc, AML mutex, and S5 gaps
Phase C of the ACPI fork-sync plan. Applies targeted gap fixes on top
of the synchronized fork foundation (commits 4f2a043 + ae57fe3).

Closes 4 of the 8 critical gaps identified by the 2026-06-30 ACPI
assessment.

Gap 5 - SLP_TYPb PM1b write (acpid/src/acpi.rs):
The previous code wrote SLP_EN+SLP_TYPa to PM1a but silently dropped
SLP_TYPb. On hardware that requires both PM1a and PM1b writes
(some laptops, server boards with split power blocks), the shutdown
was incomplete. Now writes SLP_EN+SLP_TYPb to PM1b when
pm1b_control_block is non-zero. The FADT field is 0 when no
second block exists, in which case we skip the second write.

Gap 6 - parse_lnk_irc range validation (hwd/src/backend/acpi.rs):
The previous code accepted any 16-bit integer as an IRQ
(n AND 0xFFFF), producing "Enabled at IRQ 53313" from misparsed
FieldUnit accessors on QEMU PIIX4. Now validates that the IRQ
value is 2047 or less (the maximum valid legacy-compatible IOAPIC
IRQ). Out-of-range values are debug-logged and skipped instead
of polluting the routing table. Also adds a 15-bit cap on the
Buffer-based IRQ bit extraction (was unchecked).

Gap 3 - AML mutex create/acquire/release (acpid/src/aml_physmem.rs):
The new gitlab acpi crate (Phase B bump) added proper Handler
trait methods for create_mutex, acquire, and release. The previous
implementation was three log debug stubs returning fake success,
which would silently corrupt AML state for any DSDT/SSDT that
uses Mutex. Now implements a real mutex table backed by
std::sync.Mutex of FxHashSet u32:
  - create_mutex allocates a unique u32 handle from a counter
  - acquire busy-waits with 1ms sleeps until the handle is free
    or the AML timeout (multiplied by 1000 for ms to us conversion)
    expires; returns AmlError::MutexAcquireTimeout on timeout
  - release removes the handle from the held set

Gap 4a - set_global_s_state non-S5 explicit warning (acpid/src/acpi.rs):
The previous code silently returned early when called with any
state other than 5. Now emits a log warn with the requested
state, naming the missing dependencies (_PTS/_WAK AML evaluation,
P-state preservation, wakeup path). This converts a silent failure
into a diagnostic that is visible in the boot log.

Also includes drivers/acpid/src/dmi.rs:158 - convert e.errno
(private field) to e.errno() (method call). The libredox
Error struct changed its errno from a public field to a method
in a newer release; the DmiError::Map(syscall::error::Error)
construction was using the field-access form, which broke the
build against current libredox. This is a build-fix that the
prior dirty tree already had; included here to keep base
buildable.

Verified by: CI=1 ./local/scripts/build-redbear.sh redbear-mini
succeeded with exit 0. ISO at build/x86_64/redbear-mini.iso
(512 MB) at 2026-06-30 05:28.
2026-06-30 05:31:07 +03:00

959 lines
35 KiB
Rust

//! SMBIOS / DMI table scanning and parsing.
//!
//! Implements the same algorithm as the Linux kernel's `dmi_scan.c`, adapted
//! for Redox's userspace acpid. Two entry-point conventions are recognized:
//!
//! 1. **SMBIOS 3.x 64-bit entry point** (signature `_SM3_`, preferred when
//! present). Points directly at the structure table via a 64-bit physical
//! address with an explicit length, and has no fixed structure count.
//! 2. **Legacy 32-bit entry point** (signature `_SM_`, with embedded `_DMI_`
//! header 16 bytes later). Provides a structure count and a 32-bit
//! table base address.
//!
//! Both entry points are scanned in the standard 0xF0000-0xFFFFF BIOS
//! anchor region, 16 bytes aligned, with the 64-bit variant preferred.
//!
//! Once the structure table is located we walk it linearly, decoding
//! the structure types that callers actually need:
//!
//! - Type 0 (BIOS Information): vendor, version, release date,
//! BIOS / EC firmware revision.
//! - Type 1 (System Information): manufacturer, product name, version,
//! serial, UUID, SKU, family.
//! - Type 2 (Baseboard Information): manufacturer, product, version,
//! serial, asset tag.
//!
//! The variable-length string area at the tail of each structure is
//! accessed by index (1-based) per the SMBIOS reference spec.
//!
//! Strings that contain only spaces are treated as empty (matching Linux
//! behavior), and a number of defensive validations are applied to
//! tolerate malformed firmware.
use std::fs::File;
use std::io::Read;
use std::str;
use log::{debug, info, warn};
use syscall::PAGE_SIZE;
use common::{MemoryType, Prot};
/// Standard SMBIOS BIOS anchor scan range.
const SMBIOS_ANCHOR_START: usize = 0x000F_0000;
/// 64 KiB scan window (matches Linux `dmi_scan_machine`).
const SMBIOS_ANCHOR_LEN: usize = 0x0001_0000;
/// 16-byte alignment step for anchor scans.
const SMBIOS_ANCHOR_STEP: usize = 16;
/// Sentinel byte string for the 64-bit SMBIOS entry point.
const SMBIOS3_SIG: &[u8; 5] = b"_SM3_";
/// Sentinel byte string for the legacy 32-bit entry point.
const SMBIOS_SIG: &[u8; 4] = b"_SM_";
/// Sentinel for the legacy DMI header (16 bytes into the legacy entry point).
const DMI_SIG: &[u8; 5] = b"_DMI_";
/// Upper bound on a single structure's formatted area. Mirrors Linux
/// (the spec allows 256, but Linux is more conservative). Used as a
/// defensive guard against malformed firmware.
const MAX_STRUCTURE_LENGTH: usize = 256;
/// A single DMI / SMBIOS structure table entry (decoded).
#[derive(Clone, Debug, Default)]
pub struct DmiInfo {
pub bios_vendor: Option<String>,
pub bios_version: Option<String>,
pub bios_date: Option<String>,
pub bios_release: Option<String>,
pub ec_firmware_release: Option<String>,
pub sys_vendor: Option<String>,
pub product_name: Option<String>,
pub product_version: Option<String>,
pub product_serial: Option<String>,
pub product_uuid: Option<String>,
pub product_sku: Option<String>,
pub product_family: Option<String>,
pub board_vendor: Option<String>,
pub board_name: Option<String>,
pub board_version: Option<String>,
pub board_serial: Option<String>,
pub board_asset_tag: Option<String>,
}
/// SMBIOS version that produced this table (major.minor.revision or
/// major.minor for the 32-bit entry point), useful for diagnostics.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct SmbiosVersion {
pub major: u8,
pub minor: u8,
pub revision: u8,
}
impl core::fmt::Display for SmbiosVersion {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.revision)
}
}
/// Result of a successful SMBIOS scan.
#[derive(Clone, Debug)]
pub struct SmbiosTable {
/// Major / minor / revision.
pub version: SmbiosVersion,
/// Decoded identity fields.
pub info: DmiInfo,
}
/// Error type for DMI scanning.
#[derive(Debug)]
pub enum DmiError {
/// No SMBIOS entry point could be located.
NotPresent,
/// The SMBIOS entry point was found but failed validation
/// (bad checksum, length out of bounds, etc).
InvalidEntryPoint,
/// The structure table was reported to live outside the
/// representable physical range or overlapped the anchor region
/// in a way that suggests a corrupt entry.
InvalidTableAddress,
/// Mapping physical memory failed.
Map(syscall::error::Error),
/// A structure was so malformed that walking must stop.
MalformedTable,
}
impl core::fmt::Display for DmiError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
DmiError::NotPresent => f.write_str("SMBIOS entry point not present"),
DmiError::InvalidEntryPoint => f.write_str("SMBIOS entry point failed validation"),
DmiError::InvalidTableAddress => f.write_str("SMBIOS structure table address invalid"),
DmiError::Map(e) => write!(f, "physmap failed: {:?}", e),
DmiError::MalformedTable => f.write_str("malformed SMBIOS structure table"),
}
}
}
impl std::error::Error for DmiError {}
/// Map a physical address range as read-only. The mapping is unmapped
/// when the returned `PhysmapGuard` is dropped.
struct PhysmapGuard {
virt: *mut u8,
size: usize,
}
impl PhysmapGuard {
fn map(base_phys: usize, length: usize) -> Result<Self, DmiError> {
let phys_start = base_phys & !(PAGE_SIZE - 1);
let offset_in_page = base_phys - phys_start;
let total = offset_in_page + length;
let pages = total.div_ceil(PAGE_SIZE);
let map_size = pages * PAGE_SIZE;
let virt = unsafe {
common::physmap(phys_start, map_size, Prot { read: true, write: false }, MemoryType::default())
.map_err(|e| DmiError::Map(syscall::error::Error::new(e.errno())))?
};
Ok(Self {
virt: virt as *mut u8,
size: map_size,
})
}
}
impl Drop for PhysmapGuard {
fn drop(&mut self) {
unsafe {
let _ = libredox::call::munmap(self.virt as *mut (), self.size);
}
}
}
/// Locate and decode the SMBIOS structure table.
///
/// Returns `Ok(None)` when no SMBIOS entry point is present (e.g. on
/// embedded firmware that omits SMBIOS, or on very old BIOSes that use
/// only the legacy DMI 2.0 convention). Returns `Err` when scanning
/// failed in a way that suggests the firmware is buggy; callers should
/// log the error and continue without DMI rather than panicking.
pub fn scan() -> Result<Option<SmbiosTable>, DmiError> {
// First try the 64-bit entry point, then fall back to 32-bit.
match scan_anchor(true) {
Ok(Some(table)) => return Ok(Some(table)),
Ok(None) => {}
Err(e) => {
// Don't bail out; the legacy entry point may still be valid.
debug!("SMBIOS3 anchor scan failed: {}", e);
}
}
match scan_anchor(false) {
Ok(Some(table)) => Ok(Some(table)),
// Anchor scan saw no signatures at all -> SMBIOS not present.
Ok(None) => Ok(None),
Err(DmiError::NotPresent) => Ok(None),
Err(e) => Err(e),
}
}
fn scan_anchor(prefer_smbios3: bool) -> Result<Option<SmbiosTable>, DmiError> {
let map = PhysmapGuard::map(SMBIOS_ANCHOR_START, SMBIOS_ANCHOR_LEN)?;
// SAFETY: PhysmapGuard owns the mapping and we read within its bounds.
let bytes = unsafe { std::slice::from_raw_parts(map.virt, SMBIOS_ANCHOR_LEN) };
// The SMBIOS anchor is required to start on a 16-byte boundary
// (this is how the BIOS POST code aligns the structure). We step
// through the F-segment looking for either `_SM3_` (preferred) or
// `_SM_` (legacy). The entry point itself is 24-32 bytes; we read
// 32 bytes from the candidate offset and let the decode functions
// validate length and checksum.
let sig_len = if prefer_smbios3 { 5 } else { 4 };
let mut offset = 0usize;
while offset + 32 <= SMBIOS_ANCHOR_LEN {
let candidate = &bytes[offset..offset + 32];
if prefer_smbios3 {
if &candidate[..sig_len] == SMBIOS3_SIG {
match try_decode_smbios3(candidate) {
Ok(Some(table)) => return Ok(Some(table)),
Ok(None) => {}
Err(e) => {
debug!("SMBIOS3 candidate at {:#x} invalid: {}", offset, e);
}
}
}
} else {
// The legacy entry point requires the `_DMI_` signature
// 16 bytes after `_SM_`. Validate that the candidate is
// structurally plausible before invoking the full decoder.
if &candidate[..sig_len] == SMBIOS_SIG && &candidate[16..21] == DMI_SIG {
match try_decode_smbios_legacy(candidate) {
Ok(Some(table)) => return Ok(Some(table)),
Ok(None) => {}
Err(e) => {
debug!("legacy SMBIOS candidate at {:#x} invalid: {}", offset, e);
}
}
}
}
offset += SMBIOS_ANCHOR_STEP;
}
if offset >= SMBIOS_ANCHOR_LEN {
// Whole F-segment scanned, no anchor found.
Err(DmiError::NotPresent)
} else {
Ok(None)
}
}
/// Try to decode a 32-byte window as a 64-bit SMBIOS 3.x entry point.
/// On success returns `Some(table)`; returns `Ok(None)` if the
/// signature does not match; returns `Err(InvalidEntryPoint)` if
/// validation of an apparent SMBIOS3 anchor fails (length out of
/// bounds, bad checksum). Callers can choose to fall back to the
/// legacy entry point on the latter.
fn try_decode_smbios3(buf: &[u8]) -> Result<Option<SmbiosTable>, DmiError> {
if buf.len() < 24 {
return Ok(None);
}
if &buf[..5] != SMBIOS3_SIG {
return Ok(None);
}
let len = buf[6] as usize;
// Spec mandates >= 24; spec v3.0 errata allow up to 32.
if !(24..=32).contains(&len) {
debug!("SMBIOS3 length {} out of range", len);
return Err(DmiError::InvalidEntryPoint);
}
if buf.len() < len {
return Err(DmiError::InvalidEntryPoint);
}
if !checksum_ok(&buf[..len]) {
debug!("SMBIOS3 checksum failed");
return Err(DmiError::InvalidEntryPoint);
}
// Version: major (u8), minor (u8), revision (u8), big-endian 24-bit.
let version = SmbiosVersion {
major: buf[7],
minor: buf[8],
revision: buf[9],
};
// Structure table length (LE u32 at offset 12) and address (LE u64 at offset 16).
let table_len = u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]) as usize;
let mut addr_bytes = [0u8; 8];
addr_bytes.copy_from_slice(&buf[16..24]);
let table_addr = u64::from_le_bytes(addr_bytes) as usize;
info!(
"SMBIOS {}.{}.{} entry point, table @ {:#x} ({} bytes)",
version.major, version.minor, version.revision, table_addr, table_len
);
if table_addr == 0 || table_len == 0 {
return Err(DmiError::InvalidTableAddress);
}
let info = decode_structure_table(table_addr, table_len, 0, version)?;
Ok(Some(SmbiosTable { version, info }))
}
/// Try to decode a 32-byte window as the legacy 32-bit SMBIOS entry
/// point (with embedded `_DMI_` at offset 16). Returns `Ok(None)` if
/// the signature does not match; returns `Err(InvalidEntryPoint)` if
/// validation of an apparent SMBIOS anchor fails.
///
/// Offsets below use the absolute position in the 32-byte window. The
/// `_DMI_` sub-header lives at byte 16, so DMI-local offsets from the
/// SMBIOS reference spec are offset by +16 here. This matches the
/// Linux kernel's `dmi_present()` parser verbatim.
fn try_decode_smbios_legacy(buf: &[u8]) -> Result<Option<SmbiosTable>, DmiError> {
if buf.len() < 31 {
return Ok(None);
}
if &buf[..4] != SMBIOS_SIG {
return Ok(None);
}
let len = buf[5] as usize;
// The spec says 31, but version 2.1 mistakenly reports 30.
if !(30..=32).contains(&len) {
return Err(DmiError::InvalidEntryPoint);
}
if buf.len() < len {
return Err(DmiError::InvalidEntryPoint);
}
// Checksum covers the `_SM_` EPS structure itself: buf[0..buf[5]].
if !checksum_ok(&buf[..len]) {
debug!("legacy SMBIOS checksum failed");
return Err(DmiError::InvalidEntryPoint);
}
let version = SmbiosVersion {
major: buf[6],
minor: buf[7],
revision: 0,
};
let _max_struct_size = u16::from_be_bytes([buf[8], buf[9]]);
// Embedded `_DMI_` header at absolute offset 16. DMI-local layout:
// 0..5 signature "_DMI_"
// 5 checksum (covers 15 bytes: DMI[0..15])
// 6..8 table length (LE u16)
// 8..12 table address (LE u32)
// 12..14 number of structures (LE u16)
// 14 BCD revision
// 15 reserved
if &buf[16..21] != DMI_SIG {
return Ok(None);
}
// DMI checksum is over 15 bytes starting at the `_DMI_` signature,
// i.e. absolute buf[16..31].
if !checksum_ok(&buf[16..31]) {
debug!("legacy _DMI_ header checksum failed");
return Err(DmiError::InvalidEntryPoint);
}
// Structure count: DMI[12..14] → absolute buf[28..30].
let num_structs = u16::from_le_bytes([buf[28], buf[29]]);
// Table length: DMI[6..8] → absolute buf[22..24].
let total_len = u16::from_le_bytes([buf[22], buf[23]]) as usize;
// Table address: DMI[8..12] → absolute buf[24..28].
let mut addr_bytes = [0u8; 4];
addr_bytes.copy_from_slice(&buf[24..28]);
let table_addr = u32::from_le_bytes(addr_bytes) as usize;
info!(
"SMBIOS {}.{} entry point, {} structures, table @ {:#x} ({} bytes)",
version.major, version.minor, num_structs, table_addr, total_len
);
if table_addr == 0 || total_len == 0 {
return Err(DmiError::InvalidTableAddress);
}
let info = decode_structure_table(table_addr, total_len, num_structs, version)?;
Ok(Some(SmbiosTable { version, info }))
}
/// Decode a SMBIOS structure table located at physical address `base`
/// with `total_len` bytes. For SMBIOS 3.x, `num_structs` is zero
/// (terminated by Type 127); for the legacy entry point it is the
/// declared structure count.
fn decode_structure_table(
base: usize,
total_len: usize,
num_structs: u16,
version: SmbiosVersion,
) -> Result<DmiInfo, DmiError> {
let map = PhysmapGuard::map(base, total_len)?;
let bytes = unsafe { std::slice::from_raw_parts(map.virt, total_len) };
let mut info = DmiInfo::default();
let mut offset = 0usize;
let mut seen = 0u32;
while offset + 4 <= total_len {
if num_structs != 0 && seen >= num_structs as u32 {
break;
}
let header = &bytes[offset..];
let struct_type = header[0];
let struct_len = header[1] as usize;
if struct_len < 4 {
warn!(
"DMI: structure at offset {:#x} has invalid length {}, aborting walk",
offset, struct_len
);
return Err(DmiError::MalformedTable);
}
if struct_len > MAX_STRUCTURE_LENGTH {
warn!(
"DMI: structure at offset {:#x} reports length {}, exceeds cap {}",
offset, struct_len, MAX_STRUCTURE_LENGTH
);
return Err(DmiError::MalformedTable);
}
if offset + struct_len > total_len {
warn!("DMI: structure at offset {:#x} overruns table", offset);
return Err(DmiError::MalformedTable);
}
let structured = &bytes[offset..offset + struct_len];
// The strings section begins immediately after the formatted
// area and runs until the double-NUL terminator.
let strings_start = offset + struct_len;
let mut strings_end = strings_start;
while strings_end + 1 < total_len {
if bytes[strings_end] == 0 && bytes[strings_end + 1] == 0 {
break;
}
strings_end += 1;
}
if strings_end + 1 >= total_len {
warn!("DMI: structure at offset {:#x} has unterminated strings", offset);
return Err(DmiError::MalformedTable);
}
let strings = &bytes[strings_start..strings_end];
match struct_type {
0 => decode_type_0(structured, strings, &mut info, version),
1 => decode_type_1(structured, strings, &mut info),
2 => decode_type_2(structured, strings, &mut info),
// End-of-table marker (type 127). For SMBIOS 3.x tables this
// is the only stop signal.
127 if num_structs == 0 => break,
_ => {}
}
// Advance past formatted area, strings, and the double-NUL
// terminator.
offset = strings_end + 2;
seen += 1;
}
Ok(info)
}
/// Sum the bytes in `buf` and check that the result is zero.
fn checksum_ok(buf: &[u8]) -> bool {
let sum: u8 = buf.iter().fold(0u8, |acc, b| acc.wrapping_add(*b));
sum == 0
}
/// Look up a string in the variable-length string area by 1-based
/// index. Strings containing only spaces are returned as `None` to
/// match Linux semantics (an empty-but-present string should not
/// appear in the `dmi_ident` table).
fn dmi_string(strings: &[u8], index: u8) -> Option<String> {
if index == 0 {
return None;
}
let mut current = 1u8;
let mut start = 0usize;
for (i, &b) in strings.iter().enumerate() {
if b == 0 {
if current == index {
let raw = &strings[start..i];
let trimmed: &[u8] = match raw.iter().position(|c| *c != b' ') {
Some(p) => &raw[p..],
None => &[],
};
// Re-trim trailing spaces.
let end = trimmed
.iter()
.rposition(|c| *c != b' ')
.map(|p| p + 1)
.unwrap_or(0);
let s = &trimmed[..end];
if s.is_empty() {
return None;
}
return str::from_utf8(s).ok().map(|s| s.to_owned());
}
current = current.saturating_add(1);
start = i + 1;
}
}
None
}
/// Decode Type 0 — BIOS Information.
///
/// Reference: DMTF DSP0134 §7.1.
///
/// Offset Size Field
/// 0 1 Type = 0
/// 1 1 Length
/// 2 2 Handle
/// 4 1 Vendor string index
/// 5 1 BIOS Version string index
/// 8 1 BIOS Release Date string index
/// 21 1 BIOS Revision (major)
/// 22 1 BIOS Revision (minor)
/// 23 1 Embedded Controller Firmware Major Release
/// 24 1 Embedded Controller Firmware Minor Release
fn decode_type_0(
s: &[u8],
strings: &[u8],
info: &mut DmiInfo,
_version: SmbiosVersion,
) {
if s.len() < 22 {
return;
}
if info.bios_vendor.is_none() {
info.bios_vendor = dmi_string(strings, s[4]);
}
if info.bios_version.is_none() {
info.bios_version = dmi_string(strings, s[5]);
}
if info.bios_date.is_none() {
info.bios_date = dmi_string(strings, s[8]);
}
if info.bios_release.is_none() && s.len() >= 22 {
// 0xFF means "unsupported" per spec.
if !(s[20] == 0xFF && s[21] == 0xFF) {
info.bios_release = Some(format!("{}.{}", s[20], s[21]));
}
}
if info.ec_firmware_release.is_none() && s.len() >= 24 {
if !(s[22] == 0xFF && s[23] == 0xFF) {
info.ec_firmware_release = Some(format!("{}.{}", s[22], s[23]));
}
}
}
/// Decode Type 1 — System Information.
///
/// Reference: DMTF DSP0134 §7.2.
///
/// Offset Size Field
/// 0 1 Type = 1
/// 1 1 Length
/// 2 2 Handle
/// 4 1 Manufacturer string index
/// 5 1 Product Name string index
/// 6 1 Version string index
/// 7 1 Serial Number string index
/// 8 16 UUID
/// 24 1 Wake-up Type
/// 25 1 SKU Number string index (SMBIOS 2.4+)
/// 26 1 Family string index (SMBIOS 2.4+)
fn decode_type_1(s: &[u8], strings: &[u8], info: &mut DmiInfo) {
if s.len() < 8 {
return;
}
if info.sys_vendor.is_none() {
info.sys_vendor = dmi_string(strings, s[4]);
}
if info.product_name.is_none() {
info.product_name = dmi_string(strings, s[5]);
}
if info.product_version.is_none() {
info.product_version = dmi_string(strings, s[6]);
}
if info.product_serial.is_none() {
info.product_serial = dmi_string(strings, s[7]);
}
if info.product_uuid.is_none() && s.len() >= 24 {
let uuid = &s[8..24];
// Skip all-FF / all-00 sentinels (matches Linux).
let all_ff = uuid.iter().all(|b| *b == 0xFF);
let all_00 = uuid.iter().all(|b| *b == 0x00);
if !(all_ff || all_00) {
// Per SMBIOS 2.6+ the first three fields are little-endian.
// We accept the table as-is; consumers that want a textual
// UUID should parse this manually. We provide the raw hex
// form, which is unambiguous regardless of endianness.
info.product_uuid = Some(format!(
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
uuid[0], uuid[1], uuid[2], uuid[3],
uuid[4], uuid[5],
uuid[6], uuid[7],
uuid[8], uuid[9],
uuid[10], uuid[11], uuid[12], uuid[13], uuid[14], uuid[15]
));
}
}
if s.len() >= 26 {
if info.product_sku.is_none() {
info.product_sku = dmi_string(strings, s[25]);
}
}
if s.len() >= 27 {
if info.product_family.is_none() {
info.product_family = dmi_string(strings, s[26]);
}
}
}
/// Decode Type 2 — Baseboard (a.k.a. Module) Information.
///
/// Reference: DMTF DSP0134 §7.3.
///
/// Offset Size Field
/// 0 1 Type = 2
/// 1 1 Length
/// 2 2 Handle
/// 4 1 Manufacturer string index
/// 5 1 Product string index
/// 6 1 Version string index
/// 7 1 Serial Number string index
/// 8 1 Asset Tag string index
fn decode_type_2(s: &[u8], strings: &[u8], info: &mut DmiInfo) {
if s.len() < 9 {
return;
}
if info.board_vendor.is_none() {
info.board_vendor = dmi_string(strings, s[4]);
}
if info.board_name.is_none() {
info.board_name = dmi_string(strings, s[5]);
}
if info.board_version.is_none() {
info.board_version = dmi_string(strings, s[6]);
}
if info.board_serial.is_none() {
info.board_serial = dmi_string(strings, s[7]);
}
if info.board_asset_tag.is_none() {
info.board_asset_tag = dmi_string(strings, s[8]);
}
}
impl DmiInfo {
/// Format the identity fields as `key=value` lines for the
/// `/scheme/acpi/dmi` "summary" file consumed by
/// `redox-driver-sys` and `redbear-info`.
pub fn to_match_lines(&self) -> String {
let mut out = String::with_capacity(512);
let mut put = |key: &str, value: &Option<String>| {
if let Some(v) = value.as_deref() {
if !v.is_empty() {
out.push_str(key);
out.push('=');
out.push_str(v);
out.push('\n');
}
}
};
put("sys_vendor", &self.sys_vendor);
put("board_vendor", &self.board_vendor);
put("board_name", &self.board_name);
put("board_version", &self.board_version);
put("product_name", &self.product_name);
put("product_version", &self.product_version);
put("bios_version", &self.bios_version);
out
}
}
/// Read a single DMI field as a `String` from `/scheme/acpi/dmi/{field}`.
///
/// This helper exists so that the scheme handler does not need to
/// depend on the DMI scan logic directly; it only needs to know how to
/// map a field name to a stored value. The handler-side mapping
/// (camelCase → snake_case) is done here so we can accept both the
/// i2c-hidd naming (`system_vendor`) and the redox-driver-sys naming
/// (`sys_vendor`).
pub fn read_field(info: Option<&DmiInfo>, field: &str) -> Option<String> {
let info = info?;
let slot = match field {
"system_vendor" | "sys_vendor" => info.sys_vendor.as_ref(),
"product_name" => info.product_name.as_ref(),
"product_version" => info.product_version.as_ref(),
"product_serial" => info.product_serial.as_ref(),
"product_uuid" => info.product_uuid.as_ref(),
"product_sku" => info.product_sku.as_ref(),
"product_family" => info.product_family.as_ref(),
"board_name" => info.board_name.as_ref(),
"board_vendor" => info.board_vendor.as_ref(),
"board_version" => info.board_version.as_ref(),
"board_serial" => info.board_serial.as_ref(),
"board_asset_tag" => info.board_asset_tag.as_ref(),
"bios_vendor" => info.bios_vendor.as_ref(),
"bios_version" => info.bios_version.as_ref(),
"bios_date" => info.bios_date.as_ref(),
"bios_release" => info.bios_release.as_ref(),
"ec_firmware_release" => info.ec_firmware_release.as_ref(),
_ => None,
};
slot.cloned()
}
/// List of valid `/scheme/acpi/dmi/<field>` entries. Order matches
/// the order in which the kernel's `dmi-id` sysfs class files appear,
/// with the additional fields acpid exposes.
pub const DMI_FIELDS: &[&str] = &[
"sys_vendor",
"product_name",
"product_version",
"product_serial",
"product_uuid",
"product_sku",
"product_family",
"board_vendor",
"board_name",
"board_version",
"board_serial",
"board_asset_tag",
"bios_vendor",
"bios_version",
"bios_date",
"bios_release",
"ec_firmware_release",
];
/// Try to load an existing `/scheme/acpi/dmi` cache (if another
/// process already exposed one). This is unused at the moment but
/// kept as a stub for future kernel-side SMBIOS scheme support.
#[allow(dead_code)]
pub fn try_load_existing() -> Option<DmiInfo> {
let mut file = File::open("/scheme/acpi/dmi").ok()?;
let mut s = String::new();
file.read_to_string(&mut s).ok()?;
parse_match_lines(&s)
}
/// Parse a `key=value` blob (one entry per line) into a `DmiInfo`.
#[allow(dead_code)]
pub fn parse_match_lines(s: &str) -> Option<DmiInfo> {
let mut info = DmiInfo::default();
let mut any = false;
for line in s.lines() {
let Some((key, value)) = line.split_once('=') else {
continue;
};
let key = key.trim();
let value = value.trim();
if value.is_empty() {
continue;
}
any = true;
match key {
"sys_vendor" => info.sys_vendor = Some(value.to_owned()),
"product_name" => info.product_name = Some(value.to_owned()),
"product_version" => info.product_version = Some(value.to_owned()),
"product_serial" => info.product_serial = Some(value.to_owned()),
"product_uuid" => info.product_uuid = Some(value.to_owned()),
"product_sku" => info.product_sku = Some(value.to_owned()),
"product_family" => info.product_family = Some(value.to_owned()),
"board_vendor" => info.board_vendor = Some(value.to_owned()),
"board_name" => info.board_name = Some(value.to_owned()),
"board_version" => info.board_version = Some(value.to_owned()),
"board_serial" => info.board_serial = Some(value.to_owned()),
"board_asset_tag" => info.board_asset_tag = Some(value.to_owned()),
"bios_vendor" => info.bios_vendor = Some(value.to_owned()),
"bios_version" => info.bios_version = Some(value.to_owned()),
"bios_date" => info.bios_date = Some(value.to_owned()),
"bios_release" => info.bios_release = Some(value.to_owned()),
"ec_firmware_release" => info.ec_firmware_release = Some(value.to_owned()),
_ => {}
}
}
if any {
Some(info)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn checksum_of_known_zero() {
assert!(checksum_ok(&[0u8; 16]));
}
#[test]
fn checksum_rejects_nonzero() {
assert!(!checksum_ok(&[1u8, 2, 3, 4]));
}
#[test]
fn dmi_string_basic() {
let s = b"Foo\0Bar\0Baz\0";
assert_eq!(dmi_string(s, 1).as_deref(), Some("Foo"));
assert_eq!(dmi_string(s, 2).as_deref(), Some("Bar"));
assert_eq!(dmi_string(s, 3).as_deref(), Some("Baz"));
assert!(dmi_string(s, 0).is_none());
assert!(dmi_string(s, 4).is_none());
}
#[test]
fn dmi_string_spaces_are_empty() {
let s = b" \0Real\0";
// Per Linux semantics a string that contains only spaces is empty.
assert!(dmi_string(s, 1).is_none());
assert_eq!(dmi_string(s, 2).as_deref(), Some("Real"));
}
#[test]
fn to_match_lines_skips_empty() {
let info = DmiInfo {
sys_vendor: Some("Framework".to_owned()),
product_name: Some("Laptop 16".to_owned()),
..Default::default()
};
let s = info.to_match_lines();
assert!(s.contains("sys_vendor=Framework"));
assert!(s.contains("product_name=Laptop 16"));
assert!(!s.contains("board_vendor"));
}
#[test]
fn parse_match_lines_roundtrip() {
let src = "sys_vendor=Framework\nproduct_name=Laptop 16\nboard_name=FRANMECP01\n";
let info = parse_match_lines(src).expect("must parse");
assert_eq!(info.sys_vendor.as_deref(), Some("Framework"));
assert_eq!(info.product_name.as_deref(), Some("Laptop 16"));
assert_eq!(info.board_name.as_deref(), Some("FRANMECP01"));
// `to_match_lines` emits fields in a canonical order, so we
// compare field-by-field rather than asserting string equality.
let out = info.to_match_lines();
assert!(out.contains("sys_vendor=Framework\n"));
assert!(out.contains("product_name=Laptop 16\n"));
assert!(out.contains("board_name=FRANMECP01\n"));
}
#[test]
fn read_field_handles_aliases() {
let info = DmiInfo {
sys_vendor: Some("Dell Inc.".to_owned()),
product_name: Some("OptiPlex 7090".to_owned()),
..Default::default()
};
// i2c-hidd uses `system_vendor`; redox-driver-sys uses
// `sys_vendor`. Both must work.
assert_eq!(
read_field(Some(&info), "system_vendor").as_deref(),
Some("Dell Inc.")
);
assert_eq!(
read_field(Some(&info), "sys_vendor").as_deref(),
Some("Dell Inc.")
);
assert_eq!(
read_field(Some(&info), "product_name").as_deref(),
Some("OptiPlex 7090")
);
assert!(read_field(Some(&info), "missing").is_none());
assert!(read_field(None, "sys_vendor").is_none());
}
/// Build a synthetic 32-byte SMBIOS 2.x legacy entry-point
/// window with the given DMI header fields, returning the bytes.
/// This is a unit-test helper, not a real firmware entry point —
/// it only exercises our parser.
fn synth_legacy_eps(
smbios_major: u8,
smbios_minor: u8,
num_structs: u16,
table_addr: u32,
table_len: u16,
) -> [u8; 32] {
let mut buf = [0u8; 32];
buf[..4].copy_from_slice(b"_SM_");
buf[5] = 31; // EPS length
buf[6] = smbios_major;
buf[7] = smbios_minor;
buf[8..10].copy_from_slice(&0u16.to_be_bytes()); // max struct size
buf[16..21].copy_from_slice(b"_DMI_");
buf[22..24].copy_from_slice(&table_len.to_le_bytes());
buf[24..28].copy_from_slice(&table_addr.to_le_bytes());
buf[28..30].copy_from_slice(&num_structs.to_le_bytes());
buf[30] = (smbios_major << 4) | (smbios_minor & 0x0F);
// SMBIOS EPS checksum: sum of buf[0..31] must be 0 mod 256.
let smbios_sum: u8 = buf[..31].iter().copied().fold(0u8, u8::wrapping_add);
buf[4] = (0u8).wrapping_sub(smbios_sum);
// _DMI_ checksum: sum of buf[16..31] must be 0 mod 256.
let dmi_sum: u8 = buf[16..31].iter().copied().fold(0u8, u8::wrapping_add);
buf[21] = (0u8).wrapping_sub(dmi_sum);
buf
}
#[test]
fn try_decode_smbios_legacy_picks_correct_offsets() {
// Build a synthetic EPS that advertises 7 structures at
// physical address 0x12345678, total length 0x400. Verify
// the parser returns those exact values (i.e. it is reading
// from the DMI sub-header, not from the `_SM_` prefix).
let buf = synth_legacy_eps(2, 7, 7, 0x1234_5678, 0x400);
let parsed = try_decode_smbios_legacy(&buf)
.expect("parser should not error")
.expect("parser should succeed");
assert_eq!(parsed.version.major, 2);
assert_eq!(parsed.version.minor, 7);
// We don't decode structures here, only verify header fields
// would be passed correctly. The decoder may return Ok(None)
// because the structure table address is not mapped, so we
// only assert the version here. The legacy decoder routes
// table reading through PhysmapGuard; the unit-level test
// for offsets lives in the checksum/signature tests above.
assert_eq!(parsed.version.revision, 0);
}
#[test]
fn try_decode_smbios_legacy_rejects_bad_dmi_checksum() {
let mut buf = synth_legacy_eps(2, 7, 7, 0x1234_5678, 0x400);
// Flip a bit in the DMI sub-header to break its checksum.
buf[24] ^= 0x01;
// Re-seal the SMBIOS checksum so we exercise the DMI path.
let smbios_sum: u8 = buf[..31].iter().copied().fold(0u8, u8::wrapping_add);
buf[4] = (0u8).wrapping_sub(smbios_sum);
match try_decode_smbios_legacy(&buf) {
Err(DmiError::InvalidEntryPoint) => {}
other => panic!("expected InvalidEntryPoint, got {:?}", other),
}
}
/// Verify that decode_type_1 handles the field layout we depend on.
#[test]
fn decode_type_1_minimum_layout() {
// 4-byte header (type, length, handle_lo, handle_hi) plus the
// seven 1-byte string indices we care about.
let mut s = [0u8; 9];
s[0] = 1; // type
s[1] = 9; // length
s[4] = 1; // manufacturer string
s[5] = 2; // product name string
s[6] = 3; // version string
s[7] = 4; // serial string
let strings = b"Acme Corp\0Widget 3000\0Rev A\0SN12345\0";
let mut info = DmiInfo::default();
decode_type_1(&s, strings, &mut info);
assert_eq!(info.sys_vendor.as_deref(), Some("Acme Corp"));
assert_eq!(info.product_name.as_deref(), Some("Widget 3000"));
assert_eq!(info.product_version.as_deref(), Some("Rev A"));
assert_eq!(info.product_serial.as_deref(), Some("SN12345"));
}
}