Zephyr Loadable Extensions: Define a Symbol ABI and Safe Unload Boundary
Loading an ELF extension successfully establishes neither function-type compatibility nor permission to free its code while callbacks still reference it. Treat Zephyr LLEXT integration as two contracts: a versioned binary interface at load time, and a quiescence protocol before final unload. A resolved symbol address is only useful when both contracts hold.
This guide pins upstream Zephyr 3.7.0 LLEXT. The release overview limits architecture support to ARM and Xtensa. The concrete design scope here is a trusted ARM extension built for the same application configuration, using the relocatable-object route rather than an ARM shared library. No extension was compiled, loaded or executed on target hardware for this article.
Freeze the build identity before the function signature
Record the base firmware build identifier, Zephyr revision, compiler version, CPU and ABI flags, generated headers, ELF type and relevant LLEXT configuration. The tagged build guide supports building extensions in the application’s CMake build or exporting matching headers and flags through the Extension Development Kit. Reusing an EDK from another firmware configuration is a compatibility risk even when the public function name is unchanged.
The release configuration guide states that shared-library loading is not supported on ARM in this version. It also requires matching symbol-link-identifier settings between base and extension, and identifies that setting as incompatible with the EDK route in 3.7.0. Pin the selected route rather than mixing instructions from newer documentation.

Publish a small ABI instead of exposing internals
Define a narrow interface with explicit integer widths, function prototypes, ownership rules and an application ABI version. Specify who allocates and frees each buffer, whether callbacks are synchronous, and whether pointers remain valid after a call. Avoid exposing private kernel or application structure layouts as a convenient shortcut; those layouts can change with configuration and compiler decisions.
A useful ABI descriptor can identify the interface version, descriptor size, supported operations and required capabilities. Validate it before registering any extension callback. This descriptor is an application design recommendation, not an automatic compatibility feature supplied by LLEXT. A matching symbol string does not check a C function prototype or the interpretation of a structure field.
The loading guide describes resolving exported symbols. Its no-argument convenience call is not a general typed-call adapter. The tagged implementation calls that helper’s resolved function as void (*)(void). Use an explicitly agreed interface for functions with arguments or results, rather than casting incompatible prototypes until compilation succeeds.
Keep three lifetime counts distinct
| Lifetime | What it represents | Who must account for it |
|---|---|---|
| Loader use count | References maintained by repeated loads and unloads | LLEXT plus balanced application ownership |
| Active execution | Calls or threads currently executing extension code | Application integration protocol |
| Deferred references | Registered callbacks, queued work and stored data/function pointers | Every owner that can use them later |
In this release, loading an already named extension increments its use count. Unload decrements it; a positive return indicates remaining loader references, while the final release frees regions and metadata and nulls the supplied extension pointer. Do not mistake the count for automatic tracking of every running function or callback. The convenience call does not acquire a new loader reference around each invocation.
After final unload, previously resolved addresses into the extension are invalid. Clearing one manager pointer does not clear copies stored in an interrupt table, work item or another component. Also, name lookup releases the internal list lock before returning the extension, so the source explicitly leaves lookup-versus-free race prevention to callers. Serialize management operations through a defined application owner.
Use an unload gate that can be tested
- Close admission of new calls and callback registrations for this extension generation.
- Unregister external callbacks and stop sources that can enqueue new extension work.
- Wait for active calls and already queued work to finish through the relevant subsystem’s completion mechanism.
- Run the agreed extension shutdown operation while its code and data remain valid; arrange its dependencies so it cannot wait on the manager indefinitely.
- Release application-owned external resources and invalidate stored symbol references according to the ABI contract.
- Balance loader references and perform final unload only after the earlier obligations are satisfied.
If writable-storage optimization allows sections to refer directly into the ELF input buffer, that buffer must remain allocated until unload. The linker can modify it in place, so the configuration guide also requires reloading its contents before using it for another load. An input-buffer lifetime bug can otherwise resemble a bad relocation.
A hypothetical replacement qualification
Suppose a measurement extension registers a periodic callback and exports a conversion function. Test an ABI-version mismatch before activation, a missing symbol, an unsupported ELF feature and insufficient load memory. A failed candidate must never become the active callback provider. These are proposed target tests, not reported results.
For unload, hold one conversion call at a controlled test gate and queue one callback. Request replacement and verify that final unload remains blocked by the application protocol until both complete. Then verify that no stale generation can enter through a saved dispatch reference. Test repeated loads separately to confirm loader-reference balancing, without assuming that reference balancing proves execution quiescence.
Keep extension authenticity and trust policy separate from ELF parsing. Structural loading and relocation are not a claim of cryptographic approval or automatic sandboxing. Archive the base build, extension artifact, interface manifest and unload trace together. The useful release criterion is a compatible callable interface whose last possible user is known before its memory disappears.