Repository navigation
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Uninitialized VMA state and unchecked ELF header and section-table accesses can cause invalid restore mappings and out-of-bounds memory access, while relocation can violate larger section-alignment requirements.
3 open issues
What changed in this PR
This PR integrates relocatable LLEXT modules with SOF’s shared virtual-page allocator, preserving section layout and keeping assigned addresses across DSP power cycles.
Changes:
- Places ET_REL modules in reserved virtual pages while retaining fixed addresses for ET_DYN modules and fallback configurations.
- Adds reservation APIs and reclaims module reservations during D3 restore.
- Refactors allocator bookkeeping and corrects allocation-record removal.
| File | Description |
|---|---|
zephyr/lib/vpage.c |
Adds reservation operations and shared allocation bookkeeping. |
zephyr/include/sof/lib/vpage.h |
Declares and documents the new allocator APIs. |
zephyr/CMakeLists.txt |
Builds the virtual-page allocator when the library manager is enabled. |
src/library_manager/llext_manager.c |
Places relocatable sections and selects the appropriate mapping region. |
src/library_manager/llext_manager_dram.c |
Reclaims reservations during restore and releases them during error cleanup. |
src/include/sof/lib_manager.h |
Tracks each module’s reserved virtual address range. |
Review effort: Balanced
PR 11388: test resultsRun date: 2026-11-04 16:41 UTC Tested commit: 43fbae6236e7199c9ddb013c854687e9d551ecc3 |
45117b2 to
ba12b0f
Compare
There was a problem hiding this comment.
🟡 Changes recommended
An ELF section table overlapping the header can pass the bounds checks and overwrite e_shnum, causing out-of-bounds section accesses before llext_load() validates the image.
1 open issue
3 resolved since last review
Review effort: Balanced
| if (ehdr->e_shentsize != sizeof(*shdr) || ehdr->e_shoff > len || | ||
| ehdr->e_shnum > (len - ehdr->e_shoff) / sizeof(*shdr) || | ||
| !IS_ALIGNED(elf + ehdr->e_shoff, sizeof(uint32_t))) { |
There was a problem hiding this comment.
Fixed in adbd339. The section header table must now start after the ELF header (e_shoff >= sizeof(*ehdr)), and e_shnum is read once into a local before the table is checked, so rewriting section addresses can't change the loop bounds.
lgirdwood
left a comment
There was a problem hiding this comment.
Needs more ztests and comments atm.
| } | ||
|
|
||
| /* move the last element over the released one, clear the last element */ | ||
| vpage_ctx.num_elems_in_use--; |
There was a problem hiding this comment.
wonder why this was not picked up earlier, can we add a ztest to the PR that we test/validate this part.
There was a problem hiding this comment.
Added in 84ae7ca: vpage_free_order frees the first of three allocations, allocates again, then checks that the third one's pages really are freed (with vpage_reserve_at()). The existing test freed in an order that never moved an allocation element, which is why this wasn't caught. I checked on PTL that the new test fails with the old free path ("pages at 0xa02d2000 not freed") and passes with the fix.
| void *vpage_reserve(unsigned int pages); | ||
|
|
||
| /** | ||
| * @brief Reserve contiguous virtual pages at a given address. | ||
| * | ||
| * Like vpage_reserve() but for a range at a fixed address, e.g. for users that | ||
| * had the range reserved before the DSP was powered down and are restored at | ||
| * the same address. | ||
| * | ||
| * @param[in] ptr Page aligned address of the first page to reserve. | ||
| * @param[in] pages Number of pages (typically 4KB per page) to reserve. | ||
| * @return 0 on success or a negative error code if any of the pages is already | ||
| * in use or outside of the virtual page region. | ||
| */ | ||
| int vpage_reserve_at(void *ptr, unsigned int pages); | ||
|
|
||
| /** | ||
| * @brief Release reserved virtual pages. | ||
| * | ||
| * Releases virtual memory pages, reserved with vpage_reserve() or | ||
| * vpage_reserve_at(). The caller has to unmap any pages it has mapped first. | ||
| * | ||
| * @param[in] ptr Pointer to the first reserved page. | ||
| */ | ||
| void vpage_release(void *ptr); |
There was a problem hiding this comment.
lets make sure we have ztests for these too.
There was a problem hiding this comment.
Added in 84ae7ca: vpage_reserve checks that reserved pages are in the region and not mapped, that the range or part of it can't be reserved again, and that it's free again after vpage_release(). vpage_reserve_at_invalid checks that ranges below, past the end of or crossing the end of the region, and unaligned ones, are rejected, and that 0 pages is a no-op. Both pass on PTL.
Note that boot tests currently don't run on PTL hardware: a CONFIG_ZTEST=y build of plain main hangs loading the LLEXT library; the suite runs inside the logging-enable IPC and exceeds the 500 ms IPC timeout; and test_sys_sem and virtual_memory_heap fail. CI only runs boot tests on qemu, where vpage isn't built. To run the new tests I used a test-only build (no libraries, those two tests left out) and read the log from the DSP memory window. I'll raise the harness problems separately.
| if (ehdr->e_shentsize != sizeof(*shdr) || ehdr->e_shoff > len || | ||
| ehdr->e_shnum > (len - ehdr->e_shoff) / sizeof(*shdr) || | ||
| !IS_ALIGNED(elf + ehdr->e_shoff, sizeof(uint32_t))) { | ||
| tr_err(&lib_manager_tr, "invalid section header table"); | ||
| return -ENOEXEC; | ||
| } | ||
|
|
||
| shdr = (elf_shdr_t *)(elf + ehdr->e_shoff); | ||
|
|
||
| for (i = 0; i < ehdr->e_shnum; i++) { | ||
| if (!(shdr[i].sh_flags & SHF_ALLOC) || !shdr[i].sh_size || | ||
| llext_manager_section_detached(shdr + i)) | ||
| continue; | ||
|
|
||
| if (shdr[i].sh_addr + shdr[i].sh_size < shdr[i].sh_addr || | ||
| !is_power_of_two(shdr[i].sh_addralign ? : 1)) { | ||
| tr_err(&lib_manager_tr, "invalid section %u", i); | ||
| return -ENOEXEC; | ||
| } | ||
|
|
||
| start = MIN(start, shdr[i].sh_addr); | ||
| end = MAX(end, shdr[i].sh_addr + shdr[i].sh_size); | ||
| align = MAX(align, shdr[i].sh_addralign); | ||
| } |
There was a problem hiding this comment.
can we have a inline comment for each block.
There was a problem hiding this comment.
Done in adbd339: each block in llext_manager_place() now has a comment.
num_elems_in_use is the number of allocation elements in use, so the last element in use is at index num_elems_in_use - 1. When freeing, vpages_free_and_unmap() copied velems[num_elems_in_use] over the freed element and then cleared it. That is one past the last element in use, and past the end of the array once all VPAGE_MAX_ALLOCS elements are in use. Any free other than of the most recent allocation therefore dropped the tracking of the last allocation, which could then never be freed. Decrement the counter first, then move the last element. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
The LLEXT library manager needs virtual address space for loadable modules, but it maps the module sections itself, with different permissions for code, read-only and writable data. Using the shared virtual page allocator for that avoids a second, private virtual address range for modules. Split allocation into reserving a range of virtual pages and mapping physical pages to it, and export: - vpage_reserve() to reserve virtual pages without mapping them, - vpage_reserve_at() to reserve a range at a given address, needed to restore modules after the DSP has been powered down, because the allocator starts out empty on every boot, - vpage_release() to release a reserved range, - vpage_get_region() to get the region to map reserved pages in. vpage_alloc() and vpage_free() keep working as before. Co-developed-by: Jyri Sarha <jyri.sarha@linux.intel.com> Signed-off-by: Jyri Sarha <jyri.sarha@linux.intel.com> Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The existing vpage boot test frees allocations in an order, that never moves an allocation element, so it didn't catch the out-of-bounds element access in vpage_free(). Add tests for: - freeing an allocation, that isn't the last one: the remaining allocations must still be tracked and freed. This fails without commit "zephyr: vpage: fix out-of-bounds element access on free". - vpage_reserve() and vpage_release(): reserved pages are within the region and aren't mapped, a reserved range or a part of it can't be reserved again, and it can be after it has been released. - vpage_reserve_at() with ranges outside of the region or unaligned. Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com>
ba12b0f to
adbd339
Compare
kv2019i
left a comment
There was a problem hiding this comment.
One concern inline, otherwise looking good.
| if (len < sizeof(*ehdr) || memcmp(ehdr->e_ident, elf_magic, sizeof(elf_magic))) | ||
| return -ENOEXEC; | ||
|
|
||
| /* without a virtual page region keep the addresses from the build */ |
There was a problem hiding this comment.
What if the virtual page region exists but has run out of space? Should we fallback and keep using build-time address in this case?
There was a problem hiding this comment.
There will be no build time addresses soon.
There was a problem hiding this comment.
Add a LOG_WRN here as in future we will not assign addresses during the build phase as all addresses will be assign on use.
There was a problem hiding this comment.
Updated in 43fbae6: both fallbacks to build-time addresses (not a relocatable module, or no virtual page region) now log a warning, as build-time addresses are going away. If the region exists but is full, placement still fails with -ENOMEM and an error, rather than falling back: once modules no longer have build-time addresses, there will be nothing to fall back to. Tested on PTL: shared object modules log the warning and play, relocatable ones don't log it.
adbd339 to
43fbae6
Compare
LLEXT modules are linked at build time to run at fixed addresses in a dedicated virtual memory region. Each module gets the addresses after the previous one, so all modules have to be built together, and the region has to be large enough for all modules even if only a few of them are used. Relocatable (ET_REL) modules don't need fixed addresses: they still carry all their relocations, so the build-time addresses are only placeholders. When such a module is linked for the first time, reserve virtual pages for it from the shared virtual page allocator and move all its sections, that are copied to SRAM, by the same offset into that range before calling llext_load(). The offset is a multiple of the largest section alignment, at least a page, so moving by it keeps the relative placement and alignment of all sections. References, that the linker has already resolved, stay valid, and LLEXT relocates the rest. Detached sections stay in DRAM and aren't moved. The ELF header and section header table are checked before use, as llext_load() only validates them after the sections have been moved. As before, a module is linked only once. The addresses stay assigned to the module until the firmware is rebooted, so re-instantiating a module only maps and copies it again. After the DSP has been powered down, the modules are restored from DRAM with the addresses they were linked for, so reserve those pages again, because the allocator starts out empty on every boot. Without that the allocator hands them out again, e.g. for a DP module heap. llext_load() returns the use count of an extension, if it has already been loaded, so only release the pages on a negative return value. Shared object (ET_DYN) modules can only run at their build-time addresses, they keep using the dedicated region, and so do relocatable modules if there is no virtual page region. Co-developed-by: Jyri Sarha <jyri.sarha@linux.intel.com> Signed-off-by: Jyri Sarha <jyri.sarha@linux.intel.com> Signed-off-by: Liam Girdwood <liam.r.girdwood@linux.intel.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>


Pending Zephyr updates now and taking over from @jsarha
LLEXT modules are linked at build time for fixed addresses, one after the other, in a dedicated virtual memory region. Relocatable (ET_REL) modules don't actually need that: they still carry all their relocations, so the build-time addresses are only placeholders. With this series the library manager reserves virtual pages for each relocatable module from the shared virtual page allocator (
zephyr/lib/vpage.c, also used by vregions) when the module is first linked. It then moves all of the module's SRAM sections into that range by one page-aligned offset before callingllext_load().This replaces #11037 and Jyri's follow-up #11219, which can't be completed now. It is a rework of #11037 that includes Jyri's fixes. Changes from #11037:
R_XTENSA_RELATIVEin the pre-located case. They keep their build-time addresses in the dedicated region, as before. Relocatable modules also fall back to their build-time addresses if there is no vpage region.vpage_reserve_at()(from Jyri's Llext relocatable fixes #11219). Without that the allocator, which starts out empty on every boot, would hand those pages to a DP heap or vregion.llext_load()returns a positive use count for an already loaded extension, so pages are only released on a negative return (Jyri's review comment).@lyakh, on the
vpage.cquestion:CHECKIF()already is "assert, check or nothing" depending onCONFIG_ASSERT_ON_ERRORS/CONFIG_NO_RUNTIME_CHECKS.Tested on PTL (UPX-PTL01), Zephyr SDK GCC,
sof-hda-generic-4ch.tplg, 7 LLEXT modules (PEAKVOL, MIXIN, EQIIR, EQFIR, DRC, IIR, FIR):app/llext_relocatable.conf(ET_REL modules): the same test, 20 iterations with D3 between each. All modules are placed in vpage memory, interleaved with vregion allocations. After each D3 the firmware restores the libraries itself (no library reload by the kernel), and the restored modules' 22 pages are still reserved before the first new allocation.Related Zephyr PR: zephyrproject-rtos/zephyr#119860 makes
sys_mm_drv_map_page()refuse to map over a live TLB entry. It isn't needed for this series to work, but it turns the kind of overlap fixed here into an error instead of silent corruption.🤖 Generated with Claude Code