zephyr/subsys/llext/llext_load.c
Guennadi Liakhovetski cbb6199e67 LLEXT: no repeated linking with inline relocations
When linking and relocations are performed on the ELF object itself
with no copying, also global binding linking can break references.
Disable linking globally for such cases.

Signed-off-by: Guennadi Liakhovetski <guennadi.liakhovetski@linux.intel.com>
2024-11-20 10:17:06 +00:00

771 lines
20 KiB
C

/*
* Copyright (c) 2023 Intel Corporation
* Copyright (c) 2024 Arduino SA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/sys/util.h>
#include <zephyr/llext/elf.h>
#include <zephyr/llext/loader.h>
#include <zephyr/llext/llext.h>
#include <zephyr/llext/llext_internal.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(llext, CONFIG_LLEXT_LOG_LEVEL);
#include <string.h>
#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;
}
ext->sect_cnt = ldr->hdr.e_shnum;
size_t sect_map_sz = ext->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 < ext->sect_cnt; i++) {
ldr->sect_map[i].mem_idx = LLEXT_MEM_COUNT;
ldr->sect_map[i].offset = 0;
}
ext->sect_hdrs = (elf_shdr_t *)llext_peek(ldr, ldr->hdr.e_shoff);
if (ext->sect_hdrs) {
ext->sect_hdrs_on_heap = false;
} else {
size_t sect_hdrs_sz = ext->sect_cnt * sizeof(ext->sect_hdrs[0]);
ext->sect_hdrs_on_heap = true;
ext->sect_hdrs = llext_alloc(sect_hdrs_sz);
if (!ext->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, ext->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, struct llext *ext)
{
int table_cnt, i;
memset(ldr->sects, 0, sizeof(ldr->sects));
/* Find symbol and string tables */
for (i = 0, table_cnt = 0; i < ext->sect_cnt && table_cnt < 3; ++i) {
elf_shdr_t *shdr = ext->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 < ext->sect_cnt; ++i) {
elf_shdr_t *shdr = ext->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 < ext->sect_cnt; ++i) {
elf_shdr_t *shdr = ext->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 = ext->sect_hdrs + shndx;
uintptr_t section_addr = shdr->sh_addr;
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 {
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;
}
}
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, ext);
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, ldr_parm);
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;
}
if (ldr_parm->relocate_local) {
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;
/* 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;
}