diff --git a/src/acpi/mod.rs b/src/acpi/mod.rs index 301edef047..8fa805b166 100644 --- a/src/acpi/mod.rs +++ b/src/acpi/mod.rs @@ -26,6 +26,7 @@ mod rsdp; mod rsdt; mod rxsdt; pub mod sdt; +pub mod smbios; #[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64"))] pub mod slit; #[cfg(target_arch = "aarch64")] @@ -55,6 +56,14 @@ unsafe fn map_linearly( } } +pub(crate) unsafe fn map_physical_range( + addr: PhysicalAddress, + len: usize, + mapper: &mut PageMapper, +) { + unsafe { map_linearly(addr, len, mapper) } +} + pub fn get_sdt( sdt_address: PhysicalAddress, mapper: &mut PageMapper, @@ -181,6 +190,7 @@ pub unsafe fn init_after_mem(already_supplied_rsdp: Option>) { // TODO: Enumerate processors in userspace, and then provide an ACPI-independent interface // to initialize enumerated processors to userspace? + smbios::init(); Madt::init(); //TODO: support this on any arch // SPCR must be initialized after MADT for interrupt controllers diff --git a/src/acpi/smbios.rs b/src/acpi/smbios.rs new file mode 100644 index 0000000000..f2eff41bf1 --- /dev/null +++ b/src/acpi/smbios.rs @@ -0,0 +1,321 @@ +//! Minimal SMBIOS / DMI table scanning for kernel-side platform +//! identification at boot. +//! +//! Used by `ioapic::handle_src_override` to skip the IRQ1 ActiveHigh +//! override on platforms with the `acpi_irq1_skip_override` DMI quirk +//! (LG Gram and similar — mirrors Linux's +//! `drivers/acpi/resource.c` `irq1_level_low_skip_override[]`). +//! +//! Defensive by design: every error returns early, no panics. A failed +//! or absent SMBIOS scan is never fatal — callers fall through to +//! "no quirks". Only the fields needed for DMI matching are extracted +//! (sys_vendor, product_name, board_name); the full SMBIOS decode +//! lives in userspace `acpid::dmi`. + +use spin::Once; + +use crate::memory::{PhysicalAddress, RmmA, RmmArch}; + +/// Standard SMBIOS BIOS anchor scan range (ACPI 6.5 §5). +const SMBIOS_ANCHOR_START: usize = 0x000F_0000; +const SMBIOS_ANCHOR_LEN: usize = 0x0001_0000; +const SMBIOS_ANCHOR_STEP: usize = 16; + +const SMBIOS3_SIG: &[u8; 5] = b"_SM3_"; +const SMBIOS_SIG: &[u8; 4] = b"_SM_"; +const DMI_SIG: &[u8; 5] = b"_DMI_"; + +/// Upper bound on a single SMBIOS structure's formatted area. Mirrors +/// Linux and the SMBIOS reference spec cap. +const MAX_STRUCTURE_LENGTH: usize = 256; + +/// Identity fields extracted from the SMBIOS Type 1 (System Information) +/// and Type 2 (Baseboard Information) structures. All string lifetimes +/// are `'static` because the underlying SMBIOS structure table is mapped +/// into the kernel's linear map during `init()` and never unmapped. +#[derive(Clone, Debug, Default)] +pub struct SmbiosInfo { + pub sys_vendor: Option<&'static str>, + pub product_name: Option<&'static str>, + pub board_vendor: Option<&'static str>, + pub board_name: Option<&'static str>, +} + +/// Global SMBIOS identity, populated once during `acpi::init_after_mem` +/// before `Madt::init()`. `None` fields mean "SMBIOS absent or the field +/// was not declared". Callers must treat absent data as "no match" rather +/// than as an error. +pub static SMBIOS_INFO: Once = Once::new(); + +/// Initialise the global [`SMBIOS_INFO`] from a fresh SMBIOS scan. +/// Safe to call exactly once; subsequent calls are no-ops (the `Once` +/// guard deduplicates). Non-fatal on every failure path — the kernel +/// proceeds without DMI-based quirks when SMBIOS is absent or corrupt. +pub fn init() { + SMBIOS_INFO.call_once(|| { + let info = scan().unwrap_or_default(); + if info.sys_vendor.is_some() || info.product_name.is_some() { + info!( + "SMBIOS: sys_vendor={:?} product_name={:?} board_name={:?}", + info.sys_vendor, info.product_name, info.board_name + ); + } else { + debug!("SMBIOS: no usable entry point found (DMI-based quirks disabled)"); + } + info + }); +} + +/// Convenience accessor used by `ioapic::handle_src_override`. +pub fn info() -> &'static SmbiosInfo { + SMBIOS_INFO.get().expect("SMBIOS_INFO not initialized — acpi::init_after_mem order bug") +} + +/// Full scan: locate anchor, validate, walk structures, extract identity. +/// Returns `None` on any failure — callers treat that as "no SMBIOS". +fn scan() -> Option { + // Map the 64 KiB BIOS anchor scan window. The kernel mapper lock is + // held for the duration; no allocations are performed (per KernelMapper + // docs). The linear-map entries persist for the lifetime of the kernel. + { + use crate::memory::KernelMapper; + let mut mapper = KernelMapper::lock_rw(); + super::map_physical_range( + PhysicalAddress::new(SMBIOS_ANCHOR_START), + SMBIOS_ANCHOR_LEN, + &mut mapper, + ); + } + + let anchor_virt = RmmA::phys_to_virt(PhysicalAddress::new(SMBIOS_ANCHOR_START)).data(); + // SAFETY: We just mapped the entire SMBIOS_ANCHOR_LEN window via + // map_physical_range. The slice is read-only and lives for the + // lifetime of the kernel (the linear map is never torn down). + let anchor_bytes: &[u8] = + unsafe { core::slice::from_raw_parts(anchor_virt as *const u8, SMBIOS_ANCHOR_LEN) }; + + let mut offset = 0; + while offset + 32 <= SMBIOS_ANCHOR_LEN { + let candidate = &anchor_bytes[offset..offset + 32]; + + // SMBIOS 3.x 64-bit entry point (preferred). + if candidate.len() >= 24 && &candidate[..5] == SMBIOS3_SIG { + if let Some((table_addr, table_len)) = parse_smbios3_anchor(candidate) { + if let Some(info) = walk_table(table_addr, table_len, 0) { + return Some(info); + } + } + } + + // Legacy 32-bit SMBIOS 2.x entry point. + if &candidate[..4] == SMBIOS_SIG && &candidate[16..21] == DMI_SIG { + if let Some((table_addr, table_len, num_structs)) = parse_smbios_legacy_anchor(candidate) + { + if let Some(info) = walk_table(table_addr, table_len, num_structs) { + return Some(info); + } + } + } + + offset += SMBIOS_ANCHOR_STEP; + } + + None +} + +/// Validate an SMBIOS 3.x anchor and return `(table_addr, table_len)`. +fn parse_smbios3_anchor(buf: &[u8]) -> Option<(usize, usize)> { + if buf.len() < 24 { + return None; + } + let len = buf[6] as usize; + if !(24..=32).contains(&len) || buf.len() < len { + return None; + } + if !checksum_ok(&buf[..len]) { + return None; + } + 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; + if table_addr == 0 || table_len == 0 { + return None; + } + Some((table_addr, table_len)) +} + +/// Validate a legacy SMBIOS 2.x anchor and return `(table_addr, table_len, num_structs)`. +fn parse_smbios_legacy_anchor(buf: &[u8]) -> Option<(usize, usize, u16)> { + if buf.len() < 31 { + return None; + } + let len = buf[5] as usize; + if !(30..=32).contains(&len) || buf.len() < len { + return None; + } + if !checksum_ok(&buf[..len]) { + return None; + } + if !checksum_ok(&buf[16..31]) { + return None; + } + let num_structs = u16::from_le_bytes([buf[28], buf[29]]); + let total_len = u16::from_le_bytes([buf[22], buf[23]]) as usize; + 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; + if table_addr == 0 || total_len == 0 { + return None; + } + Some((table_addr, total_len, num_structs)) +} + +/// Map and walk the SMBIOS structure table. `num_structs` is 0 for +/// SMBIOS 3.x (terminated by Type 127); nonzero for legacy (declared count). +fn walk_table(table_addr: usize, total_len: usize, num_structs: u16) -> Option { + { + use crate::memory::KernelMapper; + let mut mapper = KernelMapper::lock_rw(); + super::map_physical_range(PhysicalAddress::new(table_addr), total_len, &mut mapper); + } + + let virt = RmmA::phys_to_virt(PhysicalAddress::new(table_addr)).data(); + // SAFETY: `total_len` bytes at `table_addr` were just mapped. + let bytes: &[u8] = unsafe { core::slice::from_raw_parts(virt as *const u8, total_len) }; + + let mut info = SmbiosInfo::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 struct_type = bytes[offset]; + let struct_len = bytes[offset + 1] as usize; + if struct_len < 4 || struct_len > MAX_STRUCTURE_LENGTH { + return Some(info); + } + if offset + struct_len > total_len { + return Some(info); + } + + let formatted = &bytes[offset..offset + struct_len]; + let strings_start = offset + struct_len; + let (strings_end, ok) = find_strings_terminator(bytes, strings_start); + if !ok { + return Some(info); + } + let strings = &bytes[strings_start..strings_end]; + + match struct_type { + 1 => decode_type_1(formatted, strings, &mut info), + 2 => decode_type_2(formatted, strings, &mut info), + 127 if num_structs == 0 => break, + _ => {} + } + + offset = strings_end + 2; + seen += 1; + } + + Some(info) +} + +/// Find the double-NUL terminator that ends a structure's strings area. +/// Returns (end_offset, true) on success, (0, false) if not found within bounds. +fn find_strings_terminator(bytes: &[u8], start: usize) -> (usize, bool) { + let mut i = start; + while i + 1 < bytes.len() { + if bytes[i] == 0 && bytes[i + 1] == 0 { + return (i, true); + } + i += 1; + } + (0, false) +} + +/// Sum bytes and check for zero (SMBIOS checksum rule). +fn checksum_ok(buf: &[u8]) -> bool { + buf.iter().fold(0u8, |a, b| a.wrapping_add(*b)) == 0 +} + +/// Extract a NUL-terminated string from the strings area by 1-based index. +/// Returns a `'static` slice into the mapped SMBIOS table. Index 0 means +/// "not present". Strings containing only spaces are treated as absent +/// (matches Linux semantics). +fn dmi_string(strings: &[u8], index: u8) -> Option<&'static str> { + 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 = raw.iter().position(|c| *c != b' ').map(|p| &raw[p..]).unwrap_or(&[]); + 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; + } + // SAFETY: `strings` is a slice over the kernel's linear-mapped + // SMBIOS table, which is never unmapped. The bytes are valid + // UTF-8 because SMBIOS strings are ASCII per the spec; if a + // weird BIOS emits non-ASCII we fall through to None. + return core::str::from_utf8(s).ok(); + } + current = current.saturating_add(1); + start = i + 1; + } + } + None +} + +/// Decode SMBIOS Type 1 (System Information) — DMTF DSP0134 §7.2. +/// Offset 4 = manufacturer string index, offset 5 = product name string index. +fn decode_type_1(formatted: &[u8], strings: &[u8], info: &mut SmbiosInfo) { + if formatted.len() < 6 { + return; + } + if info.sys_vendor.is_none() { + info.sys_vendor = dmi_string(strings, formatted[4]); + } + if info.product_name.is_none() { + info.product_name = dmi_string(strings, formatted[5]); + } +} + +/// Decode SMBIOS Type 2 (Baseboard Information) — DMTF DSP0134 §7.3. +/// Offset 4 = manufacturer string index, offset 5 = product string index. +fn decode_type_2(formatted: &[u8], strings: &[u8], info: &mut SmbiosInfo) { + if formatted.len() < 6 { + return; + } + if info.board_vendor.is_none() { + info.board_vendor = dmi_string(strings, formatted[4]); + } + if info.board_name.is_none() { + info.board_name = dmi_string(strings, formatted[5]); + } +} + +impl SmbiosInfo { + /// True if any field matches the given substring (case-sensitive). + /// Used by `ioapic::handle_src_override` for the IRQ1 skip table. + pub fn sys_vendor_contains(&self, needle: &str) -> bool { + self.sys_vendor + .map(|s| s.contains(needle)) + .unwrap_or(false) + } + + /// True if the board_name field starts with the given prefix. + /// Used for matching LG Gram's `board_name = "16Z90TP"` style. + pub fn board_name_starts_with(&self, prefix: &str) -> bool { + self.board_name + .map(|s| s.starts_with(prefix)) + .unwrap_or(false) + } +} diff --git a/src/arch/x86_shared/device/ioapic.rs b/src/arch/x86_shared/device/ioapic.rs index fb66d3bf2b..90cc8014d9 100644 --- a/src/arch/x86_shared/device/ioapic.rs +++ b/src/arch/x86_shared/device/ioapic.rs @@ -6,6 +6,7 @@ use spin::Mutex; use super::{local_apic::ApicId, pic}; use crate::{ acpi::madt::{self, Madt, MadtEntry, MadtIntSrcOverride, MadtIoApic}, + acpi::smbios, arch::{cpuid::cpuid, interrupt::irq}, memory::{map_device_memory, PhysicalAddress}, }; @@ -253,6 +254,30 @@ pub unsafe fn handle_src_override(src_override: &'static MadtIntSrcOverride) { let polarity_raw = (flags & 0x0003) as u8; let trigger_mode_raw = ((flags & 0x000C) >> 2) as u8; + // ACPI IRQ1 (i8042 keyboard) ActiveHigh-override skip table. + // + // Mirrors Linux `drivers/acpi/resource.c` + // `irq1_level_low_skip_override[]`: platforms whose firmware + // declares IRQ1 as ActiveLow in the DSDT but the MADT + // Interrupt Source Override tries to flip it to ActiveHigh. + // On these platforms the override causes the i8042 keyboard + // IRQ to stop firing (keys are dropped or the controller + // wedges). The fix is to skip the override so the DSDT's + // ActiveLow trigger stays in effect. + // + // The DMI match is done via the kernel's early-boot SMBIOS + // scan (`acpi::smbios`), which runs before MADT init. If + // SMBIOS is unavailable the check is a no-op (no skip). + if src_override.irq_source == 1 + && polarity_raw == 0b01 // Polarity::ActiveHigh + && should_skip_irq1_override() + { + info!( + "ioapic: skipping IRQ1 ActiveHigh override due to platform quirk (keeping DSDT ActiveLow)" + ); + return; + } + let polarity = match polarity_raw { 0b00 => Polarity::ConformsToSpecs, 0b01 => Polarity::ActiveHigh, @@ -282,6 +307,31 @@ pub unsafe fn handle_src_override(src_override: &'static MadtIntSrcOverride) { } } +/// DMI match entries for platforms that need the IRQ1 ActiveHigh-override +/// skip. Each entry is a substring match against the SMBIOS sys_vendor +/// field. The LG Electronics entry covers all LG Gram models (LG's firmware +/// consistently declares IRQ1 ActiveLow in DSDT while MADT tries to flip +/// it). Add entries here when new affected platforms are identified. +const IRQ1_SKIP_OVERRIDE_VENDORS: &[&str] = &[ + "LG Electronics", +]; + +/// True if the running platform's SMBIOS identity matches a known +/// affected vendor in [`IRQ1_SKIP_OVERRIDE_VENDORS`]. Returns `false` +/// when SMBIOS is unavailable (no skip — the standard MADT override +/// applies). +fn should_skip_irq1_override() -> bool { + let Some(info) = smbios::SMBIOS_INFO.get() else { + return false; + }; + for vendor in IRQ1_SKIP_OVERRIDE_VENDORS { + if info.sys_vendor_contains(vendor) { + return true; + } + } + false +} + #[allow(dead_code)] pub unsafe fn init() { unsafe {