378 lines
15 KiB
C
378 lines
15 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/**
|
|
* @file ft_tt_elf.c
|
|
* @brief Implementation of the Tensix ELF image parser (ft_tt_elf.h).
|
|
*
|
|
* Field offsets are those of the generic ELF32 format (System V ABI, ELF header /
|
|
* program header / section header). Every multi-byte field is read byte by byte as
|
|
* little-endian, so the blob needs no alignment and the host's byte order is irrelevant.
|
|
* Every range check is done in 64-bit arithmetic so a hostile offset cannot wrap.
|
|
*
|
|
* @status Clean-room rewrite. Green against tests/test_launch.c.
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
#include "ft_tt_elf.h"
|
|
|
|
#include <string.h>
|
|
|
|
#include "ft_tt_device.h"
|
|
|
|
/* ELF32 constants (generic ELF specification). */
|
|
#define ELF_HEADER_BYTES 52u
|
|
#define ELF_PROGRAM_HEADER_BYTES 32u
|
|
#define ELF_SECTION_HEADER_BYTES 40u
|
|
#define ELF_CLASS_32 1u
|
|
#define ELF_DATA_LITTLE_ENDIAN 1u
|
|
#define ELF_TYPE_EXECUTABLE 2u
|
|
#define ELF_MACHINE_RISCV 243u
|
|
#define ELF_PROGRAM_LOAD 1u
|
|
#define ELF_SECTION_NO_BITS 8u
|
|
|
|
/** Every Tensix load is done in 32-bit words. */
|
|
#define WORD_BYTES 4u
|
|
|
|
/** One `.segments` record from fw/ft_tensix_*.ld: three 32-bit words per segment. */
|
|
#define SEGMENT_RECORD_BYTES 12u
|
|
|
|
/** ELF header fields this parser uses. */
|
|
typedef struct elf_header_fields {
|
|
uint32_t entry;
|
|
uint32_t program_header_offset;
|
|
uint32_t section_header_offset;
|
|
uint16_t program_header_size;
|
|
uint16_t program_header_count;
|
|
uint16_t section_header_size;
|
|
uint16_t section_header_count;
|
|
uint16_t section_name_table_index;
|
|
} elf_header_fields;
|
|
|
|
/** Where a section's bytes are, and how many. */
|
|
typedef struct elf_section_span {
|
|
uint32_t offset;
|
|
uint32_t size;
|
|
} elf_section_span;
|
|
|
|
static uint16_t read_u16(const uint8_t *bytes, size_t offset)
|
|
{
|
|
return (uint16_t)(bytes[offset] | (bytes[offset + 1u] << 8));
|
|
}
|
|
|
|
static uint32_t read_u32(const uint8_t *bytes, size_t offset)
|
|
{
|
|
return (uint32_t)bytes[offset] | ((uint32_t)bytes[offset + 1u] << 8) | ((uint32_t)bytes[offset + 2u] << 16) |
|
|
((uint32_t)bytes[offset + 3u] << 24);
|
|
}
|
|
|
|
/** Whether [offset, offset + length) lies inside a blob of `blob_size` bytes. */
|
|
static int span_is_inside(uint64_t offset, uint64_t length, size_t blob_size)
|
|
{
|
|
return offset <= (uint64_t)blob_size && length <= (uint64_t)blob_size - offset;
|
|
}
|
|
|
|
/** Identification, type and machine, then the fields the rest of the parse needs. */
|
|
static int read_elf_header(const uint8_t *bytes, size_t blob_size, elf_header_fields *header)
|
|
{
|
|
if (blob_size < ELF_HEADER_BYTES) {
|
|
ft_tt_log_error("elf: blob is %zu bytes, shorter than an ELF32 header", blob_size);
|
|
return FT_TT_EINVAL;
|
|
}
|
|
if (memcmp(bytes, "\x7f" "ELF", 4u) != 0) {
|
|
ft_tt_log_error("elf: bad magic");
|
|
return FT_TT_EINVAL;
|
|
}
|
|
if (bytes[4] != ELF_CLASS_32) {
|
|
ft_tt_log_error("elf: class %u is not ELFCLASS32", bytes[4]);
|
|
return FT_TT_EINVAL;
|
|
}
|
|
if (bytes[5] != ELF_DATA_LITTLE_ENDIAN) {
|
|
ft_tt_log_error("elf: data encoding %u is not little-endian", bytes[5]);
|
|
return FT_TT_EINVAL;
|
|
}
|
|
if (read_u16(bytes, 16u) != ELF_TYPE_EXECUTABLE) {
|
|
ft_tt_log_error("elf: type %u is not ET_EXEC", read_u16(bytes, 16u));
|
|
return FT_TT_EINVAL;
|
|
}
|
|
if (read_u16(bytes, 18u) != ELF_MACHINE_RISCV) {
|
|
ft_tt_log_error("elf: machine %u is not EM_RISCV", read_u16(bytes, 18u));
|
|
return FT_TT_EINVAL;
|
|
}
|
|
header->entry = read_u32(bytes, 24u);
|
|
header->program_header_offset = read_u32(bytes, 28u);
|
|
header->section_header_offset = read_u32(bytes, 32u);
|
|
header->program_header_size = read_u16(bytes, 42u);
|
|
header->program_header_count = read_u16(bytes, 44u);
|
|
header->section_header_size = read_u16(bytes, 46u);
|
|
header->section_header_count = read_u16(bytes, 48u);
|
|
header->section_name_table_index = read_u16(bytes, 50u);
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
static int program_header_table_is_valid(const elf_header_fields *header, size_t blob_size)
|
|
{
|
|
if (header->program_header_offset == 0u || header->program_header_count == 0u) {
|
|
ft_tt_log_error("elf: no program headers");
|
|
return 0;
|
|
}
|
|
if (header->program_header_size != ELF_PROGRAM_HEADER_BYTES) {
|
|
ft_tt_log_error("elf: program header entry size %u, expected %u", header->program_header_size,
|
|
ELF_PROGRAM_HEADER_BYTES);
|
|
return 0;
|
|
}
|
|
if (header->program_header_offset % WORD_BYTES != 0u) {
|
|
ft_tt_log_error("elf: program header table at 0x%x is not word aligned", header->program_header_offset);
|
|
return 0;
|
|
}
|
|
if (!span_is_inside(header->program_header_offset,
|
|
(uint64_t)header->program_header_count * ELF_PROGRAM_HEADER_BYTES, blob_size)) {
|
|
ft_tt_log_error("elf: program header table runs past the end of the blob");
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/** Validate one PT_LOAD and record it as the next segment of the image. */
|
|
static int add_load_segment(const uint8_t *bytes, size_t blob_size, size_t header_at, ft_tt_elf_image *image)
|
|
{
|
|
uint32_t file_offset = read_u32(bytes, header_at + 4u);
|
|
uint32_t exec_address = read_u32(bytes, header_at + 8u);
|
|
uint32_t load_address = read_u32(bytes, header_at + 12u);
|
|
uint32_t file_bytes = read_u32(bytes, header_at + 16u);
|
|
uint32_t memory_bytes = read_u32(bytes, header_at + 20u);
|
|
unsigned index = image->segment_count;
|
|
ft_tt_elf_segment *segment;
|
|
|
|
if (index >= FT_TT_ELF_MAX_SEGMENTS) {
|
|
ft_tt_log_error("elf: more than %u loadable segments", FT_TT_ELF_MAX_SEGMENTS);
|
|
return FT_TT_EINVAL;
|
|
}
|
|
if (file_offset % WORD_BYTES != 0u || exec_address % WORD_BYTES != 0u || load_address % WORD_BYTES != 0u) {
|
|
ft_tt_log_error("elf: segment %u is not word aligned (offset 0x%x, vaddr 0x%x, paddr 0x%x)", index,
|
|
file_offset, exec_address, load_address);
|
|
return FT_TT_EINVAL;
|
|
}
|
|
if (file_bytes % WORD_BYTES != 0u) {
|
|
ft_tt_log_error("elf: segment %u file size %u is not a multiple of 4", index, file_bytes);
|
|
return FT_TT_EINVAL;
|
|
}
|
|
if (!span_is_inside(file_offset, file_bytes, blob_size)) {
|
|
ft_tt_log_error("elf: segment %u contents [0x%x, +%u) run past the %zu-byte blob", index, file_offset,
|
|
file_bytes, blob_size);
|
|
return FT_TT_EINVAL;
|
|
}
|
|
if (file_bytes > memory_bytes || (uint64_t)load_address + file_bytes > UINT32_MAX) {
|
|
ft_tt_log_error("elf: segment %u sizes are inconsistent (file %u, memory %u, load 0x%x)", index,
|
|
file_bytes, memory_bytes, load_address);
|
|
return FT_TT_EINVAL;
|
|
}
|
|
|
|
segment = &image->segments[index];
|
|
segment->exec_address = exec_address;
|
|
segment->load_address = load_address;
|
|
segment->file_data = bytes + file_offset;
|
|
segment->file_bytes = file_bytes;
|
|
/* The host transfers and clears whole 32-bit words, so a segment's memory size is
|
|
* counted in whole words: rounded UP, never down (a partial last word is still
|
|
* written). Real images are already word-sized (fw/ft_tensix_*.ld ALIGN(4)). */
|
|
segment->memory_bytes = (memory_bytes + 3u) & ~3u;
|
|
image->segment_count = index + 1u;
|
|
image->loaded_bytes += file_bytes;
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
static int collect_load_segments(const uint8_t *bytes, size_t blob_size, const elf_header_fields *header,
|
|
ft_tt_elf_image *image)
|
|
{
|
|
uint32_t index;
|
|
|
|
for (index = 0; index < header->program_header_count; index++) {
|
|
size_t header_at = (size_t)header->program_header_offset + (size_t)index * ELF_PROGRAM_HEADER_BYTES;
|
|
|
|
if (read_u32(bytes, header_at) != ELF_PROGRAM_LOAD)
|
|
continue;
|
|
if (add_load_segment(bytes, blob_size, header_at, image) != FT_TT_OK)
|
|
return FT_TT_EINVAL;
|
|
}
|
|
if (image->segment_count == 0u) {
|
|
ft_tt_log_error("elf: no PT_LOAD segments");
|
|
return FT_TT_EINVAL;
|
|
}
|
|
if (header->entry != image->segments[0].exec_address) {
|
|
ft_tt_log_error("elf: entry 0x%x is not the start of the text segment (0x%x)", header->entry,
|
|
image->segments[0].exec_address);
|
|
return FT_TT_EINVAL;
|
|
}
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
/** Bounds of section `index`'s bytes. The section table itself is already validated. */
|
|
static elf_section_span section_span(const uint8_t *bytes, const elf_header_fields *header, uint32_t index)
|
|
{
|
|
size_t at = (size_t)header->section_header_offset + (size_t)index * ELF_SECTION_HEADER_BYTES;
|
|
elf_section_span span;
|
|
|
|
span.offset = read_u32(bytes, at + 16u);
|
|
span.size = read_u32(bytes, at + 20u);
|
|
if (read_u32(bytes, at + 4u) == ELF_SECTION_NO_BITS)
|
|
span.size = 0u; /* occupies no file bytes */
|
|
return span;
|
|
}
|
|
|
|
/**
|
|
* Find the `.segments` section.
|
|
*
|
|
* @return 1 and `*found` when present, 0 when the ELF has no such section (or no section
|
|
* table at all), -1 when the section table is malformed.
|
|
*/
|
|
static int find_segments_section(const uint8_t *bytes, size_t blob_size, const elf_header_fields *header,
|
|
elf_section_span *found)
|
|
{
|
|
static const char wanted_name[] = ".segments";
|
|
elf_section_span names;
|
|
uint32_t index;
|
|
|
|
if (header->section_header_offset == 0u || header->section_header_count == 0u)
|
|
return 0;
|
|
if (header->section_header_size != ELF_SECTION_HEADER_BYTES ||
|
|
!span_is_inside(header->section_header_offset,
|
|
(uint64_t)header->section_header_count * ELF_SECTION_HEADER_BYTES, blob_size) ||
|
|
header->section_name_table_index >= header->section_header_count) {
|
|
ft_tt_log_error("elf: section header table is malformed or out of bounds");
|
|
return -1;
|
|
}
|
|
names = section_span(bytes, header, header->section_name_table_index);
|
|
if (!span_is_inside(names.offset, names.size, blob_size)) {
|
|
ft_tt_log_error("elf: section name table runs past the end of the blob");
|
|
return -1;
|
|
}
|
|
for (index = 0; index < header->section_header_count; index++) {
|
|
size_t at = (size_t)header->section_header_offset + (size_t)index * ELF_SECTION_HEADER_BYTES;
|
|
uint32_t name_offset = read_u32(bytes, at);
|
|
|
|
if (name_offset >= names.size || names.size - name_offset < sizeof(wanted_name))
|
|
continue;
|
|
if (memcmp(bytes + names.offset + name_offset, wanted_name, sizeof(wanted_name)) != 0)
|
|
continue;
|
|
*found = section_span(bytes, header, index);
|
|
if (!span_is_inside(found->offset, found->size, blob_size)) {
|
|
ft_tt_log_error("elf: .segments runs past the end of the blob");
|
|
return -1;
|
|
}
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Hold each PT_LOAD to the region the linker script recorded for it.
|
|
*
|
|
* The linker scripts write, per output region in segment order: the execution address
|
|
* (twice; the second copy is ADDR() again, not the load address) and the region's size
|
|
* limit. A PT_LOAD that starts elsewhere than its record says, or whose memory image is
|
|
* larger than the limit, did not come from the layout the loader expects.
|
|
*/
|
|
static int check_against_segment_records(const uint8_t *bytes, elf_section_span records,
|
|
const ft_tt_elf_image *image)
|
|
{
|
|
uint32_t record_count = records.size / SEGMENT_RECORD_BYTES;
|
|
unsigned index;
|
|
|
|
if (records.size % SEGMENT_RECORD_BYTES != 0u) {
|
|
ft_tt_log_error("elf: .segments is %u bytes, not a whole number of records", records.size);
|
|
return FT_TT_EINVAL;
|
|
}
|
|
if (image->segment_count > record_count && record_count != 0u) {
|
|
ft_tt_log_error("elf: %u loadable segments but .segments describes only %u", image->segment_count,
|
|
record_count);
|
|
return FT_TT_EINVAL;
|
|
}
|
|
for (index = 0; index < image->segment_count && index < record_count; index++) {
|
|
size_t at = (size_t)records.offset + (size_t)index * SEGMENT_RECORD_BYTES;
|
|
const ft_tt_elf_segment *segment = &image->segments[index];
|
|
uint32_t recorded_exec = read_u32(bytes, at);
|
|
uint32_t recorded_second = read_u32(bytes, at + 4u);
|
|
uint32_t limit = read_u32(bytes, at + 8u);
|
|
|
|
if (recorded_exec != segment->exec_address || recorded_second != segment->exec_address) {
|
|
ft_tt_log_error("elf: segment %u is at 0x%x but .segments records 0x%x/0x%x", index,
|
|
segment->exec_address, recorded_exec, recorded_second);
|
|
return FT_TT_EINVAL;
|
|
}
|
|
if (segment->memory_bytes > limit) {
|
|
ft_tt_log_error("elf: segment %u is %u bytes, over its %u-byte region", index, segment->memory_bytes,
|
|
limit);
|
|
return FT_TT_EINVAL;
|
|
}
|
|
}
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
int ft_tt_elf_parse(const void *blob, size_t blob_size, ft_tt_elf_image *out_image)
|
|
{
|
|
const uint8_t *bytes = blob;
|
|
elf_header_fields header;
|
|
elf_section_span records;
|
|
int found;
|
|
|
|
if (!blob || !out_image) {
|
|
ft_tt_log_error("elf: NULL %s", blob ? "output image" : "blob");
|
|
return FT_TT_EINVAL;
|
|
}
|
|
memset(out_image, 0, sizeof(*out_image));
|
|
|
|
if (read_elf_header(bytes, blob_size, &header) != FT_TT_OK)
|
|
return FT_TT_EINVAL;
|
|
if (!program_header_table_is_valid(&header, blob_size))
|
|
return FT_TT_EINVAL;
|
|
if (collect_load_segments(bytes, blob_size, &header, out_image) != FT_TT_OK)
|
|
return FT_TT_EINVAL;
|
|
|
|
found = find_segments_section(bytes, blob_size, &header, &records);
|
|
if (found < 0)
|
|
return FT_TT_EINVAL;
|
|
if (found > 0 && check_against_segment_records(bytes, records, out_image) != FT_TT_OK)
|
|
return FT_TT_EINVAL;
|
|
|
|
out_image->entry_address = header.entry;
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
const ft_tt_elf_segment *ft_tt_elf_text_segment(const ft_tt_elf_image *image)
|
|
{
|
|
if (!image || image->segment_count == 0u)
|
|
return NULL;
|
|
return &image->segments[0];
|
|
}
|
|
|
|
int ft_tt_elf_load_extent(const ft_tt_elf_image *image, uint32_t *out_low, uint32_t *out_high)
|
|
{
|
|
uint32_t low = UINT32_MAX;
|
|
uint32_t high = 0u;
|
|
unsigned index;
|
|
|
|
if (!image || !out_low || !out_high || image->segment_count == 0u ||
|
|
image->segment_count > FT_TT_ELF_MAX_SEGMENTS)
|
|
return FT_TT_EINVAL;
|
|
|
|
for (index = 0; index < image->segment_count; index++) {
|
|
const ft_tt_elf_segment *segment = &image->segments[index];
|
|
/* ft_tt_elf_parse() refused any segment whose end would not fit in 32 bits. */
|
|
uint32_t end = segment->load_address + segment->file_bytes;
|
|
|
|
if (segment->file_bytes == 0u)
|
|
continue;
|
|
if (segment->load_address < low)
|
|
low = segment->load_address;
|
|
if (end > high)
|
|
high = end;
|
|
}
|
|
if (high == 0u) {
|
|
low = image->segments[0].load_address;
|
|
high = low;
|
|
}
|
|
*out_low = low;
|
|
*out_high = high;
|
|
return FT_TT_OK;
|
|
}
|