diff --git a/doc/services/llext/load.rst b/doc/services/llext/load.rst index 0898cb3a40e77..78fc897b87d43 100644 --- a/doc/services/llext/load.rst +++ b/doc/services/llext/load.rst @@ -24,6 +24,32 @@ successfully, the extension is loaded into memory and is ready to be used. included in any user memory domain. To allow access from user mode, the :c:func:`llext_add_domain` function must be called. +Initializing and cleaning up the extension +========================================== + +The extension may define a number of initialization functions that must be +called after loading but before any function in it can be used; this is typical +in languages such as C++ that provide the concept of object constructors. The +same is true for cleanup functions that must be called before unloading the +extension. + +LLEXT supports calling the functions listed in the ``.preinit_array`` and +``.init_array`` sections of the ELF file with the :c:func:`llext_bringup` +function, and the functions listed in the ``.fini_array`` section with the +:c:func:`llext_teardown` function. These APIs are compatible with +:ref:`User Mode `, and thus can be called from either kernel or +user context. + +.. important:: + The code run by these functions is fully determined by the contents of the + ELF file. This may have security implications if its origin is untrusted. + +If the extension requires a dedicated thread, the :c:func:`llext_bootstrap` +function can be used to minimize boilerplate code. This function has a +signature that is compatible with the :c:func:`k_thread_create` API, and will +call :c:func:`llext_bringup`, then a user-specified function in the same +context, and finally :c:func:`llext_teardown` before returning. + Accessing code and data ======================= diff --git a/include/zephyr/llext/elf.h b/include/zephyr/llext/elf.h index ca9f498a49ac0..7230d13181b72 100644 --- a/include/zephyr/llext/elf.h +++ b/include/zephyr/llext/elf.h @@ -206,6 +206,9 @@ struct elf64_shdr { #define SHT_NOBITS 0x8 /**< Program data with no file image */ #define SHT_REL 0x9 /**< Relocation entries without addends */ #define SHT_DYNSYM 0xB /**< Dynamic linking symbol table */ +#define SHT_INIT_ARRAY 0xe /**< Array of pointers to init functions */ +#define SHT_FINI_ARRAY 0xf /**< Array of pointers to termination functions */ +#define SHT_PREINIT_ARRAY 0x10 /**< Array of pointers to early init functions */ /** ELF section flags */ #define SHF_WRITE 0x1 /**< Section is writable */ diff --git a/include/zephyr/llext/llext.h b/include/zephyr/llext/llext.h index 9256f1db8a65d..2aa588f8243c6 100644 --- a/include/zephyr/llext/llext.h +++ b/include/zephyr/llext/llext.h @@ -50,6 +50,9 @@ enum llext_mem { LLEXT_MEM_SYMTAB, /**< Symbol table */ LLEXT_MEM_STRTAB, /**< Symbol name strings */ LLEXT_MEM_SHSTRTAB, /**< Section name strings */ + LLEXT_MEM_PREINIT, /**< Array of early setup functions */ + LLEXT_MEM_INIT, /**< Array of setup functions */ + LLEXT_MEM_FINI, /**< Array of cleanup functions */ LLEXT_MEM_COUNT, /**< Number of regions managed by LLEXT */ }; @@ -183,6 +186,74 @@ int llext_load(struct llext_loader *loader, const char *name, struct llext **ext */ int llext_unload(struct llext **ext); +/** @brief Entry point function signature for an extension. */ +typedef void (*llext_entry_fn_t)(void *user_data); + +/** + * @brief Calls bringup functions for an extension. + * + * Must be called before accessing any symbol in the extension. Will execute + * the extension's own setup functions in the caller context. + * @see llext_bootstrap + * + * @param[in] ext Extension to initialize. + * @returns 0 on success, or a negative error code. + * @retval -EFAULT A relocation issue was detected + */ +int llext_bringup(struct llext *ext); + +/** + * @brief Calls teardown functions for an extension. + * + * Will execute the extension's own cleanup functions in the caller context. + * After this function completes, the extension is no longer usable and must be + * fully unloaded with @ref llext_unload. + * @see llext_bootstrap + * + * @param[in] ext Extension to de-initialize. + * @returns 0 on success, or a negative error code. + * @retval -EFAULT A relocation issue was detected + */ +int llext_teardown(struct llext *ext); + +/** + * @brief Bring up, execute, and teardown an extension. + * + * Calls the extension's own setup functions, an additional entry point and + * the extension's cleanup functions in the current thread context. + * + * This is a convenient wrapper around @ref llext_bringup and @ref + * llext_teardown that matches the @ref k_thread_entry_t signature, so it can + * be directly started as a new user or kernel thread via @ref k_thread_create. + * + * @param[in] ext Extension to execute. Passed as `p1` in @ref k_thread_create. + * @param[in] entry_fn Main entry point of the thread after performing + * extension setup. Passed as `p2` in @ref k_thread_create. + * @param[in] user_data Argument passed to @a entry_fn. Passed as `p3` in + * @ref k_thread_create. + */ +void llext_bootstrap(struct llext *ext, llext_entry_fn_t entry_fn, void *user_data); + +/** + * @brief Get pointers to setup or cleanup functions for an extension. + * + * This syscall can be used to get the addresses of all the functions that + * have to be called for full extension setup or cleanup. + * + * @see llext_bootstrap + * + * @param[in] ext Extension to initialize. + * @param[in] is_init `true` to get functions to be called at setup time, + * `false` to get the cleanup ones. + * @param[inout] buf Buffer to store the function pointers in. Can be `NULL` + * to only get the minimum required size. + * @param[in] size Allocated size of the buffer in bytes. + * @returns the size used by the array in bytes, or a negative error code. + * @retval -EFAULT A relocation issue was detected + * @retval -ENOMEM Array does not fit in the allocated buffer + */ +__syscall ssize_t llext_get_fn_table(struct llext *ext, bool is_init, void *buf, size_t size); + /** * @brief Find the address for an arbitrary symbol. * @@ -252,8 +323,8 @@ int arch_elf_relocate(elf_rela_t *rel, uintptr_t loc, * * Searches for a section by name in the ELF file and returns its offset. * - * @param loader Extension loader data and context - * @param search_name Section name to search for + * @param[in] loader Extension loader data and context + * @param[in] search_name Section name to search for * @returns the section offset or a negative error code */ ssize_t llext_find_section(struct llext_loader *loader, const char *search_name); @@ -278,4 +349,6 @@ void arch_elf_relocate_local(struct llext_loader *loader, struct llext *ext, } #endif +#include + #endif /* ZEPHYR_LLEXT_H */ diff --git a/subsys/llext/CMakeLists.txt b/subsys/llext/CMakeLists.txt index 415e012a3d2f7..870216401ef8d 100644 --- a/subsys/llext/CMakeLists.txt +++ b/subsys/llext/CMakeLists.txt @@ -1,4 +1,6 @@ if(CONFIG_LLEXT) + zephyr_syscall_header(${ZEPHYR_BASE}/include/zephyr/llext/llext.h) + zephyr_library() # For strnlen() @@ -10,6 +12,7 @@ if(CONFIG_LLEXT) llext_load.c llext_link.c llext_export.c + llext_handlers.c buf_loader.c ) zephyr_library_sources_ifdef(CONFIG_LLEXT_SHELL shell.c) diff --git a/subsys/llext/llext.c b/subsys/llext/llext.c index 9e8880d5bc7db..8d25198726ae4 100644 --- a/subsys/llext/llext.c +++ b/subsys/llext/llext.c @@ -273,3 +273,66 @@ int llext_call_fn(struct llext *ext, const char *sym_name) return 0; } + +static int call_fn_table(struct llext *ext, bool is_init) +{ + ssize_t ret; + + ret = llext_get_fn_table(ext, is_init, NULL, 0); + if (ret < 0) { + LOG_ERR("Failed to get table size: %d", (int)ret); + return ret; + } + + typedef void (*elf_void_fn_t)(void); + + int fn_count = ret / sizeof(elf_void_fn_t); + elf_void_fn_t fn_table[fn_count]; + + ret = llext_get_fn_table(ext, is_init, &fn_table, sizeof(fn_table)); + if (ret < 0) { + LOG_ERR("Failed to get function table: %d", (int)ret); + return ret; + } + + for (int i = 0; i < fn_count; i++) { + LOG_DBG("calling %s function %p()", + is_init ? "bringup" : "teardown", fn_table[i]); + fn_table[i](); + } + + return 0; +} + +inline int llext_bringup(struct llext *ext) +{ + return call_fn_table(ext, true); +} + +inline int llext_teardown(struct llext *ext) +{ + return call_fn_table(ext, false); +} + +void llext_bootstrap(struct llext *ext, llext_entry_fn_t entry_fn, void *user_data) +{ + int ret; + + /* Call initialization functions */ + ret = llext_bringup(ext); + if (ret < 0) { + LOG_ERR("Failed to call init functions: %d", ret); + return; + } + + /* Start extension main function */ + LOG_DBG("calling entry function %p(%p)", entry_fn, user_data); + entry_fn(user_data); + + /* Call de-initialization functions */ + ret = llext_teardown(ext); + if (ret < 0) { + LOG_ERR("Failed to call de-init functions: %d", ret); + return; + } +} diff --git a/subsys/llext/llext_handlers.c b/subsys/llext/llext_handlers.c new file mode 100644 index 0000000000000..c79af8383e2c2 --- /dev/null +++ b/subsys/llext/llext_handlers.c @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2024 Arduino SA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +#include +LOG_MODULE_DECLARE(llext, CONFIG_LLEXT_LOG_LEVEL); + +#include "llext_priv.h" + +ssize_t z_impl_llext_get_fn_table(struct llext *ext, bool is_init, void *buf, size_t buf_size) +{ + size_t table_size; + + if (!ext) { + return -EINVAL; + } + + if (is_init) { + table_size = ext->mem_size[LLEXT_MEM_PREINIT] + + ext->mem_size[LLEXT_MEM_INIT]; + } else { + table_size = ext->mem_size[LLEXT_MEM_FINI]; + } + + if (buf) { + char *byte_ptr = buf; + + if (buf_size < table_size) { + return -ENOMEM; + } + + if (is_init) { + /* setup functions from preinit_array and init_array */ + memcpy(byte_ptr, + ext->mem[LLEXT_MEM_PREINIT], ext->mem_size[LLEXT_MEM_PREINIT]); + memcpy(byte_ptr + ext->mem_size[LLEXT_MEM_PREINIT], + ext->mem[LLEXT_MEM_INIT], ext->mem_size[LLEXT_MEM_INIT]); + } else { + /* cleanup functions from fini_array */ + memcpy(byte_ptr, + ext->mem[LLEXT_MEM_FINI], ext->mem_size[LLEXT_MEM_FINI]); + } + + /* Sanity check: pointers in this table must map inside the + * text region of the extension. If this fails, something went + * wrong during the relocation process. + * Using "char *" for these simplifies pointer arithmetic. + */ + const char *text_start = ext->mem[LLEXT_MEM_TEXT]; + const char *text_end = text_start + ext->mem_size[LLEXT_MEM_TEXT]; + const char **fn_ptrs = buf; + + for (int i = 0; i < table_size / sizeof(void *); i++) { + if (fn_ptrs[i] < text_start || fn_ptrs[i] >= text_end) { + LOG_ERR("init function %i (%p) outside text region", + i, fn_ptrs[i]); + return -EFAULT; + } + } + } + + return table_size; +} + +#ifdef CONFIG_USERSPACE + +static int ext_is_valid(struct llext *ext, void *arg) +{ + return ext == arg; +} + +static inline ssize_t z_vrfy_llext_get_fn_table(struct llext *ext, bool is_init, + void *buf, size_t size) +{ + /* Test that ext matches a loaded extension */ + K_OOPS(llext_iterate(ext_is_valid, ext) == 0); + + if (buf) { + /* Test that buf is a valid user-accessible pointer */ + K_OOPS(K_SYSCALL_MEMORY_WRITE(buf, size)); + } + + return z_impl_llext_get_fn_table(ext, is_init, buf, size); +} +#include + +#endif /* CONFIG_USERSPACE */ diff --git a/subsys/llext/llext_load.c b/subsys/llext/llext_load.c index fcc26210ebcfc..28574806a9579 100644 --- a/subsys/llext/llext_load.c +++ b/subsys/llext/llext_load.c @@ -208,6 +208,12 @@ static int llext_map_sections(struct llext_loader *ldr, struct llext *ext) 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; @@ -224,6 +230,15 @@ static int llext_map_sections(struct llext_loader *ldr, struct llext *ext) 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; @@ -241,6 +256,19 @@ static int llext_map_sections(struct llext_loader *ldr, struct llext *ext) 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; @@ -261,7 +289,9 @@ static int llext_map_sections(struct llext_loader *ldr, struct llext *ext) return -ENOEXEC; } - if (mem_idx == LLEXT_MEM_BSS) { + /* 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 @@ -269,6 +299,16 @@ static int llext_map_sections(struct llext_loader *ldr, struct llext *ext) */ 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) {