Files
chipsec-chipsec/chipsec/helper/linuxnative/linuxnativehelper.py
dscott90 32e2ba62c3 Add firmware data retrieval (#2710)
* Add firmware data retrieval
* Apply suggestions from code review
* Remove FW info native versions from linux/win helpers and added results to info module

---------

Signed-off-by: Dan Scott <dan.scott@intel.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Nathaniel Mitchell <nathaniel.p.mitchell@intel.com>
2026-03-31 13:54:29 -07:00

485 lines
18 KiB
Python

# CHIPSEC: Platform Security Assessment Framework
# Copyright (c) 2023, Intel Corporation
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; Version 2.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# Contact information:
# chipsec@intel.com
#
"""
Native Linux helper
"""
import mmap
import os
import platform
import resource
import struct
import sys
from typing import Optional, Tuple, TYPE_CHECKING
if TYPE_CHECKING:
from ctypes import Array
from chipsec.library import defines
from chipsec.library.exceptions import OsHelperError
from chipsec.helper.basehelper import Helper
from chipsec.helper.linuxnative.cpuid import CPUID
from chipsec.helper.linuxnative.legacy_pci import LegacyPci
from chipsec.library.logger import logger
class MemoryMapping(mmap.mmap):
"""Memory mapping based on Python's mmap.
This subclass keeps tracks of the start and end of the mapping.
"""
def __init__(self, fileno, length, flags, prot, offset):
self.start = offset
self.end = offset + length
super().__init__()
class LinuxNativeHelper(Helper):
DEV_MEM = '/dev/mem'
DEV_PORT = '/dev/port'
def __init__(self):
super(LinuxNativeHelper, self).__init__()
self.os_system = platform.system()
self.os_release = platform.release()
self.os_version = platform.version()
self.os_machine = platform.machine()
self.os_uname = platform.uname()
self.name = 'LinuxNativeHelper'
self.dev_fh = None
self.dev_mem = None
self.dev_port = None
self.dev_msr = None
# A list of all the mappings allocated via map_io_space. When using
# read/write MMIO, if the region is already mapped in the process's
# memory, simply read/write from there.
self.mappings = []
###############################################################################################
# Driver/service management functions
###############################################################################################
def create(self) -> bool:
logger().log_debug('[helper] Linux Helper created')
return True
def start(self) -> bool:
self.init()
logger().log_debug('[helper] Linux Helper started/loaded')
return True
def stop(self) -> bool:
self.close()
logger().log_debug('[helper] Linux Helper stopped/unloaded')
return True
def delete(self) -> bool:
logger().log_debug('[helper] Linux Helper deleted')
return True
def init(self):
x64 = True if sys.maxsize > 2**32 else False
self._pack = 'Q' if x64 else 'I'
def devmem_available(self) -> bool:
"""Check if /dev/mem is usable.
In case the driver is not loaded, we might be able to perform the
requested operation via /dev/mem. Returns True if /dev/mem is
accessible.
"""
if self.dev_mem:
return True
try:
self.dev_mem = os.open(self.DEV_MEM, os.O_RDWR)
return True
except IOError as err:
raise OsHelperError('Unable to open /dev/mem.\n'
'This command requires access to /dev/mem.\n'
'Are you running this command as root?\n'
f'{str(err)}', err.errno)
def devport_available(self) -> bool:
"""Check if /dev/port is usable.
In case the driver is not loaded, we might be able to perform the
requested operation via /dev/port. Returns True if /dev/port is
accessible.
"""
if self.dev_port:
return True
try:
self.dev_port = os.open(self.DEV_PORT, os.O_RDWR)
return True
except IOError as err:
raise OsHelperError('Unable to open /dev/port.\n'
'This command requires access to /dev/port.\n'
'Are you running this command as root?\n'
f'{str(err)}', err.errno)
def devmsr_available(self) -> bool:
"""Check if /dev/cpu/CPUNUM/msr is usable.
In case the driver is not loaded, we might be able to perform the
requested operation via /dev/cpu/CPUNUM/msr. This requires loading
the (more standard) msr driver. Returns True if /dev/cpu/CPUNUM/msr
is accessible.
"""
if self.dev_msr:
return True
try:
self.dev_msr = {}
if not os.path.exists('/dev/cpu/0/msr'):
os.system('modprobe msr')
for cpu in os.listdir('/dev/cpu'):
logger().log_debug(f'found cpu = {str(cpu)}')
if cpu.isdigit():
cpu = int(cpu)
self.dev_msr[cpu] = os.open(f'/dev/cpu/{str(cpu)}/msr', os.O_RDWR)
logger().log_debug(f'Added dev_msr {str(cpu)}')
return True
except IOError as err:
raise OsHelperError('Unable to open /dev/cpu/CPUNUM/msr.\n'
'This command requires access to /dev/cpu/CPUNUM/msr.\n'
'Are you running this command as root?\n'
'Do you have the msr kernel module installed?\n'
f'{str(err)}', err.errno)
def close(self):
if self.dev_mem:
os.close(self.dev_mem)
self.dev_mem = None
###############################################################################################
# Actual API functions to access HW resources
###############################################################################################
def read_pci_reg(self, bus: int, device: int, function: int, offset: int, size: int, domain: int = 0) -> int:
device_name = f'{domain:04x}:{bus:02x}:{device:02x}.{function}'
device_path = f'/sys/bus/pci/devices/{device_name}/config'
if not os.path.exists(device_path):
if offset < 256:
value = LegacyPci.read_pci_config(bus, device, function, offset)
if size == 1:
value = value & 0xFF
elif size == 2:
value = value & 0xFFFF
elif size == 4:
value = value & 0xFFFF_FFFF
elif size == 8:
value = value & 0xFFFF_FFFF_FFFF_FFFF
return value
else:
raise ValueError('Offset out of bounds')
try:
with open(device_path, 'rb') as config:
config.seek(offset)
reg = config.read(size)
reg = defines.unpack1(reg, size)
return reg
except IOError as err:
raise OsHelperError(f'Unable to open {device_path}', err.errno)
def write_pci_reg(self, bus: int, device: int, function: int, offset: int, value: int, size: int = 4, domain: int = 0) -> int:
device_name = f'{domain:04x}:{bus:02x}:{device:02x}.{function}'
device_path = f'/sys/bus/pci/devices/{device_name}/config'
if not os.path.exists(device_path):
if offset < 256:
LegacyPci.write_pci_config(bus, device, function, offset, value)
return -1
try:
with open(device_path, 'wb') as config:
config.seek(offset)
config.write(defines.pack1(value, size))
except IOError as err:
raise OsHelperError(f'Unable to open {device_path}', err.errno)
return 0
# @TODO fix memory mapping and bar_size
def read_mmio_reg(self, phys_address: int, size: int) -> int:
if self.devmem_available():
region = self.memory_mapping(phys_address, size)
if not region:
self.map_io_space(phys_address, size, 0)
region = self.memory_mapping(phys_address, size)
if not region:
logger().log_error(f'Unable to map region {phys_address:08x}')
# Create memoryview into mmap'ed region
region_mv = memoryview(region)
offset_in_region = phys_address - region.start
if size == 1:
return region_mv[offset_in_region]
if offset_in_region % size == 0:
# Read aligned value
region_casted = region_mv.cast(defines.SIZE2FORMAT[size])
return region_casted[offset_in_region // size]
# Read unaligned value
return defines.unpack1(region_mv[offset_in_region:offset_in_region + size], size)
return 0
# @TODO fix memory mapping and bar_size
def write_mmio_reg(self, phys_address: int, size: int, value: int) -> None:
if self.devmem_available():
reg = defines.pack1(value, size)
region = self.memory_mapping(phys_address, size)
if not region:
self.map_io_space(phys_address, size, 0)
region = self.memory_mapping(phys_address, size)
if not region:
logger().log_error(f'Unable to map region {phys_address:08x}')
# Create memoryview into mmap'ed region
region_mv = memoryview(region)
offset_in_region = phys_address - region.start
if size == 1:
region_mv[offset_in_region] = value
return
if offset_in_region % size == 0:
# Write aligned value
region_casted = region_mv.cast(defines.SIZE2FORMAT[size])
region_casted[offset_in_region // size] = value
return
# Write unaligned value
region_mv[offset_in_region:offset_in_region + size] = reg
def memory_mapping(self, base: int, size: int) -> Optional[MemoryMapping]:
"""Returns the mmap region that fully encompasses this area.
Returns None if no region matches.
"""
for region in self.mappings:
if region.start <= base and region.end >= base + size:
return region
return None
def map_io_space(self, base: int, size: int, cache_type: int) -> None:
"""Map to memory a specific region."""
if self.devmem_available() and not self.memory_mapping(base, size):
logger().log_debug(f'[helper] Mapping 0x{base:x} to memory')
length = max(size, resource.getpagesize())
page_aligned_base = base - (base % resource.getpagesize())
mapping = MemoryMapping(self.dev_mem, length, mmap.MAP_SHARED,
mmap.PROT_READ | mmap.PROT_WRITE,
offset=page_aligned_base)
self.mappings.append(mapping)
def read_phys_mem(self, phys_address, length: int) -> bytes:
if self.devmem_available():
os.lseek(self.dev_mem, phys_address, os.SEEK_SET)
return os.read(self.dev_mem, length)
return b'\x00'
def write_phys_mem(self, phys_address, length: int, newval: bytes) -> int:
if newval is None:
return None
if self.devmem_available():
os.lseek(self.dev_mem, phys_address, os.SEEK_SET)
written = os.write(self.dev_mem, newval)
if written != length:
logger().log_debug(f'Cannot write {newval} to memory {phys_address:016X} (wrote {written:d} of {length:d})')
return written
return -1
def alloc_phys_mem(self, length, max_phys_address):
raise NotImplementedError()
def free_phys_mem(self, physical_address):
raise NotImplementedError()
def va2pa(self, va):
raise NotImplementedError()
def read_io_port(self, io_port: int, size: int) -> int:
if self.devport_available():
os.lseek(self.dev_port, io_port, os.SEEK_SET)
value = os.read(self.dev_port, size)
if 1 == size:
return struct.unpack('B', value)[0]
elif 2 == size:
return struct.unpack('H', value)[0]
elif 4 == size:
return struct.unpack('I', value)[0]
else:
raise ValueError('Invalid size')
return -1
def write_io_port(self, io_port: int, value: int, size: int) -> bool:
if self.devport_available():
os.lseek(self.dev_port, io_port, os.SEEK_SET)
if 1 == size:
fmt = 'B'
elif 2 == size:
fmt = 'H'
elif 4 == size:
fmt = 'I'
else:
raise ValueError('Invalid size')
written = os.write(self.dev_port, struct.pack(fmt, value))
if written != size:
logger().log_debug(f'Cannot write {value} to port {io_port:x} (wrote {written:d} of {size:d})')
return False
return True
return False
def read_cr(self, cpu_thread_id, cr_number):
raise NotImplementedError()
def write_cr(self, cpu_thread_id, cr_number, value):
raise NotImplementedError()
def read_msr(self, thread_id: int, msr_addr: int) -> Tuple[int, int]:
if self.devmsr_available():
os.lseek(self.dev_msr[thread_id], msr_addr, os.SEEK_SET)
buf = os.read(self.dev_msr[thread_id], 8)
unbuf = struct.unpack('2I', buf)
return (unbuf[0], unbuf[1])
return (-1, -1)
def write_msr(self, thread_id: int, msr_addr: int, eax: int, edx: int) -> int:
if self.devmsr_available():
os.lseek(self.dev_msr[thread_id], msr_addr, os.SEEK_SET)
buf = struct.pack('2I', eax, edx)
written = os.write(self.dev_msr[thread_id], buf)
if written != 8:
logger().log_debug(f'Cannot write {buf.hex()} to MSR {msr_addr:x}')
return written
return False
def load_ucode_update(self, cpu_thread_id, ucode_update_buf):
raise NotImplementedError()
def get_descriptor_table(self, cpu_thread_id, desc_table_code):
raise NotImplementedError()
def EFI_supported(self):
raise NotImplementedError()
def get_EFI_variable(self, name, guid):
raise NotImplementedError()
def set_EFI_variable(self, name, guid, buffer, buffer_size=None, attrs=None):
raise NotImplementedError()
def delete_EFI_variable(self, name, guid):
raise NotImplementedError()
def list_EFI_variables(self):
raise NotImplementedError()
def get_ACPI_table(self, table_name: str) -> Optional['Array']:
raise NotImplementedError()
def enum_ACPI_tables(self) -> Optional['Array']:
raise NotImplementedError()
def cpuid(self, eax: int, ecx: int) -> Tuple[int, int, int, int]:
_cpuid = CPUID()
return _cpuid(eax, ecx)
def msgbus_send_read_message(self, mcr, mcrx):
raise NotImplementedError()
def msgbus_send_write_message(self, mcr, mcrx, mdr):
raise NotImplementedError()
def msgbus_send_message(self, mcr, mcrx, mdr):
raise NotImplementedError()
#
# Affinity functions
#
def get_affinity(self) -> Optional[int]:
try:
affinity = os.sched_getaffinity(0)
return list(affinity)[0]
except Exception:
return None
def set_affinity(self, thread_id: int) -> Optional[int]:
try:
os.sched_setaffinity(os.getpid(), {thread_id})
return thread_id
except Exception:
return None
#
# Logical CPU count
#
def get_threads_count(self) -> int:
import multiprocessing
return multiprocessing.cpu_count()
#
# Send SW SMI
#
def send_sw_smi(self, cpu_thread_id, SMI_code_data, _rax, _rbx, _rcx, _rdx, _rsi, _rdi):
raise NotImplementedError()
#
# Hypercall
#
def hypercall(self, rcx=0, rdx=0, r8=0, r9=0, r10=0, r11=0, rax=0, rbx=0, rdi=0, rsi=0, xmm_buffer=0):
raise NotImplementedError()
#
# Speculation control
#
def retpoline_enabled(self):
raise NotImplementedError('retpoline_enabled')
def _read_sysfs_text(self, filename: str) -> Optional[str]:
try:
with open(filename, 'r', encoding='utf-8') as infile:
value = infile.read().strip()
except OSError:
return None
return value or None
def firmware_vendor(self) -> Optional[str]:
return self._read_sysfs_text('/sys/class/dmi/id/bios_vendor')
def firmware_product(self) -> Optional[str]:
return self._read_sysfs_text('/sys/class/dmi/id/product_name')
def firmware_version(self) -> Optional[str]:
return self._read_sysfs_text('/sys/class/dmi/id/bios_version')
def firmware_type(self) -> Optional[str]:
return 'UEFI' if os.path.exists('/sys/firmware/efi') else 'BIOS'
def get_bios_version(self) -> str:
try:
filename = '/sys/class/dmi/id/bios_version'
with open(filename, 'r') as outfile:
return outfile.read().strip()
except FileNotFoundError:
return 'Unable to read bios version'
def get_helper():
return LinuxNativeHelper()