arm-trusted-firmware/tools/tlc/tlc/tl.py
Harrison Mutai f0e15ddca3 fix(tlc): relax entry addition from YAML files
Relax entry addition from YAML files to allow the addition of
entries not known to the tool. It is not possible to keep track of
every possible TE, and a user might want to add an entry that hasn't
yet been specified.

Change-Id: Ib5d227bc41cd3dd8b530699c1bab3165a3114a3c
Signed-off-by: Harrison Mutai <harrison.mutai@arm.com>
2024-12-09 18:21:11 +00:00

388 lines
13 KiB
Python

#!/usr/bin/env python3
#
# Copyright (c) 2024, Arm Limited. All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
#
"""Module containing definitions pertaining to the 'Transfer List' (TL) type."""
from typing import Any, Dict, List, Optional
import math
import struct
from dataclasses import dataclass
from functools import reduce
from pathlib import Path
from tlc.te import TransferEntry
TRANSFER_LIST_ENABLE_CHECKSUM = 0b1
# Description of each TE type. For each TE, there is a tag ID, a format (to be
# used in struct.pack to encode the TE), and a list of field names that can
# appear in the yaml file for that TE. Some fields are missing, if that TE has
# to be processed differently, or if it can only be added with a blob file.
transfer_entry_formats: Dict[int, Any] = {
0: {
"tag_name": "empty",
"format": "4x",
"fields": [],
},
1: {
"tag_name": "fdt",
},
2: {
"tag_name": "hob_block",
},
3: {
"tag_name": "hob_list",
},
4: {
"tag_name": "acpi_table_aggregate",
},
5: {
"tag_name": "tpm_event_log_table",
"fields": ["event_log", "flags"],
},
6: {
"tag_name": "tpm_crb_base_address_table",
"format": "QI",
"fields": ["crb_base_address", "crb_size"],
},
0x100: {
"tag_name": "optee_pageable_part",
"format": "Q",
"fields": ["pp_addr"],
},
0x101: {
"tag_name": "dt_spmc_manifest",
},
0x102: {
"tag_name": "exec_ep_info",
"format": "2BHIQI4x8Q",
"fields": ["ep_info"],
},
0x104: {
"tag_name": "sram_layout",
"format": "2Q",
"fields": ["addr", "size"],
},
}
tag_name_to_tag_id = {
te["tag_name"]: tag_id for tag_id, te in transfer_entry_formats.items()
}
class TransferList:
"""Class representing a Transfer List based on version 1.0 of the Firmware Handoff specification."""
# Header encoding, with little-endian byte order.
encoding = "<I4B4I"
hdr_size = 0x18
signature = 0x4A0FB10B
version = 1
granule = 8
def __init__(
self,
max_size: int = hdr_size,
flags: int = TRANSFER_LIST_ENABLE_CHECKSUM,
alignment: int = 3,
) -> None:
assert max_size >= self.hdr_size
self.checksum: int = 0
self.alignment: int = alignment
self.size = self.hdr_size
self.total_size = max_size
self.flags = flags
self.entries: List[TransferEntry] = []
self.update_checksum()
def __str__(self) -> str:
return "\n".join(
[
f"{k:<10} {hex(v)}"
for k, v in vars(self).items()
if not isinstance(v, list)
]
)
def get_transfer_entries_str(self):
return "\n----\n".join([str(te) for _, te in enumerate(self.entries)])
@classmethod
def fromfile(cls, filepath: Path) -> "TransferList":
tl = cls()
with open(filepath, "rb") as f:
(
tl.signature,
tl.checksum,
tl.version,
tl.hdr_size,
tl.alignment,
used_size,
tl.total_size,
tl.flags,
_,
) = struct.unpack(
cls.encoding,
f.read(tl.hdr_size),
)
if tl.signature != TransferList.signature:
raise ValueError(f"Invalid TL signature 0x{tl.signature:x}!")
elif tl.version == 0 or tl.version > 0xFF:
raise ValueError(f"Invalid TL version 0x{tl.version:x}!")
else:
while tl.size < used_size:
# We add an extra padding byte into the header so we can extract
# the 3-byte wide ID as a 4-byte uint, shift out this padding
# once we have the id.
te_base = f.tell()
(id, _, data_size) = struct.unpack(
TransferEntry.encoding[0] + "I" + TransferEntry.encoding[1:],
b"\x00" + f.read(TransferEntry.hdr_size),
)
id >>= 8
te = tl.add_transfer_entry(id, f.read(data_size))
te.offset = te_base
f.seek(align(f.tell(), tl.granule))
return tl
@classmethod
def from_dict(cls, config: Dict[str, Any]) -> "TransferList":
"""Create a TL from data in a dictionary
The dictionary should have the same format as the yaml config files.
See the readme for more detail.
:param config: Dictionary containing the data described above.
"""
# get settings from config and set defaults
max_size = config.get("max_size", 0x1000)
has_checksum = config.get("has_checksum", True)
align = config.get("alignment", None)
flags = TRANSFER_LIST_ENABLE_CHECKSUM if has_checksum else 0
if align:
tl = cls(max_size, flags, alignment=align)
else:
tl = cls(max_size, flags)
for entry in config["entries"]:
tl.add_transfer_entry_from_dict(entry)
return tl
def header_to_bytes(self) -> bytes:
return struct.pack(
self.encoding,
self.signature,
self.checksum,
self.version,
self.hdr_size,
self.alignment,
self.size,
self.total_size,
self.flags,
0,
)
def update_checksum(self) -> None:
"""Calculates the checksum based on the sum of bytes."""
self.checksum = (256 - (self.sum_of_bytes() - self.checksum)) % 256
assert self.checksum <= 0xFF
def to_bytes(self) -> bytes:
return self.header_to_bytes() + b"".join([te.to_bytes() for te in self.entries])
def sum_of_bytes(self) -> int:
"""Sum of all bytes between the base address and the end of that last TE (modulo 0xff)."""
return (sum(self.to_bytes())) % 256
def get_entry(self, tag_id: int) -> Optional[TransferEntry]:
for te in self.entries:
if te.id == tag_id:
return te
return None
def get_entry_data_offset(self, tag_id: int) -> int:
"""Returns offset of data of a TE from the base of the TL."""
te = self.get_entry(tag_id)
if not te:
raise ValueError(f"Tag {tag_id} not found in TL!")
return te.offset + te.hdr_size
def add_transfer_entry(
self, tag_id: int, data: bytes, data_align: int = 0
) -> TransferEntry:
"""Appends a TransferEntry into the internal list of TE's."""
data_offset = TransferEntry.hdr_size + self.size
data_align = self.alignment if not data_align else data_align
aligned_data_offset = align(data_offset, 1 << data_align)
if tag_id != 0 and data_offset != aligned_data_offset:
void_len = aligned_data_offset - data_offset - TransferEntry.hdr_size
self.add_transfer_entry(0, bytes(void_len))
assert align(self.size, self.granule)
if not (self.total_size >= self.size + TransferEntry.hdr_size + len(data)):
raise MemoryError(
f"TL size has exceeded the maximum allocation {self.total_size}."
)
te = TransferEntry(tag_id, len(data), data, offset=self.size)
self.entries.append(te)
self.size += align(te.size, self.granule)
if data_align > self.alignment:
self.alignment = data_align
self.update_checksum()
return te
def add_transfer_entry_from_struct_format(
self, tag_id: int, struct_format: str, *args: Any
) -> TransferEntry:
struct_format = "<" + struct_format
data = struct.pack(struct_format, *args)
return self.add_transfer_entry(tag_id, data)
def add_entry_point_info_transfer_entry(
self, entry: Dict[str, Any]
) -> TransferEntry:
"""Add entry_point_info transfer entry
:param entry: Dictionary of the transfer entry, in the same format as
the YAML file.
"""
ep_info = entry["ep_info"]
header = ep_info["h"]
# size of the entry_point_info struct
entry_point_size = 88
attr = header["attr"]
if type(attr) is str:
# convert string of flags names to an integer
# bit number | 0 | 1 |
# ------------|-----------------------|----------------------|
# 0 | secure | non-secure |
# 1 | little endian | big-endian |
# 2 | disable secure timer | enable secure timer |
# 3 | executable | non-executable |
# 4 | first exe | not first exe |
#
# Bit 5 and bit 0 are used to determine the security state.
flag_names = {
"EP_SECURE": 0x0,
"EP_NON_SECURE": 0x1,
"EP_REALM": 0x21,
"EP_EE_LITTLE": 0x0,
"EP_EE_BIG": 0x2,
"EP_ST_DISABLE": 0x0,
"EP_ST_ENABLE": 0x4,
"EP_NON_EXECUTABLE": 0x0,
"EP_EXECUTABLE": 0x8,
"EP_FIRST_EXE": 0x10,
}
# create list of integer flags, then bitwise-or them together
flags = [flag_names[f.strip()] for f in attr.split("|")]
attr = reduce(lambda x, y: x | y, flags)
return self.add_transfer_entry_from_struct_format(
0x102,
transfer_entry_formats[0x102]["format"],
header["type"],
header["version"],
entry_point_size,
attr,
ep_info["pc"],
ep_info["spsr"],
*ep_info["args"],
)
def add_transfer_entry_from_dict(
self,
entry: Dict[str, Any],
) -> TransferEntry:
"""Add a transfer entry from data in a dictionary
The dictionary should have the same format as the entries in the yaml
config files. See the readme for more detail.
:param entry: Dictionary containing the data described above.
"""
# Tag_id is either a tag name or a tag id. Use it to get the TE format.
tag_id = entry["tag_id"]
if tag_id in tag_name_to_tag_id:
tag_id = tag_name_to_tag_id[tag_id]
align = entry.get("alignment", None)
if "blob_file_path" in entry:
return self.add_transfer_entry_from_file(
tag_id, entry["blob_file_path"], data_align=align
)
else:
te_format = transfer_entry_formats[tag_id]
tag_name = te_format["tag_name"]
if tag_name == "tpm_event_log_table":
with open(entry["event_log"], "rb") as f:
event_log_data = f.read()
flags_bytes = entry["flags"].to_bytes(4, "little")
data = flags_bytes + event_log_data
return self.add_transfer_entry(tag_id, data, data_align=align)
elif tag_name == "exec_ep_info":
return self.add_entry_point_info_transfer_entry(entry)
elif "format" in te_format and "fields" in te_format:
fields = [entry[field] for field in te_format["fields"]]
return self.add_transfer_entry_from_struct_format(
tag_id, te_format["format"], *fields
)
else:
raise ValueError(f"Invalid transfer entry {entry}.")
def add_transfer_entry_from_file(
self, tag_id: int, path: Path, data_align: int = 0
) -> TransferEntry:
with open(path, "rb") as f:
return self.add_transfer_entry(tag_id, f.read(), data_align=data_align)
def write_to_file(self, file: Path) -> None:
"""Write the contents of the TL to a file."""
with open(file, "wb") as f:
f.write(self.header_to_bytes())
for te in self.entries:
assert f.tell() + te.hdr_size + te.data_size < self.total_size
f.write(te.header_to_bytes())
f.write(te.data)
# Ensure the next TE is at an 8-byte aligned address
f.write(bytes((align(f.tell(), self.granule) - f.tell())))
def remove_tag(self, tag: int) -> None:
self.entries = list(filter(lambda te: te.id != tag, self.entries))
self.size = self.hdr_size + sum(map(lambda te: te.size, self.entries))
self.update_checksum()
def align(n, alignment):
return int(math.ceil(n / alignment) * alignment)