/* * Copyright (c) 2023 Intel Corporation * Copyright (c) 2024 Arduino SA * * SPDX-License-Identifier: Apache-2.0 */ #include #include #include #include #include #include #include LOG_MODULE_DECLARE(llext, CONFIG_LLEXT_LOG_LEVEL); #include #include "llext_priv.h" /* * NOTICE: Functions in this file do not clean up allocations in their error * paths; instead, this is performed once and for all when leaving the parent * `do_llext_load()` function. This approach consolidates memory management * in a single place, simplifying error handling and reducing the risk of * memory leaks. * * The following rationale applies: * * - The input `struct llext` and fields in `struct loader` are zero-filled * at the beginning of the do_llext_load function, so that every pointer is * set to NULL and every bool is false. * - If some function called by do_llext_load allocates memory, it does so by * immediately writing the pointer in the `ext` and `ldr` structures. * - do_llext_load() will clean up the memory allocated by the functions it * calls, taking into account if the load process was successful or not. */ static const char ELF_MAGIC[] = {0x7f, 'E', 'L', 'F'}; const void *llext_loaded_sect_ptr(struct llext_loader *ldr, struct llext *ext, unsigned int sh_ndx) { enum llext_mem mem_idx = ldr->sect_map[sh_ndx].mem_idx; if (mem_idx == LLEXT_MEM_COUNT) { return NULL; } return (const uint8_t *)ext->mem[mem_idx] + ldr->sect_map[sh_ndx].offset; } /* * Load basic ELF file data */ static int llext_load_elf_data(struct llext_loader *ldr, struct llext *ext) { int ret; /* read ELF header */ ret = llext_seek(ldr, 0); if (ret != 0) { LOG_ERR("Failed to seek for ELF header"); return ret; } ret = llext_read(ldr, &ldr->hdr, sizeof(ldr->hdr)); if (ret != 0) { LOG_ERR("Failed to read ELF header"); return ret; } /* check whether this is a valid ELF file */ if (memcmp(ldr->hdr.e_ident, ELF_MAGIC, sizeof(ELF_MAGIC)) != 0) { LOG_HEXDUMP_ERR(ldr->hdr.e_ident, 16, "Invalid ELF, magic does not match"); return -ENOEXEC; } switch (ldr->hdr.e_type) { case ET_REL: LOG_DBG("Loading relocatable ELF"); break; case ET_DYN: LOG_DBG("Loading shared ELF"); break; default: LOG_ERR("Unsupported ELF file type %x", ldr->hdr.e_type); return -ENOEXEC; } /* * Read all ELF section headers and initialize maps. Buffers allocated * below are freed when leaving do_llext_load(), so don't count them in * alloc_size. */ if (ldr->hdr.e_shentsize != sizeof(elf_shdr_t)) { LOG_ERR("Invalid section header size %d", ldr->hdr.e_shentsize); return -ENOEXEC; } ldr->sect_cnt = ldr->hdr.e_shnum; size_t sect_map_sz = ldr->sect_cnt * sizeof(ldr->sect_map[0]); ldr->sect_map = llext_alloc(sect_map_sz); if (!ldr->sect_map) { LOG_ERR("Failed to allocate section map, size %zu", sect_map_sz); return -ENOMEM; } for (int i = 0; i < ldr->sect_cnt; i++) { ldr->sect_map[i].mem_idx = LLEXT_MEM_COUNT; ldr->sect_map[i].offset = 0; } ldr->sect_hdrs = (elf_shdr_t *) llext_peek(ldr, ldr->hdr.e_shoff); if (ldr->sect_hdrs) { ldr->sect_hdrs_on_heap = false; } else { size_t sect_hdrs_sz = ldr->sect_cnt * sizeof(ldr->sect_hdrs[0]); ldr->sect_hdrs_on_heap = true; ldr->sect_hdrs = llext_alloc(sect_hdrs_sz); if (!ldr->sect_hdrs) { LOG_ERR("Failed to allocate section headers, size %zu", sect_hdrs_sz); return -ENOMEM; } ret = llext_seek(ldr, ldr->hdr.e_shoff); if (ret != 0) { LOG_ERR("Failed to seek for section headers"); return ret; } ret = llext_read(ldr, ldr->sect_hdrs, sect_hdrs_sz); if (ret != 0) { LOG_ERR("Failed to read section headers"); return ret; } } return 0; } /* * Find all relevant string and symbol tables */ static int llext_find_tables(struct llext_loader *ldr) { int table_cnt, i; memset(ldr->sects, 0, sizeof(ldr->sects)); /* Find symbol and string tables */ for (i = 0, table_cnt = 0; i < ldr->sect_cnt && table_cnt < 3; ++i) { elf_shdr_t *shdr = ldr->sect_hdrs + i; LOG_DBG("section %d at 0x%zx: name %d, type %d, flags 0x%zx, " "addr 0x%zx, size %zd, link %d, info %d", i, (size_t)shdr->sh_offset, shdr->sh_name, shdr->sh_type, (size_t)shdr->sh_flags, (size_t)shdr->sh_addr, (size_t)shdr->sh_size, shdr->sh_link, shdr->sh_info); switch (shdr->sh_type) { case SHT_SYMTAB: case SHT_DYNSYM: LOG_DBG("symtab at %d", i); ldr->sects[LLEXT_MEM_SYMTAB] = *shdr; ldr->sect_map[i].mem_idx = LLEXT_MEM_SYMTAB; table_cnt++; break; case SHT_STRTAB: if (ldr->hdr.e_shstrndx == i) { LOG_DBG("shstrtab at %d", i); ldr->sects[LLEXT_MEM_SHSTRTAB] = *shdr; ldr->sect_map[i].mem_idx = LLEXT_MEM_SHSTRTAB; } else { LOG_DBG("strtab at %d", i); ldr->sects[LLEXT_MEM_STRTAB] = *shdr; ldr->sect_map[i].mem_idx = LLEXT_MEM_STRTAB; } table_cnt++; break; default: break; } } if (!ldr->sects[LLEXT_MEM_SHSTRTAB].sh_type || !ldr->sects[LLEXT_MEM_STRTAB].sh_type || !ldr->sects[LLEXT_MEM_SYMTAB].sh_type) { LOG_ERR("Some sections are missing or present multiple times!"); return -ENOEXEC; } return 0; } /* * Maps the ELF sections into regions according to their usage flags, * calculating ldr->sects and ldr->sect_map. */ static int llext_map_sections(struct llext_loader *ldr, struct llext *ext, const struct llext_load_param *ldr_parm) { int i, j; const char *name; for (i = 0; i < ldr->sect_cnt; ++i) { elf_shdr_t *shdr = ldr->sect_hdrs + i; name = llext_string(ldr, ext, LLEXT_MEM_SHSTRTAB, shdr->sh_name); if (ldr->sect_map[i].mem_idx != LLEXT_MEM_COUNT) { LOG_DBG("section %d name %s already mapped to region %d", i, name, ldr->sect_map[i].mem_idx); continue; } /* Identify the section type by its flags */ enum llext_mem mem_idx; switch (shdr->sh_type) { case SHT_NOBITS: mem_idx = LLEXT_MEM_BSS; break; case SHT_PROGBITS: if (shdr->sh_flags & SHF_EXECINSTR) { mem_idx = LLEXT_MEM_TEXT; } else if (shdr->sh_flags & SHF_WRITE) { mem_idx = LLEXT_MEM_DATA; } else { mem_idx = LLEXT_MEM_RODATA; } break; case SHT_PREINIT_ARRAY: mem_idx = LLEXT_MEM_PREINIT; break; case SHT_INIT_ARRAY: mem_idx = LLEXT_MEM_INIT; break; case SHT_FINI_ARRAY: mem_idx = LLEXT_MEM_FINI; break; default: mem_idx = LLEXT_MEM_COUNT; break; } /* Special exception for .exported_sym */ if (strcmp(name, ".exported_sym") == 0) { mem_idx = LLEXT_MEM_EXPORT; } if (mem_idx == LLEXT_MEM_COUNT || !(shdr->sh_flags & SHF_ALLOC) || shdr->sh_size == 0) { LOG_DBG("section %d name %s skipped", i, name); continue; } switch (mem_idx) { case LLEXT_MEM_PREINIT: case LLEXT_MEM_INIT: case LLEXT_MEM_FINI: if (shdr->sh_entsize != sizeof(void *) || shdr->sh_size % shdr->sh_entsize != 0) { LOG_ERR("Invalid %s array in section %d", name, i); return -ENOEXEC; } default: break; } LOG_DBG("section %d name %s maps to region %d", i, name, mem_idx); ldr->sect_map[i].mem_idx = mem_idx; elf_shdr_t *region = ldr->sects + mem_idx; /* * ELF objects can have sections for memory regions, detached from * other sections of the same type. E.g. executable sections that will be * placed in slower memory. Don't merge such sections into main regions */ if (ldr_parm->section_detached && ldr_parm->section_detached(shdr)) { continue; } if (region->sh_type == SHT_NULL) { /* First section of this type, copy all info to the * region descriptor. */ memcpy(region, shdr, sizeof(*region)); } else { /* Make sure this section is compatible with the region */ if ((shdr->sh_flags & SHF_BASIC_TYPE_MASK) != (region->sh_flags & SHF_BASIC_TYPE_MASK)) { LOG_ERR("Unsupported section flags %#x / %#x for %s (region %d)", (uint32_t)shdr->sh_flags, (uint32_t)region->sh_flags, name, mem_idx); return -ENOEXEC; } /* Check if this region type is extendable */ switch (mem_idx) { case LLEXT_MEM_BSS: /* SHT_NOBITS sections cannot be merged properly: * as they use no space in the file, the logic * below does not work; they must be treated as * independent entities. */ LOG_ERR("Multiple SHT_NOBITS sections are not supported"); return -ENOTSUP; case LLEXT_MEM_PREINIT: case LLEXT_MEM_INIT: case LLEXT_MEM_FINI: /* These regions are not extendable and must be * referenced at most once in the ELF file. */ LOG_ERR("Region %d redefined", mem_idx); return -ENOEXEC; default: break; } if (ldr->hdr.e_type == ET_DYN) { /* In shared objects, sh_addr is the VMA. * Before merging this section in the region, * make sure the delta in VMAs matches that of * file offsets. */ if (shdr->sh_addr - region->sh_addr != shdr->sh_offset - region->sh_offset) { LOG_ERR("Incompatible section addresses " "for %s (region %d)", name, mem_idx); return -ENOEXEC; } } /* * Extend the current region to include the new section * (overlaps are detected later) */ size_t address = MIN(region->sh_addr, shdr->sh_addr); size_t bot_ofs = MIN(region->sh_offset, shdr->sh_offset); size_t top_ofs = MAX(region->sh_offset + region->sh_size, shdr->sh_offset + shdr->sh_size); region->sh_addr = address; region->sh_offset = bot_ofs; region->sh_size = top_ofs - bot_ofs; } } /* * Test that no computed region overlaps. This can happen if sections of * different llext_mem type are interleaved in the ELF file or in VMAs. */ for (i = 0; i < LLEXT_MEM_COUNT; i++) { for (j = i+1; j < LLEXT_MEM_COUNT; j++) { elf_shdr_t *x = ldr->sects + i; elf_shdr_t *y = ldr->sects + j; if (x->sh_type == SHT_NULL || x->sh_size == 0 || y->sh_type == SHT_NULL || y->sh_size == 0) { /* Skip empty regions */ continue; } /* * The export symbol table may be surrounded by * other data sections. Ignore overlaps in that * case. */ if ((i == LLEXT_MEM_DATA || i == LLEXT_MEM_RODATA) && j == LLEXT_MEM_EXPORT) { continue; } /* * Exported symbols region can also overlap * with rodata. */ if (i == LLEXT_MEM_EXPORT || j == LLEXT_MEM_EXPORT) { continue; } if (ldr->hdr.e_type == ET_DYN) { /* * Test all merged VMA ranges for overlaps */ if ((x->sh_addr <= y->sh_addr && x->sh_addr + x->sh_size > y->sh_addr) || (y->sh_addr <= x->sh_addr && y->sh_addr + y->sh_size > x->sh_addr)) { LOG_ERR("Region %d VMA range (0x%zx +%zd) " "overlaps with %d (0x%zx +%zd)", i, (size_t)x->sh_addr, (size_t)x->sh_size, j, (size_t)y->sh_addr, (size_t)y->sh_size); return -ENOEXEC; } } /* * Test file offsets. BSS sections store no * data in the file and must not be included * in checks to avoid false positives. */ if (i == LLEXT_MEM_BSS || j == LLEXT_MEM_BSS) { continue; } if ((x->sh_offset <= y->sh_offset && x->sh_offset + x->sh_size > y->sh_offset) || (y->sh_offset <= x->sh_offset && y->sh_offset + y->sh_size > x->sh_offset)) { LOG_ERR("Region %d ELF file range (0x%zx +%zd) " "overlaps with %d (0x%zx +%zd)", i, (size_t)x->sh_offset, (size_t)x->sh_size, j, (size_t)y->sh_offset, (size_t)y->sh_size); return -ENOEXEC; } } } /* * Calculate each ELF section's offset inside its memory region. This * is done as a separate pass so the final regions are already defined. */ for (i = 0; i < ldr->sect_cnt; ++i) { elf_shdr_t *shdr = ldr->sect_hdrs + i; enum llext_mem mem_idx = ldr->sect_map[i].mem_idx; if (mem_idx != LLEXT_MEM_COUNT) { ldr->sect_map[i].offset = shdr->sh_offset - ldr->sects[mem_idx].sh_offset; } } return 0; } static int llext_count_export_syms(struct llext_loader *ldr, struct llext *ext) { size_t ent_size = ldr->sects[LLEXT_MEM_SYMTAB].sh_entsize; size_t syms_size = ldr->sects[LLEXT_MEM_SYMTAB].sh_size; int sym_cnt = syms_size / sizeof(elf_sym_t); const char *name; elf_sym_t sym; int i, ret; size_t pos; LOG_DBG("symbol count %u", sym_cnt); ext->sym_tab.sym_cnt = 0; for (i = 0, pos = ldr->sects[LLEXT_MEM_SYMTAB].sh_offset; i < sym_cnt; i++, pos += ent_size) { if (!i) { /* A dummy entry */ continue; } ret = llext_seek(ldr, pos); if (ret != 0) { return ret; } ret = llext_read(ldr, &sym, ent_size); if (ret != 0) { return ret; } uint32_t stt = ELF_ST_TYPE(sym.st_info); uint32_t stb = ELF_ST_BIND(sym.st_info); uint32_t sect = sym.st_shndx; name = llext_string(ldr, ext, LLEXT_MEM_STRTAB, sym.st_name); if ((stt == STT_FUNC || stt == STT_OBJECT) && stb == STB_GLOBAL) { LOG_DBG("function symbol %d, name %s, type tag %d, bind %d, sect %d", i, name, stt, stb, sect); ext->sym_tab.sym_cnt++; } else { LOG_DBG("unhandled symbol %d, name %s, type tag %d, bind %d, sect %d", i, name, stt, stb, sect); } } return 0; } static int llext_allocate_symtab(struct llext_loader *ldr, struct llext *ext) { struct llext_symtable *sym_tab = &ext->sym_tab; size_t syms_size = sym_tab->sym_cnt * sizeof(struct llext_symbol); sym_tab->syms = llext_alloc(syms_size); if (!sym_tab->syms) { return -ENOMEM; } memset(sym_tab->syms, 0, syms_size); ext->alloc_size += syms_size; return 0; } static int llext_export_symbols(struct llext_loader *ldr, struct llext *ext) { elf_shdr_t *shdr = ldr->sects + LLEXT_MEM_EXPORT; struct llext_symbol *sym; unsigned int i; if (shdr->sh_size < sizeof(struct llext_symbol)) { /* Not found, no symbols exported */ return 0; } struct llext_symtable *exp_tab = &ext->exp_tab; exp_tab->sym_cnt = shdr->sh_size / sizeof(struct llext_symbol); exp_tab->syms = llext_alloc(exp_tab->sym_cnt * sizeof(struct llext_symbol)); if (!exp_tab->syms) { return -ENOMEM; } for (i = 0, sym = ext->mem[LLEXT_MEM_EXPORT]; i < exp_tab->sym_cnt; i++, sym++) { exp_tab->syms[i].name = sym->name; exp_tab->syms[i].addr = sym->addr; LOG_DBG("sym %p name %s in %p", sym->addr, sym->name, exp_tab->syms + i); } return 0; } static int llext_copy_symbols(struct llext_loader *ldr, struct llext *ext, const struct llext_load_param *ldr_parm) { size_t ent_size = ldr->sects[LLEXT_MEM_SYMTAB].sh_entsize; size_t syms_size = ldr->sects[LLEXT_MEM_SYMTAB].sh_size; int sym_cnt = syms_size / sizeof(elf_sym_t); struct llext_symtable *sym_tab = &ext->sym_tab; elf_sym_t sym; int i, j, ret; size_t pos; for (i = 0, pos = ldr->sects[LLEXT_MEM_SYMTAB].sh_offset, j = 0; i < sym_cnt; i++, pos += ent_size) { if (!i) { /* A dummy entry */ continue; } ret = llext_seek(ldr, pos); if (ret != 0) { return ret; } ret = llext_read(ldr, &sym, ent_size); if (ret != 0) { return ret; } uint32_t stt = ELF_ST_TYPE(sym.st_info); uint32_t stb = ELF_ST_BIND(sym.st_info); unsigned int shndx = sym.st_shndx; if ((stt == STT_FUNC || stt == STT_OBJECT) && stb == STB_GLOBAL && shndx != SHN_UNDEF) { const char *name = llext_string(ldr, ext, LLEXT_MEM_STRTAB, sym.st_name); __ASSERT(j <= sym_tab->sym_cnt, "Miscalculated symbol number %u\n", j); sym_tab->syms[j].name = name; elf_shdr_t *shdr = ldr->sect_hdrs + shndx; uintptr_t section_addr = shdr->sh_addr; const void *base; base = llext_loaded_sect_ptr(ldr, ext, shndx); if (!base) { /* If the section is not mapped, try to peek. * Be noisy about it, since this is addressing * data that was missed by llext_map_sections. */ base = llext_peek(ldr, shdr->sh_offset); if (base) { LOG_DBG("section %d peeked at %p", shndx, base); } else { LOG_ERR("No data for section %d", shndx); return -ENOTSUP; } } if (ldr_parm->pre_located && (!ldr_parm->section_detached || !ldr_parm->section_detached(shdr))) { sym_tab->syms[j].addr = (uint8_t *)sym.st_value + (ldr->hdr.e_type == ET_REL ? section_addr : 0); } else { sym_tab->syms[j].addr = (uint8_t *)base + sym.st_value - (ldr->hdr.e_type == ET_REL ? 0 : section_addr); } LOG_DBG("function symbol %d name %s addr %p", j, name, sym_tab->syms[j].addr); j++; } } return 0; } /* * Load a valid ELF as an extension */ int do_llext_load(struct llext_loader *ldr, struct llext *ext, const struct llext_load_param *ldr_parm) { const struct llext_load_param default_ldr_parm = LLEXT_LOAD_PARAM_DEFAULT; int ret; if (!ldr_parm) { ldr_parm = &default_ldr_parm; } /* Zero all memory that is affected by the loading process * (see the NOTICE at the top of this file). */ memset(ext, 0, sizeof(*ext)); ldr->sect_map = NULL; LOG_DBG("Loading ELF data..."); ret = llext_prepare(ldr); if (ret != 0) { LOG_ERR("Failed to prepare the loader, ret %d", ret); goto out; } ret = llext_load_elf_data(ldr, ext); if (ret != 0) { LOG_ERR("Failed to load basic ELF data, ret %d", ret); goto out; } #ifdef CONFIG_USERSPACE ret = k_mem_domain_init(&ext->mem_domain, 0, NULL); if (ret != 0) { LOG_ERR("Failed to initialize extenion memory domain %d", ret); goto out; } #endif 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("Allocate and copy strings..."); ret = llext_copy_strings(ldr, ext); if (ret != 0) { LOG_ERR("Failed to copy ELF string sections, ret %d", ret); goto out; } LOG_DBG("Mapping ELF sections..."); ret = llext_map_sections(ldr, ext, ldr_parm); if (ret != 0) { LOG_ERR("Failed to map ELF sections, ret %d", ret); goto out; } LOG_DBG("Allocate and copy regions..."); ret = llext_copy_regions(ldr, ext); if (ret != 0) { LOG_ERR("Failed to copy regions, ret %d", ret); goto out; } LOG_DBG("Counting exported symbols..."); ret = llext_count_export_syms(ldr, ext); 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, ldr_parm); if (ret != 0) { LOG_ERR("Failed to copy symbols, ret %d", ret); goto out; } LOG_DBG("Linking ELF..."); ret = llext_link(ldr, ext, ldr_parm); if (ret != 0) { LOG_ERR("Failed to link, ret %d", ret); goto out; } ret = llext_export_symbols(ldr, ext); if (ret != 0) { LOG_ERR("Failed to export, ret %d", ret); goto out; } llext_adjust_mmu_permissions(ext); out: /* * Free resources only used during loading. Note that this exploits * the fact that freeing a NULL pointer has no effect. */ llext_free(ldr->sect_map); ldr->sect_map = NULL; if (ldr->sect_hdrs_on_heap) { llext_free(ldr->sect_hdrs); } ldr->sect_hdrs = NULL; /* Until proper inter-llext linking is implemented, the symbol table is * not useful outside of the loading process; keep it only if debugging * is enabled and no error is detected. */ if (!(IS_ENABLED(CONFIG_LLEXT_LOG_LEVEL_DBG) && ret == 0)) { llext_free(ext->sym_tab.syms); ext->sym_tab.sym_cnt = 0; ext->sym_tab.syms = NULL; } if (ret != 0) { LOG_DBG("Failed to load extension: %d", ret); /* Since the loading process failed, free the resources that * were allocated for the lifetime of the extension as well, * such as regions and exported symbols. */ llext_free_regions(ext); llext_free(ext->exp_tab.syms); ext->exp_tab.sym_cnt = 0; ext->exp_tab.syms = NULL; } else { LOG_DBG("loaded module, .text at %p, .rodata at %p", ext->mem[LLEXT_MEM_TEXT], ext->mem[LLEXT_MEM_RODATA]); } llext_finalize(ldr); return ret; }