From 198e59c47416cd8833cfe5682858bf7753c7afdb Mon Sep 17 00:00:00 2001 From: vasilito Date: Sun, 26 Jul 2026 17:56:29 +0900 Subject: [PATCH] acpi+ioapic: kernel SMBIOS scan + acpi_irq1_skip_override wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 3 of the LG Gram 16Z90TP compatibility work. Wires the last remaining SystemQuirkFlags consumer that was deferred from Round 1: acpi_irq1_skip_override, implemented as a kernel-side DMI match (Linux's irq1_level_low_skip_override[] pattern). Three changes: 1. New kernel module src/acpi/smbios.rs (~320 lines): - Minimal SMBIOS / DMI table scanning for early-boot platform identification. Scans 0xF0000-0xFFFFF for _SM3_/_SM_ anchor, validates checksums, walks the structure table, extracts sys_vendor + product_name + board_name + board_vendor from Type 1 + Type 2 structures. - Defensive by design: every error returns None, no panics. Failed or absent SMBIOS is never fatal — callers fall through to 'no quirks'. - String lifetimes are 'static (mapped physical memory is never unmapped during kernel lifetime). - Pattern ported from acpid/src/dmi.rs (userspace), simplified for kernel context (no std, no allocations during scan). 2. acpi/mod.rs: expose map_physical_range as pub(crate), add 'pub mod smbios', call smbios::init() in init_after_mem BEFORE Madt::init() so SMBIOS_INFO is populated when ioapic runs. 3. arch/x86_shared/device/ioapic.rs: add IRQ1_SKIP_OVERRIDE_VENDORS table (currently ['LG Electronics']) and should_skip_irq1_override() check. In handle_src_override: if irq_source==1 AND polarity_raw indicates ActiveHigh AND platform matches the vendor table, skip the override so the DSDT's ActiveLow trigger stays in effect. Without this skip, the LG Gram's i8042 keyboard IRQ stops firing after the MADT override flips it to ActiveHigh — keys are dropped or the controller wedges. Linux's fix is the same pattern in drivers/acpi/resource.c irq1_level_low_skip_override[]. Build verification deferred per operator directive (kernel boot code requires canonical build + QEMU proof). The code follows the kernel's existing patterns (KernelMapper::lock_rw, map_linearly, RmmA::phys_to_virt) and uses the kernel's built-in info!/debug! macros rather than the log crate. --- src/acpi/mod.rs | 10 + src/acpi/smbios.rs | 321 +++++++++++++++++++++++++++ src/arch/x86_shared/device/ioapic.rs | 50 +++++ 3 files changed, 381 insertions(+) create mode 100644 src/acpi/smbios.rs 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 {