l0der.c 17.41 KiB
#include "l0der/l0der.h"
#include <alloca.h>
#include <stdio.h>
#include <string.h>
#include <ff.h>
#include "epicardium.h"
#include "l0der/elf.h"
#include "modules/log.h"
/*
* l0der is, in reality, a boneless operating-system style ELF loader.
*
* To implement it, we parse an ELF file somewhat defensively, trying to
* not DoS ourselves by overallocating RAM (no heap allocations, no recursion).
*
* Currently we support only relocatable, PIE binaries. Adding support for
* static ELFs would be trivial, however we want to keep the possibility to
* shuffle around memory areas in future versions of card10 (possibly giving
* l0dables more RAM than 256k) without having to recompile all l0dables. We
* are also keeping the opportunity to have separate loading schemes in the
* future, for instance:
* - l0dables running next to pycardium, without unloading it
* - multiple l0dables running next to each other (TSR-style)
*
* Thus, we use PIE l0dables to keep these possibilities open and not write down
* a memory map in stone.
*/
#define WEAK_SYMBOL_MAX 128
struct _pie_load_info {
/// Populated by _load_pie
// Addresses within ELF file.
uint32_t image_start;
uint32_t image_limit;
// Highest alignment request for a segment.
uint32_t strictest_alignment;
/// Populated by _parse_dynamic_symbols
// List of weak symbols for which relocations can be ignored.
uint32_t weak_symbols[WEAK_SYMBOL_MAX];
uint32_t weak_symbol_count;
/// Populated by _get_load_addr
// Load address of ELF file.
uint32_t load_address;
// Addresses within memory of ELF file.
uint32_t image_load_start;
uint32_t image_load_limit;
// Stack top.
uint32_t stack_top;
};
/*
* Read an ELF header, check E_IDENT.
*/
static int _read_elf_header(FIL *fp, Elf32_Ehdr *hdr)
{
f_lseek(fp, 0);
unsigned int read;
FRESULT fres = f_read(fp, hdr, sizeof(Elf32_Ehdr), &read);
if (fres != FR_OK) {
LOG_ERR("l0der", "_read_elf_header: f_read failed: %d", fres);
return -1;
}
if (read != sizeof(Elf32_Ehdr)) {
LOG_ERR("l0der", "_read_elf_header: file truncated");
return -1;
}
if (hdr->e_ident[0] != ELFMAG0 ||
hdr->e_ident[1] != ELFMAG1 ||
hdr->e_ident[2] != ELFMAG2 ||
hdr->e_ident[3] != ELFMAG3) {
LOG_ERR("l0der", "_read_elf_header: not an ELF file");
return -1;
}
if (hdr->e_ident[4] != ELFCLASS32) {
LOG_ERR("l0der", "_read_elf_header: not a 32-bit ELF");
return -1;
}
if (hdr->e_ident[5] != ELFDATA2LSB) {
LOG_ERR("l0der", "_read_elf_header: not a little-endian ELF");
return -1;
}
if (hdr->e_ident[6] != EV_CURRENT) {
LOG_ERR("l0der", "_read_elf_header: not a v1 ELF");
return -1;
}
if (hdr->e_ehsize < sizeof(Elf32_Ehdr)) {
LOG_ERR("l0der", "_raed_elf_header: header too small");
return -1;
}
return 0;
}
/*
* Read bytes from file at a given offset.
*
* :param FIL* fp: file pointer to read from
* :param uint32_t address: address from which to read
* :param void *data: buffer into which to read
* :param size_t count: amount of bytes to read
* :returns: ``0`` on success or a negative value on error. Possible errors:
*
* - ``-EIO``: Could not read from FAT - address out of bounds of not enough bytes available.
*/
static int _seek_and_read(FIL *fp, uint32_t address, void *data, size_t count) {
FRESULT fres;
if ((fres = f_lseek(fp, address)) != FR_OK) {
LOG_ERR("l0der", "_seek_and_read: could not seek to 0x%lx: %d", address, fres);
return -EIO;
}
unsigned int read;
if ((fres = f_read(fp, data, count, &read)) != FR_OK || read < count) {
if (fres == FR_OK) {
LOG_ERR("l0der", "_seek_and_read: could not read: wanted %d bytes, got %d", count, read);
} else {
LOG_ERR("l0der", "_seek_and_read: could not read: %d", fres);
}
return -EIO;
}
return 0;
}
/*
* Read an ELF program header header.
*/
static int _read_program_header(FIL *fp, uint32_t phdr_addr, Elf32_Phdr *phdr)
{
return _seek_and_read(fp, phdr_addr, phdr, sizeof(Elf32_Phdr));
}
/*
* Read an ELF section header header.
*/
static int _read_section_header(FIL *fp, uint32_t shdr_addr, Elf32_Shdr *shdr)
{
return _seek_and_read(fp, shdr_addr, shdr, sizeof(Elf32_Shdr));
}
/*
* Check an ELF program header.
*
* This function ensures basic memory sanity of a program header / segment.
* It ensures that it points to a file region that is contained within the file fully.
*/
static int _check_program_header(FIL *fp, Elf32_Phdr *phdr) {
size_t size = f_size(fp);
// Check file size/offset.
uint32_t file_start = phdr->p_offset;
uint32_t file_limit = phdr->p_offset + phdr->p_filesz;
if (file_limit < file_start) {
LOG_ERR("l0der", "_check_program_header: file size overflow");
return -ENOEXEC;
}
if (file_limit > size) {
LOG_ERR("l0der", "_check_program_header: extends past end of file");
return -ENOEXEC;
}
if (phdr->p_type == PT_LOAD) {
// Check mem/file size.
if (phdr->p_filesz > phdr->p_memsz) {
LOG_ERR("l0der", "_check_program_header: file size larger than memory size");
return -ENOEXEC;
}
uint32_t mem_start = phdr->p_vaddr;
uint32_t mem_limit = phdr->p_vaddr + phdr->p_memsz;
if (mem_limit < mem_start) {
LOG_ERR("l0der", "_check_program_header: mem size overflow");
return -ENOEXEC;
}
}
return 0;
}
/*
* Check an ELF section header.
*
* This function ensures basic memory sanity of a section header.
* It ensures that it points to a file region that is contained within the file fully.
*/
static int _check_section_header(FIL *fp, Elf32_Shdr *shdr) {
size_t size = f_size(fp);
// Check file size/offset.
uint32_t file_start = shdr->sh_offset;
uint32_t file_limit = shdr->sh_offset + shdr->sh_size;
if (file_limit < file_start) {
LOG_ERR("l0der", "_check_section_header: file size overflow");
return -ENOEXEC;
}
if (file_limit > size) {
LOG_ERR("l0der", "_check_section_header: extends past end of file");
return -ENOEXEC;
}
return 0;
}
/*
* Interpreter expected in l0dable PIE binaries.
*/
static const char *_interpreter = "card10-l0dable";
/*
* Check that the given INTERP program header contains the correct interpreter string.
*/
static int _check_interp(FIL *fp, Elf32_Phdr *phdr)
{
int res;
uint32_t buffer_size = strlen(_interpreter) + 1;
char *interp = alloca(buffer_size);
memset(interp, 0, buffer_size);
if ((res = _seek_and_read(fp, phdr->p_offset, interp, buffer_size)) != FR_OK) {
return res;
}
if (strncmp(interp, _interpreter, strlen(_interpreter)) != 0) {
LOG_ERR("l0der", "_check_interp: invalid interpreter, want card10-l0dable");
return -1;
}
return 0;
}
/*
* Calculate address at which binary should be loaded.
*
* Currently this means trying to fit it into core1 RAM.
*/
static int _get_load_addr(struct _pie_load_info *li)
{
uint32_t image_size = li->image_limit - li->image_start;
// ref: Documentation/memorymap.rst
uint32_t core1_mem_start = 0x20040000;
uint32_t core1_mem_limit = 0x20080000;
uint32_t core1_mem_size = core1_mem_limit - core1_mem_start;
if (image_size > core1_mem_size) {
LOG_ERR("l0der", "_get_load_addr: image too large (need 0x%08lx bytes, have %08lx",
image_size, core1_mem_size);
return -ENOMEM;
}
// Place image at bottom of core1 memory range.
li->load_address = core1_mem_start;
li->image_load_start = li->load_address + li->image_start;
li->image_load_limit = li->load_address + li->image_limit;
// Ensure within alignment requests.
if ((li->load_address % li->strictest_alignment) != 0) {
LOG_ERR("l0der", "_get_load_addr: too strict alignment request for %ld bytes", li->strictest_alignment);
return -ENOEXEC;
}
// Place stack at top of core1 memory range.
li->stack_top = core1_mem_limit;
// Check that there is enough stack space.
uint32_t stack_space = li->stack_top - li->image_load_limit;
if (stack_space < 8192) {
LOG_WARN("l0der", "_get_load_addr: low stack space (%ld bytes)", stack_space);
} else if (stack_space < 256) {
LOG_ERR("l0der", "_get_load_addr: low stack space (%ld bytes), cannot continue", stack_space);
return -ENOMEM;
}
LOG_INFO("l0der", "Stack at %08lx, %ld bytes available", li->stack_top, stack_space);
return 0;
}
/*
* Load a program segment into memory.
*
* Segment must be a LOAD segment.
*/
static int _load_segment(FIL *fp, struct _pie_load_info *li, Elf32_Phdr *phdr)
{
uint32_t segment_start = li->load_address + phdr->p_vaddr;
uint32_t segment_limit = segment_start + phdr->p_memsz;
LOG_INFO("l0der", "Segment %08lx-%08lx: 0x%lx bytes from file",
segment_start, segment_limit, phdr->p_filesz);
memset((void *)segment_start, 0, phdr->p_memsz);
return _seek_and_read(fp, phdr->p_offset, (void*)segment_start, phdr->p_filesz);
}
/*
* Parse dynamic symbol sections.
*/
static int _parse_dynamic_symbols(FIL *fp, struct _pie_load_info *li, Elf32_Ehdr *hdr) {
int res;
FRESULT fres;
Elf32_Shdr shdr;
Elf32_Sym sym;
// Go through all dynamic symbol sections.
for (int i = 0; i < hdr->e_shnum; i++) {
uint32_t shdr_addr = hdr->e_shoff + (i * hdr->e_shentsize);
if ((res = _read_section_header(fp, shdr_addr, &shdr)) != 0) {
return res;
}
if (shdr.sh_type != SHT_DYNSYM) {
continue;
}
if ((res = _check_section_header(fp, &shdr)) != 0) {
return res;
}
if ((shdr.sh_size % sizeof(Elf32_Sym)) != 0) {
LOG_ERR("l0der", "_parse_dynamic_symbols: SHT_DYN section with invalid size: %ld", shdr.sh_size);
return -EIO;
}
uint32_t sym_count = shdr.sh_size / sizeof(Elf32_Sym);
// Read symbols one by one.
if ((fres = f_lseek(fp, shdr.sh_offset)) != FR_OK) {
LOG_ERR("l0der", "_parse_dynamic_symbols: seek to first relocation (at 0x%lx) failed", shdr.sh_offset);
return -EIO;
}
for (int j = 0; j < sym_count; j++) {
unsigned int read;
if ((fres = f_read(fp, &sym, sizeof(Elf32_Sym), &read)) != FR_OK || read != sizeof(Elf32_Sym)) {
LOG_ERR("l0der", "__parse_dynamic_symbols: symbol read failed: %d", fres);
return -EIO;
}
uint32_t bind = ELF32_ST_BIND(sym.st_info);
if (bind != STB_WEAK) {
continue;
}
if (li->weak_symbol_count >= WEAK_SYMBOL_MAX) {
LOG_ERR("l0der", "__parse_dynamic_symbols: too many weak symbols (limit: %d)", WEAK_SYMBOL_MAX);
return -ENOMEM;
}
li->weak_symbols[li->weak_symbol_count++] = j;
}
}
return 0;
}
/*
* Apply dynamic relocations from ELF.
*
* Currently, we only support R_ARM_RELATIVE relocations. These seem to be
* the only one used when making 'standard' PIE binaries on RAM. However, other
* kinds might have to be implemented in the future.
*/
static int _run_relocations(FIL *fp, struct _pie_load_info *li, Elf32_Ehdr *hdr) {
int res;
FRESULT fres;
Elf32_Shdr shdr;
Elf32_Rel rel;
// Go through all relocation sections.
for (int i = 0; i < hdr->e_shnum; i++) {
uint32_t shdr_addr = hdr->e_shoff + (i * hdr->e_shentsize);
if ((res = _read_section_header(fp, shdr_addr, &shdr)) != 0) {
return res;
}
// We don't support RELA (relocation with addend) sections (yet?).
if (shdr.sh_type == SHT_RELA) {
LOG_ERR("l0der", "_run_relocations: found unsupported SHT_RELA section, bailing");
return -ENOEXEC;
}
if (shdr.sh_type != SHT_REL) {
continue;
}
if ((res = _check_section_header(fp, &shdr)) != 0) {
return res;
}
if ((shdr.sh_size % sizeof(Elf32_Rel)) != 0) {
LOG_ERR("l0der", "_run_relocations: SHT_REL section with invalid size: %ld", shdr.sh_size);
return -EIO;
}
uint32_t reloc_count = shdr.sh_size / sizeof(Elf32_Rel);
// Read relocations one by one.
if ((fres = f_lseek(fp, shdr.sh_offset)) != FR_OK) {
LOG_ERR("l0der", "_run_relocations: seek to first relocation (at 0x%lx) failed", shdr.sh_offset);
return -EIO;
}
for (int j = 0; j < reloc_count; j++) {
unsigned int read;
if ((fres = f_read(fp, &rel, sizeof(Elf32_Rel), &read)) != FR_OK || read != sizeof(Elf32_Rel)) {
LOG_ERR("l0der", "_run_relocations: relocation read failed: %d", fres);
return -EIO;
}
uint32_t sym = ELF32_R_SYM(rel.r_info);
uint8_t type = ELF32_R_TYPE(rel.r_info);
// Skip relocations that are for weak symbols.
// (ie., do not resolve relocation - they default to a safe NULL)
uint8_t skip = 0;
if (sym != 0) {
for (int k = 0; k < li->weak_symbol_count; k++) {
if (li->weak_symbols[k] == sym) {
skip = 1;
break;
}
}
}
if (skip) {
continue;
}
switch (type) {
case R_ARM_RELATIVE:
// Relocate.
if ((rel.r_offset % 4) != 0) {
LOG_ERR("l0der", "_run_relocations: R_ARM_RELATIVE address must be 4-byte aligned");
return -ENOEXEC;
}
volatile uint32_t *addr = (uint32_t *)(rel.r_offset + li->load_address);
if ((uint32_t)addr < li->image_load_start || (uint32_t)addr >= li->image_load_limit) {
LOG_ERR("l0der", "_run_relocations: R_ARM_RELATIVE address (%08lx) is outside image boundaries",
(uint32_t)addr);
return -ENOEXEC;
}
*addr += li->load_address;
break;
default:
LOG_ERR("l0der", "_run_relocations: unsupported relocation type %d", type);
return -ENOEXEC;
}
}
}
return 0;
}
/*
* Load a l0dable PIE binary.
*/
static int _load_pie(FIL *fp, Elf32_Ehdr *hdr, struct l0dable_info *info)
{
int res;
struct _pie_load_info li = {0};
// First pass over program headers: sanity check sizes, calculate image
// size bounds, check alignment.
li.image_start = 0xffffffff;
li.image_limit = 0x0;
Elf32_Phdr phdr;
int status_interp = -1;
for (int i = 0; i < hdr->e_phnum; i++) {
uint32_t phdr_addr = hdr->e_phoff + (i * hdr->e_phentsize);
if ((res = _read_program_header(fp, phdr_addr, &phdr)) != 0) {
return res;
}
if ((res = _check_program_header(fp, &phdr)) != 0) {
return res;
}
if (phdr.p_type == PT_INTERP) {
status_interp = _check_interp(fp, &phdr);
continue;
}
if (phdr.p_type == PT_LOAD) {
// Check alignment request.
if ((phdr.p_vaddr % phdr.p_align) != 0) {
LOG_ERR("l0der", "_load_pie: phdr %d alignment too strict", i);
return -ENOEXEC;
}
if (phdr.p_align > li.strictest_alignment) {
li.strictest_alignment = phdr.p_align;
}
uint32_t mem_start = phdr.p_vaddr;
uint32_t mem_limit = phdr.p_vaddr + phdr.p_memsz;
// Record memory usage.
if (mem_start < li.image_start) {
li.image_start = mem_start;
}
if (mem_limit > li.image_limit) {
li.image_limit = mem_limit;
}
}
}
if (status_interp != 0) {
// Expected interpreter string was not found.
LOG_ERR("l0der", "_load_pie: not a card10 l0dable");
return -ENOEXEC;
}
if (li.image_limit < li.image_start) {
// We didn't find any LOAD segment.
LOG_ERR("l0der", "_load_pie: no loadable segments");
return -ENOEXEC;
}
LOG_INFO("l0der", "Image bounds %08lx - %08lx", li.image_start, li.image_limit);
if ((res = _get_load_addr(&li)) != 0) {
return res;
}
LOG_INFO("l0der", "Loading at %08lx", li.load_address);
// Second pass through program headers: load all LOAD segments.
for (int i = 0; i < hdr->e_phnum; i++) {
uint32_t phdr_addr = hdr->e_phoff + (i * hdr->e_phentsize);
if ((res = _read_program_header(fp, phdr_addr, &phdr)) != 0) {
return res;
}
if (phdr.p_type != PT_LOAD) {
continue;
}
if ((res = _load_segment(fp, &li, &phdr)) != 0) {
return res;
}
}
// Load dynamic symbols.
if ((res = _parse_dynamic_symbols(fp, &li, hdr)) != 0) {
return res;
}
// Run relocations.
if ((res = _run_relocations(fp, &li, hdr)) != 0) {
return res;
}
uint32_t image_entrypoint = li.load_address + hdr->e_entry;
LOG_INFO("l0der", "Entrypoint (ISR Vector) at %08lx", image_entrypoint);
// Setup stack
uint32_t *isr = (uint32_t *)image_entrypoint;
isr[0] = li.stack_top;
info->isr_vector = (void *)image_entrypoint;
return 0;
}
int l0der_load_path(const char *path, struct l0dable_info *info)
{
FIL fh;
FRESULT fres = f_open(&fh, path, FA_OPEN_EXISTING|FA_READ);
if (fres != FR_OK) {
LOG_ERR("l0der", "l0der_load_path: could not open ELF file %s: %d", path, fres);
return -ENOENT;
}
int size = f_size(&fh);
int res = 0;
// Load ELF header and ensure it's somewhat sane.
Elf32_Ehdr hdr;
if (_read_elf_header(&fh, &hdr) != 0) {
res = -EINVAL;
goto done;
}
// Sanitize segments.
uint32_t ph_start = hdr.e_phoff;
uint32_t ph_limit = hdr.e_phoff + (hdr.e_phnum * hdr.e_phentsize);
if (ph_limit < ph_start) {
LOG_ERR("l0der", "l0der_load_path: invalid program header count/size: overflow");
return -ENOEXEC;
}
if (ph_limit - ph_start == 0) {
LOG_ERR("l0der", "l0der_load_path: no segments");
return -ENOEXEC;
}
if (ph_limit > size) {
LOG_ERR("l0der", "l0der_load_path: program header table extends past end of file");
return -ENOEXEC;
}
if (hdr.e_phentsize < sizeof(Elf32_Phdr)) {
LOG_ERR("l0der", "l0der_load_path: invalid program header table entry size");
return -ENOEXEC;
}
// Sanitize sections.
uint32_t sh_start = hdr.e_shoff;
uint32_t sh_limit = hdr.e_shoff + (hdr.e_shnum + hdr.e_shentsize);
if (sh_limit < sh_start) {
LOG_ERR("l0der", "l0der_load_path: invalid section header count/size: overflow");
return -ENOEXEC;
}
if (sh_limit > size) {
LOG_ERR("l0der", "l0der_load_path: section header table extends past end of file");
return -ENOEXEC;
}
if (hdr.e_shentsize < sizeof(Elf32_Shdr)) {
LOG_ERR("l0der", "l0der_load_path: invalid section header table entry size");
return -ENOEXEC;
}
// Check whether it's something that we can load.
if (hdr.e_type == ET_DYN && hdr.e_machine == EM_ARM && hdr.e_version == EV_CURRENT) {
LOG_INFO("l0der", "Loading PIE l0dable %s ...", path);
res = _load_pie(&fh, &hdr, info);
goto done;
} else {
LOG_ERR("l0der", "l0der_load_path: %s: not an ARM PIE, cannot load.", path);
res = -ENOEXEC;
goto done;
}
done:
f_close(&fh);
return res;
}