From 0578c1b048d818f736c6d9ee07a8e1735415fd79 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 3 Aug 2026 00:49:15 +0200 Subject: [PATCH 1/8] libs/libc/elf: Translate link-time addresses through one place. The ET_DYN path computes run-time addresses from link-time ones in five places, each open-coding the arithmetic, and two of them disagree about how: libelf_relocatedyn() adds textalloc to a relocation's r_offset in one branch and subtracts datasec before adding datastart in the next, while the value translation a few lines further down picks between those two forms with an explicit test on datasec. Collect that into libelf_addr(), which makes the test once: an address below the data segment's link-time base belongs to text, anything at or above it to data. This changes nothing today. libelf_elfsize() sets segpad = datasec - (text_vaddr + textsize) and libelf_load() then places datastart = textalloc + textsize + segpad so datastart - datasec is textalloc, and the data branch reduces to textalloc + vaddr -- exactly what the text branch returns, and exactly what adding a single load bias did before. The two forms are the same arithmetic written twice. They stop being the same once text and data are placed independently, which is what an FDPIC object requires: its two PT_LOAD segments are relocated separately so that the read-only one can be mapped in place on the media while only the writable one is copied. Having the translation in one function is what makes that possible without auditing every open-coded expression again. Built for mps3-an547:picostest, which is CONFIG_ELF with CONFIG_PIC, and boots identically to the same configuration without this change. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Marco Casaroli --- libs/libc/elf/elf.h | 28 ++++++++++++++++++++++++++++ libs/libc/elf/elf_bind.c | 31 ++++++++++--------------------- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/libs/libc/elf/elf.h b/libs/libc/elf/elf.h index 986a3fa5803a9..7d5a7bcd67184 100644 --- a/libs/libc/elf/elf.h +++ b/libs/libc/elf/elf.h @@ -238,6 +238,34 @@ int libelf_reallocbuffer(FAR struct mod_loadinfo_s *loadinfo, int libelf_freebuffers(FAR struct mod_loadinfo_s *loadinfo); +/**************************************************************************** + * Name: libelf_addr + * + * Description: + * Translate a link-time address in a loaded object to the address it + * occupies now. An address below the data segment's link-time base + * belongs to text, anything at or above it to data. + * + * Input Parameters: + * loadinfo - Load state information + * vaddr - The link-time address to translate + * + * Returned Value: + * The run-time address. + * + ****************************************************************************/ + +static inline uintptr_t libelf_addr(FAR struct mod_loadinfo_s *loadinfo, + uintptr_t vaddr) +{ + if (loadinfo->datasec != 0 && vaddr >= loadinfo->datasec) + { + return loadinfo->datastart + (vaddr - loadinfo->datasec); + } + + return loadinfo->textalloc + vaddr; +} + #ifdef CONFIG_ARCH_ADDRENV /**************************************************************************** diff --git a/libs/libc/elf/elf_bind.c b/libs/libc/elf/elf_bind.c index 34f3fddf79d14..c712ac6f5f0e2 100644 --- a/libs/libc/elf/elf_bind.c +++ b/libs/libc/elf/elf_bind.c @@ -831,7 +831,7 @@ static int libelf_relocatedyn(FAR struct module_s *modp, return ret; } - addr = rel->r_offset + loadinfo->textalloc; + addr = libelf_addr(loadinfo, rel->r_offset); if (reldata.relrela[idx_rel] == 1) { @@ -848,23 +848,15 @@ static int libelf_relocatedyn(FAR struct module_s *modp, 0 }; - addr = rel->r_offset - loadinfo->datasec + loadinfo->datastart; + addr = libelf_addr(loadinfo, rel->r_offset); if (reldata.relrela[idx_rel] == 1) { addr += rela->r_addend; } - if ((*(FAR uint32_t *)addr) < loadinfo->datasec) - { - dynsym.st_value = *(FAR uint32_t *)addr + - loadinfo->textalloc; - } - else - { - dynsym.st_value = *(FAR uint32_t *)addr - - loadinfo->datasec + loadinfo->datastart; - } + dynsym.st_value = libelf_addr(loadinfo, + *(FAR uint32_t *)addr); ret = up_relocate(rel, &dynsym, addr, ARCH_ELFDATA_PARM); } @@ -968,23 +960,20 @@ int libelf_bind(FAR struct module_s *modp, loadinfo->dsymtabidx = i; break; case SHT_INIT_ARRAY: - loadinfo->initarr = loadinfo->shdr[i].sh_addr - - loadinfo->datasec + - loadinfo->datastart; + loadinfo->initarr = libelf_addr(loadinfo, + loadinfo->shdr[i].sh_addr); loadinfo->ninit = loadinfo->shdr[i].sh_size / sizeof(uintptr_t); break; case SHT_FINI_ARRAY: - loadinfo->finiarr = loadinfo->shdr[i].sh_addr - - loadinfo->datasec + - loadinfo->datastart; + loadinfo->finiarr = libelf_addr(loadinfo, + loadinfo->shdr[i].sh_addr); loadinfo->nfini = loadinfo->shdr[i].sh_size / sizeof(uintptr_t); break; case SHT_PREINIT_ARRAY: - loadinfo->preiarr = loadinfo->shdr[i].sh_addr - - loadinfo->datasec + - loadinfo->datastart; + loadinfo->preiarr = libelf_addr(loadinfo, + loadinfo->shdr[i].sh_addr); loadinfo->nprei = loadinfo->shdr[i].sh_size / sizeof(uintptr_t); break; From 35be954821c6842e393c18116606ccaa2d7ad8c2 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Wed, 26 Aug 2026 18:17:28 +0200 Subject: [PATCH 2/8] libs/libc/elf: Fix the nxstyle errors in the lines this touches. CI feeds nxstyle the diff hunks with three lines of context, so style errors that are older than this change, in the lines around the hunks, fail the check job. They are a switch body indented two columns too deep, an initializer brace one level in, and two declarations with no blank line after them. Whitespace only, no change in behaviour. Signed-off-by: Marco Casaroli --- libs/libc/elf/elf_bind.c | 56 +++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/libs/libc/elf/elf_bind.c b/libs/libc/elf/elf_bind.c index c712ac6f5f0e2..dddb626add817 100644 --- a/libs/libc/elf/elf_bind.c +++ b/libs/libc/elf/elf_bind.c @@ -815,38 +815,38 @@ static int libelf_relocatedyn(FAR struct module_s *modp, if (sym[idx_sym].st_shndx == SHN_UNDEF) { - FAR void *ep; - - ep = libelf_findglobal(modp, loadinfo, symhdr, - &sym[idx_sym]); - if ((ep == NULL) && (ELF_ST_BIND(sym[idx_sym].st_info) - != STB_WEAK)) - { - berr("ERROR: Unable to resolve addr of ext ref %s\n", - loadinfo->iobuffer); - ret = -EINVAL; - lib_free(sym); - lib_free(rels); - lib_free(dyn); - return ret; - } - - addr = libelf_addr(loadinfo, rel->r_offset); - - if (reldata.relrela[idx_rel] == 1) - { - addr += rela->r_addend; - } - - *(FAR uintptr_t *)addr = (uintptr_t)ep; + FAR void *ep; + + ep = libelf_findglobal(modp, loadinfo, symhdr, + &sym[idx_sym]); + if ((ep == NULL) && (ELF_ST_BIND(sym[idx_sym].st_info) + != STB_WEAK)) + { + berr("ERROR: Unable to resolve addr of ext ref %s\n", + loadinfo->iobuffer); + ret = -EINVAL; + lib_free(sym); + lib_free(rels); + lib_free(dyn); + return ret; + } + + addr = libelf_addr(loadinfo, rel->r_offset); + + if (reldata.relrela[idx_rel] == 1) + { + addr += rela->r_addend; + } + + *(FAR uintptr_t *)addr = (uintptr_t)ep; } } else { Elf_Sym dynsym = - { - 0 - }; + { + 0 + }; addr = libelf_addr(loadinfo, rel->r_offset); @@ -943,6 +943,7 @@ int libelf_bind(FAR struct module_s *modp, /* Get the index to the relocation section */ int infosec = loadinfo->shdr[i].sh_info; + if (infosec >= loadinfo->ehdr.e_shnum) { continue; @@ -1074,6 +1075,7 @@ int libelf_bind(FAR struct module_s *modp, if (loadinfo->addrenv != NULL) { int status = libelf_addrenv_restore(loadinfo); + if (status < 0) { berr("ERROR: libelf_addrenv_restore() failed: %d\n", status); From 892f8e99ca66dddaccb33e5082daeff2feba633b Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Sun, 23 Aug 2026 23:31:37 +0200 Subject: [PATCH 3/8] binfmt, arch/arm: Add the CONFIG_FDPIC option and the ABI header. The commits that follow teach the ELF loader to load an FDPIC object. This puts the option they hang off and the definitions they share in one place first, so each of them builds on its own. CONFIG_FDPIC depends on ARCH_HAVE_ELF_FDPIC, which an architecture selects when it has a PIC base register and the FDPIC relocations. Only armv7-m and armv8-m select it today, and it defaults off, so nothing changes for anyone who does not ask for it. include/nuttx/fdpic.h holds what both sides of the loader need: the two word function descriptor an FDPIC module passes instead of a code address, the test for whether the caller is such a module, and the call sequence that enters one with its own data base. All of it is behind CONFIG_FDPIC, thus the header is empty without it and a file may include it unconditionally. The call sequence itself is architecture specific, so arch/arm/include/arch.h supplies it as up_fdpic_invoke(), beside the other PIC base register macros. up_setpicbase() cannot serve here: the register has to hold the module's base for exactly one call and then go back, and nothing in C tells the compiler the register is live across that call, so the save, the install, the branch and the restore have to be one sequence. Built for mps3-an547:bl and mps3-an547:picostest, with CONFIG_FDPIC off, which is every configuration in the tree. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Marco Casaroli --- arch/Kconfig | 7 +++ arch/arm/Kconfig | 2 + arch/arm/include/arch.h | 39 ++++++++++++ binfmt/Kconfig | 32 ++++++++++ include/nuttx/fdpic.h | 129 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 209 insertions(+) create mode 100644 include/nuttx/fdpic.h diff --git a/arch/Kconfig b/arch/Kconfig index 95a74a3f63bbc..1590575654446 100644 --- a/arch/Kconfig +++ b/arch/Kconfig @@ -724,6 +724,13 @@ config ARCH_HAVE_ELF_EXECUTABLE bool default n +config ARCH_HAVE_ELF_FDPIC + bool + default n + ---help--- + The architecture has a PIC base register and the ELF relocations + that an FDPIC object uses. + config ARCH_HAVE_TRUSTZONE bool default n diff --git a/arch/arm/Kconfig b/arch/arm/Kconfig index 5721c73b3c7bf..bcd25f223c028 100644 --- a/arch/arm/Kconfig +++ b/arch/arm/Kconfig @@ -1096,6 +1096,7 @@ config ARCH_ARMV7M default n select ARCH_HAVE_CPUINFO select ARCH_HAVE_DEBUG + select ARCH_HAVE_ELF_FDPIC select ARCH_HAVE_PERF_EVENTS config ARCH_CORTEXM3 @@ -1245,6 +1246,7 @@ config ARCH_ARMV8M default n select ARCH_HAVE_CPUINFO select ARCH_HAVE_DEBUG + select ARCH_HAVE_ELF_FDPIC select ARCH_HAVE_PERF_EVENTS config ARCH_CORTEXM23 diff --git a/arch/arm/include/arch.h b/arch/arm/include/arch.h index 786f7e6cbd2ca..457057b489b4a 100644 --- a/arch/arm/include/arch.h +++ b/arch/arm/include/arch.h @@ -79,6 +79,45 @@ do { \ ); \ } while (0) +#ifdef CONFIG_FDPIC + +/**************************************************************************** + * Name: up_fdpic_invoke + * + * Description: + * Call a module entry point with the module data base in the PIC base + * register. Put the caller data base back after the call. + * + * Input Parameters: + * entry - The code address to enter. + * arg - The one word argument, passed in r0. + * got - The module data base to install. + * + ****************************************************************************/ + +static inline void up_fdpic_invoke(uintptr_t entry, uintptr_t arg, + uintptr_t got) +{ + register uintptr_t r0v __asm__ ("r0") = arg; + + /* arg is already in r0. r4 goes on the stack with the PIC register to + * keep the push aligned to 8 bytes. + */ + + __asm__ __volatile__ + ( + "push {r4, " PIC_REG_STRING "}\n" /* Save the caller's base */ + "mov " PIC_REG_STRING ", %[got]\n" /* Install the module's base */ + "blx %[entry]\n" /* Enter the module */ + "pop {r4, " PIC_REG_STRING "}\n" /* Restore the caller's base */ + : "+r" (r0v) + : [entry] "r" (entry), [got] "r" (got) + : "r1", "r2", "r3", "r12", "lr", "cc", "memory" + ); +} + +#endif /* CONFIG_FDPIC */ + #endif /* CONFIG_PIC */ #ifdef CONFIG_ARCH_ADDRENV diff --git a/binfmt/Kconfig b/binfmt/Kconfig index 93844898da0d4..eb3e23bad3cf5 100644 --- a/binfmt/Kconfig +++ b/binfmt/Kconfig @@ -60,6 +60,38 @@ config ELF_STACKSIZE default DEFAULT_TASK_STACKSIZE ---help--- This is the default stack size that will be used when starting ELF binaries. + +config FDPIC + bool "FDPIC modules" + default n + select PIC + depends on ARCH_HAVE_ELF_FDPIC + ---help--- + Load ELF modules built for the FDPIC ABI. + + An FDPIC module places its read-only and writable segments + independently, so its text can be executed directly out of flash + while only the writable segment is copied to RAM, once per running + instance. A filesystem that can show its media, such as XIPFS or + ROMFS, gives that result. On any other filesystem the loader copies + the text to RAM, and the module runs but shares nothing. + + Building a module needs an arm-uclinuxfdpiceabi linker. The stock + arm-none-eabi compiler emits correct FDPIC objects for both C and + C++, so only the link needs it. + + What this adds over the position independent ELF support already + present is a function pointer that carries its own data base, as a + two word descriptor rather than a bare code address. That is what + lets a module be called back on a thread it did not create, such as + the work queue worker that runs a SIGEV_THREAD notification. + + Selecting this makes ten libc and sched entry points that can + accept a callback from a module resolve such a descriptor before + storing or branching to it. Each costs a register read and a + branch on a path that is not hot. + + FDPIC is specified only for ARM Thumb-2. endif endif diff --git a/include/nuttx/fdpic.h b/include/nuttx/fdpic.h new file mode 100644 index 0000000000000..afd0ebd2d6975 --- /dev/null +++ b/include/nuttx/fdpic.h @@ -0,0 +1,129 @@ +/**************************************************************************** + * include/nuttx/fdpic.h + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +#ifndef __INCLUDE_NUTTX_FDPIC_H +#define __INCLUDE_NUTTX_FDPIC_H + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include + +#include +#include + +/**************************************************************************** + * Public Types + ****************************************************************************/ + +/* A function descriptor: what a function pointer is under FDPIC. The + * firmware branches to a code address; a module passes one of these. + */ + +struct fdpic_desc_s +{ + uintptr_t entry; /* Address of the code */ + uintptr_t got; /* Data base to install before branching */ +}; + +/**************************************************************************** + * Inline Functions + ****************************************************************************/ + +#ifdef CONFIG_FDPIC + +/**************************************************************************** + * Name: fdpic_base + * + * Description: + * The data base of the calling context, from the PIC base register. + * Non-zero means the caller is an FDPIC module, zero means firmware. + * + ****************************************************************************/ + +static inline uintptr_t fdpic_base(void) +{ + uintptr_t base; + + up_getpicbase(&base); + return base; +} + +/**************************************************************************** + * Name: fdpic_callback + * + * Description: + * Resolve a function pointer from a caller that may be an FDPIC module. + * Only the entry point is taken: the data base is already in the register. + * + * Input Parameters: + * fn - The pointer as it was received. + * + * Returned Value: + * An address that can be branched to directly. + * + ****************************************************************************/ + +static inline FAR void *fdpic_callback(FAR void *fn) +{ + if (fn != NULL && fdpic_base() != 0) + { + return (FAR void *)((FAR struct fdpic_desc_s *)fn)->entry; + } + + return fn; +} + +/**************************************************************************** + * Name: fdpic_invoke + * + * Description: + * Call a resolved module entry point with the module data base in the PIC + * base register. For a callback that runs on a shared thread, which + * carries no module base. Elsewhere fdpic_callback() is enough. + * + * Input Parameters: + * entry - The code address to enter, already resolved from the descriptor. + * arg - The one word argument. + * got - The module data base to install. + * + ****************************************************************************/ + +static inline void fdpic_invoke(uintptr_t entry, uintptr_t arg, + uintptr_t got) +{ + up_fdpic_invoke(entry, arg, got); +} + +#else + +# define fdpic_base() (0) +# define fdpic_callback(fn) (fn) +# define fdpic_invoke(entry, arg, got) \ + ((void)(got), (((CODE void (*)(uintptr_t))(uintptr_t)(entry))(arg))) + +#endif /* CONFIG_FDPIC */ + +#endif /* __INCLUDE_NUTTX_FDPIC_H */ From 0d92125443cdf2858165ceeeaf071b94ec56fe57 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 3 Aug 2026 00:56:27 +0200 Subject: [PATCH 4/8] libs/libc/elf: Place an FDPIC object's segments independently. An ET_DYN object is loaded into one allocation with its data behind its text, because its data references sit at a fixed distance from the code that makes them. An FDPIC object does not work that way: it reaches its data through a base register, so the two segments can be placed wherever suits, and the point of the format is that the read-only one is left on the media and executed there while only the writable one is copied. One copy of the text then serves every instance. So libelf_load() grows a second case. The object announces itself in the OS/ABI byte, which is noted once in libelf_loadhdrs() rather than re-derived; e_flags cannot be used for this, as an FDPIC object's are an unremarkable EABI version and testing them would reject every valid module. Text is taken from the media address plus the segment's own file offset -- the same arithmetic the ET_REL path already does with sh_offset -- and libelf_loadfile() does not read it. If the filesystem cannot show its media, the loader copies the text to RAM instead. The module then loses the shared text and the flash saving, but it runs. Obtaining that address needs two mechanisms, and they are not interchangeable. A compacting filesystem can move a file's blocks, so it hands out an address only with a pin that holds them still and expects the pin back; xipfs is the one in tree. A filesystem whose layout never changes has nothing to hold and answers FIOC_XIPBASE with a bare address; romfs and tmpfs are those. libelf_xipacquire() asks for the pin first, because a filesystem that needs one is not safe without it, and libelf_unload() gives it back. The loader asks for a pin only if it can hold one, or the pin would stay for ever. The pin is thus not specific to FDPIC. Any module that executes in place from a compacting filesystem takes one, and gives it back at unload. mmap() is not used, though both filesystems implement it. The mapping would be recorded against whichever task called the loader, while the release happens when the module's own task exits, which is a different group -- so the pin would outlive the module and the extent would never become movable again. Unloading has to change with placement: the existing path frees only textalloc because ET_DYN had a single allocation, which would leak an FDPIC object's data and free media the filesystem only lent us. Nothing here runs for a non-FDPIC object; every branch is behind the flag and the single-allocation path is untouched. Built and booted mps3-an547:picostest, which is CONFIG_ELF with CONFIG_PIC, with no change in behaviour. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Marco Casaroli --- arch/arm/include/elf.h | 12 ++ include/elf.h | 1 + include/nuttx/lib/elf.h | 26 ++++ libs/libc/elf/elf.h | 22 ++++ libs/libc/elf/elf_load.c | 224 +++++++++++++++++++++++++++++++---- libs/libc/elf/elf_loadhdrs.c | 15 +++ libs/libc/elf/elf_unload.c | 42 ++++++- 7 files changed, 316 insertions(+), 26 deletions(-) diff --git a/arch/arm/include/elf.h b/arch/arm/include/elf.h index e5c2780472c18..29e72a1e8f946 100644 --- a/arch/arm/include/elf.h +++ b/arch/arm/include/elf.h @@ -206,6 +206,18 @@ #define R_ARM_THM_TLS_DESCSEQ16 129 /* Thumb16 */ #define R_ARM_THM_TLS_DESCSEQ32 130 /* Thumb32 */ +/* FDPIC relocations. Values from the ARM FDPIC ABI as implemented by + * binutils (include/elf/arm.h). + */ + +#define R_ARM_GOTFUNCDESC 161 /* Data GOT entry holding a descriptor */ +#define R_ARM_GOTOFFFUNCDESC 162 /* Data GOT-relative descriptor */ +#define R_ARM_FUNCDESC 163 /* Data Address of a descriptor */ +#define R_ARM_FUNCDESC_VALUE 164 /* Data The descriptor itself: {code, GOT} */ +#define R_ARM_TLS_GD32_FDPIC 165 /* Data */ +#define R_ARM_TLS_LDM32_FDPIC 166 /* Data */ +#define R_ARM_TLS_IE32_FDPIC 167 /* Data */ + /* Processor specific values for the Phdr p_type field. */ #define PT_ARM_EXIDX (PT_LOPROC + 1) /* ARM unwind segment. */ diff --git a/include/elf.h b/include/elf.h index a3d6dc8f927fa..f940dbabb675d 100644 --- a/include/elf.h +++ b/include/elf.h @@ -149,6 +149,7 @@ #define ELFOSABI_MODESTO 11 /* Novell Modesto. */ #define ELFOSABI_OPENBSD 12 /* OpenBSD. */ #define ELFOSABI_ARM_AEABI 64 /* ARM EABI */ +#define ELFOSABI_ARM_FDPIC 65 /* ARM FDPIC */ #define ELFOSABI_ARM 97 /* ARM */ #define ELFOSABI_STANDALONE 255 /* Standalone (embedded) application */ diff --git a/include/nuttx/lib/elf.h b/include/nuttx/lib/elf.h index bcb8a039c8090..4a9bb730bf5fa 100644 --- a/include/nuttx/lib/elf.h +++ b/include/nuttx/lib/elf.h @@ -44,6 +44,15 @@ # define CONFIG_LIBC_ELF_MAXDEPEND 0 #endif +/* A compacting filesystem gives its media address with a pin that holds the + * blocks in place. The loader holds the pin through a file reference, + * because the unload runs on another task. Flat build only. + */ + +#if defined(CONFIG_FS_XIPFS) && defined(CONFIG_BUILD_FLAT) +# define HAVE_LIBC_ELF_PIN 1 +#endif + #ifndef CONFIG_LIBC_ELF_ALIGN_LOG2 # define CONFIG_LIBC_ELF_ALIGN_LOG2 2 #endif @@ -123,6 +132,7 @@ typedef CODE int (*mod_uninitializer_t)(FAR void *arg); * nexports - The number of symbols in the exported symbol table. */ +struct file; struct symtab_s; struct mod_info_s { @@ -252,6 +262,22 @@ struct mod_loadinfo_s * skip the copy. */ + /* FDPIC state. + * + * fdpic - True if e_ident[EI_OSABI] marked this an FDPIC object. + * textpin - True if a filesystem pin holds the read-only segment, which + * the loader gives back at unload. + */ + + bool fdpic; + bool textpin; + +#ifdef HAVE_LIBC_ELF_PIN + /* The file the pin is held through, handed to the module once it loads. */ + + FAR struct file *pinfile; +#endif + /* Address environment. * * addrenv - This is the handle created by addrenv_allocate() that can be diff --git a/libs/libc/elf/elf.h b/libs/libc/elf/elf.h index 7d5a7bcd67184..fcf3227cf112a 100644 --- a/libs/libc/elf/elf.h +++ b/libs/libc/elf/elf.h @@ -349,4 +349,26 @@ int libelf_addrenv_restore(FAR struct mod_loadinfo_s *loadinfo); void libelf_addrenv_free(FAR struct mod_loadinfo_s *loadinfo); #endif /* CONFIG_ARCH_ADDRENV */ + +#ifdef HAVE_LIBC_ELF_PIN + +/**************************************************************************** + * Name: libelf_pinrelease + * + * Description: + * Give back an XIP pin that the loader took, and the file that holds it. + * Does nothing if the loader took no pin. + * + * Input Parameters: + * pinfile - The held file. Cleared on return. + * + * Returned Value: + * None. + * + ****************************************************************************/ + +void libelf_pinrelease(FAR struct file **pinfile); + +#endif + #endif /* __LIBS_LIBC_LIBC_ELF_LIBC_ELF_H */ diff --git a/libs/libc/elf/elf_load.c b/libs/libc/elf/elf_load.c index e01e2fd748982..a7156e7ab6845 100644 --- a/libs/libc/elf/elf_load.c +++ b/libs/libc/elf/elf_load.c @@ -40,6 +40,7 @@ #include #include +#include #include #include "libc.h" @@ -364,6 +365,13 @@ static inline int libelf_loadfile(FAR struct mod_loadinfo_s *loadinfo) { if (phdr->p_flags & PF_X) { + if (loadinfo->fdpic && loadinfo->xipbase != 0) + { + /* Mapped, not copied. */ + + continue; + } + ret = libelf_read(loadinfo, buffer_data_address(text), phdr->p_filesz, phdr->p_offset); @@ -539,6 +547,106 @@ static inline int libelf_loadfile(FAR struct mod_loadinfo_s *loadinfo) return OK; } +/**************************************************************************** + * Name: libelf_xipacquire + * + * Description: + * Ask the filesystem for the address of this file on its media, so the + * read-only part of the object can run where it lies. Ask for a pin + * first: a compacting filesystem is not safe without one. Do not ask at + * all if this build cannot hold a pin. + * + * Returned Value: + * Zero if an address was obtained, a negated errno otherwise. Callers + * that can live without one may ignore the failure. + * + ****************************************************************************/ + +#ifdef HAVE_LIBC_ELF_PIN +static int libelf_pinhold(FAR struct mod_loadinfo_s *loadinfo) +{ + FAR struct file *filep; + int ret; + + /* The descriptor belongs to the task that called the loader, and the + * unload runs on another task. Hold the file instead. + */ + + loadinfo->pinfile = lib_zalloc(sizeof(struct file)); + if (loadinfo->pinfile == NULL) + { + return -ENOMEM; + } + + ret = file_get(loadinfo->filfd, &filep); + if (ret >= 0) + { + ret = file_dup2(filep, loadinfo->pinfile); + file_put(filep); + } + + if (ret < 0) + { + lib_free(loadinfo->pinfile); + loadinfo->pinfile = NULL; + } + + return ret; +} + +/**************************************************************************** + * Name: libelf_pinrelease + * + * Description: + * Give back an XIP pin and the file it was held through, so the + * filesystem can reclaim the extent. + * + ****************************************************************************/ + +void libelf_pinrelease(FAR struct file **pinfile) +{ + if (*pinfile != NULL) + { + file_ioctl(*pinfile, XIPFSIOC_UNPIN, 0); + file_close(*pinfile); + lib_free(*pinfile); + *pinfile = NULL; + } +} +#endif + +static int libelf_xipacquire(FAR struct mod_loadinfo_s *loadinfo) +{ + uintptr_t base = 0; + +#ifdef HAVE_LIBC_ELF_PIN + if (ioctl(loadinfo->filfd, XIPFSIOC_PIN, (unsigned long)&base) >= 0) + { + int ret = libelf_pinhold(loadinfo); + if (ret < 0) + { + berr("ERROR: Failed to hold the pinned file: %d\n", ret); + ioctl(loadinfo->filfd, XIPFSIOC_UNPIN, 0); + return ret; + } + + loadinfo->xipbase = base; + loadinfo->textpin = true; + binfo("pinned xipbase %zx\n", (size_t)loadinfo->xipbase); + return OK; + } +#endif + + if (ioctl(loadinfo->filfd, FIOC_XIPBASE, (unsigned long)&base) >= 0) + { + loadinfo->xipbase = base; + binfo("can use xipbase %zx\n", (size_t)loadinfo->xipbase); + return OK; + } + + return -ENOTTY; +} + /**************************************************************************** * Public Functions ****************************************************************************/ @@ -559,6 +667,7 @@ static inline int libelf_loadfile(FAR struct mod_loadinfo_s *loadinfo) int libelf_load(FAR struct mod_loadinfo_s *loadinfo) { int ret; + int i; binfo("loadinfo: %p\n", loadinfo); DEBUGASSERT(loadinfo && loadinfo->filfd >= 0); @@ -573,14 +682,10 @@ int libelf_load(FAR struct mod_loadinfo_s *loadinfo) } loadinfo->gotindex = libelf_findsection(loadinfo, ".got"); - if (loadinfo->gotindex >= 0) + if (loadinfo->gotindex >= 0 || loadinfo->fdpic) { binfo("GOT section found! index %d\n", loadinfo->gotindex); - if (ioctl(loadinfo->filfd, FIOC_XIPBASE, - (unsigned long)&loadinfo->xipbase) >= 0) - { - binfo("can use xipbase %zu\n", loadinfo->xipbase); - } + libelf_xipacquire(loadinfo); } /* Determine total size to allocate */ @@ -647,21 +752,96 @@ int libelf_load(FAR struct mod_loadinfo_s *loadinfo) } else if (loadinfo->ehdr.e_type == ET_DYN) { - loadinfo->textalloc = (uintptr_t)lib_memalign(loadinfo->textalign, - loadinfo->textsize + - loadinfo->datasize + - loadinfo->segpad); - - if (!loadinfo->textalloc) + if (loadinfo->fdpic) { - berr("ERROR: Failed to allocate memory for the module\n"); - ret = -ENOMEM; - goto errout_with_buffers; + /* The two segments are placed independently, thus only the + * writable segment is allocated, once per instance. + */ + + if (loadinfo->xipbase != 0) + { + /* The text stays on the media. The media address is the base + * of the file, thus add the file offset of the segment. + */ + + for (i = 0; i < loadinfo->ehdr.e_phnum; i++) + { + FAR Elf_Phdr *phdr = &loadinfo->phdr[i]; + + if (phdr->p_type == PT_LOAD && + (phdr->p_flags & PF_X) != 0) + { + loadinfo->textalloc = loadinfo->xipbase + + phdr->p_offset; + break; + } + } + } + else if (loadinfo->textsize > 0) + { + /* The filesystem cannot show its media, thus copy the text + * to RAM. The instances no longer share it. + */ + +# if defined(CONFIG_ARCH_USE_TEXT_HEAP) && \ + defined(CONFIG_ARCH_USE_SEPARATED_SECTION) + loadinfo->textalloc = (uintptr_t) + up_textheap_memalign(".text", + loadinfo->textalign, + loadinfo->textsize); +# elif defined(CONFIG_ARCH_USE_TEXT_HEAP) + loadinfo->textalloc = (uintptr_t) + up_textheap_memalign(loadinfo->textalign, + loadinfo->textsize); +# else + loadinfo->textalloc = (uintptr_t) + lib_memalign(loadinfo->textalign, + loadinfo->textsize); +# endif + if (loadinfo->textalloc == 0) + { + berr("ERROR: Failed to allocate the module's text\n"); + ret = -ENOMEM; + goto errout_with_buffers; + } + } + + if (loadinfo->datasize > 0) + { + loadinfo->datastart = + (uintptr_t)lib_memalign(loadinfo->dataalign, + loadinfo->datasize); + if (!loadinfo->datastart) + { + berr("ERROR: Failed to allocate the module's data\n"); + ret = -ENOMEM; + goto errout_with_buffers; + } + } } + else + { + /* Everything else keeps text and data adjacent: one allocation, + * data behind text. + */ - loadinfo->datastart = loadinfo->textalloc + - loadinfo->textsize + - loadinfo->segpad; + loadinfo->textalloc = (uintptr_t) + lib_memalign(loadinfo->textalign, + loadinfo->textsize + + loadinfo->datasize + + loadinfo->segpad); + + if (!loadinfo->textalloc) + { + berr("ERROR: Failed to allocate memory for the module\n"); + ret = -ENOMEM; + goto errout_with_buffers; + } + + loadinfo->datastart = loadinfo->textalloc + + loadinfo->textsize + + loadinfo->segpad; + } } #endif /* CONFIG_LIBC_ELF_LOADTO_LMA */ @@ -729,14 +909,10 @@ int libelf_load_with_addrenv(FAR struct mod_loadinfo_s *loadinfo) } loadinfo->gotindex = libelf_findsection(loadinfo, ".got"); - if (loadinfo->gotindex >= 0) + if (loadinfo->gotindex >= 0 || loadinfo->fdpic) { binfo("GOT section found! index %d\n", loadinfo->gotindex); - if (ioctl(loadinfo->filfd, FIOC_XIPBASE, - (unsigned long)&loadinfo->xipbase) >= 0) - { - binfo("can use xipbase %zu\n", loadinfo->xipbase); - } + libelf_xipacquire(loadinfo); } /* Determine total size to allocate */ diff --git a/libs/libc/elf/elf_loadhdrs.c b/libs/libc/elf/elf_loadhdrs.c index 6e3e3afb0410d..21fbfe1a7629e 100644 --- a/libs/libc/elf/elf_loadhdrs.c +++ b/libs/libc/elf/elf_loadhdrs.c @@ -66,6 +66,21 @@ int libelf_loadhdrs(FAR struct mod_loadinfo_s *loadinfo) /* Verify that there are sections */ + /* An FDPIC object announces itself in the OS/ABI byte. Note it once. */ + + loadinfo->fdpic = (loadinfo->ehdr.e_ident[EI_OSABI] == ELFOSABI_ARM_FDPIC); + + /* A module is a shared object. An FDPIC object that is anything else + * would be placed through the wrong path. + */ + + if (loadinfo->fdpic && loadinfo->ehdr.e_type != ET_DYN) + { + berr("ERROR: FDPIC object is not a shared object: e_type=%u\n", + loadinfo->ehdr.e_type); + return -ENOEXEC; + } + if (loadinfo->ehdr.e_shnum < 1) { berr("ERROR: No sections(?)\n"); diff --git a/libs/libc/elf/elf_unload.c b/libs/libc/elf/elf_unload.c index c1c7609bf2f6c..63111cd15b3be 100644 --- a/libs/libc/elf/elf_unload.c +++ b/libs/libc/elf/elf_unload.c @@ -59,6 +59,18 @@ int libelf_unload(FAR struct mod_loadinfo_s *loadinfo) libelf_freebuffers(loadinfo); +#ifdef HAVE_LIBC_ELF_PIN + /* Give the pin back if the loader took one, so the filesystem can + * reclaim the extent. + */ + + if (loadinfo->textpin) + { + libelf_pinrelease(&loadinfo->pinfile); + loadinfo->textpin = false; + } +#endif + #ifdef CONFIG_ARCH_ADDRENV if (loadinfo->addrenv != NULL) { @@ -68,9 +80,35 @@ int libelf_unload(FAR struct mod_loadinfo_s *loadinfo) #endif /* Release memory holding the relocated ELF image */ - /* ET_DYN has a single allocation so we only free textalloc */ + /* An FDPIC object placed its two segments separately. Free each one. If + * the text stayed on the media, it was never allocated, thus leave it. + */ + + if (loadinfo->fdpic) + { + if (loadinfo->textalloc != 0 && loadinfo->xipbase == 0) + { +#ifdef CONFIG_ARCH_USE_TEXT_HEAP + up_textheap_free((FAR void *)loadinfo->textalloc); +#else + lib_free((FAR void *)loadinfo->textalloc); +#endif + } + + if (loadinfo->datastart != 0) + { + lib_free((FAR void *)loadinfo->datastart); + loadinfo->datastart = 0; + } + + loadinfo->textalloc = 0; + loadinfo->textsize = 0; + loadinfo->datasize = 0; + } + + /* Any other ET_DYN has a single allocation so we only free textalloc */ - if (loadinfo->ehdr.e_type != ET_DYN) + else if (loadinfo->ehdr.e_type != ET_DYN) { #ifdef CONFIG_ARCH_USE_SEPARATED_SECTION int i; From 17fcb2afbcb2a221eb302014c746a83c394d3a3d Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 3 Aug 2026 08:49:25 +0200 Subject: [PATCH 5/8] libs/libc/elf: Read the dynamic tags an FDPIC object needs. libelf_relocatedyn() reads the handful of DT_* tags it needs to walk the relocation tables and ignores the rest. Three more matter now. DT_PLTGOT is where the object's data base lives. An FDPIC module runs with that in the PIC base register, and every function descriptor built for it names the same base as the one its callee should run with, so without it there is nothing to put in a descriptor's second word. The DT_*_ARRAY tags are the constructor and destructor tables. These are already found through the section headers a few lines further down, and that path is kept, but the dynamic tags are the authoritative copy and an object is not obliged to carry section headers at all. Both paths now translate through libelf_addr(), so they agree on the answer rather than depending on which ran last. The tag values themselves were missing from include/elf.h and are added. Sizing the descriptor pool has to happen here rather than later. R_ARM_FUNCDESC asks the loader to manufacture a descriptor and hand back its address, which means the space must exist by the time the relocation is applied, and by then the segment has been placed. So libelf_elfsize() reserves it behind the writable data, bounded by the relocation count -- one relocation cannot ask for more than one descriptor. That bound has slack in it, but a descriptor is two words and modules are small, which is cheaper than walking every relocation twice to get an exact count. Nothing here runs for a non-FDPIC object. Built and booted mps3-an547:picostest with no change in behaviour. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Marco Casaroli --- include/elf.h | 6 +++++ include/nuttx/lib/elf.h | 14 ++++++++++++ libs/libc/elf/elf_bind.c | 49 ++++++++++++++++++++++++++++++++++++++++ libs/libc/elf/elf_load.c | 32 ++++++++++++++++++++++++++ 4 files changed, 101 insertions(+) diff --git a/include/elf.h b/include/elf.h index f940dbabb675d..fdecf44c075c4 100644 --- a/include/elf.h +++ b/include/elf.h @@ -279,6 +279,12 @@ #define DT_TEXTREL 22 /* d_un=ignored */ #define DT_JMPREL 23 /* d_un=d_ptr */ #define DT_BINDNOW 24 /* d_un=ignored */ +#define DT_INIT_ARRAY 25 /* d_un=d_ptr */ +#define DT_FINI_ARRAY 26 /* d_un=d_ptr */ +#define DT_INIT_ARRAYSZ 27 /* d_un=d_val */ +#define DT_FINI_ARRAYSZ 28 /* d_un=d_val */ +#define DT_PREINIT_ARRAY 32 /* d_un=d_ptr */ +#define DT_PREINIT_ARRAYSZ 33 /* d_un=d_val */ #define DT_LOPROC 0x70000000 /* d_un=unspecified */ #define DT_HIPROC 0x7fffffff /* d_un= unspecified */ diff --git a/include/nuttx/lib/elf.h b/include/nuttx/lib/elf.h index 4a9bb730bf5fa..a9a9918704a10 100644 --- a/include/nuttx/lib/elf.h +++ b/include/nuttx/lib/elf.h @@ -278,6 +278,20 @@ struct mod_loadinfo_s FAR struct file *pinfile; #endif + /* The object's data base, from DT_PLTGOT. An FDPIC module runs with this + * in the PIC base register. + */ + + uintptr_t gotaddr; + + /* Pool of function descriptors behind the writable segment. Reserved + * when the segment is sized, and bounded by the relocation count. + */ + + uintptr_t descpool; + uint16_t ndesc; /* Capacity */ + uint16_t usedesc; /* Next free slot */ + /* Address environment. * * addrenv - This is the handle created by addrenv_allocate() that can be diff --git a/libs/libc/elf/elf_bind.c b/libs/libc/elf/elf_bind.c index dddb626add817..0935123cead08 100644 --- a/libs/libc/elf/elf_bind.c +++ b/libs/libc/elf/elf_bind.c @@ -712,12 +712,61 @@ static int libelf_relocatedyn(FAR struct module_s *modp, case DT_PLTRELSZ: reldata.relsz[I_PLT] = dyn[i].d_un.d_val; break; + case DT_PLTGOT: + + /* The object's data base. Every function descriptor built + * for it names this base. + */ + + loadinfo->gotaddr = dyn[i].d_un.d_ptr; + break; + + /* The constructor and destructor tables. Section headers are + * optional, so the dynamic tags are the authoritative copy. + */ + + case DT_INIT_ARRAY: + loadinfo->initarr = libelf_addr(loadinfo, dyn[i].d_un.d_ptr); + break; + + case DT_INIT_ARRAYSZ: + loadinfo->ninit = dyn[i].d_un.d_val / sizeof(uintptr_t); + break; + + case DT_FINI_ARRAY: + loadinfo->finiarr = libelf_addr(loadinfo, dyn[i].d_un.d_ptr); + break; + + case DT_FINI_ARRAYSZ: + loadinfo->nfini = dyn[i].d_un.d_val / sizeof(uintptr_t); + break; + + case DT_PREINIT_ARRAY: + loadinfo->preiarr = libelf_addr(loadinfo, dyn[i].d_un.d_ptr); + break; + + case DT_PREINIT_ARRAYSZ: + loadinfo->nprei = dyn[i].d_un.d_val / sizeof(uintptr_t); + break; + case DT_PLTREL: if (dyn[i].d_un.d_val == DT_REL) { reldata.relentsz[I_PLT] = sizeof(Elf_Rel); reldata.relrela[I_PLT] = 0; } + else if (loadinfo->fdpic) + { + /* The ARM FDPIC ABI is REL throughout. RELA entries are + * longer, so walking them as REL reads the wrong place. + */ + + berr("ERROR: FDPIC object claims RELA PLT relocations\n"); + lib_free(sym); + lib_free(rels); + lib_free(dyn); + return -ENOEXEC; + } else { reldata.relentsz[I_PLT] = sizeof(Elf_Rela); diff --git a/libs/libc/elf/elf_load.c b/libs/libc/elf/elf_load.c index a7156e7ab6845..4317dfcff780f 100644 --- a/libs/libc/elf/elf_load.c +++ b/libs/libc/elf/elf_load.c @@ -240,6 +240,32 @@ static void libelf_elfsize(FAR struct mod_loadinfo_s *loadinfo, bool alloc) } } + /* Reserve the descriptor pool. R_ARM_FUNCDESC asks the loader to + * manufacture a descriptor after the segment is placed, and the + * relocation count bounds how many. + */ + + if (loadinfo->fdpic) + { + size_t nrels = 0; + + for (i = 0; i < loadinfo->ehdr.e_shnum; i++) + { + FAR Elf_Shdr *shdr = &loadinfo->shdr[i]; + + if (shdr->sh_type == SHT_REL && shdr->sh_entsize != 0) + { + nrels += shdr->sh_size / shdr->sh_entsize; + } + } + + loadinfo->ndesc = nrels; + loadinfo->descpool = datasize; + datasize += nrels * 2 * sizeof(uintptr_t); + + binfo("fdpic: reserving %zu descriptors behind the data\n", nrels); + } + /* An ET_DYN object is sized from its program headers, which give no * section alignment. A word is enough. */ @@ -818,6 +844,12 @@ int libelf_load(FAR struct mod_loadinfo_s *loadinfo) goto errout_with_buffers; } } + + /* The pool was sized as an offset past the end of the real + * data; now that the segment has an address, make it one. + */ + + loadinfo->descpool += loadinfo->datastart; } else { From 90b2e0c07d95ee8635b8e36892553a99d657ddf0 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 3 Aug 2026 11:28:49 +0200 Subject: [PATCH 6/8] libs/libc/elf: Fix two ways an FDPIC module failed to relocate. Running one for the first time turned up two holes in the ET_DYN path. Neither shows up in a build. An undefined symbol is resolved with libelf_findglobal(), which searches only the table of globally registered symbols. The export table that exec() hands its caller went no further than the ET_REL path, so an ET_DYN module could not import anything the caller supplied. Invisible while such modules resolved everything internally; an FDPIC module imports its libc, and every import failed with "Unable to resolve addr of ext ref printf" although the caller had passed a table containing printf. The export table is now threaded into libelf_relocatedyn() and consulted when the global table has no answer, leaving the existing lookup order intact. A relocation naming a symbol defined inside the object was dropped silently. The code handles a relocation with no symbol, and one against an undefined symbol, but a defined symbol fell through both. That was harmless while every dynamic relocation arriving here had symbol index zero, which is the case for R_ARM_RELATIVE. FDPIC brings the first ones that do not: a pointer to a static function is emitted against the *section* symbol, so the value is the section base and the offset within it -- including the Thumb bit -- is carried as the addend. Deriving a value from the word being patched, as the no-symbol case does, would translate that addend as though it were an address. Confirmed against a real module: .text at 0x23c plus an addend of 0x95 gives 0x2d1, which is the function with its Thumb bit. Also stop libelf_symname() reporting a nameless symbol as an error. A section symbol has no name, and libelf_findsymbol() walks the whole table looking for optional entries such as nx_stacksize, so it meets these routinely and checks for -ESRCH itself. At error level it printed ten or more lines per module load and buried the diagnostics that matter. Built and run on lm3s6965-ek with the examples/elf ROMFS. The ET_REL test modules load as before, and an FDPIC module now loads, relocates, resolves printf and puts from the table exec() supplied, and calls through a function descriptor of its own. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Marco Casaroli --- libs/libc/elf/elf_bind.c | 59 +++++++++++++++++++++++++++++++++++-- libs/libc/elf/elf_symbols.c | 6 +++- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/libs/libc/elf/elf_bind.c b/libs/libc/elf/elf_bind.c index 0935123cead08..9702ff9fdf399 100644 --- a/libs/libc/elf/elf_bind.c +++ b/libs/libc/elf/elf_bind.c @@ -35,6 +35,7 @@ #include #include #include +#include #include #include "libc.h" @@ -637,7 +638,9 @@ static int libelf_relocateadd(FAR struct module_s *modp, static int libelf_relocatedyn(FAR struct module_s *modp, FAR struct mod_loadinfo_s *loadinfo, - int relidx) + int relidx, + FAR const struct symtab_s *exports, + int nexports) { FAR Elf_Shdr *shdr = &loadinfo->shdr[relidx]; FAR Elf_Shdr *symhdr; @@ -868,6 +871,26 @@ static int libelf_relocatedyn(FAR struct module_s *modp, ep = libelf_findglobal(modp, loadinfo, symhdr, &sym[idx_sym]); + + /* libelf_findglobal() searches only the registered + * symbols. A module from exec() has its own export + * table, and an FDPIC module imports its libc there. + */ + + if (ep == NULL && exports != NULL) + { + FAR const struct symtab_s *sm; + + sm = symtab_findbyname(exports, + (FAR char *) + loadinfo->iobuffer, + nexports); + if (sm != NULL) + { + ep = (FAR void *)sm->sym_value; + } + } + if ((ep == NULL) && (ELF_ST_BIND(sym[idx_sym].st_info) != STB_WEAK)) { @@ -889,6 +912,37 @@ static int libelf_relocatedyn(FAR struct module_s *modp, *(FAR uintptr_t *)addr = (uintptr_t)ep; } + else if (loadinfo->fdpic) + { + /* A relocation naming a symbol inside this object. A + * pointer to a static function is emitted against the + * section symbol, so the offset, Thumb bit included, is + * the addend and must not come from the patched word. + */ + + Elf_Sym defsym = sym[idx_sym]; + + defsym.st_value = libelf_addr(loadinfo, + sym[idx_sym].st_value); + + addr = libelf_addr(loadinfo, rel->r_offset); + + if (reldata.relrela[idx_rel] == 1) + { + addr += rela->r_addend; + } + + ret = up_relocate(rel, &defsym, addr, ARCH_ELFDATA_PARM); + if (ret < 0) + { + berr("ERROR: Section %d reloc %d: " + "Relocation failed: %d\n", relidx, i, ret); + lib_free(sym); + lib_free(rels); + lib_free(dyn); + return ret; + } + } } else { @@ -1004,7 +1058,8 @@ int libelf_bind(FAR struct module_s *modp, switch (loadinfo->shdr[i].sh_type) { case SHT_DYNAMIC: - ret = libelf_relocatedyn(modp, loadinfo, i); + ret = libelf_relocatedyn(modp, loadinfo, i, + exports, nexports); break; case SHT_DYNSYM: loadinfo->dsymtabidx = i; diff --git a/libs/libc/elf/elf_symbols.c b/libs/libc/elf/elf_symbols.c index 39ad66f085853..aa55d595fed80 100644 --- a/libs/libc/elf/elf_symbols.c +++ b/libs/libc/elf/elf_symbols.c @@ -107,7 +107,11 @@ static int libelf_symname(FAR struct mod_loadinfo_s *loadinfo, if (sym->st_name == 0) { - berr("ERROR: Symbol has no name\n"); + /* Not a failure. A section symbol has no name, and + * libelf_findsymbol() meets these routinely and checks for -ESRCH. + */ + + binfo("Symbol has no name\n"); return -ESRCH; } From 0fdd937c7006d2bac5e6057f04b6622101a03dfd Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 3 Aug 2026 11:34:35 +0200 Subject: [PATCH 7/8] libs/libc/elf: Publish FDPIC functions as descriptors for dlsym. A module that dlopen()s a library gets back function addresses from dlsym() and calls them. Under FDPIC a bare code address is not enough: the callee needs its own data base as well, so what dlsym() returns has to be a function descriptor. The exported symbol table carries no type information -- symtab_s is a name and a value, and its own comment says typing would have to be added to support anything but function pointers -- so by the time dlsym() is asked there is no way to tell a function from an object. libelf_insertsymtab() is the last point that can: st_info is still in hand there. So an FDPIC object's exported functions are published as the address of a descriptor carved from the module's pool, and dlopen(), dlsym() and the module registry need no knowledge of FDPIC at all. The pool is sized for the dynamic symbol table as well as the relocations, since both can draw from it. That leaves the symbol values themselves, which were wrong for any ET_DYN object. libelf_loadsymtab() adds the symbol's section address to its value, which is right for ET_REL, where the section address is where the section was actually placed and the value is relative to it. In a shared object both are already full link-time addresses, so adding them counts the section twice. It needs translating onto wherever the object was placed instead. Library data is shared between everything that dlopen()s it, because the registry holds one instance per name. Giving each user its own copy would mean teaching the registry about instances, which is a much larger change to shared code; an executable loaded through exec() already gets its own data, since that path loads a fresh copy each time. Built and run on lm3s6965-ek with the examples/elf ROMFS; the FDPIC module continues to load, relocate and call through its own descriptors. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Marco Casaroli --- libs/libc/elf/elf_insert.c | 16 ++++++++++++++-- libs/libc/elf/elf_load.c | 14 ++++++++++++++ libs/libc/elf/elf_symbols.c | 21 +++++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/libs/libc/elf/elf_insert.c b/libs/libc/elf/elf_insert.c index 5b646115248bf..d9e7739e8de25 100644 --- a/libs/libc/elf/elf_insert.c +++ b/libs/libc/elf/elf_insert.c @@ -247,9 +247,21 @@ static int libelf_loadsymtab(FAR struct module_s *modp, if (sym[i].st_shndx != SHN_UNDEF && sym[i].st_shndx < loadinfo->ehdr.e_shnum) { - FAR Elf_Shdr *s = &loadinfo->shdr[sym[i].st_shndx]; + if (loadinfo->ehdr.e_type == ET_DYN) + { + /* A shared object's symbol value is already the full + * link-time address. It only needs translating onto where + * the object was placed. + */ - sym[i].st_value = sym[i].st_value + s->sh_addr; + sym[i].st_value = libelf_addr(loadinfo, sym[i].st_value); + } + else + { + FAR Elf_Shdr *s = &loadinfo->shdr[sym[i].st_shndx]; + + sym[i].st_value = sym[i].st_value + s->sh_addr; + } } } diff --git a/libs/libc/elf/elf_load.c b/libs/libc/elf/elf_load.c index 4317dfcff780f..6198ee9f8ed55 100644 --- a/libs/libc/elf/elf_load.c +++ b/libs/libc/elf/elf_load.c @@ -259,6 +259,20 @@ static void libelf_elfsize(FAR struct mod_loadinfo_s *loadinfo, bool alloc) } } + /* A library also publishes a descriptor per exported function, for + * dlsym(). The dynamic symbol table bounds how many. + */ + + for (i = 0; i < loadinfo->ehdr.e_shnum; i++) + { + FAR Elf_Shdr *shdr = &loadinfo->shdr[i]; + + if (shdr->sh_type == SHT_DYNSYM && shdr->sh_entsize != 0) + { + nrels += shdr->sh_size / shdr->sh_entsize; + } + } + loadinfo->ndesc = nrels; loadinfo->descpool = datasize; datasize += nrels * 2 * sizeof(uintptr_t); diff --git a/libs/libc/elf/elf_symbols.c b/libs/libc/elf/elf_symbols.c index aa55d595fed80..79da9861cb3b0 100644 --- a/libs/libc/elf/elf_symbols.c +++ b/libs/libc/elf/elf_symbols.c @@ -34,6 +34,7 @@ #include #include +#include #include #include "libc.h" @@ -542,6 +543,26 @@ int libelf_insertsymtab(FAR struct module_s *modp, strdup((FAR char *)loadinfo->iobuffer); symbol[j].sym_value = (FAR const void *)(uintptr_t)sym[i].st_value; + + /* Publish an FDPIC function as a descriptor, so dlsym() + * hands back something callable. Only here does st_info + * still say what is a function. + */ + + if (loadinfo->fdpic && + ELF_ST_TYPE(sym[i].st_info) == STT_FUNC && + loadinfo->usedesc < loadinfo->ndesc) + { + FAR struct fdpic_desc_s *desc = + (FAR struct fdpic_desc_s *)loadinfo->descpool + + loadinfo->usedesc++; + + desc->entry = sym[i].st_value; + desc->got = loadinfo->gotaddr; + + symbol[j].sym_value = (FAR const void *)desc; + } + j++; } } From 5df7e8fe9a22a0a20ca9435315b42fd469c2f60f Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Wed, 26 Aug 2026 18:16:13 +0200 Subject: [PATCH 8/8] libs/libc/elf: Fix the nxstyle errors around the FDPIC changes. The FDPIC work touches these files, and nxstyle reports errors on the lines around every hunk, which fails the check job. The errors are older than this series: a switch body indented two columns too deep in elf_symbols.c, and declarations with no blank line after them. Whitespace and one reworded comment, no change in behaviour. Signed-off-by: Marco Casaroli --- libs/libc/elf/elf_insert.c | 35 +++--- libs/libc/elf/elf_load.c | 2 + libs/libc/elf/elf_symbols.c | 206 ++++++++++++++++++------------------ 3 files changed, 124 insertions(+), 119 deletions(-) diff --git a/libs/libc/elf/elf_insert.c b/libs/libc/elf/elf_insert.c index d9e7739e8de25..09d1cb2623d23 100644 --- a/libs/libc/elf/elf_insert.c +++ b/libs/libc/elf/elf_insert.c @@ -85,6 +85,7 @@ void libelf_dumploadinfo(FAR struct mod_loadinfo_s *loadinfo) for (i = 0; i < loadinfo->ehdr.e_shnum; i++) { FAR Elf_Shdr *shdr = &loadinfo->shdr[i]; + binfo("Sections %d:\n", i); # ifdef CONFIG_ARCH_USE_SEPARATED_SECTION if (loadinfo->ehdr.e_type == ET_REL) @@ -420,27 +421,27 @@ FAR void *libelf_insert(FAR const char *filename, FAR const char *modname) case ET_REL : case ET_DYN : - /* Process any preinit_array entries */ + /* Process any preinit_array entries */ - array = (FAR void (**)(void))loadinfo.preiarr; - for (i = 0; i < loadinfo.nprei; i++) - { - array[i](); - } + array = (FAR void (**)(void))loadinfo.preiarr; + for (i = 0; i < loadinfo.nprei; i++) + { + array[i](); + } - /* Process any init_array entries */ + /* Process any init_array entries */ - array = (FAR void (**)(void))loadinfo.initarr; - for (i = 0; i < loadinfo.ninit; i++) - { - array[i](); - } + array = (FAR void (**)(void))loadinfo.initarr; + for (i = 0; i < loadinfo.ninit; i++) + { + array[i](); + } - modp->initarr = loadinfo.initarr; - modp->ninit = loadinfo.ninit; - modp->finiarr = loadinfo.finiarr; - modp->nfini = loadinfo.nfini; - break; + modp->initarr = loadinfo.initarr; + modp->ninit = loadinfo.ninit; + modp->finiarr = loadinfo.finiarr; + modp->nfini = loadinfo.nfini; + break; } /* Add the new module entry to the registry */ diff --git a/libs/libc/elf/elf_load.c b/libs/libc/elf/elf_load.c index 6198ee9f8ed55..3f62a287d46fe 100644 --- a/libs/libc/elf/elf_load.c +++ b/libs/libc/elf/elf_load.c @@ -419,6 +419,7 @@ static inline int libelf_loadfile(FAR struct mod_loadinfo_s *loadinfo) else { size_t bsssize = phdr->p_memsz - phdr->p_filesz; + ret = libelf_read(loadinfo, data, phdr->p_filesz, phdr->p_offset); memset(data + phdr->p_filesz, 0, bsssize); @@ -663,6 +664,7 @@ static int libelf_xipacquire(FAR struct mod_loadinfo_s *loadinfo) if (ioctl(loadinfo->filfd, XIPFSIOC_PIN, (unsigned long)&base) >= 0) { int ret = libelf_pinhold(loadinfo); + if (ret < 0) { berr("ERROR: Failed to hold the pinned file: %d\n", ret); diff --git a/libs/libc/elf/elf_symbols.c b/libs/libc/elf/elf_symbols.c index 79da9861cb3b0..55725af775ad3 100644 --- a/libs/libc/elf/elf_symbols.c +++ b/libs/libc/elf/elf_symbols.c @@ -220,6 +220,7 @@ static int libelf_symcallback(FAR struct module_s *modp, FAR void *arg) #if CONFIG_LIBC_ELF_MAXDEPEND > 0 int ret = libelf_depend(exportinfo->modp, modp); + if (ret < 0) { berr("ERROR: libelf_depend failed: %d\n", ret); @@ -354,108 +355,108 @@ int libelf_symvalue(FAR struct module_s *modp, switch (sym->st_shndx) { - case SHN_COMMON: - { - /* NuttX ELF modules should be compiled with -fno-common. */ - - berr("ERROR: SHN_COMMON: Re-compile with -fno-common\n"); - return -ENOSYS; - } - - case SHN_ABS: - { - /* st_value already holds the correct value */ - - binfo("SHN_ABS: st_value=%08lx\n", (long)sym->st_value); - return OK; - } - - case SHN_UNDEF: - { - /* Get the name of the undefined symbol */ - - ret = libelf_symname(loadinfo, sym, sh_offset); - if (ret < 0) - { - /* There are a few relocations for a few architectures that do - * no depend upon a named symbol. We don't know if that is the - * case here, but return and special error to the caller to - * indicate the nameless symbol. - */ - - berr("ERROR: SHN_UNDEF: Failed to get symbol name: %d\n", ret); - return ret; - } - - /* First check if the symbol is exported by an installed module. - * Newest modules are installed at the head of the list. Therefore, - * if the symbol is exported by numerous modules, then the most - * recently installed will take precedence. - */ - - exportinfo.name = (FAR const char *)loadinfo->iobuffer; - exportinfo.modp = modp; - exportinfo.symbol = NULL; - - ret = libelf_registry_foreach(libelf_symcallback, - (FAR void *)&exportinfo); - if (ret < 0) - { - berr("ERROR: libelf_symcallback failed: %d\n", ret); - return ret; - } - - symbol = exportinfo.symbol; - - /* If the symbol is not exported by any module, then check if the - * base code exports a symbol of this name. - */ - - if (symbol == NULL) - { - symbol = symtab_findbyname(exports, exportinfo.name, - nexports); - } - - /* Was the symbol found from any exporter? */ - - if (symbol == NULL) - { - berr("ERROR: SHN_UNDEF: Exported symbol \"%s\" not found\n", - loadinfo->iobuffer); - return -ENOENT; - } - - /* Yes... add the exported symbol value to the ELF symbol tablei - * entry - */ - - binfo("SHN_UNDEF: name=%s " - "%08" PRIxPTR "+%08" PRIxPTR "=%08" PRIxPTR "\n", - loadinfo->iobuffer, - (uintptr_t)sym->st_value, (uintptr_t)symbol->sym_value, - (uintptr_t)(sym->st_value + (uintptr_t)symbol->sym_value)); - - sym->st_value += ((uintptr_t)symbol->sym_value); - } - break; - - default: - { - secbase = loadinfo->shdr[sym->st_shndx].sh_addr; - - binfo("Other[%d]: %08" PRIxPTR "+%08" PRIxPTR "=%08" PRIxPTR "\n", - sym->st_shndx, - (uintptr_t)sym->st_value, secbase, - (uintptr_t)(sym->st_value + secbase)); - - sym->st_value += secbase; - if (loadinfo->gotindex >= 0) - { - sym->st_value -= loadinfo->shdr[sym->st_shndx].sh_offset; - } - } - break; + case SHN_COMMON: + { + /* NuttX ELF modules should be compiled with -fno-common. */ + + berr("ERROR: SHN_COMMON: Re-compile with -fno-common\n"); + return -ENOSYS; + } + + case SHN_ABS: + { + /* st_value already holds the correct value */ + + binfo("SHN_ABS: st_value=%08lx\n", (long)sym->st_value); + return OK; + } + + case SHN_UNDEF: + { + /* Get the name of the undefined symbol */ + + ret = libelf_symname(loadinfo, sym, sh_offset); + if (ret < 0) + { + /* There are a few relocations for a few architectures that do + * no depend upon a named symbol. We don't know if that is the + * case here, but return and special error to the caller to + * indicate the nameless symbol. + */ + + berr("ERROR: SHN_UNDEF: Failed to get symbol name: %d\n", ret); + return ret; + } + + /* First check if the symbol is exported by an installed module. + * Newest modules are installed at the head of the list. So if + * the symbol is exported by numerous modules, then the most + * recently installed will take precedence. + */ + + exportinfo.name = (FAR const char *)loadinfo->iobuffer; + exportinfo.modp = modp; + exportinfo.symbol = NULL; + + ret = libelf_registry_foreach(libelf_symcallback, + (FAR void *)&exportinfo); + if (ret < 0) + { + berr("ERROR: libelf_symcallback failed: %d\n", ret); + return ret; + } + + symbol = exportinfo.symbol; + + /* If the symbol is not exported by any module, then check if the + * base code exports a symbol of this name. + */ + + if (symbol == NULL) + { + symbol = symtab_findbyname(exports, exportinfo.name, + nexports); + } + + /* Was the symbol found from any exporter? */ + + if (symbol == NULL) + { + berr("ERROR: SHN_UNDEF: Exported symbol \"%s\" not found\n", + loadinfo->iobuffer); + return -ENOENT; + } + + /* Yes... add the exported symbol value to the ELF symbol tablei + * entry + */ + + binfo("SHN_UNDEF: name=%s " + "%08" PRIxPTR "+%08" PRIxPTR "=%08" PRIxPTR "\n", + loadinfo->iobuffer, + (uintptr_t)sym->st_value, (uintptr_t)symbol->sym_value, + (uintptr_t)(sym->st_value + (uintptr_t)symbol->sym_value)); + + sym->st_value += ((uintptr_t)symbol->sym_value); + } + break; + + default: + { + secbase = loadinfo->shdr[sym->st_shndx].sh_addr; + + binfo("Other[%d]: %08" PRIxPTR "+%08" PRIxPTR "=%08" PRIxPTR "\n", + sym->st_shndx, + (uintptr_t)sym->st_value, secbase, + (uintptr_t)(sym->st_value + secbase)); + + sym->st_value += secbase; + if (loadinfo->gotindex >= 0) + { + sym->st_value -= loadinfo->shdr[sym->st_shndx].sh_offset; + } + } + break; } return OK; @@ -597,6 +598,7 @@ static int findep(FAR const void *c1, FAR const void *c2) { FAR const struct eptable_s *m1 = (FAR const struct eptable_s *)c1; FAR const struct eptable_s *m2 = (FAR const struct eptable_s *)c2; + return strcmp((FAR const char *)m1->epname, (FAR const char *)m2->epname); }