/* * Copyright (c) 2023 Intel Corporation * * SPDX-License-Identifier: Apache-2.0 * */ #include "zephyr/sys/__assert.h" #include #include #include #include #include #include LOG_MODULE_REGISTER(llext, CONFIG_LLEXT_LOG_LEVEL); #include K_HEAP_DEFINE(llext_heap, CONFIG_LLEXT_HEAP_SIZE * 1024); static const char ELF_MAGIC[] = {0x7f, 'E', 'L', 'F'}; static inline int llext_read(struct llext_loader *l, void *buf, size_t len) { return l->read(l, buf, len); } static inline int llext_seek(struct llext_loader *l, size_t pos) { return l->seek(l, pos); } static sys_slist_t _llext_list = SYS_SLIST_STATIC_INIT(&_llext_list); sys_slist_t *llext_list(void) { return &_llext_list; } struct llext *llext_by_name(const char *name) { sys_slist_t *mlist = llext_list(); sys_snode_t *node = sys_slist_peek_head(mlist); struct llext *ext = CONTAINER_OF(node, struct llext, _llext_list); while (node != NULL) { if (strncmp(ext->name, name, sizeof(ext->name)) == 0) { return ext; } node = sys_slist_peek_next(node); ext = CONTAINER_OF(node, struct llext, _llext_list); } return NULL; } const void * const llext_find_sym(const struct llext_symtable *sym_table, const char *sym_name) { if (sym_table == NULL) { /* Buildin symbol table */ STRUCT_SECTION_FOREACH(llext_const_symbol, sym) { if (strcmp(sym->name, sym_name) == 0) { return sym->addr; } } } else { /* find symbols in module */ for (size_t i = 0; i < sym_table->sym_cnt; i++) { if (strcmp(sym_table->syms[i].name, sym_name) == 0) { return sym_table->syms[i].addr; } } } return NULL; } /* * Find all relevant string and symbol tables */ static int llext_find_tables(struct llext_loader *ldr) { int ret = 0; size_t pos = ldr->hdr.e_shoff; elf_shdr_t shdr; ldr->sects[LLEXT_SECT_SHSTRTAB] = ldr->sects[LLEXT_SECT_STRTAB] = ldr->sects[LLEXT_SECT_SYMTAB] = (elf_shdr_t){0}; /* Find symbol and string tables */ for (int i = 0, str_cnt = 0; i < ldr->hdr.e_shnum && str_cnt < 3; i++) { ret = llext_seek(ldr, pos); if (ret != 0) { LOG_ERR("failed seeking to position %u\n", pos); goto out; } ret = llext_read(ldr, &shdr, sizeof(elf_shdr_t)); if (ret != 0) { LOG_ERR("failed reading section header at position %u\n", pos); goto out; } pos += ldr->hdr.e_shentsize; LOG_DBG("section %d at %x: name %d, type %d, flags %x, addr %x, size %d", i, ldr->hdr.e_shoff + i * ldr->hdr.e_shentsize, shdr.sh_name, shdr.sh_type, shdr.sh_flags, shdr.sh_addr, shdr.sh_size); switch (shdr.sh_type) { case SHT_SYMTAB: case SHT_DYNSYM: LOG_DBG("symtab at %d", i); ldr->sects[LLEXT_SECT_SYMTAB] = shdr; ldr->sect_map[i] = LLEXT_SECT_SYMTAB; str_cnt++; break; case SHT_STRTAB: if (ldr->hdr.e_shstrndx == i) { LOG_DBG("shstrtab at %d", i); ldr->sects[LLEXT_SECT_SHSTRTAB] = shdr; ldr->sect_map[i] = LLEXT_SECT_SHSTRTAB; } else { LOG_DBG("strtab at %d", i); ldr->sects[LLEXT_SECT_STRTAB] = shdr; ldr->sect_map[i] = LLEXT_SECT_STRTAB; } str_cnt++; break; default: break; } } if (!ldr->sects[LLEXT_SECT_SHSTRTAB].sh_type || !ldr->sects[LLEXT_SECT_STRTAB].sh_type || !ldr->sects[LLEXT_SECT_SYMTAB].sh_type) { LOG_ERR("Some sections are missing or present multiple times!"); ret = -ENOENT; } out: return ret; } /* * Maps the section indexes and copies special section headers for easier use */ static int llext_map_sections(struct llext_loader *ldr) { int ret = 0; size_t pos = ldr->hdr.e_shoff; elf_shdr_t shdr; char name[32]; for (int i = 0; i < ldr->hdr.e_shnum; i++) { ret = llext_seek(ldr, pos); if (ret != 0) { goto out; } ret = llext_read(ldr, &shdr, sizeof(elf_shdr_t)); if (ret != 0) { goto out; } pos += ldr->hdr.e_shentsize; elf_word str_idx = shdr.sh_name; ret = llext_seek(ldr, ldr->sects[LLEXT_SECT_SHSTRTAB].sh_offset + str_idx); if (ret != 0) { goto out; } ret = llext_read(ldr, name, sizeof(name)); if (ret != 0) { goto out; } name[sizeof(name) - 1] = '\0'; LOG_DBG("section %d name %s", i, name); enum llext_section sect_idx; if (strncmp(name, ".text", sizeof(name)) == 0) { sect_idx = LLEXT_SECT_TEXT; } else if (strncmp(name, ".data", sizeof(name)) == 0) { sect_idx = LLEXT_SECT_DATA; } else if (strncmp(name, ".rodata", sizeof(name)) == 0) { sect_idx = LLEXT_SECT_RODATA; } else if (strncmp(name, ".bss", sizeof(name)) == 0) { sect_idx = LLEXT_SECT_BSS; } else { LOG_DBG("Not copied section %s", name); continue; } ldr->sects[sect_idx] = shdr; ldr->sect_map[i] = sect_idx; } out: return ret; } static inline enum llext_section llext_sect_from_mem(enum llext_mem m) { enum llext_section s; switch (m) { case LLEXT_MEM_BSS: s = LLEXT_SECT_BSS; break; case LLEXT_MEM_DATA: s = LLEXT_SECT_DATA; break; case LLEXT_MEM_RODATA: s = LLEXT_SECT_RODATA; break; case LLEXT_MEM_TEXT: s = LLEXT_SECT_TEXT; break; default: CODE_UNREACHABLE; } return s; } static int llext_allocate_mem(struct llext_loader *ldr, struct llext *ext) { int ret = 0; enum llext_section sect_idx; for (enum llext_mem mem_idx = 0; mem_idx < LLEXT_MEM_COUNT; mem_idx++) { sect_idx = llext_sect_from_mem(mem_idx); if (ldr->sects[sect_idx].sh_size > 0) { ext->mem[mem_idx] = k_heap_aligned_alloc(&llext_heap, sizeof(uintptr_t), ldr->sects[sect_idx].sh_size, K_NO_WAIT); if (ext->mem[mem_idx] == NULL) { ret = -ENOMEM; goto out; } } } out: return ret; } static int llext_copy_sections(struct llext_loader *ldr, struct llext *ext) { int ret = 0; enum llext_section sect_idx; for (enum llext_mem mem_idx = 0; mem_idx < LLEXT_MEM_COUNT; mem_idx++) { sect_idx = llext_sect_from_mem(mem_idx); if (ldr->sects[sect_idx].sh_size > 0) { ret = llext_seek(ldr, ldr->sects[sect_idx].sh_offset); if (ret != 0) { goto out; } ret = llext_read(ldr, ext->mem[mem_idx], ldr->sects[sect_idx].sh_size); if (ret != 0) { goto out; } } } out: return ret; } static int llext_count_export_syms(struct llext_loader *ldr) { int ret = 0; elf_sym_t sym; size_t ent_size = ldr->sects[LLEXT_SECT_SYMTAB].sh_entsize; size_t syms_size = ldr->sects[LLEXT_SECT_SYMTAB].sh_size; size_t pos = ldr->sects[LLEXT_SECT_SYMTAB].sh_offset; size_t sym_cnt = syms_size / sizeof(elf_sym_t); char name[32]; LOG_DBG("symbol count %u", sym_cnt); for (int i = 0; i < sym_cnt; i++) { ret = llext_seek(ldr, pos); if (ret != 0) { goto out; } ret = llext_read(ldr, &sym, ent_size); if (ret != 0) { goto out; } pos += ent_size; uint32_t stt = ELF_ST_TYPE(sym.st_info); uint32_t stb = ELF_ST_BIND(sym.st_info); uint32_t sect = sym.st_shndx; ret = llext_seek(ldr, ldr->sects[LLEXT_SECT_STRTAB].sh_offset + sym.st_name); if (ret != 0) { goto out; } ret = llext_read(ldr, name, sizeof(name)); if (ret != 0) { goto out; } name[sizeof(name) - 1] = '\0'; if (stt == STT_FUNC && stb == STB_GLOBAL) { LOG_DBG("function symbol %d, name %s, type tag %d, bind %d, sect %d", i, name, stt, stb, sect); ldr->sym_cnt++; } else { LOG_DBG("unhandled symbol %d, name %s, type tag %d, bind %d, sect %d", i, name, stt, stb, sect); } } out: return ret; } static inline int llext_allocate_symtab(struct llext_loader *ldr, struct llext *ext) { int ret = 0; ext->sym_tab.syms = k_heap_alloc(&llext_heap, ldr->sym_cnt * sizeof(struct llext_symbol), K_NO_WAIT); ext->sym_tab.sym_cnt = ldr->sym_cnt; memset(ext->sym_tab.syms, 0, ldr->sym_cnt * sizeof(struct llext_symbol)); return ret; } static inline int llext_copy_symbols(struct llext_loader *ldr, struct llext *ext) { int ret = 0; elf_sym_t sym; size_t ent_size = ldr->sects[LLEXT_SECT_SYMTAB].sh_entsize; size_t syms_size = ldr->sects[LLEXT_SECT_SYMTAB].sh_size; size_t pos = ldr->sects[LLEXT_SECT_SYMTAB].sh_offset; size_t sym_cnt = syms_size / sizeof(elf_sym_t); char name[32]; int i, j = 0; for (i = 0; i < sym_cnt; i++) { ret = llext_seek(ldr, pos); if (ret != 0) { goto out; } ret = llext_read(ldr, &sym, ent_size); if (ret != 0) { goto out; } pos += ent_size; uint32_t stt = ELF_ST_TYPE(sym.st_info); uint32_t stb = ELF_ST_BIND(sym.st_info); uint32_t sect = sym.st_shndx; ret = llext_seek(ldr, ldr->sects[LLEXT_SECT_STRTAB].sh_offset + sym.st_name); if (ret != 0) { goto out; } ret = llext_read(ldr, name, sizeof(name)); if (ret != 0) { goto out; } if (stt == STT_FUNC && stb == STB_GLOBAL && sect != SHN_UNDEF) { ext->sym_tab.syms[j].name = k_heap_alloc(&llext_heap, sizeof(name), K_NO_WAIT); strcpy(ext->sym_tab.syms[j].name, name); ext->sym_tab.syms[j].addr = (void *)((uintptr_t)ext->mem[ldr->sect_map[sym.st_shndx]] + sym.st_value); LOG_DBG("function symbol %d name %s addr %p", j, name, ext->sym_tab.syms[j].addr); j++; } } out: return ret; } static int llext_link(struct llext_loader *ldr, struct llext *ext) { int ret = 0; uintptr_t loc = 0; elf_shdr_t shdr; elf_rel_t rel; elf_sym_t sym; size_t pos = ldr->hdr.e_shoff; elf_word rel_cnt = 0; char name[32]; for (int i = 0; i < ldr->hdr.e_shnum - 1; i++) { ret = llext_seek(ldr, pos); if (ret != 0) { goto out; } ret = llext_read(ldr, &shdr, sizeof(elf_shdr_t)); if (ret != 0) { goto out; } pos += ldr->hdr.e_shentsize; /* find relocation sections */ if (shdr.sh_type != SHT_REL && shdr.sh_type != SHT_RELA) { continue; } rel_cnt = shdr.sh_size / sizeof(elf_rel_t); ret = llext_seek(ldr, ldr->sects[LLEXT_SECT_SHSTRTAB].sh_offset + shdr.sh_name); if (ret != 0) { goto out; } ret = llext_read(ldr, name, sizeof(name)); if (ret != 0) { goto out; } if (strncmp(name, ".rel.text", sizeof(name)) == 0 || strncmp(name, ".rela.text", sizeof(name)) == 0) { loc = (uintptr_t)ext->mem[LLEXT_MEM_TEXT]; } else if (strncmp(name, ".rel.bss", sizeof(name)) == 0) { loc = (uintptr_t)ext->mem[LLEXT_MEM_BSS]; } else if (strncmp(name, ".rel.rodata", sizeof(name)) == 0) { loc = (uintptr_t)ext->mem[LLEXT_MEM_RODATA]; } else if (strncmp(name, ".rel.data", sizeof(name)) == 0) { loc = (uintptr_t)ext->mem[LLEXT_MEM_DATA]; } LOG_DBG("relocation section %s (%d) linked to section %d has %d relocations", name, i, shdr.sh_link, rel_cnt); for (int j = 0; j < rel_cnt; j++) { /* get each relocation entry */ ret = llext_seek(ldr, shdr.sh_offset + j * sizeof(elf_rel_t)); if (ret != 0) { goto out; } ret = llext_read(ldr, &rel, sizeof(elf_rel_t)); if (ret != 0) { goto out; } /* get corresponding symbol */ ret = llext_seek(ldr, ldr->sects[LLEXT_SECT_SYMTAB].sh_offset + ELF_R_SYM(rel.r_info) * sizeof(elf_sym_t)); if (ret != 0) { goto out; } ret = llext_read(ldr, &sym, sizeof(elf_sym_t)); if (ret != 0) { goto out; } ret = llext_seek(ldr, ldr->sects[LLEXT_SECT_STRTAB].sh_offset + sym.st_name); if (ret != 0) { goto out; } ret = llext_read(ldr, name, sizeof(name)); if (ret != 0) { goto out; } LOG_DBG("relocation %d:%d info %x (type %d, sym %d) offset %d sym_name " "%s sym_type %d sym_bind %d sym_ndx %d", i, j, rel.r_info, ELF_R_TYPE(rel.r_info), ELF_R_SYM(rel.r_info), rel.r_offset, name, ELF_ST_TYPE(sym.st_info), ELF_ST_BIND(sym.st_info), sym.st_shndx); uintptr_t link_addr, op_loc, op_code; op_loc = loc + rel.r_offset; /* If symbol is undefined, then we need to look it up */ if (sym.st_shndx == SHN_UNDEF) { link_addr = (uintptr_t)llext_find_sym(NULL, name); if (link_addr == 0) { LOG_ERR("Undefined symbol with no entry in " "symbol table %s, offset %d, link section %d", name, rel.r_offset, shdr.sh_link); ret = -ENODATA; goto out; } else { op_code = (uintptr_t)(loc + rel.r_offset); LOG_INF("found symbol %s at 0x%lx, updating op code 0x%lx", name, link_addr, op_code); } } else if (ELF_ST_TYPE(sym.st_info) == STT_SECTION) { /* Current relocation location holds an offset into the section */ link_addr = (uintptr_t)ext->mem[ldr->sect_map[sym.st_shndx]] + sym.st_value + *((uintptr_t *)op_loc); LOG_INF("found section symbol %s addr 0x%lx", name, link_addr); } else { /* Nothing to relocate here */ continue; } LOG_INF("relocating (linking) symbol %s type %d binding %d ndx %d offset " "%d link section %d", name, ELF_ST_TYPE(sym.st_info), ELF_ST_BIND(sym.st_info), sym.st_shndx, rel.r_offset, shdr.sh_link); LOG_INF("writing relocation symbol %s type %d sym %d at addr 0x%lx " "addr 0x%lx", name, ELF_R_TYPE(rel.r_info), ELF_R_SYM(rel.r_info), op_loc, link_addr); /* relocation */ arch_elf_relocate(&rel, op_loc, link_addr); } } out: return ret; } /* * Load a valid ELF as an extension */ static int do_llext_load(struct llext_loader *ldr, struct llext *ext) { int ret = 0; memset(ldr->sects, 0, sizeof(ldr->sects)); ldr->sect_cnt = 0; ldr->sym_cnt = 0; size_t sect_map_sz = ldr->hdr.e_shnum * sizeof(uint32_t); ldr->sect_map = k_heap_alloc(&llext_heap, sect_map_sz, K_NO_WAIT); if (!ldr->sect_map) { LOG_ERR("Failed to allocate memory for section map, size %u", sect_map_sz); ret = -ENOMEM; goto out; } memset(ldr->sect_map, 0, ldr->hdr.e_shnum*sizeof(uint32_t)); ldr->sect_cnt = ldr->hdr.e_shnum; LOG_DBG("Finding ELF tables..."); ret = llext_find_tables(ldr); if (ret != 0) { LOG_ERR("Failed to find important ELF tables, ret %d", ret); goto out; } LOG_DBG("Mapping ELF sections..."); ret = llext_map_sections(ldr); if (ret != 0) { LOG_ERR("Failed to map ELF sections, ret %d", ret); goto out; } LOG_DBG("Allocation memory for ELF sections..."); ret = llext_allocate_mem(ldr, ext); if (ret != 0) { LOG_ERR("Failed to map memory for ELF sections, ret %d", ret); goto out; } LOG_DBG("Copying sections..."); ret = llext_copy_sections(ldr, ext); if (ret != 0) { LOG_ERR("Failed to copy ELF sections, ret %d", ret); goto out; } LOG_DBG("Counting exported symbols..."); ret = llext_count_export_syms(ldr); if (ret != 0) { LOG_ERR("Failed to count exported ELF symbols, ret %d", ret); goto out; } LOG_DBG("Allocating memory for symbol table..."); ret = llext_allocate_symtab(ldr, ext); if (ret != 0) { LOG_ERR("Failed to allocate extension symbol table, ret %d", ret); goto out; } LOG_DBG("Copying symbols..."); ret = llext_copy_symbols(ldr, ext); if (ret != 0) { LOG_ERR("Failed to copy symbols, ret %d", ret); goto out; } LOG_DBG("Linking ELF..."); ret = llext_link(ldr, ext); if (ret != 0) { LOG_ERR("Failed to link, ret %d", ret); goto out; } out: if (ldr->sect_map != NULL) { k_heap_free(&llext_heap, ldr->sect_map); } if (ret != 0) { LOG_DBG("Failed to load extension, freeing memory..."); for (enum llext_mem mem_idx = 0; mem_idx < LLEXT_MEM_COUNT; mem_idx++) { if (ext->mem[mem_idx] != NULL) { k_heap_free(&llext_heap, ext->mem[mem_idx]); } } for (int i = 0; i < ext->sym_tab.sym_cnt; i++) { if (ext->sym_tab.syms[i].name != NULL) { k_heap_free(&llext_heap, ext->sym_tab.syms[i].name); } } k_heap_free(&llext_heap, ext->sym_tab.syms); } else { LOG_DBG("loaded module, .text at %p, .rodata at %p", ext->mem[LLEXT_MEM_TEXT], ext->mem[LLEXT_MEM_RODATA]); } return ret; } int llext_load(struct llext_loader *ldr, const char *name, struct llext **ext) { int ret = 0; elf_ehdr_t ehdr; ret = llext_seek(ldr, 0); if (ret != 0) { LOG_ERR("Failed to seek for ELF header"); goto out; } ret = llext_read(ldr, &ehdr, sizeof(ehdr)); if (ret != 0) { LOG_ERR("Failed to read ELF header"); goto out; } /* check whether this is an valid elf file */ if (memcmp(ehdr.e_ident, ELF_MAGIC, sizeof(ELF_MAGIC)) != 0) { LOG_HEXDUMP_ERR(ehdr.e_ident, 16, "Invalid ELF, magic does not match"); ret = -EINVAL; goto out; } switch (ehdr.e_type) { case ET_REL: case ET_DYN: LOG_DBG("Loading relocatable or shared elf"); *ext = k_heap_alloc(&llext_heap, sizeof(struct llext), K_NO_WAIT); if (*ext == NULL) { LOG_ERR("Not enough memory for extension metadata"); ret = -ENOMEM; goto out; } memset(*ext, 0, sizeof(struct llext)); for (int i = 0; i < LLEXT_MEM_COUNT; i++) { (*ext)->mem[i] = NULL; } ldr->hdr = ehdr; ret = do_llext_load(ldr, *ext); break; default: LOG_ERR("Unsupported elf file type %x", ehdr.e_type); *ext = NULL; ret = -EINVAL; goto out; } if (ret == 0) { strncpy((*ext)->name, name, sizeof((*ext)->name)); (*ext)->name[sizeof((*ext)->name) - 1] = '\0'; sys_slist_append(&_llext_list, &(*ext)->_llext_list); LOG_INF("Loaded extension %s", (*ext)->name); } out: return ret; } void llext_unload(struct llext *ext) { __ASSERT(ext, "Expected non-null extension"); sys_slist_find_and_remove(&_llext_list, &ext->_llext_list); for (int i = 0; i < LLEXT_MEM_COUNT; i++) { if (ext->mem[i] != NULL) { LOG_DBG("freeing memory region %d", i); k_heap_free(&llext_heap, ext->mem[i]); ext->mem[i] = NULL; } } if (ext->sym_tab.syms != NULL) { for (int i = 0; i < ext->sym_tab.sym_cnt; i++) { k_heap_free(&llext_heap, ext->sym_tab.syms[i].name); } k_heap_free(&llext_heap, ext->sym_tab.syms); } k_heap_free(&llext_heap, ext); } int llext_call_fn(struct llext *ext, const char *sym_name) { void (*fn)(void); fn = llext_find_sym(&ext->sym_tab, sym_name); if (fn == NULL) { return -EINVAL; } fn(); return 0; }