Skip to content

llext: place relocatable LLEXT modules in virtual pages - #11388

Open
lgirdwood wants to merge 4 commits into
thesofproject:mainfrom
lgirdwood:llext/relocatable-vpage
Open

lgirdwood wants to merge 4 commits into
thesofproject:mainfrom
lgirdwood:llext/relocatable-vpage

Conversation

@lgirdwood

@lgirdwood lgirdwood commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

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 calling llext_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:

  • No re-layout and no build changes. The previous version dropped the fixed addresses from the link helper and laid sections out again at load time. That broke references the linker had already resolved between sections, and it lost the cold/detached sections. Moving all SRAM sections by a constant offset keeps their relative placement and alignment. Zephyr's region bookkeeping stays valid, and detached sections stay in DRAM unchanged. That also made Jyri's region-extent and .bss mapping fixes unnecessary, since they fixed side effects of the re-layout.
  • Only ET_REL modules are moved. Shared object (ET_DYN) modules, which are the default for Zephyr SDK GCC builds, can't be moved: Zephyr skips R_XTENSA_RELATIVE in 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.
  • Link once, as before (@lyakh). A module is linked only on first use, and its addresses stay assigned until reboot. Re-instantiation only maps and copies it again. After D3 the modules are restored from DRAM at the same addresses, and their pages are reserved again with the new 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).
  • The off-by-one in the vpage free path that Copilot found is a separate fix, because it is also on main.
  • SLID symbol linking is left out of this PR, as it isn't related; it will be a separate PR.

@lyakh, on the vpage.c question: CHECKIF() already is "assert, check or nothing" depending on CONFIG_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):

  • default build (ET_DYN modules): playback, capture, and concurrent playback + deep-buffer playback + capture, with DSP D3 between iterations; no errors. Behaviour is unchanged.
  • with 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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread src/library_manager/llext_manager.c Resolved
Comment thread src/library_manager/llext_manager.c Resolved
Comment thread src/library_manager/llext_manager.c Resolved
@lgirdwood lgirdwood added the DNM Do Not Merge tag label Nov 2, 2026
@intel-sofci

intel-sofci commented Nov 2, 2026 •

Copy link
Copy Markdown

PR 11388: test results

Run date: 2026-11-04 16:41 UTC

Tested commit: 43fbae6236e7199c9ddb013c854687e9d551ecc3

mtl pass rate lnl pass rate ptl pass rate wcl pass rate nvl pass rate

@lgirdwood
lgirdwood force-pushed the llext/relocatable-vpage branch from 45117b2 to ba12b0f Compare November 2, 2026 15:33
@lgirdwood

Copy link
Copy Markdown
Member Author

Squashed the review fixes into "library_manager: llext: place relocatable modules in virtual pages" (now ba12b0f), so the 45117b2 in my replies above is that commit. No code changes beyond the squash.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread src/library_manager/llext_manager.c Outdated
Comment on lines +477 to +479
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))) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 lgirdwood left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs more ztests and comments atm.

Comment thread zephyr/lib/vpage.c Outdated
}

/* move the last element over the released one, clear the last element */
vpage_ctx.num_elems_in_use--;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wonder why this was not picked up earlier, can we add a ztest to the PR that we test/validate this part.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +55 to +79
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);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets make sure we have ztests for these too.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/library_manager/llext_manager.c Outdated
Comment on lines +477 to +500
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);
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have a inline comment for each block.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in adbd339: each block in llext_manager_place() now has a comment.

lrgirdwo and others added 3 commits November 4, 2026 13:02
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>
@lgirdwood
lgirdwood force-pushed the llext/relocatable-vpage branch from ba12b0f to adbd339 Compare November 4, 2026 13:30
kv2019i
kv2019i previously requested changes Nov 4, 2026

@kv2019i kv2019i left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One concern inline, otherwise looking good.

Comment thread src/library_manager/llext_manager.c Outdated
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 */

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There will be no build time addresses soon.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@lgirdwood
lgirdwood force-pushed the llext/relocatable-vpage branch from adbd339 to 43fbae6 Compare November 4, 2026 16:16
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>
@kv2019i
kv2019i dismissed their stale review November 5, 2026 14:17

My concern addressed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DNM Do Not Merge tag

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants