From df030ba24cf995d8e755617b5218259beacade94 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 3 Aug 2026 00:49:15 +0200 Subject: [PATCH 01/12] 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 | 35 +++++++++++++++++++++++++++++++++++ libs/libc/elf/elf_bind.c | 31 ++++++++++--------------------- 2 files changed, 45 insertions(+), 21 deletions(-) diff --git a/libs/libc/elf/elf.h b/libs/libc/elf/elf.h index 986a3fa5803a9..88b9a359df0d9 100644 --- a/libs/libc/elf/elf.h +++ b/libs/libc/elf/elf.h @@ -238,6 +238,41 @@ 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 + * actually occupies now. + * + * An object is placed as two pieces, text and data, and this is the one + * place that knows how to get from one space to the other. The split is + * the data segment's link-time base: anything below it belongs to text, + * anything at or above it to data. For ET_REL the two pieces are already + * placed independently; for ET_DYN they are, today, adjacent in a single + * allocation, in which case this returns exactly what adding a single + * load bias would have. + * + * 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 79bf571cca8bafab7fb46e82729026037b58eb8a Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 3 Aug 2026 00:56:27 +0200 Subject: [PATCH 02/12] 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() skips reading it, since copying it would put it in RAM and forfeit the only thing the format was chosen for. A module whose text cannot be mapped is refused rather than quietly copied. 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 cannot safely be used without it, and libelf_unload() returns it. 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 | 18 +++ include/elf.h | 1 + include/nuttx/lib/elf.h | 34 +++++ libs/libc/elf/elf.h | 20 +++ libs/libc/elf/elf_load.c | 240 +++++++++++++++++++++++++++++++---- libs/libc/elf/elf_loadhdrs.c | 19 +++ libs/libc/elf/elf_unload.c | 39 +++++- 7 files changed, 345 insertions(+), 26 deletions(-) diff --git a/arch/arm/include/elf.h b/arch/arm/include/elf.h index e5c2780472c18..44e3550d39182 100644 --- a/arch/arm/include/elf.h +++ b/arch/arm/include/elf.h @@ -206,6 +206,24 @@ #define R_ARM_THM_TLS_DESCSEQ16 129 /* Thumb16 */ #define R_ARM_THM_TLS_DESCSEQ32 130 /* Thumb32 */ +/* FDPIC relocations. + * + * Under the FDPIC ABI each PT_LOAD segment is placed independently, so a + * function pointer cannot be a bare code address: it has to carry the data + * base its callee will need. A "function descriptor" is that pair, and + * these relocations are how the loader is asked to build and reference + * them. Values are 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..bf2dc8e0ee5eb 100644 --- a/include/nuttx/lib/elf.h +++ b/include/nuttx/lib/elf.h @@ -44,6 +44,17 @@ # define CONFIG_LIBC_ELF_MAXDEPEND 0 #endif +/* Holding an XIP pin past the load means holding the file itself: the pin is + * released when the module is unloaded, which happens on a task other than + * the one that loaded it, so a descriptor from that task's group cannot + * serve. That needs the file interface, which is why CONFIG_FDPIC depends + * on the flat build. + */ + +#ifdef CONFIG_FDPIC +# define HAVE_LIBC_ELF_PIN 1 +#endif + #ifndef CONFIG_LIBC_ELF_ALIGN_LOG2 # define CONFIG_LIBC_ELF_ALIGN_LOG2 2 #endif @@ -123,6 +134,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 +264,28 @@ struct mod_loadinfo_s * skip the copy. */ + /* FDPIC state. + * + * An FDPIC object places its two PT_LOAD segments independently: the + * read-only one is mapped where it already sits on the media and the + * writable one is copied to RAM, once per running instance. That is + * what lets several instances share one copy of the text. + * + * fdpic - True if e_ident[EI_OSABI] marked this an FDPIC object. + * textpin - True if the read-only segment is held by a filesystem pin + * that has to be dropped at unload, rather than by an + * address the filesystem simply handed over. + */ + + 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 88b9a359df0d9..e00e1e4a19cc6 100644 --- a/libs/libc/elf/elf.h +++ b/libs/libc/elf/elf.h @@ -356,4 +356,24 @@ 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 taken while loading, and the file it was held + * through. Does nothing if no pin was taken. + * + * 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 c58f8060ade67..d1a1dee5bfbaa 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" @@ -239,6 +240,22 @@ static void libelf_elfsize(FAR struct mod_loadinfo_s *loadinfo, bool alloc) } } + /* A shared object is sized from its program headers, which carry no + * per-section alignment to take: p_align is the linker's page + * granularity, and honouring it would cost a page per module for nothing. + * Its sections need no more than a natural word, so ask for that. + */ + + if (loadinfo->textalign == 0) + { + loadinfo->textalign = sizeof(uintptr_t); + } + + if (loadinfo->dataalign == 0) + { + loadinfo->dataalign = sizeof(uintptr_t); + } + /* Save the allocation size */ loadinfo->textsize = textsize; @@ -350,6 +367,16 @@ static inline int libelf_loadfile(FAR struct mod_loadinfo_s *loadinfo) { if (phdr->p_flags & PF_X) { + if (loadinfo->fdpic) + { + /* Mapped, not copied. Copying it here would put the + * text in RAM and forfeit the only thing this format + * was chosen for. + */ + + continue; + } + ret = libelf_read(loadinfo, buffer_data_address(text), phdr->p_filesz, phdr->p_offset); @@ -525,6 +552,123 @@ 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 that + * the read-only part of the object can be used where it lies instead of + * being copied. + * + * Two mechanisms exist and they are not interchangeable. A compacting + * filesystem can move a file's blocks, so it hands out an address only + * together 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. Ask for the pin first, because a filesystem that + * needs one cannot safely be used without it. + * + * mmap() is deliberately not used here even 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 -- a different group -- so the pin would outlive the module + * and the extent would never become movable again. + * + * 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 the pin was taken through belongs to whichever task + * called the loader, and the pin has to be given back when the module is + * unloaded -- which for an executed module happens on the spawned task, + * in another group entirely. Take a reference to the file instead, which + * belongs to no group. + */ + + 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. A compacting + * filesystem cannot reclaim the extent until every instance executing + * from it has let go, so this is not merely tidiness. + * + ****************************************************************************/ + +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; + + if (ioctl(loadinfo->filfd, XIPFSIOC_PIN, (unsigned long)&base) >= 0) + { +#ifdef HAVE_LIBC_ELF_PIN + 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; + } +#endif + + loadinfo->xipbase = base; + loadinfo->textpin = true; + binfo("pinned xipbase %zx\n", (size_t)loadinfo->xipbase); + return OK; + } + + 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 ****************************************************************************/ @@ -545,6 +689,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); @@ -559,14 +704,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 */ @@ -633,21 +774,76 @@ 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; + /* An FDPIC object reaches its data through the GOT rather than + * at a fixed distance from its code, so the two segments do not + * have to stay adjacent -- which is the entire point. The + * read-only one is mapped where it already lies on the media + * and never copied; only the writable one is allocated, and + * that happens once per running instance. + */ + + if (loadinfo->xipbase == 0) + { + berr("ERROR: FDPIC module cannot be executed in place\n"); + ret = -ENOEXEC; + goto errout_with_buffers; + } + + /* The media address is the base of the file, so the segment's + * own file offset still has to be added -- the same arithmetic + * the ET_REL path does with sh_offset. + */ + + 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; + } + } + + 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 the relative position of text and data, + * because its data references are at a fixed offset from the + * code that makes them. One allocation, data behind text. + */ + + loadinfo->textalloc = (uintptr_t) + lib_memalign(loadinfo->textalign, + loadinfo->textsize + + loadinfo->datasize + + loadinfo->segpad); - loadinfo->datastart = loadinfo->textalloc + - loadinfo->textsize + - 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 */ @@ -715,14 +911,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..09ddf21d4b334 100644 --- a/libs/libc/elf/elf_loadhdrs.c +++ b/libs/libc/elf/elf_loadhdrs.c @@ -66,6 +66,25 @@ 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, + * here, so that placement and relocation do not each have to re-derive + * it from the header. + */ + + loadinfo->fdpic = (loadinfo->ehdr.e_ident[EI_OSABI] == ELFOSABI_ARM_FDPIC); + + /* A module is a shared object. An object that claims the FDPIC ABI and + * is anything else would be placed and entered through the paths meant + * for a relocatable object, which is not what its relocations expect. + */ + + 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..6bc0281f36d3d 100644 --- a/libs/libc/elf/elf_unload.c +++ b/libs/libc/elf/elf_unload.c @@ -30,6 +30,8 @@ #include #include +#include +#include #include #include "libc.h" @@ -68,9 +70,42 @@ 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, and its text was + * never allocated at all -- it is media the filesystem lent us. Free + * the data on its own and leave the text alone. + */ - if (loadinfo->ehdr.e_type != ET_DYN) + if (loadinfo->fdpic) + { + /* Give the pin back if one was taken. A compacting filesystem + * cannot reclaim the extent until every instance executing from it + * has let go, so this is not merely tidiness. + */ + + if (loadinfo->textpin) + { +#ifdef HAVE_LIBC_ELF_PIN + libelf_pinrelease(&loadinfo->pinfile); +#else + ioctl(loadinfo->filfd, XIPFSIOC_UNPIN, 0); +#endif + loadinfo->textpin = false; + } + + 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 */ + + else if (loadinfo->ehdr.e_type != ET_DYN) { #ifdef CONFIG_ARCH_USE_SEPARATED_SECTION int i; From ba5bf102e912a1e0251371e850ce9a99c928c06e Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 3 Aug 2026 08:49:25 +0200 Subject: [PATCH 03/12] 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 | 19 ++++++++++++++ libs/libc/elf/elf_bind.c | 55 ++++++++++++++++++++++++++++++++++++++++ libs/libc/elf/elf_load.c | 39 ++++++++++++++++++++++++++++ 4 files changed, 119 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 bf2dc8e0ee5eb..d6981e5bc3798 100644 --- a/include/nuttx/lib/elf.h +++ b/include/nuttx/lib/elf.h @@ -286,6 +286,25 @@ struct mod_loadinfo_s FAR struct file *pinfile; #endif + /* Where the object's data base lives, from DT_PLTGOT. An FDPIC module + * runs with this in the PIC base register, and it is the base every + * function descriptor built for the module names. + */ + + uintptr_t gotaddr; + + /* Pool of function descriptors, carved out behind the writable segment. + * + * R_ARM_FUNCDESC asks the loader to manufacture a descriptor and hand + * back its address, so the space has to be reserved when the segment is + * sized, before any relocation is applied. Sized from the relocation + * count, which bounds how many can be asked for. + */ + + 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 c712ac6f5f0e2..bd2bde0f788e6 100644 --- a/libs/libc/elf/elf_bind.c +++ b/libs/libc/elf/elf_bind.c @@ -712,12 +712,67 @@ 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: + + /* Where the object's data base lives. An FDPIC module is + * entered with this in the PIC base register, and every + * function descriptor built for it names it as the base its + * callee should run with. + */ + + loadinfo->gotaddr = dyn[i].d_un.d_ptr; + break; + + /* The constructor and destructor tables. These are also + * reachable through the section headers, and are read from + * there below, but an object is not obliged to carry section + * headers and 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. An object claiming + * RELA for its PLT has to be refused rather than walked as + * REL: the entries are half as long again, so every one + * after the first would be read from 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 d1a1dee5bfbaa..e11647e04df5e 100644 --- a/libs/libc/elf/elf_load.c +++ b/libs/libc/elf/elf_load.c @@ -240,6 +240,39 @@ static void libelf_elfsize(FAR struct mod_loadinfo_s *loadinfo, bool alloc) } } + /* An FDPIC object may ask the loader to manufacture function + * descriptors -- that is what R_ARM_FUNCDESC means -- and hand back + * their addresses. They have to live somewhere the module can reach + * through its data base, and the space has to be reserved now, because + * by the time the relocation is applied the segment has been placed. + * + * One relocation cannot ask for more than one descriptor, so the + * relocation count bounds the pool. Modules are small and a descriptor + * is two words, so the slack in that bound is cheaper than walking the + * relocations twice. + */ + + 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); + } + /* A shared object is sized from its program headers, which carry no * per-section alignment to take: p_align is the linker's page * granularity, and honouring it would cost a page per module for nothing. @@ -819,6 +852,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 611e6bd78732938af9c7fcba4bae6522e137af90 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 3 Aug 2026 08:58:41 +0200 Subject: [PATCH 04/12] libs/libc/machine/arm: Relocate FDPIC function descriptors. A function pointer under FDPIC is not a code address. Because each PT_LOAD segment is placed independently, a pointer has to carry the data base its callee will need, so it is a two-word descriptor: the entry point, and the base to install in the PIC register before branching. R_ARM_FUNCDESC_VALUE says "the thing you are patching is such a descriptor", and R_ARM_FUNCDESC says "manufacture one and give me its address". Both need state a relocation cannot carry. A descriptor's second word is the *object's* data base, from DT_PLTGOT, and R_ARM_FUNCDESC carves descriptors from a pool whose cursor has to survive from one relocation to the next. up_relocate() is handed only a relocation, a resolved symbol and an address to patch. arch_data is the existing channel for exactly this -- RISC-V already uses it to remember a HI20 relocation while its LO12 partner is processed -- but nothing has ever put loader state into it: it is declared zeroed and written only by up_relocate() itself. So ARCH_ELFDATA_INIT and ARCH_ELFDATA_FINI are added, seeding the block from the loadinfo before the relocation loop and reading the cursor back after. Both default to nothing, so an architecture that does not define them is unaffected, and RISC-V's use of arch_data is untouched. libelf_relocatedyn() walks both dynamic tables under one arch_data, so the cursor spans the whole object. The addend handling is the part that is easy to get wrong. REL format keeps the addend in place, in the word about to become the entry point, and a pointer to a static function is referenced through its *section* symbol -- the value is the section base and the offset, including the Thumb bit, is entirely in the addend. Dropping it yields an even address and the core faults trying to execute it as ARM code. The GOT written into a descriptor is the loading object's own, even for an imported function, which is what makes a callback work: when the base firmware's qsort() calls back into a module's comparison function, the module needs its own data base in the PIC register. libelf_relocatedyn()'s imported-symbol path needed a change to suit. It stores the resolved address directly and never calls up_relocate(), which cannot produce a two-word descriptor, so under FDPIC the resolved value now goes through up_relocate() and the relocation type decides what to write. Implemented for armv7-m and armv8-m, the profiles FDPIC targets; the other ARM variants gain the arch_data block but no new relocations. Built and booted mps3-an547:picostest and lm3s6965-ek:qemu-nxflat, the ELF PIC and NXFLAT users of this code, both unchanged. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Marco Casaroli --- arch/arm/include/elf.h | 72 +++++++++++++++ libs/libc/elf/elf_bind.c | 113 ++++++++++++++++++++++- libs/libc/machine/arm/armv7-m/arch_elf.c | 106 +++++++++++++++++++++ libs/libc/machine/arm/armv8-m/arch_elf.c | 106 +++++++++++++++++++++ 4 files changed, 395 insertions(+), 2 deletions(-) diff --git a/arch/arm/include/elf.h b/arch/arm/include/elf.h index 44e3550d39182..4e6411de733ee 100644 --- a/arch/arm/include/elf.h +++ b/arch/arm/include/elf.h @@ -265,10 +265,82 @@ #define DT_ARM_PREEMPTMAP 0x70000002 #define DT_ARM_RESERVED2 0x70000003 +/* Per-object state that the FDPIC relocations need and that a relocation's + * own arguments cannot supply. + * + * up_relocate() is handed a relocation, a resolved symbol and the address + * to patch, which is enough for every other ARM relocation. The two + * FDPIC ones need more: a function descriptor's second word is the + * *object's* data base, and R_ARM_FUNCDESC has to manufacture descriptors + * from a pool whose cursor must survive from one relocation to the next. + * Both are loader state, so they arrive through the arch_data channel, + * seeded by libelf_bind() before the relocation loop and read back after. + */ + +/* The relocations that only an FDPIC object may use. Seeing one in an + * object whose OS/ABI byte does not say FDPIC means the marker was lost. + */ + +#define ARCH_ELF_RELOC_ISFDPIC(t) \ + ((t) == R_ARM_FUNCDESC || (t) == R_ARM_FUNCDESC_VALUE) + +#define ARCH_ELFDATA 1 + +#define ARCH_ELFDATA_INIT(d, l) \ + do \ + { \ + (d)->fdpic = (l)->fdpic; \ + (d)->gotaddr = (l)->gotaddr; \ + (d)->descpool = (l)->descpool; \ + (d)->ndesc = (l)->ndesc; \ + (d)->usedesc = (l)->usedesc; \ + } \ + while (0) + +#define ARCH_ELFDATA_FINI(d, l) \ + do \ + { \ + (l)->usedesc = (d)->usedesc; \ + } \ + while (0) + /**************************************************************************** * Public Types ****************************************************************************/ +#ifndef __ASSEMBLY__ + +/* A function descriptor: what an FDPIC function pointer actually is. + * + * Base firmware is not built FDPIC, so to it a function pointer is a code + * address. A module passes the address of one of these instead, and the + * callee is entered with got in the PIC base register so that it can + * reach its own data. + */ + +struct arm_fdpic_desc_s +{ + uintptr_t entry; /* Address of the code */ + uintptr_t got; /* Data base to install before branching */ +}; + +struct arch_elfdata_s +{ + uint8_t fdpic; /* The object is an FDPIC one */ + uintptr_t gotaddr; /* DT_PLTGOT: this object's data base */ + uintptr_t descpool; /* Base of the descriptor pool */ + uint16_t ndesc; /* Capacity, in descriptors */ + uint16_t usedesc; /* Next free slot */ + uint8_t pltrel; /* Relocation comes from DT_JMPREL, so the word + * it overwrites is a lazy binding stub and not + * an addend + */ +}; + +typedef struct arch_elfdata_s arch_elfdata_t; + +#endif /* __ASSEMBLY__ */ + typedef struct __EIT_entry { unsigned long fnoffset; diff --git a/libs/libc/elf/elf_bind.c b/libs/libc/elf/elf_bind.c index bd2bde0f788e6..8d8b8e1ef44de 100644 --- a/libs/libc/elf/elf_bind.c +++ b/libs/libc/elf/elf_bind.c @@ -48,6 +48,22 @@ #define I_PLT 1 /* ... for PLTs */ #define N_RELS 2 /* Number of relxxx[] indexes */ +/* Relocation types that only an FDPIC object may use. An architecture + * that has none leaves this alone. + */ + +#ifdef ARCH_ELF_RELOC_ISFDPIC + +/* An architecture that has FDPIC relocations also carries the fields they + * need in its arch_elfdata_t. Everyone else has neither, so the code that + * fills those fields has to go with them. + */ + +# define HAVE_ARCH_ELF_FDPIC 1 +#else +# define ARCH_ELF_RELOC_ISFDPIC(t) 0 +#endif + #ifdef ARCH_ELFDATA # define ARCH_ELFDATA_DEF arch_elfdata_t arch_data; \ memset(&arch_data, 0, sizeof(arch_elfdata_t)) @@ -57,6 +73,20 @@ # define ARCH_ELFDATA_PARM NULL #endif +/* Some relocations need state the loader holds rather than state a single + * relocation carries. An architecture that has such relocations defines + * these to move it in and out of the arch_data block; for everyone else + * they are nothing. + */ + +#if defined(ARCH_ELFDATA) && defined(ARCH_ELFDATA_INIT) +# define ARCH_ELFDATA_SETUP(l) ARCH_ELFDATA_INIT(&arch_data, l) +# define ARCH_ELFDATA_TEARDOWN(l) ARCH_ELFDATA_FINI(&arch_data, l) +#else +# define ARCH_ELFDATA_SETUP(l) +# define ARCH_ELFDATA_TEARDOWN(l) +#endif + /**************************************************************************** * Private Types ****************************************************************************/ @@ -720,7 +750,8 @@ static int libelf_relocatedyn(FAR struct module_s *modp, * callee should run with. */ - loadinfo->gotaddr = dyn[i].d_un.d_ptr; + loadinfo->gotaddr = libelf_addr(loadinfo, + dyn[i].d_un.d_ptr); break; /* The constructor and destructor tables. These are also @@ -782,6 +813,17 @@ static int libelf_relocatedyn(FAR struct module_s *modp, } } + /* Hand the loader's per-object state to the relocations that need it. + * This has to follow the loop above, not precede it: DT_PLTGOT is read + * there, and a descriptor built with a data base of zero would fault on + * the module's first call through it. + * + * Both relocation tables are then walked under this one arch_data, so a + * cursor kept in it survives from one table to the next. + */ + + ARCH_ELFDATA_SETUP(loadinfo); + symhdr = &loadinfo->shdr[loadinfo->dsymtabidx]; sym = lib_malloc(symhdr->sh_size); if (!sym) @@ -819,6 +861,15 @@ static int libelf_relocatedyn(FAR struct module_s *modp, ret = OK; lrelent = reldata.relsz[idx_rel] / reldata.relentsz[idx_rel]; +#ifdef HAVE_ARCH_ELF_FDPIC + /* Say which table this is. A relocation out of DT_JMPREL overwrites + * a word the linker pre-loaded with a lazy binding stub, which is not + * an addend and must not be added to. + */ + + arch_data.pltrel = (idx_rel == I_PLT); +#endif + for (i = 0; i < lrelent; i++) { /* Process each relocation entry @@ -862,6 +913,25 @@ static int libelf_relocatedyn(FAR struct module_s *modp, } } + /* An object that uses FDPIC relocations but does not say it is + * FDPIC cannot be run: nothing would place its segments apart or + * install its data base, and the relocations below would write + * plain addresses where descriptors belong. The OS/ABI byte is + * the only thing that says so, so a cleared one has to be an + * error rather than something to march past. + */ + + if (!loadinfo->fdpic && + ARCH_ELF_RELOC_ISFDPIC(ELF_R_TYPE(rel->r_info))) + { + berr("ERROR: FDPIC relocation %d in an object that is not " + "marked FDPIC\n", (int)ELF_R_TYPE(rel->r_info)); + lib_free(sym); + lib_free(rels); + lib_free(dyn); + return -ENOEXEC; + } + /* Now perform the architecture-specific relocation */ if ((idx_sym = ELF_R_SYM(rel->r_info)) != 0) @@ -893,7 +963,40 @@ static int libelf_relocatedyn(FAR struct module_s *modp, addr += rela->r_addend; } - *(FAR uintptr_t *)addr = (uintptr_t)ep; + if (loadinfo->fdpic) + { + /* An imported symbol is not always a plain + * address to be stored. Under FDPIC it may be a + * function descriptor, which is two words and has + * to be built rather than assigned, so hand the + * resolved value to up_relocate() and let the + * relocation type decide what to write. + */ + + Elf_Sym extsym = + { + 0 + }; + + extsym.st_value = (uintptr_t)ep; + + ret = up_relocate(rel, &extsym, 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 + { + *(FAR uintptr_t *)addr = (uintptr_t)ep; + } } } else @@ -928,6 +1031,12 @@ static int libelf_relocatedyn(FAR struct module_s *modp, } } + /* Hand back what the relocations consumed. The error paths above do + * not bother: the load is being abandoned, so the cursor has no reader. + */ + + ARCH_ELFDATA_TEARDOWN(loadinfo); + lib_free(sym); lib_free(rels); lib_free(dyn); diff --git a/libs/libc/machine/arm/armv7-m/arch_elf.c b/libs/libc/machine/arm/armv7-m/arch_elf.c index c91e92460d7ac..205ffaca59796 100644 --- a/libs/libc/machine/arm/armv7-m/arch_elf.c +++ b/libs/libc/machine/arm/armv7-m/arch_elf.c @@ -175,6 +175,112 @@ int up_relocate(const Elf32_Rel *rel, const Elf32_Sym *sym, uintptr_t addr, } break; + case R_ARM_FUNCDESC_VALUE: + { + /* The target is the descriptor: two words, the entry point and + * the data base to install before branching to it. + * + * REL format keeps the addend in place, in the word that is about + * to become the entry point, and it matters. A pointer to a + * static function is referenced through its *section* symbol, + * whose value is the section base, with the offset -- and the + * Thumb bit -- carried entirely by the addend. Dropping it + * yields an even address and the core faults trying to execute it + * as ARM code. + * + * The GOT written here is this object's own, even for an imported + * function. That is deliberate and is what makes a callback + * work: when the base firmware's qsort() calls back into the + * module's comparison function, the module needs its own data + * base in the PIC register. + */ + + FAR struct arm_fdpic_desc_s *desc = + (FAR struct arm_fdpic_desc_s *)addr; + FAR arch_elfdata_t *data = (FAR arch_elfdata_t *)arch_data; + + if (data == NULL) + { + berr("ERROR: FUNCDESC_VALUE without loader state\n"); + return -EINVAL; + } + + /* A descriptor only means anything in an FDPIC object. An object + * that carries these relocations without saying it is FDPIC cannot + * be run: nothing would install its data base. + */ + + if (!data->fdpic) + { + berr("ERROR: FUNCDESC_VALUE in a non-FDPIC object\n"); + return -ENOEXEC; + } + + binfo("Performing FUNCDESC_VALUE link " + "at addr=%08" PRIxPTR " to sym=%p st_value=%08" PRIx32 "\n", + addr, sym, sym->st_value); + + if (data->pltrel) + { + /* A lazy descriptor holds the address of its own PLT resolution + * stub and a data base of -1, for a resolver to overwrite on + * the first call. Binding eagerly means overwriting it here; + * adding to it would produce an arbitrary address. + */ + + desc->entry = sym->st_value; + } + else + { + desc->entry = sym->st_value + desc->entry; + } + + desc->got = data->gotaddr; + } + break; + + case R_ARM_FUNCDESC: + { + /* A pointer to a descriptor, which the loader has to supply. + * Carve one out of the pool reserved behind the writable segment + * and store its address. + */ + + FAR struct arm_fdpic_desc_s *desc; + FAR arch_elfdata_t *data = (FAR arch_elfdata_t *)arch_data; + + if (data == NULL) + { + berr("ERROR: FUNCDESC without loader state\n"); + return -EINVAL; + } + + if (!data->fdpic) + { + berr("ERROR: FUNCDESC in a non-FDPIC object\n"); + return -ENOEXEC; + } + + if (data->usedesc >= data->ndesc) + { + berr("ERROR: Out of function descriptors\n"); + return -ENOMEM; + } + + desc = (FAR struct arm_fdpic_desc_s *)data->descpool + + data->usedesc++; + + binfo("Performing FUNCDESC link " + "at addr=%08" PRIxPTR " to sym=%p st_value=%08" PRIx32 "\n", + addr, sym, sym->st_value); + + desc->entry = sym->st_value + *(uint32_t *)addr; + desc->got = data->gotaddr; + + *(uint32_t *)addr = (uint32_t)(uintptr_t)desc; + } + break; + case R_ARM_ABS32: case R_ARM_TARGET1: /* New ABI: TARGET1 always treated as ABS32 */ { diff --git a/libs/libc/machine/arm/armv8-m/arch_elf.c b/libs/libc/machine/arm/armv8-m/arch_elf.c index 9a3c68811bd65..2972d737d229e 100644 --- a/libs/libc/machine/arm/armv8-m/arch_elf.c +++ b/libs/libc/machine/arm/armv8-m/arch_elf.c @@ -175,6 +175,112 @@ int up_relocate(const Elf32_Rel *rel, const Elf32_Sym *sym, uintptr_t addr, } break; + case R_ARM_FUNCDESC_VALUE: + { + /* The target is the descriptor: two words, the entry point and + * the data base to install before branching to it. + * + * REL format keeps the addend in place, in the word that is about + * to become the entry point, and it matters. A pointer to a + * static function is referenced through its *section* symbol, + * whose value is the section base, with the offset -- and the + * Thumb bit -- carried entirely by the addend. Dropping it + * yields an even address and the core faults trying to execute it + * as ARM code. + * + * The GOT written here is this object's own, even for an imported + * function. That is deliberate and is what makes a callback + * work: when the base firmware's qsort() calls back into the + * module's comparison function, the module needs its own data + * base in the PIC register. + */ + + FAR struct arm_fdpic_desc_s *desc = + (FAR struct arm_fdpic_desc_s *)addr; + FAR arch_elfdata_t *data = (FAR arch_elfdata_t *)arch_data; + + if (data == NULL) + { + berr("ERROR: FUNCDESC_VALUE without loader state\n"); + return -EINVAL; + } + + /* A descriptor only means anything in an FDPIC object. An object + * that carries these relocations without saying it is FDPIC cannot + * be run: nothing would install its data base. + */ + + if (!data->fdpic) + { + berr("ERROR: FUNCDESC_VALUE in a non-FDPIC object\n"); + return -ENOEXEC; + } + + binfo("Performing FUNCDESC_VALUE link " + "at addr=%08" PRIxPTR " to sym=%p st_value=%08" PRIx32 "\n", + addr, sym, sym->st_value); + + if (data->pltrel) + { + /* A lazy descriptor holds the address of its own PLT resolution + * stub and a data base of -1, for a resolver to overwrite on + * the first call. Binding eagerly means overwriting it here; + * adding to it would produce an arbitrary address. + */ + + desc->entry = sym->st_value; + } + else + { + desc->entry = sym->st_value + desc->entry; + } + + desc->got = data->gotaddr; + } + break; + + case R_ARM_FUNCDESC: + { + /* A pointer to a descriptor, which the loader has to supply. + * Carve one out of the pool reserved behind the writable segment + * and store its address. + */ + + FAR struct arm_fdpic_desc_s *desc; + FAR arch_elfdata_t *data = (FAR arch_elfdata_t *)arch_data; + + if (data == NULL) + { + berr("ERROR: FUNCDESC without loader state\n"); + return -EINVAL; + } + + if (!data->fdpic) + { + berr("ERROR: FUNCDESC in a non-FDPIC object\n"); + return -ENOEXEC; + } + + if (data->usedesc >= data->ndesc) + { + berr("ERROR: Out of function descriptors\n"); + return -ENOMEM; + } + + desc = (FAR struct arm_fdpic_desc_s *)data->descpool + + data->usedesc++; + + binfo("Performing FUNCDESC link " + "at addr=%08" PRIxPTR " to sym=%p st_value=%08" PRIx32 "\n", + addr, sym, sym->st_value); + + desc->entry = sym->st_value + *(uint32_t *)addr; + desc->got = data->gotaddr; + + *(uint32_t *)addr = (uint32_t)(uintptr_t)desc; + } + break; + case R_ARM_ABS32: case R_ARM_TARGET1: /* New ABI: TARGET1 always treated as ABS32 */ { From 76f003f796301675578ebada9b9e57d7af1b9508 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 3 Aug 2026 09:07:14 +0200 Subject: [PATCH 05/12] !binfmt/elf: Load FDPIC modules through the ELF loader. With placement, the dynamic tags and the relocations in hand, the last thing an FDPIC module needs is for the ELF loader to recognise it and hand the scheduler its data base. The data base arrives by a different route than for everything else. A PIC ELF object has it as the address of its .got section, which the loader finds by name; an FDPIC object names it in DT_PLTGOT, which is read while the dynamic tags are parsed. Both end up in the dspace_s that up_initial_state() installs in the PIC base register when the task starts, so both kinds of module run the same way from there on. A module carrying DT_NEEDED is refused rather than loaded. Shared libraries belong to dlopen() rather than to a loader that walks dependencies itself, and nothing in the tree resolves DT_NEEDED today -- the tag appears exactly once, as a constant in include/elf.h. Loading such a module anyway would leave it to fault on its first call into a library that was never brought in, so it fails at load with a message naming the cause. Built and booted mps3-an547:picostest, which shares this path. BREAKING CHANGE: CONFIG_BINFMT_CONSTRUCTORS starts working. It has never had any effect on a module loaded through exec(): elf_loadbinary() recorded .init_array and .fini_array and nothing ever called them, so a C++ module's global objects were left as .bss and its constructors silently skipped. They now run, at the end of the load. Quick fix: a module that worked around this by initializing from its entry point will find the constructor has already run, and the workaround can be removed. A module with no constructors is unaffected, as is any configuration with CONFIG_BINFMT_CONSTRUCTORS disabled. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Marco Casaroli --- binfmt/elf.c | 74 ++++++++++++++++++++++++++++++++++++-- include/nuttx/lib/elf.h | 12 +++++++ libs/libc/elf/elf_bind.c | 21 +++++++++++ libs/libc/elf/elf_insert.c | 33 +++++++++++++++-- libs/libc/elf/elf_remove.c | 32 ++++++++++++++++- 5 files changed, 166 insertions(+), 6 deletions(-) diff --git a/binfmt/elf.c b/binfmt/elf.c index fc89630053806..e3d0b6ec4d81a 100644 --- a/binfmt/elf.c +++ b/binfmt/elf.c @@ -35,6 +35,7 @@ #include #include +#include #include #include "binfmt.h" @@ -98,6 +99,10 @@ static int elf_loadbinary(FAR struct binary_s *binp, int nexports) { struct mod_loadinfo_s loadinfo; +#ifdef CONFIG_BINFMT_CONSTRUCTORS + FAR void (**array)(void); + int i; +#endif Elf_Sym sym; int ret; @@ -252,6 +257,8 @@ static int elf_loadbinary(FAR struct binary_s *binp, binp->mod.textalloc = (FAR void *)loadinfo.textalloc; binp->mod.dataalloc = (FAR void *)loadinfo.datastart; + binp->mod.fdpic = loadinfo.fdpic; + binp->mod.gotaddr = loadinfo.gotaddr; # ifdef CONFIG_BINFMT_CONSTRUCTORS binp->mod.initarr = loadinfo.initarr; binp->mod.finiarr = loadinfo.finiarr; @@ -270,7 +277,7 @@ static int elf_loadbinary(FAR struct binary_s *binp, libelf_dumpentrypt(&loadinfo); #ifdef CONFIG_PIC - if (loadinfo.gotindex >= 0) + if (loadinfo.gotindex >= 0 || loadinfo.fdpic) { FAR struct dspace_s *dspaces = kmm_zalloc(sizeof(struct dspace_s)); @@ -280,12 +287,75 @@ static int elf_loadbinary(FAR struct binary_s *binp, goto errout_with_load; } - dspaces->region = (FAR void *)loadinfo.shdr[loadinfo.gotindex].sh_addr; + /* An FDPIC object names its data base in DT_PLTGOT, which the + * loader has already translated; everything else has it as the + * address of the .got section. The two are the same idea reached + * by different routes, and both are what up_initial_state() puts + * in the PIC base register when the task starts. + */ + + if (loadinfo.fdpic) + { + dspaces->region = (FAR void *)loadinfo.gotaddr; + } + else + { + dspaces->region = + (FAR void *)loadinfo.shdr[loadinfo.gotindex].sh_addr; + } + dspaces->crefs = 1; binp->picbase = (FAR void *)dspaces; } #endif +#ifdef CONFIG_BINFMT_CONSTRUCTORS + /* Run the constructors. This is the last thing the load does, so a + * global is initialized by the time the module's main() can see it, and + * nothing that could still fail runs after a constructor has. + * + * They run here rather than on the spawned task because there is no hook + * to enter it with, and because it is where libelf_insert() runs them for + * a module that arrives through dlopen(). An FDPIC object's reach its + * globals through its own data base, which this task does not carry. + */ + + array = (FAR void (**)(void))loadinfo.preiarr; + for (i = 0; i < loadinfo.nprei; i++) + { + if (loadinfo.fdpic) + { + fdpic_invoke((uintptr_t)array[i], 0, loadinfo.gotaddr); + } + else + { + array[i](); + } + } + + array = (FAR void (**)(void))loadinfo.initarr; + for (i = 0; i < loadinfo.ninit; i++) + { + if (loadinfo.fdpic) + { + fdpic_invoke((uintptr_t)array[i], 0, loadinfo.gotaddr); + } + else + { + array[i](); + } + } +#endif + +#ifdef HAVE_LIBC_ELF_PIN + /* Past the last thing that can fail, so the module owns the pin now: it + * is given back when the task that runs the module exits. + */ + + binp->mod.pinfile = loadinfo.pinfile; + loadinfo.pinfile = NULL; +#endif + libelf_uninitialize(&loadinfo); return OK; diff --git a/include/nuttx/lib/elf.h b/include/nuttx/lib/elf.h index d6981e5bc3798..832616ca16045 100644 --- a/include/nuttx/lib/elf.h +++ b/include/nuttx/lib/elf.h @@ -186,6 +186,18 @@ struct module_s uint16_t nsect; /* Number of entries in sectalloc array */ #endif int dynamic; /* Module is a dynamic shared object */ + bool fdpic; /* Module is an FDPIC object: its two + * segments were placed separately and + * the text is media, not an allocation + */ + uintptr_t gotaddr; /* An FDPIC object's data base, to + * enter its destructors with + */ +#ifdef HAVE_LIBC_ELF_PIN + FAR struct file *pinfile; /* Holds the XIP pin on the text until + * the module is unloaded + */ +#endif #if defined(CONFIG_FS_PROCFS) && !defined(CONFIG_FS_PROCFS_EXCLUDE_MODULE) size_t textsize; /* Size of the kernel .text memory allocation */ size_t datasize; /* Size of the kernel .bss/.data memory allocation */ diff --git a/libs/libc/elf/elf_bind.c b/libs/libc/elf/elf_bind.c index 8d8b8e1ef44de..eaf6a5940f881 100644 --- a/libs/libc/elf/elf_bind.c +++ b/libs/libc/elf/elf_bind.c @@ -742,6 +742,27 @@ 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_NEEDED: + + /* Shared libraries belong to dlopen(), not to a loader that + * walks dependencies itself. Nothing in the tree loads + * DT_NEEDED today, so rather than resolve it badly, refuse + * the module and say why -- otherwise it would load and then + * fault on its first call into the library that is not there. + */ + + if (loadinfo->fdpic) + { + berr("ERROR: FDPIC module has a DT_NEEDED entry. Shared " + "libraries are not supported; link it statically.\n"); + lib_free(sym); + lib_free(rels); + lib_free(dyn); + return -ENOEXEC; + } + + break; + case DT_PLTGOT: /* Where the object's data base lives. An FDPIC module is diff --git a/libs/libc/elf/elf_insert.c b/libs/libc/elf/elf_insert.c index 5b646115248bf..df094d6cb18ba 100644 --- a/libs/libc/elf/elf_insert.c +++ b/libs/libc/elf/elf_insert.c @@ -29,6 +29,7 @@ #include #include +#include #include #include "elf.h" @@ -391,6 +392,12 @@ FAR void *libelf_insert(FAR const char *filename, FAR const char *modname) modp->textalloc = (FAR void *)loadinfo.textalloc; modp->dataalloc = (FAR void *)loadinfo.datastart; + modp->fdpic = loadinfo.fdpic; + modp->gotaddr = loadinfo.gotaddr; +#ifdef HAVE_LIBC_ELF_PIN + modp->pinfile = loadinfo.pinfile; + loadinfo.pinfile = NULL; +#endif #ifdef CONFIG_ARCH_USE_SEPARATED_SECTION modp->sectalloc = (FAR void **)loadinfo.sectalloc; modp->nsect = loadinfo.ehdr.e_shnum; @@ -408,12 +415,25 @@ 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. + * + * An FDPIC object's constructors touch its globals, so they have + * to run with its own data base rather than with whatever the + * loading thread happens to carry -- which for a DT_NEEDED + * library is the importing module's. + */ array = (FAR void (**)(void))loadinfo.preiarr; for (i = 0; i < loadinfo.nprei; i++) { - array[i](); + if (loadinfo.fdpic) + { + fdpic_invoke((uintptr_t)array[i], 0, loadinfo.gotaddr); + } + else + { + array[i](); + } } /* Process any init_array entries */ @@ -421,7 +441,14 @@ FAR void *libelf_insert(FAR const char *filename, FAR const char *modname) array = (FAR void (**)(void))loadinfo.initarr; for (i = 0; i < loadinfo.ninit; i++) { - array[i](); + if (loadinfo.fdpic) + { + fdpic_invoke((uintptr_t)array[i], 0, loadinfo.gotaddr); + } + else + { + array[i](); + } } modp->initarr = loadinfo.initarr; diff --git a/libs/libc/elf/elf_remove.c b/libs/libc/elf/elf_remove.c index 67722937b3302..fe4d97b10239f 100644 --- a/libs/libc/elf/elf_remove.c +++ b/libs/libc/elf/elf_remove.c @@ -29,8 +29,11 @@ #include #include +#include #include +#include "elf/elf.h" + /**************************************************************************** * Public Functions ****************************************************************************/ @@ -64,7 +67,19 @@ int libelf_uninit(FAR struct module_s *modp) array = (FAR void (**)(void))modp->finiarr; for (i = 0; i < modp->nfini; i++) { - array[i](); + /* Like the constructors, an FDPIC object's destructors reach its + * globals through its own data base, which the unloading thread does + * not carry. + */ + + if (modp->fdpic) + { + fdpic_invoke((uintptr_t)array[i], 0, modp->gotaddr); + } + else + { + array[i](); + } } if (modp->modinfo.uninitializer != NULL) @@ -148,6 +163,21 @@ int libelf_uninit(FAR struct module_s *modp) # endif #endif } + else if (modp->fdpic) + { +#ifdef HAVE_LIBC_ELF_PIN + /* Give the pin back before the text goes out of use. */ + + libelf_pinrelease(&modp->pinfile); +#endif + + /* An FDPIC object placed its two segments separately, and its + * text was never allocated at all -- it is media the filesystem + * lent us. Free the data on its own and leave the text alone. + */ + + lib_free((FAR void *)modp->dataalloc); + } else { lib_free((FAR void *)modp->textalloc); From 0dc6f3cc41d6f6b7dd119810db262d938ffdddd2 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 3 Aug 2026 10:10:04 +0200 Subject: [PATCH 06/12] libc, sched: Resolve FDPIC descriptors at module callback entry points. The base firmware and an FDPIC module disagree about what a function pointer is. Firmware is not built FDPIC, so to it a pointer is a code address and it branches there. A module passes the address of a two word descriptor instead, because its code and data are placed independently and a bare code address would leave the callee unable to find its own data. A firmware routine that takes a callback therefore branches into the module's data segment and faults. So the ten entry points that can be handed a callback by a module resolve the descriptor before storing or branching to it: qsort, bsearch, pthread_create, signal, sigaction, task_create and task_create_with_stack, task_spawn, pthread_once, scandir, and mq_notify and timer_create with SIGEV_THREAD. Which one resolves matters as much as that one does. Resolving twice would take an already resolved code address for a descriptor and read two words from the instruction stream, so each pointer is resolved exactly once, at the outermost point that sees it. signal() passes its argument through untouched because sigaction() and then nxsig_action() will resolve it, which covers a module calling sigaction() directly as well. qsort() is split so that the public entry resolves and the recursive implementation does not. scandir() resolves its filter but not its comparison function, which it hands to qsort(). Whether a caller is a module at all is asked of the PIC base register, which up_initial_state() sets only for a task that has a D-Space. A plain kernel task therefore reads zero and is left alone. SIGEV_THREAD is the case the register cannot answer, because the callback runs later on a work queue worker that carries no module's base at all. The base is captured instead when the notification is registered, in the module's own context, and installed around the call. fdpic_invoke() keeps hand written assembly rather than using up_setpicbase(). 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 saving, installing, branching and restoring have to be a single sequence. All of it is behind CONFIG_ELF_FDPIC, which is new here and defaults off. Built mps3-an547:picostest both ways; with it off the entry points compile to what they were, and with it on qsort() calls fdpic_callback(), which reads the PIC base register through up_getpicbase(). Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Marco Casaroli --- binfmt/Kconfig | 29 +++++ include/nuttx/fdpic.h | 178 +++++++++++++++++++++++++++++ include/nuttx/signal.h | 5 + libs/libc/dirent/lib_scandir.c | 17 +++ libs/libc/pthread/pthread_create.c | 18 +++ libs/libc/pthread/pthread_once.c | 19 +++ libs/libc/signal/sig_signal.c | 8 ++ libs/libc/stdlib/lib_bsearch.c | 11 ++ libs/libc/stdlib/lib_qsort.c | 39 ++++++- sched/mqueue/mq_notify.c | 17 +++ sched/signal/sig_action.c | 25 ++++ sched/signal/sig_notification.c | 39 ++++++- sched/task/task_create.c | 23 +++- sched/task/task_spawn.c | 15 +++ sched/timer/timer_create.c | 17 +++ 15 files changed, 454 insertions(+), 6 deletions(-) create mode 100644 include/nuttx/fdpic.h diff --git a/binfmt/Kconfig b/binfmt/Kconfig index 93844898da0d4..c7cc0ad109c0c 100644 --- a/binfmt/Kconfig +++ b/binfmt/Kconfig @@ -60,6 +60,35 @@ config ELF_STACKSIZE default DEFAULT_TASK_STACKSIZE ---help--- This is the default stack size that will be used when starting ELF binaries. + +config ELF_FDPIC + bool "FDPIC modules" + default n + select PIC + depends on ARCH_ARMV7M || ARCH_ARMV8M + ---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. This needs a filesystem that can expose its media, such + as XIPFS or ROMFS, and an arm-uclinuxfdpiceabi linker to build the + modules; 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..8dcd5e1b02a2a --- /dev/null +++ b/include/nuttx/fdpic.h @@ -0,0 +1,178 @@ +/**************************************************************************** + * 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 base firmware is not built FDPIC, so to it a function pointer is a + * code address and it simply branches there. An FDPIC module passes the + * address of one of these instead, because its code and data are placed + * independently and a bare code address would leave the callee unable to + * find its own data. + */ + +struct fdpic_desc_s +{ + uintptr_t entry; /* Address of the code */ + uintptr_t got; /* Data base to install before branching */ +}; + +/**************************************************************************** + * Inline Functions + ****************************************************************************/ + +#ifdef CONFIG_ELF_FDPIC + +/**************************************************************************** + * Name: fdpic_base + * + * Description: + * The data base of the calling context, read from the PIC base register. + * Non-zero means the caller is an FDPIC module; zero means base firmware, + * because up_initial_state() only installs a value for a task that has a + * D-Space. + * + * This is what lets a shared entry point tell the two apart without being + * told, so that a plain kernel task calling qsort() is unaffected. + * + ****************************************************************************/ + +static inline uintptr_t fdpic_base(void) +{ + uintptr_t base; + + up_getpicbase(&base); + return base; +} + +/**************************************************************************** + * Name: fdpic_callback + * + * Description: + * Resolve a function pointer that arrived from a caller which may be an + * FDPIC module. + * + * Only the entry point is taken from the descriptor. The data base is + * already correct in the register: the base firmware is built with that + * register reserved, so the module's own base survives the call in, and + * any task the module creates inherits its D-Space. + * + * 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's data base in the + * PIC base register, and restore the caller's afterwards. + * + * This is for the one case where the register cannot already be right: a + * callback a module registered that runs on a shared thread -- the + * signal-notification work queue -- which carries no module's base. The + * base is captured at registration time, in the module's own context, and + * installed here around the call. Everywhere else the callback runs on a + * task that inherited the module's D-Space and fdpic_callback() suffices. + * + * A context switch or interrupt during the call is safe: the register is + * REG_PIC in the saved context, so it is preserved across a switch, and + * the base firmware is built with it reserved so no handler disturbs it. + * + * Input Parameters: + * entry - The code address to enter, already resolved from the descriptor. + * arg - The single word argument, passed in r0. + * got - The module data base to install. + * + ****************************************************************************/ + +static inline void fdpic_invoke(uintptr_t entry, uintptr_t arg, + uintptr_t got) +{ + register uintptr_t r0v __asm__ ("r0") = arg; + + /* up_setpicbase() cannot serve here. The register has to hold the + * module's base for the duration of one call and then go back, and + * nothing in C tells the compiler the register is live across that call, + * so save, install, branch and restore have to be one sequence. + * + * arg is pinned in r0, the first argument and the call's scratch, so the + * asm needs registers only for entry and got -- deliberately few, so the + * allocator has room on builds that reserve a frame pointer. The PIC + * register is saved on the stack rather than in a scratch register; r4 + * rides along only to keep the push 8-byte aligned and comes back + * untouched. + */ + + __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" + ); +} + +#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_ELF_FDPIC */ + +#endif /* __INCLUDE_NUTTX_FDPIC_H */ diff --git a/include/nuttx/signal.h b/include/nuttx/signal.h index 79fa22b39d501..2274b4b0460cd 100644 --- a/include/nuttx/signal.h +++ b/include/nuttx/signal.h @@ -68,6 +68,11 @@ struct sigwork_s struct work_s work; /* Work queue structure */ union sigval value; /* Data passed with notification */ sigev_notify_function_t func; /* Notification function */ +#ifdef CONFIG_ELF_FDPIC + uintptr_t got; /* FDPIC data base of a module callback, or + * zero. Captured at registration, installed + * around the call on the worker thread. */ +#endif }; #ifdef __cplusplus diff --git a/libs/libc/dirent/lib_scandir.c b/libs/libc/dirent/lib_scandir.c index c734ff1d243cb..098bfd4366cae 100644 --- a/libs/libc/dirent/lib_scandir.c +++ b/libs/libc/dirent/lib_scandir.c @@ -31,6 +31,10 @@ #include #include +#ifdef CONFIG_ELF_FDPIC +# include +#endif + #include "libc.h" /* The scandir() function is not appropriate for use within the kernel in its @@ -91,6 +95,19 @@ int scandir(FAR const char *path, FAR struct dirent ***namelist, * the original errno value to be able to restore it in case of success. */ +#ifdef CONFIG_ELF_FDPIC + /* An FDPIC module passes the address of a function descriptor, not a code + * address. Resolve the filter, which is called from the loop below. + * + * compar is deliberately NOT resolved here. It is handed to qsort(), + * whose public entry point resolves it, and resolving it twice would + * treat an already-resolved code address as a descriptor. + */ + + filter = (CODE int (*)(FAR const struct dirent *)) + fdpic_callback((FAR void *)filter); +#endif + errsv = get_errno(); dirp = opendir(path); diff --git a/libs/libc/pthread/pthread_create.c b/libs/libc/pthread/pthread_create.c index 6c87140361cd4..84406072c3dd0 100644 --- a/libs/libc/pthread/pthread_create.c +++ b/libs/libc/pthread/pthread_create.c @@ -30,6 +30,10 @@ #include +#ifdef CONFIG_ELF_FDPIC +# include +#endif + /**************************************************************************** * Private Functions ****************************************************************************/ @@ -88,6 +92,20 @@ static void pthread_startup(pthread_startroutine_t entry, int pthread_create(FAR pthread_t *thread, FAR const pthread_attr_t *attr, pthread_startroutine_t pthread_entry, pthread_addr_t arg) { +#ifdef CONFIG_ELF_FDPIC + /* An FDPIC module passes the address of a function descriptor, not a code + * address. Resolve it here, once, in the public entry point. + * + * The new thread inherits the creator's D-Space -- nxtask_dup_dspace() + * runs before up_initial_state() installs it in the FDPIC register -- so + * it starts with the module's own data base already in place, and needs + * only the code address. + */ + + pthread_entry = (pthread_startroutine_t) + fdpic_callback((FAR void *)pthread_entry); +#endif + return nx_pthread_create(pthread_startup, thread, attr, pthread_entry, arg); } diff --git a/libs/libc/pthread/pthread_once.c b/libs/libc/pthread/pthread_once.c index ccd854b878843..afc4c655c2631 100644 --- a/libs/libc/pthread/pthread_once.c +++ b/libs/libc/pthread/pthread_once.c @@ -33,6 +33,10 @@ #include #include +#ifdef CONFIG_ELF_FDPIC +# include +#endif + /**************************************************************************** * Public Functions ****************************************************************************/ @@ -73,6 +77,21 @@ int pthread_once(FAR pthread_once_t *once_control, return EINVAL; } +#ifdef CONFIG_ELF_FDPIC + /* An FDPIC module passes the address of a function descriptor, not a code + * address. Resolve it here, in the public entry point. + * + * init_routine() runs on this thread, so the module's data base is + * already in the FDPIC register and only the code address is needed. The + * resolved value is a local copy and is never stored, so a later call + * through the same once_control resolves the caller's descriptor afresh + * rather than re-resolving a code address. + */ + + init_routine = (CODE void (*)(void)) + fdpic_callback((FAR void *)init_routine); +#endif + if (!once_control->done) { pthread_mutex_lock(&once_control->mutex); diff --git a/libs/libc/signal/sig_signal.c b/libs/libc/signal/sig_signal.c index 8ed40cf2ef2d6..bf19d765ea088 100644 --- a/libs/libc/signal/sig_signal.c +++ b/libs/libc/signal/sig_signal.c @@ -71,6 +71,14 @@ _sa_handler_t signal(int signo, _sa_handler_t func) DEBUGASSERT(func != SIG_ERR && func != SIG_HOLD); + /* An FDPIC module passes the address of a function descriptor rather than + * a code address, but it is not resolved here: sigaction() then + * nxsig_action() resolves the handler in the innermost common code, which + * covers both this path and a module that calls sigaction() directly. + * Resolving here as well would resolve it twice and branch through a code + * address as if it were a descriptor. + */ + /* Initialize the sigaction structure */ act.sa_handler = func; diff --git a/libs/libc/stdlib/lib_bsearch.c b/libs/libc/stdlib/lib_bsearch.c index a4e3047bf9030..cecd62b15857a 100644 --- a/libs/libc/stdlib/lib_bsearch.c +++ b/libs/libc/stdlib/lib_bsearch.c @@ -37,6 +37,10 @@ ****************************************************************************/ #include + +#ifdef CONFIG_ELF_FDPIC +# include +#endif #include /**************************************************************************** @@ -114,6 +118,13 @@ FAR void *bsearch(FAR const void *key, FAR const void *base, size_t nel, DEBUGASSERT(base != NULL || nel == 0); DEBUGASSERT(compar != NULL); +#ifdef CONFIG_ELF_FDPIC + /* See qsort(): an FDPIC caller passes a descriptor, not a code address */ + + compar = (CODE int (*)(FAR const void *, FAR const void *)) + fdpic_callback((FAR void *)compar); +#endif + for (lim = nel, lower = (const char *)base; lim != 0; lim >>= 1) { middle = lower + (lim >> 1) * width; diff --git a/libs/libc/stdlib/lib_qsort.c b/libs/libc/stdlib/lib_qsort.c index 5646452388f1b..e82dd671e714d 100644 --- a/libs/libc/stdlib/lib_qsort.c +++ b/libs/libc/stdlib/lib_qsort.c @@ -45,6 +45,10 @@ #include #include +#ifdef CONFIG_ELF_FDPIC +# include +#endif + /**************************************************************************** * Pre-processor Definitions ****************************************************************************/ @@ -156,8 +160,9 @@ static inline FAR char *med3(FAR char *a, FAR char *b, FAR char *c, * ****************************************************************************/ -void qsort(FAR void *base, size_t nel, size_t width, - CODE int(*compar)(FAR const void *, FAR const void *)) +static void qsort_internal(FAR void *base, size_t nel, size_t width, + CODE int(*compar)(FAR const void *, + FAR const void *)) { FAR char *pa; FAR char *pb; @@ -277,7 +282,7 @@ void qsort(FAR void *base, size_t nel, size_t width, if ((r = pb - pa) > width) { - qsort(base, r / width, width, compar); + qsort_internal(base, r / width, width, compar); } if ((r = pd - pc) > width) @@ -289,3 +294,31 @@ void qsort(FAR void *base, size_t nel, size_t width, goto loop; } } + +/**************************************************************************** + * Name: qsort + * + * Description: + * Public entry point. Resolves the comparison function once and then + * hands an ordinary pointer to the implementation. + * + * The split matters: qsort_internal() recurses, and resolving on every + * entry would treat an already-resolved code address as a descriptor the + * second time round and branch somewhere meaningless. + * + ****************************************************************************/ + +void qsort(FAR void *base, size_t nel, size_t width, + CODE int(*compar)(FAR const void *, FAR const void *)) +{ +#ifdef CONFIG_ELF_FDPIC + /* An FDPIC module passes the address of a function descriptor, not a + * code address. + */ + + compar = (CODE int (*)(FAR const void *, FAR const void *)) + fdpic_callback((FAR void *)compar); +#endif + + qsort_internal(base, nel, width, compar); +} diff --git a/sched/mqueue/mq_notify.c b/sched/mqueue/mq_notify.c index ea1f35fc27933..fc13c96122e35 100644 --- a/sched/mqueue/mq_notify.c +++ b/sched/mqueue/mq_notify.c @@ -34,6 +34,10 @@ #include #include +#if defined(CONFIG_ELF_FDPIC) && defined(CONFIG_SIG_EVTHREAD) +# include +#endif + #include "sched/sched.h" #include "mqueue/mqueue.h" @@ -156,6 +160,19 @@ int mq_notify(mqd_t mqdes, FAR const struct sigevent *notification) sizeof(struct sigevent)); msgq->ntpid = rtcb->pid; + +#if defined(CONFIG_ELF_FDPIC) && defined(CONFIG_SIG_EVTHREAD) + /* If a module registered a SIGEV_THREAD callback, capture its data + * base now, while this runs in the module's context. The callback + * fires later on a work-queue worker that has no base of its own; + * nxsig_notification() reads this to install it around the call. + */ + + msgq->ntwork.got = + (fdpic_base() != 0 && + (notification->sigev_notify & SIGEV_THREAD) != 0) ? + fdpic_base() : 0; +#endif } } diff --git a/sched/signal/sig_action.c b/sched/signal/sig_action.c index ab37db83d262b..1891218fdfaa1 100644 --- a/sched/signal/sig_action.c +++ b/sched/signal/sig_action.c @@ -38,6 +38,10 @@ #include #include +#ifdef CONFIG_ELF_FDPIC +# include +#endif + #include "sched/sched.h" #include "group/group.h" #include "signal/signal.h" @@ -326,6 +330,27 @@ int nxsig_action(int signo, FAR const struct sigaction *act, handler = act->sa_handler; +#ifdef CONFIG_ELF_FDPIC + /* An FDPIC module passes the address of a function descriptor, not a code + * address. Resolve it here, in the innermost common code, so a module + * that calls sigaction() directly is covered as well as one that goes + * through signal(), and each exactly once -- signal() passes its argument + * through unresolved for that reason. + * + * The dispositions have to be excluded by hand. SIG_ERR, SIG_IGN, + * SIG_DFL and SIG_HOLD are the small integers -1, 0, 1 and 2 rather than + * addresses, and fdpic_callback() only declines to dereference NULL. + * sa_handler and sa_sigaction are a union, so this one resolution serves + * both handler forms. + */ + + if (handler != SIG_ERR && handler != SIG_IGN && handler != SIG_DFL && + handler != SIG_HOLD) + { + handler = (_sa_handler_t)fdpic_callback((FAR void *)handler); + } +#endif + #ifdef CONFIG_SIG_DEFAULT /* If the caller is setting the handler to SIG_DFL, then we need to * replace this with the correct, internal default signal action handler. diff --git a/sched/signal/sig_notification.c b/sched/signal/sig_notification.c index 86909faf62bc5..b6f38ef5de386 100644 --- a/sched/signal/sig_notification.c +++ b/sched/signal/sig_notification.c @@ -34,6 +34,10 @@ #include +#ifdef CONFIG_ELF_FDPIC +# include +#endif + #include "sched/sched.h" #include "signal/signal.h" @@ -70,7 +74,24 @@ static void nxsig_notification_worker(FAR void *arg) /* Perform the callback */ - work->func(work->value); +#ifdef CONFIG_ELF_FDPIC + /* A module's callback runs here on a shared worker thread, which does not + * carry the module's data base. Install the base captured at + * registration around the call so the callback can reach its own globals; + * work->func has already been resolved to the code address. A non-module + * callback has a zero base and is called directly. + */ + + if (work->got != 0) + { + fdpic_invoke((uintptr_t)work->func, (uintptr_t)work->value.sival_ptr, + work->got); + } + else +#endif + { + work->func(work->value); + } } #endif /* CONFIG_SIG_EVTHREAD */ @@ -157,6 +178,22 @@ int nxsig_notification(pid_t pid, FAR struct sigevent *event, work->value = event->sigev_value; work->func = event->sigev_notify_function; +#ifdef CONFIG_ELF_FDPIC + /* When the callback is a module's, work->got was set at registration + * to the module's data base (this runs at send or expiry time, whose + * context is not the module's, so it cannot be read here). The + * callback is a descriptor: resolve it to the code address now -- + * reading the descriptor is just a memory access and needs no base -- + * and the worker installs the base around the call. + */ + + if (work->got != 0) + { + work->func = (sigev_notify_function_t) + ((FAR struct fdpic_desc_s *)event->sigev_notify_function)->entry; + } +#endif + /* Then queue the work */ return work_queue(SIG_EVTHREAD_WORK, &work->work, diff --git a/sched/task/task_create.c b/sched/task/task_create.c index 4530a742491a0..a75b16994926d 100644 --- a/sched/task/task_create.c +++ b/sched/task/task_create.c @@ -37,6 +37,10 @@ #include #include +#ifdef CONFIG_ELF_FDPIC +# include +#endif + #include "sched/sched.h" #include "group/group.h" #include "task/task.h" @@ -202,8 +206,23 @@ int task_create_with_stack(FAR const char *name, int priority, FAR void *stack_addr, int stack_size, main_t entry, FAR char * const argv[]) { - int ret = nxtask_create(name, priority, stack_addr, - stack_size, entry, argv, NULL); + int ret; + +#ifdef CONFIG_ELF_FDPIC + /* An FDPIC module passes the address of a function descriptor, not a code + * address. Resolving it here covers task_create() too, which is a plain + * forwarder -- and covers it exactly once, which matters: resolving twice + * would treat an already-resolved code address as a descriptor. + * + * The new task inherits the creator's D-Space, so it starts with the + * module's own data base installed and needs only the code address. + */ + + entry = (main_t)fdpic_callback((FAR void *)entry); +#endif + + ret = nxtask_create(name, priority, stack_addr, + stack_size, entry, argv, NULL); if (ret < 0) { set_errno(-ret); diff --git a/sched/task/task_spawn.c b/sched/task/task_spawn.c index a29db7f893c0a..9042e40ad8be3 100644 --- a/sched/task/task_spawn.c +++ b/sched/task/task_spawn.c @@ -38,6 +38,10 @@ #include #include +#ifdef CONFIG_ELF_FDPIC +# include +#endif + #include "sched/sched.h" #include "group/group.h" #include "task/spawn.h" @@ -335,6 +339,17 @@ int task_spawn(FAR const char *name, main_t entry, pid_t pid = INVALID_PROCESS_ID; int ret; +#ifdef CONFIG_ELF_FDPIC + /* An FDPIC module passes the address of a function descriptor, not a code + * address. Resolve it here, once, in the public entry point. + * + * The new task inherits the creator's D-Space, so it starts with the + * module's own data base installed and needs only the code address. + */ + + entry = (main_t)fdpic_callback((FAR void *)entry); +#endif + sinfo("name=%s entry=%p file_actions=%p attr=%p argv=%p\n", name, entry, file_actions, attr, argv); diff --git a/sched/timer/timer_create.c b/sched/timer/timer_create.c index 1811f3e68af00..5c25999252ba5 100644 --- a/sched/timer/timer_create.c +++ b/sched/timer/timer_create.c @@ -37,6 +37,10 @@ #include #include +#if defined(CONFIG_ELF_FDPIC) && defined(CONFIG_SIG_EVTHREAD) +# include +#endif + #include "sched/sched.h" #include "timer/timer.h" @@ -196,6 +200,19 @@ int timer_create(clockid_t clockid, FAR struct sigevent *evp, /* Yes, copy the entire struct sigevent content */ memcpy(&ret->pt_event, evp, sizeof(struct sigevent)); + +#if defined(CONFIG_ELF_FDPIC) && defined(CONFIG_SIG_EVTHREAD) + /* If a module registered a SIGEV_THREAD callback, capture its + * data base now, while this runs in the module's context. The + * callback fires later on a work-queue worker with no base of + * its own; nxsig_notification() installs this around the call. + */ + + ret->pt_work.got = + (fdpic_base() != 0 && + (evp->sigev_notify & SIGEV_THREAD) != 0) ? + fdpic_base() : 0; +#endif } else { From 9813d622e15243e34e308fcf1fbc567cd8dfaa1a Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 3 Aug 2026 11:28:49 +0200 Subject: [PATCH 07/12] 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 | 73 ++++++++++++++++++++++++++++++++++++- libs/libc/elf/elf_symbols.c | 9 ++++- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/libs/libc/elf/elf_bind.c b/libs/libc/elf/elf_bind.c index eaf6a5940f881..28350a622d19d 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" @@ -667,7 +668,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; @@ -965,6 +968,31 @@ static int libelf_relocatedyn(FAR struct module_s *modp, ep = libelf_findglobal(modp, loadinfo, symhdr, &sym[idx_sym]); + + /* libelf_findglobal() searches only the table of + * globally registered symbols. A module loaded + * through exec() is given its own export table + * instead, and until now nothing on this path looked + * at it -- harmless while ET_DYN modules resolved + * everything internally, but an FDPIC module imports + * its libc from exactly there. libelf_symname() has + * just left the name in the I/O buffer. + */ + + 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)) { @@ -1019,6 +1047,46 @@ 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 defined inside this + * object. Nothing handled these before, which was + * harmless while every dynamic relocation reaching here + * carried no symbol at all -- R_ARM_RELATIVE has symbol + * index zero and is dealt with below. + * + * FDPIC brings the first ones that do. 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. + */ + + 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 { @@ -1139,7 +1207,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..0c973245cc263 100644 --- a/libs/libc/elf/elf_symbols.c +++ b/libs/libc/elf/elf_symbols.c @@ -107,7 +107,14 @@ 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() walks the whole table looking for optional + * symbols such as nx_stacksize, so it meets these routinely and + * checks for -ESRCH itself. Reporting it as an error buries the + * real diagnostics on every module load. + */ + + binfo("Symbol has no name\n"); return -ESRCH; } From 3cc45332ac61f0b4d0c299d50ad6fb20680b11ca Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 3 Aug 2026 11:34:35 +0200 Subject: [PATCH 08/12] 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 | 17 +++++++++++++++-- libs/libc/elf/elf_load.c | 15 +++++++++++++++ libs/libc/elf/elf_symbols.c | 29 +++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/libs/libc/elf/elf_insert.c b/libs/libc/elf/elf_insert.c index df094d6cb18ba..208e8f16ce4c2 100644 --- a/libs/libc/elf/elf_insert.c +++ b/libs/libc/elf/elf_insert.c @@ -248,9 +248,22 @@ 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, and so is its section's, so adding + * the two would count the section twice. What it needs is + * translating onto wherever 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 e11647e04df5e..93e1d040616d8 100644 --- a/libs/libc/elf/elf_load.c +++ b/libs/libc/elf/elf_load.c @@ -266,6 +266,21 @@ static void libelf_elfsize(FAR struct mod_loadinfo_s *loadinfo, bool alloc) } } + /* A library also publishes a descriptor for each function it + * exports, so that dlsym() can hand back something callable. The + * dynamic symbol table bounds how many that can be. + */ + + 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 0c973245cc263..53e7f9afe43db 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" @@ -545,6 +546,34 @@ 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; + + /* An FDPIC caller cannot branch to a bare code address: + * it needs the callee's data base too. So a function + * exported by an FDPIC object is published as the + * address of a descriptor rather than of its code, and + * dlsym() hands back something that can simply be + * called. + * + * This is the only point that can do it. The exported + * table carries no type information, so by the time + * dlsym() is asked there is no way to tell a function + * from an object; here st_info still says. + */ + + 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 ae7c64c36f1f0fb79c90746ab5b287ab1f8798dc Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 3 Aug 2026 13:22:17 +0200 Subject: [PATCH 09/12] libs/libc/elf: Load DT_NEEDED libraries with dlopen(). A module that names a shared library in DT_NEEDED now gets it loaded and its imports bound against it, rather than being left with undefined symbols. dlopen() does the loading. It is already the loader for a shared library, so the work goes there rather than into a dependency walker of the loader's own: the library lands in the module registry like anything else, its exports come back through libelf_getsymbol() -- the same call dlsym() uses -- and a library named by two modules is opened once and reference counted. Undefined symbols are resolved against the globally registered symbols first, then the opened libraries, then the table exec() supplied. The handles are closed when the module is removed. Four things had to be fixed to make it work, none of which a build shows. reldata was a file-scope global. Opening a library from inside libelf_relocatedyn() makes that function reentrant, so the nested load overwrote the outer one's relocation offsets and the module resumed binding with the library's DT_REL. It is now per call. A cross-object call needs the callee's data base, not the caller's. A symbol resolved from an FDPIC library comes back as a descriptor, and R_ARM_FUNCDESC_VALUE was treating it as a code address and pairing it with the importing module's GOT. It now copies both words, so the library runs with its own. An object with no imports has no PLT and so no DT_PLTGOT, but it still has a GOT and still has to be entered with it. Without the fallback its descriptors carried a data base of zero and the library read its globals through a null pointer. libelf_symname() was static, and reading a DT_NEEDED name needs it. Nothing happens without CONFIG_LIBC_DLFCN; a module with DT_NEEDED is refused there, since there is no way to load what it asks for. Built and run on lm3s6965-ek: a module naming a library in DT_NEEDED calls into it and gets the right answer, and the library keeps its own data. mps3-an547:picostest and lm3s6965-ek:qemu-nxflat, which have CONFIG_LIBC_DLFCN off, build and run unchanged. CONFIG_FDPIC depends on the flat build. A module's read-only segment is held by a filesystem pin that has to be given back when the module is unloaded, which happens on a task other than the one that loaded it, so it is held through a reference to the file rather than a descriptor -- and the file interface is not reachable from the loader in the protected and kernel builds. Selecting it there would leak the pin and leave the filesystem unable to compact. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Marco Casaroli --- arch/arm/include/elf.h | 22 +-- binfmt/Kconfig | 3 +- include/nuttx/fdpic.h | 4 +- include/nuttx/lib/elf.h | 20 ++- include/nuttx/signal.h | 2 +- libs/libc/dirent/lib_scandir.c | 4 +- libs/libc/elf/Kconfig | 7 + libs/libc/elf/elf.h | 11 ++ libs/libc/elf/elf_bind.c | 189 ++++++++++++++++++----- libs/libc/elf/elf_load.c | 36 ++--- libs/libc/elf/elf_remove.c | 17 +- libs/libc/elf/elf_symbols.c | 19 +-- libs/libc/machine/arm/armv7-m/arch_elf.c | 39 ++--- libs/libc/machine/arm/armv8-m/arch_elf.c | 39 ++--- libs/libc/pthread/pthread_create.c | 4 +- libs/libc/pthread/pthread_once.c | 4 +- libs/libc/stdlib/lib_bsearch.c | 4 +- libs/libc/stdlib/lib_qsort.c | 4 +- sched/mqueue/mq_notify.c | 4 +- sched/signal/sig_action.c | 4 +- sched/signal/sig_notification.c | 6 +- sched/task/task_create.c | 4 +- sched/task/task_spawn.c | 4 +- sched/timer/timer_create.c | 4 +- 24 files changed, 283 insertions(+), 171 deletions(-) diff --git a/arch/arm/include/elf.h b/arch/arm/include/elf.h index 4e6411de733ee..1975454677b7b 100644 --- a/arch/arm/include/elf.h +++ b/arch/arm/include/elf.h @@ -265,16 +265,9 @@ #define DT_ARM_PREEMPTMAP 0x70000002 #define DT_ARM_RESERVED2 0x70000003 -/* Per-object state that the FDPIC relocations need and that a relocation's - * own arguments cannot supply. - * - * up_relocate() is handed a relocation, a resolved symbol and the address - * to patch, which is enough for every other ARM relocation. The two - * FDPIC ones need more: a function descriptor's second word is the - * *object's* data base, and R_ARM_FUNCDESC has to manufacture descriptors - * from a pool whose cursor must survive from one relocation to the next. - * Both are loader state, so they arrive through the arch_data channel, - * seeded by libelf_bind() before the relocation loop and read back after. +/* Loader state the FDPIC relocations need but a relocation cannot carry: + * the object's data base, and a descriptor pool cursor that has to survive + * from one relocation to the next. */ /* The relocations that only an FDPIC object may use. Seeing one in an @@ -310,12 +303,8 @@ #ifndef __ASSEMBLY__ -/* A function descriptor: what an FDPIC function pointer actually is. - * - * Base firmware is not built FDPIC, so to it a function pointer is a code - * address. A module passes the address of one of these instead, and the - * callee is entered with got in the PIC base register so that it can - * reach its own data. +/* An FDPIC function pointer: the code, plus the data base to enter it + * with. */ struct arm_fdpic_desc_s @@ -331,6 +320,7 @@ struct arch_elfdata_s uintptr_t descpool; /* Base of the descriptor pool */ uint16_t ndesc; /* Capacity, in descriptors */ uint16_t usedesc; /* Next free slot */ + uint8_t symisdesc; /* Symbol value is a descriptor, not code */ uint8_t pltrel; /* Relocation comes from DT_JMPREL, so the word * it overwrites is a lazy binding stub and not * an addend diff --git a/binfmt/Kconfig b/binfmt/Kconfig index c7cc0ad109c0c..810fbf648a3bd 100644 --- a/binfmt/Kconfig +++ b/binfmt/Kconfig @@ -61,11 +61,12 @@ config ELF_STACKSIZE ---help--- This is the default stack size that will be used when starting ELF binaries. -config ELF_FDPIC +config FDPIC bool "FDPIC modules" default n select PIC depends on ARCH_ARMV7M || ARCH_ARMV8M + depends on BUILD_FLAT ---help--- Load ELF modules built for the FDPIC ABI. diff --git a/include/nuttx/fdpic.h b/include/nuttx/fdpic.h index 8dcd5e1b02a2a..6babd3516e95e 100644 --- a/include/nuttx/fdpic.h +++ b/include/nuttx/fdpic.h @@ -57,7 +57,7 @@ struct fdpic_desc_s * Inline Functions ****************************************************************************/ -#ifdef CONFIG_ELF_FDPIC +#ifdef CONFIG_FDPIC /**************************************************************************** * Name: fdpic_base @@ -173,6 +173,6 @@ static inline void fdpic_invoke(uintptr_t entry, uintptr_t arg, # define fdpic_invoke(entry, arg, got) \ ((void)(got), (((CODE void (*)(uintptr_t))(uintptr_t)(entry))(arg))) -#endif /* CONFIG_ELF_FDPIC */ +#endif /* CONFIG_FDPIC */ #endif /* __INCLUDE_NUTTX_FDPIC_H */ diff --git a/include/nuttx/lib/elf.h b/include/nuttx/lib/elf.h index 832616ca16045..317bce0e144c9 100644 --- a/include/nuttx/lib/elf.h +++ b/include/nuttx/lib/elf.h @@ -44,6 +44,10 @@ # define CONFIG_LIBC_ELF_MAXDEPEND 0 #endif +#ifndef CONFIG_LIBC_ELF_MAXNEEDED +# define CONFIG_LIBC_ELF_MAXNEEDED 0 +#endif + /* Holding an XIP pin past the load means holding the file itself: the pin is * released when the module is unloaded, which happens on a task other than * the one that loaded it, so a descriptor from that task's group cannot @@ -209,6 +213,16 @@ struct module_s * module goes when the last does */ +#ifdef CONFIG_LIBC_DLFCN + /* Libraries opened with dlopen() for this module's DT_NEEDED entries. + * These are references this module holds on others, where nopen above + * counts the references others hold on this one. + */ + + FAR void *libs[CONFIG_LIBC_ELF_MAXNEEDED]; + uint8_t nlibs; +#endif + #if CONFIG_LIBC_ELF_MAXDEPEND > 0 uint8_t dependents; /* Number of modules that depend on this module */ @@ -808,7 +822,11 @@ FAR const void *libelf_getsymbol(FAR void *handle, FAR const char *name); * Name: libelf_uninit * * Description: - * Uninitialize module resources. + * Uninitialize module resources. This gives up everything the module + * holds, including any libraries it opened for its DT_NEEDED entries, so + * the caller must be releasing the last reference to it: libelf_remove() + * calls this only once nopen reaches zero, and the copy binfmt keeps + * belongs to a single exec'd binary and is never shared. * ****************************************************************************/ diff --git a/include/nuttx/signal.h b/include/nuttx/signal.h index 2274b4b0460cd..46b3a0e703f7e 100644 --- a/include/nuttx/signal.h +++ b/include/nuttx/signal.h @@ -68,7 +68,7 @@ struct sigwork_s struct work_s work; /* Work queue structure */ union sigval value; /* Data passed with notification */ sigev_notify_function_t func; /* Notification function */ -#ifdef CONFIG_ELF_FDPIC +#ifdef CONFIG_FDPIC uintptr_t got; /* FDPIC data base of a module callback, or * zero. Captured at registration, installed * around the call on the worker thread. */ diff --git a/libs/libc/dirent/lib_scandir.c b/libs/libc/dirent/lib_scandir.c index 098bfd4366cae..5557ade6fdf8f 100644 --- a/libs/libc/dirent/lib_scandir.c +++ b/libs/libc/dirent/lib_scandir.c @@ -31,7 +31,7 @@ #include #include -#ifdef CONFIG_ELF_FDPIC +#ifdef CONFIG_FDPIC # include #endif @@ -95,7 +95,7 @@ int scandir(FAR const char *path, FAR struct dirent ***namelist, * the original errno value to be able to restore it in case of success. */ -#ifdef CONFIG_ELF_FDPIC +#ifdef CONFIG_FDPIC /* An FDPIC module passes the address of a function descriptor, not a code * address. Resolve the filter, which is called from the loop below. * diff --git a/libs/libc/elf/Kconfig b/libs/libc/elf/Kconfig index 2fd36399d96b4..fafff3251992c 100644 --- a/libs/libc/elf/Kconfig +++ b/libs/libc/elf/Kconfig @@ -12,6 +12,13 @@ config LIBC_ELF menu "Module library configuration" depends on LIBC_ELF +config LIBC_ELF_MAXNEEDED + int "Maximum DT_NEEDED libraries per module" + default 4 + ---help--- + How many shared libraries one module may name in DT_NEEDED. Each + is opened with dlopen() while the module loads. + config LIBC_ELF_MAXDEPEND int "Max dependencies" default 2 diff --git a/libs/libc/elf/elf.h b/libs/libc/elf/elf.h index e00e1e4a19cc6..ef3edf53dcefb 100644 --- a/libs/libc/elf/elf.h +++ b/libs/libc/elf/elf.h @@ -262,6 +262,17 @@ int libelf_freebuffers(FAR struct mod_loadinfo_s *loadinfo); * ****************************************************************************/ +/**************************************************************************** + * Name: libelf_symname + * + * Description: + * Read a name out of a string table into the I/O buffer. + * + ****************************************************************************/ + +int libelf_symname(FAR struct mod_loadinfo_s *loadinfo, + FAR const Elf_Sym *sym, Elf_Off sh_offset); + static inline uintptr_t libelf_addr(FAR struct mod_loadinfo_s *loadinfo, uintptr_t vaddr) { diff --git a/libs/libc/elf/elf_bind.c b/libs/libc/elf/elf_bind.c index 28350a622d19d..70469ac5078aa 100644 --- a/libs/libc/elf/elf_bind.c +++ b/libs/libc/elf/elf_bind.c @@ -35,6 +35,8 @@ #include #include #include +#include + #include #include @@ -103,7 +105,14 @@ typedef struct int idx; } Elf_SymCache; -struct +/* Where a dynamic object's relocation tables live. This is per load, not + * per file: libelf_relocatedyn() dlopen()s what the object needs, which + * re-enters it for the library, so a shared instance would be overwritten + * by the nested load and the outer one would resume with the library's + * offsets. + */ + +struct reldata_s { int stroff; /* offset to string table */ int symoff; /* offset to symbol table */ @@ -112,7 +121,7 @@ struct int reloff[2]; /* offset to the relocation section */ int relsz[2]; /* size of relocation table */ int relrela[2]; /* type of relocation type - 0: DT_REL / 1: DT_RELA */ -} reldata; +}; /**************************************************************************** * Private Functions @@ -685,6 +694,15 @@ static int libelf_relocatedyn(FAR struct module_s *modp, int i; int idx_rel; int idx_sym; +#ifdef CONFIG_LIBC_DLFCN + int j; + uintptr_t libs[CONFIG_LIBC_ELF_MAXNEEDED]; +#endif + int nlibs = 0; + struct reldata_s reldata; +#ifdef HAVE_ARCH_ELF_FDPIC + bool symfromlib; +#endif /* Define potential architecture specific elf data container */ @@ -747,23 +765,24 @@ static int libelf_relocatedyn(FAR struct module_s *modp, break; case DT_NEEDED: - /* Shared libraries belong to dlopen(), not to a loader that - * walks dependencies itself. Nothing in the tree loads - * DT_NEEDED today, so rather than resolve it badly, refuse - * the module and say why -- otherwise it would load and then - * fault on its first call into the library that is not there. + /* Remember it; the name lives in the string table, which is + * not located until the loop has seen DT_STRTAB. */ - if (loadinfo->fdpic) + if (nlibs >= CONFIG_LIBC_ELF_MAXNEEDED) { - berr("ERROR: FDPIC module has a DT_NEEDED entry. Shared " - "libraries are not supported; link it statically.\n"); + berr("ERROR: More than %d DT_NEEDED entries\n", + CONFIG_LIBC_ELF_MAXNEEDED); lib_free(sym); lib_free(rels); lib_free(dyn); return -ENOEXEC; } +#ifdef CONFIG_LIBC_DLFCN + libs[nlibs] = dyn[i].d_un.d_val; +#endif + nlibs++; break; case DT_PLTGOT: @@ -837,13 +856,90 @@ static int libelf_relocatedyn(FAR struct module_s *modp, } } - /* Hand the loader's per-object state to the relocations that need it. - * This has to follow the loop above, not precede it: DT_PLTGOT is read - * there, and a descriptor built with a data base of zero would fault on - * the module's first call through it. - * - * Both relocation tables are then walked under this one arch_data, so a - * cursor kept in it survives from one table to the next. + /* An object with no imports has no PLT and so no DT_PLTGOT, but it still + * has a GOT and still has to be entered with it: the linker puts it + * immediately after the dynamic section. + */ + + if (loadinfo->fdpic && loadinfo->gotaddr == 0) + { + loadinfo->gotaddr = libelf_addr(loadinfo, + shdr->sh_addr + shdr->sh_size); + binfo("No DT_PLTGOT; taking the GOT at %08lx\n", + (unsigned long)loadinfo->gotaddr); + } + + /* Open whatever the object names in DT_NEEDED. dlopen() is the loader + * for a shared library, so hand the work to it. + */ + +#ifdef CONFIG_LIBC_DLFCN + + symhdr = &loadinfo->shdr[loadinfo->dsymtabidx]; + + for (i = 0; i < nlibs; i++) + { + Elf_Sym namesym; + FAR void *handle; + + /* The name is a string table offset, which is what st_name is, so + * the existing reader can fetch it. + */ + + memset(&namesym, 0, sizeof(namesym)); + namesym.st_name = libs[i]; + + ret = libelf_symname(loadinfo, &namesym, + loadinfo->shdr[symhdr->sh_link].sh_offset); + if (ret < 0) + { + berr("ERROR: DT_NEEDED %d has no name\n", i); + lib_free(sym); + lib_free(rels); + lib_free(dyn); + return ret; + } + + handle = dlopen((FAR const char *)loadinfo->iobuffer, RTLD_NOW); + if (handle == NULL) + { + berr("ERROR: Cannot open needed library %s\n", + (FAR char *)loadinfo->iobuffer); + lib_free(sym); + lib_free(rels); + lib_free(dyn); + return -ELIBACC; + } + + binfo("Opened needed library %s\n", (FAR char *)loadinfo->iobuffer); + + if (modp->nlibs >= CONFIG_LIBC_ELF_MAXNEEDED) + { + dlclose(handle); + lib_free(sym); + lib_free(rels); + lib_free(dyn); + return -ENOMEM; + } + + modp->libs[modp->nlibs++] = handle; + } + +#else + if (nlibs > 0) + { + berr("ERROR: DT_NEEDED needs CONFIG_LIBC_DLFCN to load %d " + "librar%s\n", nlibs, nlibs == 1 ? "y" : "ies"); + lib_free(sym); + lib_free(rels); + lib_free(dyn); + return -ENOSYS; + } +#endif + + /* Must follow the tag loop, which is where DT_PLTGOT is read. Both + * relocation tables are walked under this one arch_data, so a cursor in + * it spans the object. */ ARCH_ELFDATA_SETUP(loadinfo); @@ -966,19 +1062,38 @@ static int libelf_relocatedyn(FAR struct module_s *modp, { FAR void *ep; +#ifdef HAVE_ARCH_ELF_FDPIC + symfromlib = false; +#endif ep = libelf_findglobal(modp, loadinfo, symhdr, &sym[idx_sym]); - /* libelf_findglobal() searches only the table of - * globally registered symbols. A module loaded - * through exec() is given its own export table - * instead, and until now nothing on this path looked - * at it -- harmless while ET_DYN modules resolved - * everything internally, but an FDPIC module imports - * its libc from exactly there. libelf_symname() has - * just left the name in the I/O buffer. + /* libelf_findglobal() searches only the globally + * registered symbols, and has left the name in the + * I/O buffer. Try the DT_NEEDED libraries next, then + * the table exec() supplied. */ +#ifdef CONFIG_LIBC_DLFCN + for (j = 0; ep == NULL && j < modp->nlibs; j++) + { + ep = (FAR void *) + libelf_getsymbol(modp->libs[j], + (FAR char *)loadinfo->iobuffer); + if (ep != NULL) + { +# ifdef HAVE_ARCH_ELF_FDPIC + /* Coming from a library is what tells the + * relocation this is a descriptor. + */ + + symfromlib = true; +# endif + break; + } + } +#endif + if (ep == NULL && exports != NULL) { FAR const struct symtab_s *sm; @@ -1028,7 +1143,9 @@ static int libelf_relocatedyn(FAR struct module_s *modp, }; extsym.st_value = (uintptr_t)ep; - +#ifdef HAVE_ARCH_ELF_FDPIC + arch_data.symisdesc = symfromlib; +#endif ret = up_relocate(rel, &extsym, addr, ARCH_ELFDATA_PARM); if (ret < 0) @@ -1049,19 +1166,9 @@ static int libelf_relocatedyn(FAR struct module_s *modp, } else if (loadinfo->fdpic) { - /* A relocation naming a symbol defined inside this - * object. Nothing handled these before, which was - * harmless while every dynamic relocation reaching here - * carried no symbol at all -- R_ARM_RELATIVE has symbol - * index zero and is dealt with below. - * - * FDPIC brings the first ones that do. 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. + /* A symbol defined inside this object. Its value is + * the symbol's own, translated; the addend stays where + * the relocation type expects it. */ Elf_Sym defsym = sym[idx_sym]; @@ -1120,9 +1227,7 @@ static int libelf_relocatedyn(FAR struct module_s *modp, } } - /* Hand back what the relocations consumed. The error paths above do - * not bother: the load is being abandoned, so the cursor has no reader. - */ + /* Hand back what the relocations consumed. */ ARCH_ELFDATA_TEARDOWN(loadinfo); diff --git a/libs/libc/elf/elf_load.c b/libs/libc/elf/elf_load.c index 93e1d040616d8..18a6e25793ded 100644 --- a/libs/libc/elf/elf_load.c +++ b/libs/libc/elf/elf_load.c @@ -240,16 +240,9 @@ static void libelf_elfsize(FAR struct mod_loadinfo_s *loadinfo, bool alloc) } } - /* An FDPIC object may ask the loader to manufacture function - * descriptors -- that is what R_ARM_FUNCDESC means -- and hand back - * their addresses. They have to live somewhere the module can reach - * through its data base, and the space has to be reserved now, because - * by the time the relocation is applied the segment has been placed. - * - * One relocation cannot ask for more than one descriptor, so the - * relocation count bounds the pool. Modules are small and a descriptor - * is two words, so the slack in that bound is cheaper than walking the - * relocations twice. + /* Reserve the descriptor pool now: a relocation may ask the loader to + * manufacture one, and by then the segment has been placed. Bounded by + * the relocation and symbol counts, which is loose but cheap. */ if (loadinfo->fdpic) @@ -266,10 +259,7 @@ static void libelf_elfsize(FAR struct mod_loadinfo_s *loadinfo, bool alloc) } } - /* A library also publishes a descriptor for each function it - * exports, so that dlsym() can hand back something callable. The - * dynamic symbol table bounds how many that can be. - */ + /* Exported functions get one each, for dlsym(). */ for (i = 0; i < loadinfo->ehdr.e_shnum; i++) { @@ -417,10 +407,7 @@ static inline int libelf_loadfile(FAR struct mod_loadinfo_s *loadinfo) { if (loadinfo->fdpic) { - /* Mapped, not copied. Copying it here would put the - * text in RAM and forfeit the only thing this format - * was chosen for. - */ + /* Mapped, not copied. */ continue; } @@ -824,12 +811,8 @@ int libelf_load(FAR struct mod_loadinfo_s *loadinfo) { if (loadinfo->fdpic) { - /* An FDPIC object reaches its data through the GOT rather than - * at a fixed distance from its code, so the two segments do not - * have to stay adjacent -- which is the entire point. The - * read-only one is mapped where it already lies on the media - * and never copied; only the writable one is allocated, and - * that happens once per running instance. + /* Text is mapped where it lies on the media and never copied; + * only the writable segment is allocated, once per instance. */ if (loadinfo->xipbase == 0) @@ -839,9 +822,8 @@ int libelf_load(FAR struct mod_loadinfo_s *loadinfo) goto errout_with_buffers; } - /* The media address is the base of the file, so the segment's - * own file offset still has to be added -- the same arithmetic - * the ET_REL path does with sh_offset. + /* The media address is the base of the file, so add the + * segment's own file offset. */ for (i = 0; i < loadinfo->ehdr.e_phnum; i++) diff --git a/libs/libc/elf/elf_remove.c b/libs/libc/elf/elf_remove.c index fe4d97b10239f..fb11c47a5ef0a 100644 --- a/libs/libc/elf/elf_remove.c +++ b/libs/libc/elf/elf_remove.c @@ -29,6 +29,8 @@ #include #include +#include + #include #include @@ -42,7 +44,11 @@ * Name: libelf_uninit * * Description: - * Uninitialize module resources. + * Uninitialize module resources. This gives up everything the module + * holds, including any libraries it opened for its DT_NEEDED entries, so + * the caller must be releasing the last reference to it: libelf_remove() + * calls this only once nopen reaches zero, and the copy binfmt keeps + * belongs to a single exec'd binary and is never shared. * ****************************************************************************/ @@ -62,6 +68,15 @@ int libelf_uninit(FAR struct module_s *modp) } #endif +#ifdef CONFIG_LIBC_DLFCN + /* Let go of anything opened for DT_NEEDED. */ + + while (modp->nlibs > 0) + { + dlclose(modp->libs[--modp->nlibs]); + } +#endif + /* Is there an uninitializer? */ array = (FAR void (**)(void))modp->finiarr; diff --git a/libs/libc/elf/elf_symbols.c b/libs/libc/elf/elf_symbols.c index 53e7f9afe43db..078f8ef8cedff 100644 --- a/libs/libc/elf/elf_symbols.c +++ b/libs/libc/elf/elf_symbols.c @@ -93,8 +93,8 @@ extern int nglobals; * ****************************************************************************/ -static int libelf_symname(FAR struct mod_loadinfo_s *loadinfo, - FAR const Elf_Sym *sym, Elf_Off sh_offset) +int libelf_symname(FAR struct mod_loadinfo_s *loadinfo, + FAR const Elf_Sym *sym, Elf_Off sh_offset) { FAR uint8_t *buffer; off_t offset; @@ -547,17 +547,10 @@ int libelf_insertsymtab(FAR struct module_s *modp, symbol[j].sym_value = (FAR const void *)(uintptr_t)sym[i].st_value; - /* An FDPIC caller cannot branch to a bare code address: - * it needs the callee's data base too. So a function - * exported by an FDPIC object is published as the - * address of a descriptor rather than of its code, and - * dlsym() hands back something that can simply be - * called. - * - * This is the only point that can do it. The exported - * table carries no type information, so by the time - * dlsym() is asked there is no way to tell a function - * from an object; here st_info still says. + /* Publish a function as a descriptor, not a code + * address, so dlsym() returns something an FDPIC caller + * can branch through. Only here does st_info still say + * which symbols are functions. */ if (loadinfo->fdpic && diff --git a/libs/libc/machine/arm/armv7-m/arch_elf.c b/libs/libc/machine/arm/armv7-m/arch_elf.c index 205ffaca59796..d93968c859380 100644 --- a/libs/libc/machine/arm/armv7-m/arch_elf.c +++ b/libs/libc/machine/arm/armv7-m/arch_elf.c @@ -177,22 +177,9 @@ int up_relocate(const Elf32_Rel *rel, const Elf32_Sym *sym, uintptr_t addr, case R_ARM_FUNCDESC_VALUE: { - /* The target is the descriptor: two words, the entry point and - * the data base to install before branching to it. - * - * REL format keeps the addend in place, in the word that is about - * to become the entry point, and it matters. A pointer to a - * static function is referenced through its *section* symbol, - * whose value is the section base, with the offset -- and the - * Thumb bit -- carried entirely by the addend. Dropping it - * yields an even address and the core faults trying to execute it - * as ARM code. - * - * The GOT written here is this object's own, even for an imported - * function. That is deliberate and is what makes a callback - * work: when the base firmware's qsort() calls back into the - * module's comparison function, the module needs its own data - * base in the PIC register. + /* The target is the descriptor itself. REL keeps the addend in + * the word about to become the entry point, and it carries the + * Thumb bit, so it must be added rather than dropped. */ FAR struct arm_fdpic_desc_s *desc = @@ -220,7 +207,16 @@ int up_relocate(const Elf32_Rel *rel, const Elf32_Sym *sym, uintptr_t addr, "at addr=%08" PRIxPTR " to sym=%p st_value=%08" PRIx32 "\n", addr, sym, sym->st_value); - if (data->pltrel) + if (data->symisdesc) + { + /* Resolved to a function in another object, which published a + * descriptor of its own. Take both words: the callee has to + * run with its own data base, not ours. + */ + + *desc = *(FAR struct arm_fdpic_desc_s *)sym->st_value; + } + else if (data->pltrel) { /* A lazy descriptor holds the address of its own PLT resolution * stub and a data base of -1, for a resolver to overwrite on @@ -229,21 +225,20 @@ int up_relocate(const Elf32_Rel *rel, const Elf32_Sym *sym, uintptr_t addr, */ desc->entry = sym->st_value; + desc->got = data->gotaddr; } else { desc->entry = sym->st_value + desc->entry; + desc->got = data->gotaddr; } - - desc->got = data->gotaddr; } break; case R_ARM_FUNCDESC: { - /* A pointer to a descriptor, which the loader has to supply. - * Carve one out of the pool reserved behind the writable segment - * and store its address. + /* A pointer to a descriptor the loader must supply. Carve one + * from the pool and store its address. */ FAR struct arm_fdpic_desc_s *desc; diff --git a/libs/libc/machine/arm/armv8-m/arch_elf.c b/libs/libc/machine/arm/armv8-m/arch_elf.c index 2972d737d229e..50037bb3570b9 100644 --- a/libs/libc/machine/arm/armv8-m/arch_elf.c +++ b/libs/libc/machine/arm/armv8-m/arch_elf.c @@ -177,22 +177,9 @@ int up_relocate(const Elf32_Rel *rel, const Elf32_Sym *sym, uintptr_t addr, case R_ARM_FUNCDESC_VALUE: { - /* The target is the descriptor: two words, the entry point and - * the data base to install before branching to it. - * - * REL format keeps the addend in place, in the word that is about - * to become the entry point, and it matters. A pointer to a - * static function is referenced through its *section* symbol, - * whose value is the section base, with the offset -- and the - * Thumb bit -- carried entirely by the addend. Dropping it - * yields an even address and the core faults trying to execute it - * as ARM code. - * - * The GOT written here is this object's own, even for an imported - * function. That is deliberate and is what makes a callback - * work: when the base firmware's qsort() calls back into the - * module's comparison function, the module needs its own data - * base in the PIC register. + /* The target is the descriptor itself. REL keeps the addend in + * the word about to become the entry point, and it carries the + * Thumb bit, so it must be added rather than dropped. */ FAR struct arm_fdpic_desc_s *desc = @@ -220,7 +207,16 @@ int up_relocate(const Elf32_Rel *rel, const Elf32_Sym *sym, uintptr_t addr, "at addr=%08" PRIxPTR " to sym=%p st_value=%08" PRIx32 "\n", addr, sym, sym->st_value); - if (data->pltrel) + if (data->symisdesc) + { + /* Resolved to a function in another object, which published a + * descriptor of its own. Take both words: the callee has to + * run with its own data base, not ours. + */ + + *desc = *(FAR struct arm_fdpic_desc_s *)sym->st_value; + } + else if (data->pltrel) { /* A lazy descriptor holds the address of its own PLT resolution * stub and a data base of -1, for a resolver to overwrite on @@ -229,21 +225,20 @@ int up_relocate(const Elf32_Rel *rel, const Elf32_Sym *sym, uintptr_t addr, */ desc->entry = sym->st_value; + desc->got = data->gotaddr; } else { desc->entry = sym->st_value + desc->entry; + desc->got = data->gotaddr; } - - desc->got = data->gotaddr; } break; case R_ARM_FUNCDESC: { - /* A pointer to a descriptor, which the loader has to supply. - * Carve one out of the pool reserved behind the writable segment - * and store its address. + /* A pointer to a descriptor the loader must supply. Carve one + * from the pool and store its address. */ FAR struct arm_fdpic_desc_s *desc; diff --git a/libs/libc/pthread/pthread_create.c b/libs/libc/pthread/pthread_create.c index 84406072c3dd0..34676fada518e 100644 --- a/libs/libc/pthread/pthread_create.c +++ b/libs/libc/pthread/pthread_create.c @@ -30,7 +30,7 @@ #include -#ifdef CONFIG_ELF_FDPIC +#ifdef CONFIG_FDPIC # include #endif @@ -92,7 +92,7 @@ static void pthread_startup(pthread_startroutine_t entry, int pthread_create(FAR pthread_t *thread, FAR const pthread_attr_t *attr, pthread_startroutine_t pthread_entry, pthread_addr_t arg) { -#ifdef CONFIG_ELF_FDPIC +#ifdef CONFIG_FDPIC /* An FDPIC module passes the address of a function descriptor, not a code * address. Resolve it here, once, in the public entry point. * diff --git a/libs/libc/pthread/pthread_once.c b/libs/libc/pthread/pthread_once.c index afc4c655c2631..267f4e6891a7e 100644 --- a/libs/libc/pthread/pthread_once.c +++ b/libs/libc/pthread/pthread_once.c @@ -33,7 +33,7 @@ #include #include -#ifdef CONFIG_ELF_FDPIC +#ifdef CONFIG_FDPIC # include #endif @@ -77,7 +77,7 @@ int pthread_once(FAR pthread_once_t *once_control, return EINVAL; } -#ifdef CONFIG_ELF_FDPIC +#ifdef CONFIG_FDPIC /* An FDPIC module passes the address of a function descriptor, not a code * address. Resolve it here, in the public entry point. * diff --git a/libs/libc/stdlib/lib_bsearch.c b/libs/libc/stdlib/lib_bsearch.c index cecd62b15857a..c449480ad9152 100644 --- a/libs/libc/stdlib/lib_bsearch.c +++ b/libs/libc/stdlib/lib_bsearch.c @@ -38,7 +38,7 @@ #include -#ifdef CONFIG_ELF_FDPIC +#ifdef CONFIG_FDPIC # include #endif #include @@ -118,7 +118,7 @@ FAR void *bsearch(FAR const void *key, FAR const void *base, size_t nel, DEBUGASSERT(base != NULL || nel == 0); DEBUGASSERT(compar != NULL); -#ifdef CONFIG_ELF_FDPIC +#ifdef CONFIG_FDPIC /* See qsort(): an FDPIC caller passes a descriptor, not a code address */ compar = (CODE int (*)(FAR const void *, FAR const void *)) diff --git a/libs/libc/stdlib/lib_qsort.c b/libs/libc/stdlib/lib_qsort.c index e82dd671e714d..d57b38e007a51 100644 --- a/libs/libc/stdlib/lib_qsort.c +++ b/libs/libc/stdlib/lib_qsort.c @@ -45,7 +45,7 @@ #include #include -#ifdef CONFIG_ELF_FDPIC +#ifdef CONFIG_FDPIC # include #endif @@ -311,7 +311,7 @@ static void qsort_internal(FAR void *base, size_t nel, size_t width, void qsort(FAR void *base, size_t nel, size_t width, CODE int(*compar)(FAR const void *, FAR const void *)) { -#ifdef CONFIG_ELF_FDPIC +#ifdef CONFIG_FDPIC /* An FDPIC module passes the address of a function descriptor, not a * code address. */ diff --git a/sched/mqueue/mq_notify.c b/sched/mqueue/mq_notify.c index fc13c96122e35..3dd9372427b10 100644 --- a/sched/mqueue/mq_notify.c +++ b/sched/mqueue/mq_notify.c @@ -34,7 +34,7 @@ #include #include -#if defined(CONFIG_ELF_FDPIC) && defined(CONFIG_SIG_EVTHREAD) +#if defined(CONFIG_FDPIC) && defined(CONFIG_SIG_EVTHREAD) # include #endif @@ -161,7 +161,7 @@ int mq_notify(mqd_t mqdes, FAR const struct sigevent *notification) msgq->ntpid = rtcb->pid; -#if defined(CONFIG_ELF_FDPIC) && defined(CONFIG_SIG_EVTHREAD) +#if defined(CONFIG_FDPIC) && defined(CONFIG_SIG_EVTHREAD) /* If a module registered a SIGEV_THREAD callback, capture its data * base now, while this runs in the module's context. The callback * fires later on a work-queue worker that has no base of its own; diff --git a/sched/signal/sig_action.c b/sched/signal/sig_action.c index 1891218fdfaa1..a6b32feffbdc0 100644 --- a/sched/signal/sig_action.c +++ b/sched/signal/sig_action.c @@ -38,7 +38,7 @@ #include #include -#ifdef CONFIG_ELF_FDPIC +#ifdef CONFIG_FDPIC # include #endif @@ -330,7 +330,7 @@ int nxsig_action(int signo, FAR const struct sigaction *act, handler = act->sa_handler; -#ifdef CONFIG_ELF_FDPIC +#ifdef CONFIG_FDPIC /* An FDPIC module passes the address of a function descriptor, not a code * address. Resolve it here, in the innermost common code, so a module * that calls sigaction() directly is covered as well as one that goes diff --git a/sched/signal/sig_notification.c b/sched/signal/sig_notification.c index b6f38ef5de386..30010472c23d2 100644 --- a/sched/signal/sig_notification.c +++ b/sched/signal/sig_notification.c @@ -34,7 +34,7 @@ #include -#ifdef CONFIG_ELF_FDPIC +#ifdef CONFIG_FDPIC # include #endif @@ -74,7 +74,7 @@ static void nxsig_notification_worker(FAR void *arg) /* Perform the callback */ -#ifdef CONFIG_ELF_FDPIC +#ifdef CONFIG_FDPIC /* A module's callback runs here on a shared worker thread, which does not * carry the module's data base. Install the base captured at * registration around the call so the callback can reach its own globals; @@ -178,7 +178,7 @@ int nxsig_notification(pid_t pid, FAR struct sigevent *event, work->value = event->sigev_value; work->func = event->sigev_notify_function; -#ifdef CONFIG_ELF_FDPIC +#ifdef CONFIG_FDPIC /* When the callback is a module's, work->got was set at registration * to the module's data base (this runs at send or expiry time, whose * context is not the module's, so it cannot be read here). The diff --git a/sched/task/task_create.c b/sched/task/task_create.c index a75b16994926d..6f7e99d657bdb 100644 --- a/sched/task/task_create.c +++ b/sched/task/task_create.c @@ -37,7 +37,7 @@ #include #include -#ifdef CONFIG_ELF_FDPIC +#ifdef CONFIG_FDPIC # include #endif @@ -208,7 +208,7 @@ int task_create_with_stack(FAR const char *name, int priority, { int ret; -#ifdef CONFIG_ELF_FDPIC +#ifdef CONFIG_FDPIC /* An FDPIC module passes the address of a function descriptor, not a code * address. Resolving it here covers task_create() too, which is a plain * forwarder -- and covers it exactly once, which matters: resolving twice diff --git a/sched/task/task_spawn.c b/sched/task/task_spawn.c index 9042e40ad8be3..d475eddeebf17 100644 --- a/sched/task/task_spawn.c +++ b/sched/task/task_spawn.c @@ -38,7 +38,7 @@ #include #include -#ifdef CONFIG_ELF_FDPIC +#ifdef CONFIG_FDPIC # include #endif @@ -339,7 +339,7 @@ int task_spawn(FAR const char *name, main_t entry, pid_t pid = INVALID_PROCESS_ID; int ret; -#ifdef CONFIG_ELF_FDPIC +#ifdef CONFIG_FDPIC /* An FDPIC module passes the address of a function descriptor, not a code * address. Resolve it here, once, in the public entry point. * diff --git a/sched/timer/timer_create.c b/sched/timer/timer_create.c index 5c25999252ba5..36562fad88d39 100644 --- a/sched/timer/timer_create.c +++ b/sched/timer/timer_create.c @@ -37,7 +37,7 @@ #include #include -#if defined(CONFIG_ELF_FDPIC) && defined(CONFIG_SIG_EVTHREAD) +#if defined(CONFIG_FDPIC) && defined(CONFIG_SIG_EVTHREAD) # include #endif @@ -201,7 +201,7 @@ int timer_create(clockid_t clockid, FAR struct sigevent *evp, memcpy(&ret->pt_event, evp, sizeof(struct sigevent)); -#if defined(CONFIG_ELF_FDPIC) && defined(CONFIG_SIG_EVTHREAD) +#if defined(CONFIG_FDPIC) && defined(CONFIG_SIG_EVTHREAD) /* If a module registered a SIGEV_THREAD callback, capture its * data base now, while this runs in the module's context. The * callback fires later on a work-queue worker with no base of From f91b878120d22aa760214d23a935943cde338f53 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 3 Aug 2026 15:53:43 +0200 Subject: [PATCH 10/12] tools/fdpic: Add the module build helpers the demo apps use. apps/examples/fdpicxip and apps/testing/fs/xipfs carry their modules as committed byte arrays, and regenerate them with make -C apps/examples/fdpicxip/modules regen NUTTX_DIR=/path/to/nuttx which reads nuttx-fdpic.mk and fdpic-embed.py from here. Both apps are already upstream and cannot rebuild their own blobs from source without this. nuttx-fdpic.mk also builds a module out of tree, which is what anyone writing one starts from; the README describes the four link flags that matter and why. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Marco Casaroli --- tools/fdpic/README.md | 61 ++++++++++ tools/fdpic/build-binutils.sh | 80 ++++++++++++ tools/fdpic/fdpic-embed.py | 115 ++++++++++++++++++ tools/fdpic/fdpic-verify.sh | 99 +++++++++++++++ tools/fdpic/nuttx-exports.sh | 102 ++++++++++++++++ tools/fdpic/nuttx-fdpic.mk | 221 ++++++++++++++++++++++++++++++++++ 6 files changed, 678 insertions(+) create mode 100644 tools/fdpic/README.md create mode 100755 tools/fdpic/build-binutils.sh create mode 100755 tools/fdpic/fdpic-embed.py create mode 100755 tools/fdpic/fdpic-verify.sh create mode 100755 tools/fdpic/nuttx-exports.sh create mode 100644 tools/fdpic/nuttx-fdpic.mk diff --git a/tools/fdpic/README.md b/tools/fdpic/README.md new file mode 100644 index 0000000000000..3e1f84413c821 --- /dev/null +++ b/tools/fdpic/README.md @@ -0,0 +1,61 @@ +# FDPIC module build tooling + +Everything needed to build an FDPIC module out of tree: a module is an ELF +shared object whose read-only segment the target maps straight out of flash +and executes in place, while its writable segment is copied to RAM once per +running instance. It links against nothing -- libc and everything else are +imported from the firmware's exported symbol table at load time. + +The loader that consumes these is `binfmt/fdpic.c`, enabled by `CONFIG_FDPIC`. +The full description of the format, the toolchain and the load-time contract +is in `Documentation/components/fdpic.rst`. + +## Contents + +| File | Purpose | +| --- | --- | +| `nuttx-fdpic.mk` | the module build itself; include it from a two-line makefile | +| `fdpic-verify.sh` | checks a built module's imports resolve against the firmware | +| `nuttx-exports.sh` | turns `libs/libc/exec_symtab.c` into a symbol list | +| `fdpic-embed.py` | turns a built module into a C header, for carrying one in an image | +| `build-binutils.sh` | builds the `arm-uclinuxfdpiceabi` binutils, the one from-source dependency | + +## Building a module + +A whole module is three lines of makefile beside the source. Taking +`apps/examples/fdpicxip/modules/qsorter.c`, which is a module in its own +right, as the source: + + MODULE = qsorter + SRCS = qsorter.c + + include /path/to/nuttx/tools/fdpic/nuttx-fdpic.mk + +Then: + + make NUTTX_DIR=/path/to/nuttx + CC qsorter.c + LD qsorter.fdpic + OK qsorter.fdpic: FDPIC, entry 0x2a1, 4 imports resolved + +`NUTTX_DIR` has to be a configured, built tree: the compile needs its headers +and the verify step needs the export table generated into +`libs/libc/exec_symtab.c`. + +Two toolchains are involved. The stock `arm-none-eabi` compiler does the +compiling -- it emits perfectly good FDPIC objects for both C and C++ -- and +`arm-uclinuxfdpiceabi` **binutils** does the linking, because +`arm-none-eabi-ld` cannot produce an FDPIC object at all. So the from-source +dependency is binutils alone, which `build-binutils.sh` builds in about a +minute. + +Verification runs as part of the default target on purpose: a module that +imports a symbol the firmware does not export links perfectly happily and +fails only once it is on the target, as a bare `-ENOENT` that names nothing. + +## Modules carried inside an image + +`fdpic-embed.py` exists for apps that have to load a module before there is any +way to put files on the target, so they embed one and write it out at run +time. `apps/examples/fdpicxip/modules/` uses it that way, and is the worked +example of driving this tooling for several modules at once. diff --git a/tools/fdpic/build-binutils.sh b/tools/fdpic/build-binutils.sh new file mode 100755 index 0000000000000..28e3277441b56 --- /dev/null +++ b/tools/fdpic/build-binutils.sh @@ -0,0 +1,80 @@ +#!/bin/bash +############################################################################ +# tools/fdpic/build-binutils.sh +# +# 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. +# +############################################################################ + +# Build arm-uclinuxfdpiceabi binutils -- the only part of the module +# toolchain that has to be built from source. +# +# The compiling is done by the stock Arm bare-metal toolchain, which emits +# correct FDPIC objects for both C and C++. What it cannot do is *link* +# them: arm-none-eabi-ld is configured with the `armelf` emulation alone, so +# it produces an object marked "UNIX - System V" that the loader refuses. +# The FDPIC linker carries armelf_linux_fdpiceabi, and that is the whole of +# the gap. +# +# So this builds binutils and nothing else: about a minute, roughly 23 MB. +# An FDPIC GCC is not needed for any of this. +# +# Usage: build-binutils.sh [install-prefix] +# +# Then add /bin to PATH. + +set -e + +WORK="${1:?usage: build-binutils.sh [prefix]}" +PREFIX="${2:-$WORK/toolchain}" +TARGET=arm-uclinuxfdpiceabi +BINUTILS=binutils-2.43 +J="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)" + +mkdir -p "$WORK/src" "$WORK/build" + +cd "$WORK/src" +[ -d "$BINUTILS" ] || { + curl -fL -O "https://ftp.gnu.org/gnu/binutils/$BINUTILS.tar.xz" + tar xf "$BINUTILS.tar.xz" +} + +rm -rf "$WORK/build/binutils" +mkdir -p "$WORK/build/binutils" +cd "$WORK/build/binutils" + +# --with-system-zlib because the bundled copy does not compile against the +# macOS SDK headers. Harmless elsewhere. + +"$WORK/src/$BINUTILS/configure" \ + --target="$TARGET" \ + --prefix="$PREFIX" \ + --disable-nls \ + --disable-werror \ + --with-system-zlib + +make -j"$J" +make install + +echo +echo "Installed to $PREFIX/bin" +echo +"$PREFIX/bin/$TARGET-ld" -V | head -8 +echo +echo "armelf_linux_fdpiceabi in the list above is the one that matters." +echo "Add to PATH: export PATH=$PREFIX/bin:\$PATH" diff --git a/tools/fdpic/fdpic-embed.py b/tools/fdpic/fdpic-embed.py new file mode 100755 index 0000000000000..5ff55d5f65d29 --- /dev/null +++ b/tools/fdpic/fdpic-embed.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +############################################################################ +# tools/fdpic/fdpic-embed +# +# 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. +# +############################################################################ + +# +# fdpic-embed -- turn a built module into a C header the firmware can carry. +# +# The demo apps have to load a module before there is any way to put files +# on the target, so they embed one and write it to the filesystem at run +# time. This generates that header. +# +# fdpic-embed libshape.so g_libshape > libshape_bin.h +# +# The second argument is the symbol base: the array is and its +# length is _len. + +import os +import sys + +LICENSE = """\ +/**************************************************************************** + * %s + * + * 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. + * + ****************************************************************************/ +""" + + +def main(argv): + if len(argv) not in (3, 4): + sys.stderr.write("usage: fdpic-embed [header]\n") + return 1 + + path = argv[1] + symbol = argv[2] + + # Pass the header's own path relative to the repository root + # ("apps/testing/fs/xipfs/foo_bin.h") as the third argument when the + # result is committed: that is what nxstyle wants on line 2, and the + # module's basename -- the default -- fails the check. + + header = argv[3] if len(argv) > 3 else os.path.basename(path) + + with open(path, "rb") as f: + blob = f.read() + + out = sys.stdout + out.write(LICENSE % header) + out.write( + "\n/* Generated from %s -- do not edit.\n *\n" + " * An FDPIC module, embedded so the demo has something to " + "load without\n" + " * needing a filesystem populated from the host first.\n */\n" + % os.path.basename(path) + ) + out.write( + "\n/*****************************************************" + "***********************\n" + " * Public Data\n" + " ****************************************************" + "************************/\n\n" + ) + + # static, because this is a header that defines data. More than one app + # embeds the same module, and with external linkage the two copies + # collide at link time as soon as both are enabled. + + out.write("static const unsigned char %s[] =\n{\n" % symbol) + for i in range(0, len(blob), 12): + end = i + 12 + row = ", ".join("0x%02x" % b for b in blob[i:end]) + out.write(" %s%s\n" % (row, "," if end < len(blob) else "")) + out.write("};\n\n") + out.write("static const unsigned int %s_len = %d;\n" % (symbol, len(blob))) + + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/tools/fdpic/fdpic-verify.sh b/tools/fdpic/fdpic-verify.sh new file mode 100755 index 0000000000000..164b3372c8876 --- /dev/null +++ b/tools/fdpic/fdpic-verify.sh @@ -0,0 +1,99 @@ +#!/bin/sh +############################################################################ +# tools/fdpic/fdpic-verify +# +# 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. +# +############################################################################ + +# Check that a built module is loadable before it ever reaches the target. +# +# Two things go wrong quietly: +# - the object is not actually FDPIC (wrong toolchain, or -shared omitted), +# which the loader rejects with a generic -ENOEXEC; +# - it imports a symbol the firmware does not export, which the loader +# reports as -ENOENT with no indication of which symbol. +# +# Usage: fdpic-verify [exports-file] [lib.so ...] +# +# Any shared libraries the module links against are passed after the +# exports file; the symbols they define count as satisfied, exactly as they +# will at load time when the loader resolves DT_NEEDED. + +set -e +MOD="${1:?usage: fdpic-verify [exports-file] [libs...]}" +EXPORTS="$2" +shift 2 2>/dev/null || shift $# +LIBS="$*" +READELF="${READELF:-arm-uclinuxfdpiceabi-readelf}" + +fail=0 + +osabi=$("$READELF" -h "$MOD" | sed -n 's/.*OS\/ABI:[[:space:]]*//p') +case "$osabi" in + *"ARM FDPIC"*) ;; + *) + echo "FAIL not an FDPIC object (OS/ABI: ${osabi:-unknown})" + echo " check the toolchain is arm-uclinuxfdpiceabi and that" + echo " the link used -Wl,-shared" + fail=1 + ;; +esac + +etype=$("$READELF" -h "$MOD" | sed -n 's/.*Type:[[:space:]]*\([A-Z]*\).*/\1/p') +[ "$etype" = "DYN" ] || { echo "FAIL e_type is $etype, expected DYN"; fail=1; } + +nload=$("$READELF" -lW "$MOD" | grep -c '^ LOAD' || true) +[ "$nload" -ge 2 ] || { + echo "FAIL expected 2 LOAD segments (RX + RW), found $nload"; fail=1; } + +# Undefined dynamic symbols are the module's imports +imports=$("$READELF" --dyn-syms -W "$MOD" \ + | awk '$7 == "UND" && $8 != "" { print $8 }' | sort -u) + +if [ -n "$EXPORTS" ] && [ -f "$EXPORTS" ]; then + tmp=$(mktemp) + avail=$(mktemp) + echo "$imports" > "$tmp" + cat "$EXPORTS" > "$avail" + + # Symbols the linked libraries define are resolved at load time + for lib in $LIBS; do + [ -f "$lib" ] || continue + "$READELF" --dyn-syms -W "$lib" \ + | awk '$7 != "UND" && $4 != "SECTION" && $8 != "" { print $8 }' \ + >> "$avail" + done + + sort -u "$avail" -o "$avail" + missing=$(comm -23 "$tmp" "$avail" || true) + rm -f "$tmp" "$avail" + if [ -n "$missing" ]; then + echo "FAIL imports the firmware does not export:" + echo "$missing" | sed 's/^/ /' + fail=1 + fi +fi + +if [ "$fail" -eq 0 ]; then + entry=$("$READELF" -h "$MOD" | sed -n 's/.*Entry point address:[[:space:]]*//p') + nimp=$(echo "$imports" | grep -c . || true) + echo "OK $(basename "$MOD"): FDPIC, entry $entry, $nimp imports resolved" +fi + +exit $fail diff --git a/tools/fdpic/nuttx-exports.sh b/tools/fdpic/nuttx-exports.sh new file mode 100755 index 0000000000000..f68931d26700e --- /dev/null +++ b/tools/fdpic/nuttx-exports.sh @@ -0,0 +1,102 @@ +#!/bin/sh +############################################################################ +# tools/fdpic/nuttx-exports +# +# 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. +# +############################################################################ + +# Print the symbols the firmware exports to modules, one per line. +# +# A module links with -shared, so undefined symbols are permitted and the +# link succeeds even for a symbol the firmware does not provide. The +# failure then surfaces at load time as a bare -ENOENT. This list is what +# lets that be caught at build time instead. +# +# exec_symtab.c is generated by NuttX's tools/mksymtab from the libc, libm +# and syscall CSVs, and most of its entries sit behind #if defined(CONFIG_...) +# guards taken from a condition column in those files. The table NuttX +# compiles is therefore correct; it is reading the *source* that is not. +# +# So the file is run through the C preprocessor before the names are pulled +# out. Anything else gets it wrong in one direction or the other: +# +# * plain sed over the source ignores the guards and offers every symbol +# that could be exported rather than the ones that were. On one +# ordinary configuration that was 127 of 668 -- dlopen and the rest of +# dlfcn, the socket calls, alarm, the aio family. A module importing one +# linked cleanly, passed this check, and failed at load with a bare +# -ENOENT. +# +# * intersecting with nm on the linked firmware is closer but still wrong +# both ways. It offers symbols that are in the binary yet absent from +# the table (flockfile, task_testcancel), and withholds crc32, which is +# a macro: the table entry reads { "crc32", crc32full }, so the name +# resolves at load time even though no symbol called crc32 exists. +# +# What the loader matches is the name string in the table, so the +# preprocessed table is exactly the right question to ask. Membership in it +# is also sufficient: if a name is there, the link had to resolve its value, +# so the firmware has it. +# +# Usage: nuttx-exports +# +# CC may be set to override the compiler used to preprocess. + +set -e +NUTTX="${1:?usage: nuttx-exports }" +SYMTAB="$NUTTX/libs/libc/exec_symtab.c" + +if [ ! -f "$SYMTAB" ]; then + echo "nuttx-exports: $SYMTAB not found." >&2 + echo " Build NuttX with CONFIG_EXECFUNCS_SYSTEM_SYMTAB=y first." >&2 + exit 1 +fi + +if [ -z "$CC" ]; then + for candidate in arm-none-eabi-gcc cc gcc; do + if command -v "$candidate" >/dev/null 2>&1; then + CC="$candidate" + break + fi + done +fi + +if [ -z "$CC" ]; then + echo "nuttx-exports: no compiler found; set CC." >&2 + exit 1 +fi + +# The guards read CONFIG_* out of nuttx/config.h, which the configure step +# generates into the tree, so no -D flags are needed beyond the include path. + +if ! "$CC" -E -P -I "$NUTTX/include" -D__NuttX__ "$SYMTAB" \ + > "${TMPDIR:-/tmp}/nuttx-exports.$$" 2>"${TMPDIR:-/tmp}/nuttx-exports-err.$$" +then + echo "nuttx-exports: failed to preprocess $SYMTAB with $CC:" >&2 + head -5 "${TMPDIR:-/tmp}/nuttx-exports-err.$$" >&2 + echo " A configured and built tree is required." >&2 + rm -f "${TMPDIR:-/tmp}/nuttx-exports.$$" \ + "${TMPDIR:-/tmp}/nuttx-exports-err.$$" + exit 1 +fi + +sed -n 's/^[[:space:]]*{[[:space:]]*"\([^"]*\)".*/\1/p' \ + "${TMPDIR:-/tmp}/nuttx-exports.$$" | sort -u + +rm -f "${TMPDIR:-/tmp}/nuttx-exports.$$" "${TMPDIR:-/tmp}/nuttx-exports-err.$$" diff --git a/tools/fdpic/nuttx-fdpic.mk b/tools/fdpic/nuttx-fdpic.mk new file mode 100644 index 0000000000000..9780a866b8c78 --- /dev/null +++ b/tools/fdpic/nuttx-fdpic.mk @@ -0,0 +1,221 @@ +############################################################################ +# tools/fdpic/nuttx-fdpic.mk +# +# 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. +# +############################################################################ + +############################################################################ +# tools/fdpic/nuttx-fdpic.mk +# +# 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. +# +############################################################################ + +# nuttx-fdpic.mk -- build out-of-tree FDPIC modules for NuttX +# +# A module is an ELF shared object whose read-only segment the target maps +# straight out of flash and executes in place, and whose writable segment is +# copied to RAM once per running instance. It links against nothing: libc +# and everything else are imported from the firmware's exported symbol table +# at load time. +# +# Usage -- a whole module is this: +# +# MODULE = hello +# SRCS = hello.c +# include /path/to/nuttx/tools/fdpic/nuttx-fdpic.mk +# +# C++ sources go in CXXSRCS instead of SRCS. +# +# Two toolchains are involved, and the split is the whole trick: +# +# * the stock Arm bare-metal compiler does the compiling. It emits +# perfectly good FDPIC objects for both C and C++. +# * arm-uclinuxfdpiceabi *binutils* does the linking, because +# arm-none-eabi-ld is built with only the `armelf` emulation and cannot +# produce an FDPIC object at all -- it silently marks the output +# "UNIX - System V" and the loader refuses it. +# +# So the from-source dependency is binutils alone, which takes about a +# minute to build. No FDPIC GCC is needed. See build-binutils.sh beside +# this file, and Documentation/components/fdpic.rst. +# +# Required: +# NUTTX_DIR a configured, built NuttX tree (headers + export list) +# +# Optional: +# ARM_TOOLCHAIN prefix of the bare-metal compiler; default arm-none-eabi +# FDPIC_TOOLCHAIN prefix of the FDPIC binutils; default +# arm-uclinuxfdpiceabi +# CPU default cortex-m33 +# ENTRY entry symbol; default main, use 0 for a library +# OPT default -Os +# LIBS shared libraries to link against +# BINDNOW '-z now' by default; set empty to leave imported +# descriptors in the lazy binding table (DT_JMPREL) +# EXTRA_CFLAGS EXTRA_CXXFLAGS EXTRA_LDFLAGS +# +# EXTRA_LDFLAGS is passed straight to ld, not through a compiler driver, so +# it takes bare linker flags: `-soname libfoo.so`, not `-Wl,-soname,libfoo.so`. + +MODULE ?= module +SRCS ?= +CXXSRCS ?= +CPU ?= cortex-m33 +ENTRY ?= main +OPT ?= -Os + +ARM_TOOLCHAIN ?= arm-none-eabi +FDPIC_TOOLCHAIN ?= arm-uclinuxfdpiceabi + +FDPIC_DIR := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST))))) + +ifeq ($(NUTTX_DIR),) + $(error Set NUTTX_DIR to a configured, built NuttX tree) +endif + +# make has built-in defaults for CC and CXX, so ?= never fires for them and +# the host compiler silently gets the job. Test the origin instead. + +ifeq ($(origin CC),default) + CC := $(ARM_TOOLCHAIN)-gcc +endif + +ifeq ($(origin CXX),default) + CXX := $(ARM_TOOLCHAIN)-g++ +endif + +LD := $(FDPIC_TOOLCHAIN)-ld +READELF := $(FDPIC_TOOLCHAIN)-readelf + +# Common compile flags. +# +# -mfdpic is stated rather than assumed, so a mis-set toolchain fails loudly +# instead of quietly producing a plain ELF the loader will refuse. +# +# -fPIC is not optional and not implied. -mfdpic alone does not turn on PIC +# under the bare-metal compiler, and without it the link emits TEXTREL -- +# text relocations -- which cannot work when the text is executed in place +# out of read-only flash. +# +# -fno-builtin keeps GCC from open-coding calls into libc routines the +# module is supposed to import from the firmware. +# +# __STDC_NO_ATOMICS__ steers NuttX's away from the branch +# that includes and then redefines its macros. The effect is +# that a module using C11 atomics gets NuttX's implementation -- the same one +# the firmware uses -- rather than the compiler's. + +MODCOMMON = -mcpu=$(CPU) -mthumb -mfdpic -fPIC $(OPT) \ + -fno-builtin -Wall -Wa,--noexecstack \ + -D__STDC_NO_ATOMICS__ -D__NuttX__ + +MODCFLAGS = $(MODCOMMON) -I$(NUTTX_DIR)/include $(EXTRA_CFLAGS) + +# C++ adds three flags, none of them optional. +# +# -fno-use-cxa-atexit, because the default registers each static object's +# destructor with __cxa_atexit(dtor, obj, &__dso_handle), and __dso_handle +# comes from crtbegin, which a module does not link. The link fails +# outright with "hidden symbol `__dso_handle' isn't defined". Turning it off +# also puts the destructors in .fini_array, which is what the loader walks on +# unload -- so the flag that makes the link work is also the flag that makes +# destructors run. +# +# -fno-exceptions -fno-rtti, because both need libsupc++, which a module +# linking against nothing cannot reach. + +MODCXXFLAGS = $(MODCOMMON) \ + -fno-exceptions -fno-rtti -fno-use-cxa-atexit \ + -I$(NUTTX_DIR)/include/cxx -I$(NUTTX_DIR)/include \ + $(EXTRA_CXXFLAGS) + +# Link flags, passed straight to ld. +# +# -shared is load bearing and easy to get wrong. It is what preserves +# R_ARM_FUNCDESC_VALUE relocations for imported symbols. Linking as a PIE +# with --unresolved-symbols=ignore-all also appears to work, but silently +# degrades every import to R_ARM_NONE, and the module then branches to zero +# on its first call out. +# +# The emulation has to be named because this ld supports four. +# +# -z now keeps imported function descriptors in DT_REL rather than the lazy +# binding table DT_JMPREL. The loader binds both, so this is a default rather +# than a requirement: it keeps built modules on the layout that has had the +# most hardware exposure, and leaves the lazymod fixture, which empties +# BINDNOW, a distinct case rather than what everything does. + +BINDNOW ?= -z now + +MODLDFLAGS = -m armelf_linux_fdpiceabi -shared $(BINDNOW) -e $(ENTRY) \ + $(EXTRA_LDFLAGS) + +OBJS := $(SRCS:.c=.o) $(CXXSRCS:.cpp=.o) +TARGET := $(MODULE).fdpic +EXPORTS := .nuttx-exports + +.PHONY: all clean verify exports + +all: verify + +$(EXPORTS): $(NUTTX_DIR)/libs/libc/exec_symtab.c + @$(FDPIC_DIR)/nuttx-exports.sh $(NUTTX_DIR) > $@ + +exports: $(EXPORTS) + @echo "$(shell wc -l < $(EXPORTS)) symbols exported by the firmware" + +%.o: %.c + @echo " CC $<" + @$(CC) $(MODCFLAGS) -c $< -o $@ + +%.o: %.cpp + @echo " CXX $<" + @$(CXX) $(MODCXXFLAGS) -c $< -o $@ + +$(TARGET): $(OBJS) + @echo " LD $@" + @$(LD) $(MODLDFLAGS) -o $@ $(OBJS) $(LIBS) + +# Verification is part of the default build on purpose. A module that +# imports a symbol the firmware does not export links perfectly happily and +# only fails once it is on the target, as a bare -ENOENT with no indication +# of which symbol was at fault. + +verify: $(TARGET) $(EXPORTS) + @READELF=$(READELF) $(FDPIC_DIR)/fdpic-verify.sh \ + $(TARGET) $(EXPORTS) $(LIBS) + +clean: + @rm -f $(OBJS) $(TARGET) $(MODULE).so $(EXPORTS) From 8b3f218b9c42dce021a19066d438194c122b891c Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 3 Aug 2026 13:42:40 +0200 Subject: [PATCH 11/12] Documentation: Describe the FDPIC module support. FDPIC has no page, and the parts of it a reader has to get right are spread across binfmt/Kconfig, the ELF loader and the ARM toolchain definitions. The page covers what an FDPIC module is and what it buys over the position independent ELF support already in the tree, how the loader places one, where shared libraries come from and how they are found, which entry points resolve a function descriptor and the rules for adding another, and how to build a module and a library with a toolchain that can emit FDPIC. A comparison table places it against NXFLAT and PIC ELF, and the reference section records the object layout and the relocations. The known limitation is stated: global constructors are not run for a module loaded through binfmt. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Marco Casaroli --- Documentation/components/fdpic.rst | 406 +++++++++++++++++++++++++++++ Documentation/components/index.rst | 1 + 2 files changed, 407 insertions(+) create mode 100644 Documentation/components/fdpic.rst diff --git a/Documentation/components/fdpic.rst b/Documentation/components/fdpic.rst new file mode 100644 index 0000000000000..ed212d03704f5 --- /dev/null +++ b/Documentation/components/fdpic.rst @@ -0,0 +1,406 @@ +.. _fdpic: + +============= +FDPIC Modules +============= + +Overview +======== + +An FDPIC module is an ELF shared object whose read-only and writable +segments are placed independently of one another. NuttX uses the +read-only segment where it already lies on the media and never copies +it; only the writable segment is copied to RAM, once per running +instance. A module's code and ``.rodata`` therefore cost no RAM at all, +and several instances of one module share them. + +FDPIC is not a separate binary format and has no loader of its own. An +object announces itself in its OS/ABI byte, +``e_ident[EI_OSABI] == ELFOSABI_ARM_FDPIC`` (65), which ``readelf -h`` +reports as *OS/ABI: ARM FDPIC*, and the ELF loader takes it from there. +Everything else -- ``exec()``, ``posix_spawn()``, ``dlopen()``, the +symbol table -- is the ordinary ELF path. + +What FDPIC adds over the position independent ELF support already in the +tree is a function pointer that carries its own data base. That is what +lets a module be called back on a thread it did not create, and what +lets a module and the libraries it uses hold distinct data bases at the +same time. + +Function descriptors +-------------------- + +Code reaches its own data through a base register -- **r9** on ARM -- +holding the address of that object's GOT. Because code and data are +placed independently, a bare code address is not enough to call a +function: the callee needs its data base too. FDPIC therefore +represents a function pointer as a two word *descriptor*: + +=========== ============================================================== +Word Contents +=========== ============================================================== +``entry`` Code address, including its Thumb bit +``got`` Data base to install in the PIC base register before + branching +=========== ============================================================== + +Building those descriptors is most of what relocation does. Because +each one names its own base, a pointer handed to the base firmware +carries everything needed to call back into the module later, from any +thread. + +A module links against nothing. libc and everything else are undefined +imports, resolved at load time against the globally registered symbols +first, then any shared libraries the module names, then the symbol table +``exec()`` supplied. + +Placement +--------- + +The loader asks the filesystem where the file lies on its media. Two +mechanisms exist and they are not interchangeable: + +* ``XIPFSIOC_PIN`` is for a filesystem that can move a file's blocks. It + returns an address together with a pin that holds the extent still, and + the pin is given back with ``XIPFSIOC_UNPIN`` when the module is + unloaded. :doc:`XIPFS ` is the one in tree. + +* ``FIOC_XIPBASE`` is for a filesystem whose layout never changes, which + has nothing to hold and answers with a bare address. ROMFS and TMPFS + are those. + +The pin is asked for first, because a filesystem that needs one cannot +safely be used without it. A module whose text cannot be reached this +way is refused rather than copied to RAM: loading it anyway would +silently cost the memory the mechanism exists to save. + +The writable segment is allocated and copied per instance, and a pool of +function descriptors is reserved behind it for the relocations that ask +the loader to manufacture one. When the task starts, +``up_initial_state()`` installs the object's data base -- ``DT_PLTGOT``, +or the GOT immediately after ``PT_DYNAMIC`` in an object with no +imports -- into the PIC base register. + +Shared libraries +---------------- + +A module may name shared libraries in ``DT_NEEDED``. Each is loaded +during relocation by calling ``dlopen()`` on the name, and the module's +undefined symbols are then bound against that library's exports. This +requires ``CONFIG_LIBC_DLFCN``; without it, a module carrying +``DT_NEEDED`` is refused, because there is no way to bring in what it +asks for. ``CONFIG_LIBC_ELF_MAXNEEDED`` caps how many one module may +name. + +Because ``dlopen()`` does the work, libraries are found the way it finds +them: an absolute path is used as given, and a bare name is searched for +along ``LD_LIBRARY_PATH``, which needs ``CONFIG_LIBC_ENVPATH`` and is +seeded from ``CONFIG_LDPATH_INITIAL``. + +A library lands in the module registry, which holds one instance per +name, so its data is shared by everything that opens it. A module +started with ``exec()`` is different: that path loads a fresh copy each +time, so two running instances of one module have separate data while +sharing one copy of the text in flash. + +Comparison with NXFLAT and PIC ELF +================================== + +All three run position independent code from flash on a target with no +MMU, and all three give several instances of one module a shared +``.text`` with private ``.data``. They differ in what a *pointer* can +express and in what the toolchain has to provide. + +========================= ============== ============== ============= +Property NXFLAT PIC ELF FDPIC +========================= ============== ============== ============= +Format NuttX only ELF ELF +Extra build tools yes none assembler and + linker +Data base per task task object +Shared libraries no no yes +Foreign-thread callback no no yes +Instruction set ARM, Thumb-2 unrestricted Thumb-2 only +========================= ============== ============== ============= + +:ref:`NXFLAT ` is a NuttX-specific format. A module imports +symbols from the base firmware but cannot export any, so shared +libraries are not possible, and the build needs ``mknxflat`` to generate +a thunk, ``ldnxflat`` to link, and one of the ``binfmt/libnxflat`` linker +scripts to place the sections. + +**PIC ELF** needs no extra tools. With ``CONFIG_PIC`` the ELF loader +allocates the writable sections separately and, when the filesystem +answers ``FIOC_XIPBASE``, leaves the read-only ones on the media. Two +limits follow from having one base register per task: a shared object is +loaded as a single allocation, because the distance between its text and +its data is compiled into it, and the data base is installed once per +task, so every object in a task shares one. + +**FDPIC** pays for its descriptors with an ``arm-uclinuxfdpiceabi`` +assembler and linker, and gets back the two things a single register +cannot express. A task or pthread that a module starts inherits the +module's D-Space, so a register would be enough there; a work queue +worker was created at boot and carries no module base, and a descriptor +supplies one, which is how ``SIGEV_THREAD`` notifications reach module +code. + +Requirements +============ + +**An ARM Thumb-2 core.** The boundary is the instruction set, not the +core profile: GCC rejects FDPIC in Thumb-1 mode. + +========================= ========================== ===== +Core Architecture FDPIC +========================= ========================== ===== +Cortex-M3 / M4 / M7 ARMv7-M / ARMv7E-M yes +Cortex-M33 ARMv8-M Mainline yes +Cortex-M0 / M0+ / M23 ARMv6-M / ARMv8-M Baseline no +========================= ========================== ===== + +RISC-V has no FDPIC ABI -- the psABI addendum is an unmerged proposal and +no ``EI_OSABI`` value is assigned -- so a RISC-V target cannot use this. + +**Flash that is memory mapped and executable**, exposed by a filesystem +that answers ``XIPFSIOC_PIN`` or ``FIOC_XIPBASE``. + +**An FDPIC assembler and linker.** A stock ``arm-none-eabi`` GCC +compiles correct FDPIC code for both C and C++, but the assembler has to +be in FDPIC mode to accept the relocations that code produces, and only +``arm-uclinuxfdpiceabi`` binutils carry the ``armelf_linux_fdpiceabi`` +emulation the link needs. ``arm-none-eabi-ld``, rather than failing, +marks its output *UNIX - System V*, which the loader will not treat as +FDPIC. + +No distribution packages that target, so build binutils for it -- which +takes about a minute and needs nothing else:: + + configure --target=arm-uclinuxfdpiceabi --prefix=$HOME/fdpic \ + --disable-nls --disable-werror + make && make install + export PATH=$HOME/fdpic/bin:$PATH + +An FDPIC GCC is not needed. + +**The base firmware must reserve r9.** It is not enough for the module +to be well behaved: a firmware routine calling back into module code +arrives with the module's data base in r9 only if the compiler was never +free to allocate that register elsewhere. ``CONFIG_FDPIC`` selects +``CONFIG_PIC``, under which ``arch/arm/src/common/Toolchain.defs`` adds +``--fixed-r9``; see :ref:`nxflat` for why it goes into ``ARCHCFLAGS`` +rather than ``CFLAGS`` and how to check that it arrived. + +Configuration +============= + +``CONFIG_FDPIC`` lives under ``CONFIG_ELF``. A working configuration +also needs a symbol table for modules to import from and a filesystem +that can expose its media:: + + CONFIG_ELF=y + CONFIG_FDPIC=y + CONFIG_LIBC_EXECFUNCS=y + CONFIG_EXECFUNCS_HAVE_SYMTAB=y + CONFIG_EXECFUNCS_SYSTEM_SYMTAB=y + CONFIG_FS_XIPFS=y + +Shared libraries need three more, the last two so that a library can be +named rather than spelled out as an absolute path:: + + CONFIG_LIBC_DLFCN=y + CONFIG_LIBC_ENVPATH=y + CONFIG_LDPATH_INITIAL="/mnt/xipfs" + +``CONFIG_ELF_STACKSIZE`` gives the stack a module runs with. A module +that needs a different one can export an ``nx_stacksize`` symbol, which +the loader prefers when present. + +Building a module +================= + +Three steps: compile to assembly with the stock compiler, assemble with +the FDPIC assembler, link with the FDPIC linker:: + + arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -mfdpic -fPIC -Os \ + -fno-builtin -D__NuttX__ -I$NUTTX/include -S mod.c -o mod.s + + arm-uclinuxfdpiceabi-as --fdpic -mthumb -mcpu=cortex-m3 \ + mod.s -o mod.o + + arm-uclinuxfdpiceabi-ld -m armelf_linux_fdpiceabi -shared -z now \ + -e main -o mod.fdpic mod.o + +The detour through assembly is what makes this work with any toolchain. +Whether ``arm-none-eabi-gcc -c`` can assemble FDPIC code itself depends +on the release: newer ones pass ``--fdpic`` down to the assembler, older +ones do not and fail with *"Relocation supported only in FDPIC mode"*. +Assembling separately never depends on that. Note that the assembler's +option is ``--fdpic``, not ``-mfdpic``. + +Five flags carry weight: + +* ``-mfdpic`` is stated rather than assumed, so a mis-set toolchain fails + loudly instead of producing a plain ELF the loader will not recognize. + +* ``-fPIC`` is not implied by ``-mfdpic`` on a bare-metal target, and + without it the link emits ``TEXTREL``. Text relocations cannot work + against text executed from read-only flash. + +* ``-shared`` preserves the ``R_ARM_FUNCDESC_VALUE`` relocations for + imported symbols. A PIE link with ``--unresolved-symbols=ignore-all`` + appears to work but degrades every import to ``R_ARM_NONE``, and the + module branches to zero on its first call into the firmware. + +* ``-m armelf_linux_fdpiceabi`` is required: this linker supports several + emulations and will not guess. + +* ``-e main`` names the entry point. There is no ``crt0``; the module is + entered directly. + +A module links with ``-shared``, so importing something the firmware does +not export links cleanly and fails only on the target. Checking the +module's undefined symbols against the generated +``libs/libc/exec_symtab.c`` is worth doing as part of the module build. + +Building a shared library +------------------------- + +A library is built the same way, with a soname and no entry point, and +the module names it on its link line:: + + arm-uclinuxfdpiceabi-ld -m armelf_linux_fdpiceabi -shared -z now \ + -e 0 -soname libfoo.so -o libfoo.so libfoo.o + + arm-uclinuxfdpiceabi-ld -m armelf_linux_fdpiceabi -shared -z now \ + -e main -o mod.fdpic mod.o libfoo.so + +At run time the library must be reachable under its soname along +``LD_LIBRARY_PATH``. + +Calling back into a module +========================== + +A module's function pointer is the address of a descriptor in its +writable segment. Firmware that stores one and later branches to it +would jump into RAM data, so an entry point that accepts a callback from +a module has to resolve the descriptor first. ``CONFIG_FDPIC`` makes +these do so: + +``qsort``, ``bsearch``, ``pthread_create``, ``signal``/``sigaction``, +``task_create``/``task_create_with_stack``, ``task_spawn``, +``pthread_once``, ``scandir``, and ``mq_notify``/``timer_create`` with +``SIGEV_THREAD``. + +Whether a pointer is a descriptor is decided by reading the PIC base +register: a module's task runs with its data base there, a firmware task +with zero, so a kernel caller is unaffected. + +A new entry point that takes a module callback must resolve it too, under +three rules: + +* **Resolve once, in the innermost common routine.** Resolving twice + treats a code address as a descriptor. ``qsort()`` recurses, so its + public entry resolves and the recursive body does not; ``signal()`` + does not resolve because ``nxsig_action()`` does it for both paths; + ``scandir()`` resolves its filter but not the comparison function it + hands to ``qsort()``. + +* **Exclude sentinel values by hand.** ``fdpic_callback()`` declines to + dereference NULL and nothing else. ``sigaction()`` excludes + ``SIG_IGN``, ``SIG_DFL``, ``SIG_HOLD`` and ``SIG_ERR`` -- the integers + 0, 1, 2 and -1. + +* **A callback on a shared thread needs its base installed.** A + ``SIGEV_THREAD`` notification runs on a work queue worker that carries + no module base, so resolving the entry is not enough. Capture the base + at registration with ``fdpic_base()``, in the module's own context, and + install it around the call with ``fdpic_invoke()``. + +Everywhere else the callback runs in a task that inherited the module's +D-Space, so only the code address needs resolving. + +Limitations +=========== + +**Constructors run on the loading task, not the module's own.** +``DT_INIT_ARRAY`` runs at the end of the load and ``DT_FINI_ARRAY`` at +unload, which needs ``CONFIG_BINFMT_CONSTRUCTORS``. Both are entered +through ``fdpic_invoke()`` with the object's own data base, so a global +object reaches its own storage; but the task they run on is whichever one +called the loader, so a constructor that reads task-local state -- its own +pid, its environment -- sees that task's, not the one that will run +``main()``. + +A library named in ``DT_NEEDED`` is constructed before the module that +needs it, because the module's own relocation is what opens it, and +destroyed after, at the last ``dlclose()``. Since the library is one +instance, its constructors run once however many modules name it. + +Reference +========= + +Object layout +------------- + +A linked module already has the layout execute in place needs, with no +linker script:: + + LOAD vaddr 0x00000000 R E .text .rodata .hash .dynsym .dynstr + LOAD vaddr 0x00001244 RW .dynamic .got .data .bss + DYNAMIC DT_PLTGOT -> .got + +``.rodata`` lands in the read-only segment on its own, reached PC +relative or GOT indirect. That matters: in the writable segment it would +be copied to RAM with ``.data``, and most of the saving would evaporate +silently, with everything still working. + +The FDPIC marker is the OS/ABI byte alone. ``e_flags`` reads as an +ordinary ``0x5000000, Version5 EABI``. + +Relocations +----------- + +The static link resolves ``R_ARM_GOT_BREL`` and ``R_ARM_GOTFUNCDESC`` +into the GOT already, so only three types carry work into a linked +module. + +``R_ARM_RELATIVE`` + An address needing its segment's base added. + +``R_ARM_FUNCDESC_VALUE`` + A descriptor the linker has laid out, for the loader to fill in. This + is what a *call* to an imported function produces. When the symbol + resolves to a function in another FDPIC object, both words are copied + from that object's own descriptor, so the callee runs with its own data + base; otherwise the entry is the resolved address and the base is this + object's. + +``R_ARM_FUNCDESC`` + A pointer to a descriptor that does not exist yet, which the loader + manufactures from the pool behind the writable segment. This is what + *taking the address* of a function produces -- a different thing from + calling one, and both can appear for the same symbol. + +Constants, from binutils ``include/elf/arm.h`` and mirrored in +``arch/arm/include/elf.h``: ``R_ARM_GOTFUNCDESC`` 161, +``R_ARM_GOTOFFFUNCDESC`` 162, ``R_ARM_FUNCDESC`` 163, +``R_ARM_FUNCDESC_VALUE`` 164. + +Which table an imported function's descriptor lands in is the linker's +decision: with ``-z now`` imports stay in ``DT_REL``, without it they go +to ``DT_JMPREL``. Both are bound eagerly, so either link works, but the +two are not walked identically. In ``DT_REL`` the word being overwritten +is the addend and is added to the resolved value; in ``DT_JMPREL`` it is +the lazy binding bootstrap and the descriptor is overwritten outright. + +``.rofixup`` is skipped. It is the self-relocation list a *static* +executable's ``crt0`` walks to find its own GOT; a module has no +``crt0``, and the loader supplies the data base instead. + +Exported functions +------------------ + +A function exported by a module or library is published to ``dlsym()`` +as a descriptor rather than a code address, taken from the same pool, so +that an FDPIC caller can branch through what it gets back. diff --git a/Documentation/components/index.rst b/Documentation/components/index.rst index fe60af7b9851c..7b041920dced3 100644 --- a/Documentation/components/index.rst +++ b/Documentation/components/index.rst @@ -14,6 +14,7 @@ case, you can head to the :doc:`reference <../reference/index>`. binfmt.rst concurrency/index.rst drivers/index.rst + fdpic.rst nxflat.rst nxgraphics/index.rst paging.rst From f2239c9fe84df89b364913193d9fe050601175d4 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 3 Aug 2026 20:28:47 +0200 Subject: [PATCH 12/12] boards/rp23xx: Add an FDPIC configuration for the Pimoroni Pico Plus 2. xipfs-fdpic is the xipfs configuration plus the FDPIC loader and the fdpicxip demo, so that the fdpic and reject sections of the XIPFS test suite have something to run. It is the configuration the loader was tested with, and until now none was in the tree: FDPIC needed a dozen options set by hand, three of which are not obvious. CONFIG_LIBC_ENVPATH and CONFIG_LDPATH_INITIAL, because a library named in DT_NEEDED is opened by name and dlopen() searches LD_LIBRARY_PATH. CONFIG_LIBC_ELF_HAVE_SYMTAB, because a library resolves its own imports against the module registry's symbol table rather than the one exec() supplies. And CONFIG_ELF_STACKSIZE at 4096 rather than the board's 2048, because a module that calls into the firmware's printf family overflows 2048, and with no MPU that is a lockup with no diagnostic at all. CONFIG_DEFAULT_TASK_STACKSIZE goes to 4096 for the same reason, on the other side of the loader: the XIPFS test task itself is sized from it, and at 2048 it overflows in printf partway through the module tests. The suite then stops mid-run with no failure reported and no dump, which reads as a loader bug rather than as the test running out of stack. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Marco Casaroli --- .../boards/pimoroni-pico-2-plus/index.rst | 15 +++++ .../configs/xipfs-fdpic/defconfig | 67 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 boards/arm/rp23xx/pimoroni-pico-2-plus/configs/xipfs-fdpic/defconfig diff --git a/Documentation/platforms/arm/rp23xx/boards/pimoroni-pico-2-plus/index.rst b/Documentation/platforms/arm/rp23xx/boards/pimoroni-pico-2-plus/index.rst index 8269cc7788c05..6c4045f7ada64 100644 --- a/Documentation/platforms/arm/rp23xx/boards/pimoroni-pico-2-plus/index.rst +++ b/Documentation/platforms/arm/rp23xx/boards/pimoroni-pico-2-plus/index.rst @@ -177,6 +177,21 @@ xipfs XIPFS mounted on the on-board flash, with the ``xipfs`` command and the XIPFS test suite. +xipfs-fdpic +----------- + +Same as ``xipfs``, plus the FDPIC module loader and the +``fdpicxip`` demo, so the ``fdpic`` and ``reject`` sections of the XIPFS +test suite have something to run. The demo carries its modules as +committed byte arrays, so nothing beyond the ordinary ARM toolchain is +needed to build it; rebuilding those from source needs +``arm-uclinuxfdpiceabi`` binutils. See :doc:`/components/fdpic`. + +``CONFIG_ELF_STACKSIZE`` is 4096 here rather than the 2048 the rest of +the board's tasks use. A module that calls into the firmware's printf +family overflows 2048, and with no MPU that is a lockup rather than a +diagnostic. + xipfs-nxflat ------------ diff --git a/boards/arm/rp23xx/pimoroni-pico-2-plus/configs/xipfs-fdpic/defconfig b/boards/arm/rp23xx/pimoroni-pico-2-plus/configs/xipfs-fdpic/defconfig new file mode 100644 index 0000000000000..c01854b600c02 --- /dev/null +++ b/boards/arm/rp23xx/pimoroni-pico-2-plus/configs/xipfs-fdpic/defconfig @@ -0,0 +1,67 @@ +# +# This file is autogenerated: PLEASE DO NOT EDIT IT. +# +# You can use "make menuconfig" to make any modifications to the installed .config file. +# You can then do "make savedefconfig" to generate a new defconfig file that includes your +# modifications. +# +# CONFIG_NSH_ARGCAT is not set +# CONFIG_NSH_CMDOPT_HEXDUMP is not set +# CONFIG_NSH_DISABLE_DATE is not set +# CONFIG_NSH_DISABLE_LOSMART is not set +# CONFIG_STANDARD_SERIAL is not set +CONFIG_ARCH="arm" +CONFIG_ARCH_BOARD="pimoroni-pico-2-plus" +CONFIG_ARCH_BOARD_COMMON=y +CONFIG_ARCH_BOARD_PIMORONI_PICO_2_PLUS=y +CONFIG_ARCH_CHIP="rp23xx" +CONFIG_ARCH_CHIP_RP23XX=y +CONFIG_ARCH_RAMVECTORS=y +CONFIG_ARCH_STACKDUMP=y +CONFIG_BINFMT_CONSTRUCTORS=y +CONFIG_BOARDCTL_RESET=y +CONFIG_BOARD_LOOPSPERMSEC=10450 +CONFIG_BUILTIN=y +CONFIG_DEBUG_FULLOPT=y +CONFIG_DEBUG_SYMBOLS=y +CONFIG_DEFAULT_TASK_STACKSIZE=4096 +CONFIG_ELF=y +CONFIG_ELF_STACKSIZE=4096 +CONFIG_EXAMPLES_FDPICXIP=y +CONFIG_EXAMPLES_HELLO=y +CONFIG_EXECFUNCS_HAVE_SYMTAB=y +CONFIG_EXECFUNCS_SYSTEM_SYMTAB=y +CONFIG_FDPIC=y +CONFIG_FS_PROCFS=y +CONFIG_FS_PROCFS_REGISTER=y +CONFIG_FS_XIPFS=y +CONFIG_FS_XIPFS_FAULT_INJECT=y +CONFIG_HAVE_CXX=y +CONFIG_INIT_ENTRYPOINT="nsh_main" +CONFIG_INIT_STACKSIZE=16384 +CONFIG_LDPATH_INITIAL="/mnt/xipfs" +CONFIG_LIBC_DLFCN=y +CONFIG_LIBC_ELF_HAVE_SYMTAB=y +CONFIG_LIBC_ENVPATH=y +CONFIG_LIBC_EXECFUNCS=y +CONFIG_MTD=y +CONFIG_NFILE_DESCRIPTORS_PER_BLOCK=6 +CONFIG_NSH_BUILTIN_APPS=y +CONFIG_NSH_READLINE=y +CONFIG_RAM_SIZE=532480 +CONFIG_RAM_START=0x20000000 +CONFIG_READLINE_CMD_HISTORY=y +CONFIG_RP23XX_FLASH_MTD=y +CONFIG_RR_INTERVAL=200 +CONFIG_SCHED_HPWORK=y +CONFIG_SCHED_WAITPID=y +CONFIG_SIG_EVTHREAD=y +CONFIG_START_DAY=9 +CONFIG_START_MONTH=2 +CONFIG_START_YEAR=2021 +CONFIG_SYSLOG_CONSOLE=y +CONFIG_SYSTEM_NSH=y +CONFIG_SYSTEM_XIPFS=y +CONFIG_TESTING_FS_XIPFS=y +CONFIG_TESTING_FS_XIPFS_MTD="/dev/rpflash" +CONFIG_UART0_SERIAL_CONSOLE=y