From 1aa32bbc075b6fa6fb5dca076bf4c62a1f4f765b Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 3 Aug 2026 00:56:27 +0200 Subject: [PATCH] libs/libc/elf: Place an FDPIC object's segments independently. An ET_DYN object is loaded into one allocation with its data behind its text, because its data references sit at a fixed distance from the code that makes them. An FDPIC object does not work that way: it reaches its data through a base register, so the two segments can be placed wherever suits, and the point of the format is that the read-only one is left on the media and executed there while only the writable one is copied. One copy of the text then serves every instance. So libelf_load() grows a second case. The object announces itself in the OS/ABI byte, which is noted once in libelf_loadhdrs() rather than re-derived; e_flags cannot be used for this, as an FDPIC object's are an unremarkable EABI version and testing them would reject every valid module. Text is taken from the media address plus the segment's own file offset -- the same arithmetic the ET_REL path already does with sh_offset -- and libelf_loadfile() does not read it. If the filesystem cannot show its media, the loader copies the text to RAM instead. The module then loses the shared text and the flash saving, but it runs. Obtaining that address needs two mechanisms, and they are not interchangeable. A compacting filesystem can move a file's blocks, so it hands out an address only with a pin that holds them still and expects the pin back; xipfs is the one in tree. A filesystem whose layout never changes has nothing to hold and answers FIOC_XIPBASE with a bare address; romfs and tmpfs are those. libelf_xipacquire() asks for the pin first, because a filesystem that needs one is not safe without it, and libelf_unload() gives it back. The loader asks for a pin only if it can hold one, or the pin would stay for ever. The pin is thus not specific to FDPIC. Any module that executes in place from a compacting filesystem takes one, and gives it back at unload. mmap() is not used, though both filesystems implement it. The mapping would be recorded against whichever task called the loader, while the release happens when the module's own task exits, which is a different group -- so the pin would outlive the module and the extent would never become movable again. Unloading has to change with placement: the existing path frees only textalloc because ET_DYN had a single allocation, which would leak an FDPIC object's data and free media the filesystem only lent us. Nothing here runs for a non-FDPIC object; every branch is behind the flag and the single-allocation path is untouched. Built and booted mps3-an547:picostest, which is CONFIG_ELF with CONFIG_PIC, with no change in behaviour. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Marco Casaroli --- arch/arm/include/elf.h | 17 +++ fs/Kconfig | 11 ++ fs/xipfs/Kconfig | 1 + include/elf.h | 1 + include/nuttx/elf.h | 6 + include/nuttx/lib/elf.h | 24 ++++ libs/libc/elf/elf.h | 22 ++++ libs/libc/elf/elf_load.c | 226 +++++++++++++++++++++++++++++++---- libs/libc/elf/elf_loadhdrs.c | 18 +++ libs/libc/elf/elf_unload.c | 41 ++++++- 10 files changed, 342 insertions(+), 25 deletions(-) diff --git a/arch/arm/include/elf.h b/arch/arm/include/elf.h index e5c2780472c..460cb34ad4e 100644 --- a/arch/arm/include/elf.h +++ b/arch/arm/include/elf.h @@ -41,6 +41,11 @@ #define EM_ARCH EM_ARM +/* An object built for the FDPIC ABI says so in the OS/ABI byte. */ + +#define ELF_IS_FDPIC(ehdr) \ + ((ehdr)->e_ident[EI_OSABI] == ELFOSABI_ARM_FDPIC) + /* Table 4-2, ARM-specific e_flags */ #define EF_ARM_EABI_MASK 0xff000000 @@ -206,6 +211,18 @@ #define R_ARM_THM_TLS_DESCSEQ16 129 /* Thumb16 */ #define R_ARM_THM_TLS_DESCSEQ32 130 /* Thumb32 */ +/* FDPIC relocations. Values from the ARM FDPIC ABI as implemented by + * binutils (include/elf/arm.h). + */ + +#define R_ARM_GOTFUNCDESC 161 /* Data GOT entry holding a descriptor */ +#define R_ARM_GOTOFFFUNCDESC 162 /* Data GOT-relative descriptor */ +#define R_ARM_FUNCDESC 163 /* Data Address of a descriptor */ +#define R_ARM_FUNCDESC_VALUE 164 /* Data The descriptor itself: {code, GOT} */ +#define R_ARM_TLS_GD32_FDPIC 165 /* Data */ +#define R_ARM_TLS_LDM32_FDPIC 166 /* Data */ +#define R_ARM_TLS_IE32_FDPIC 167 /* Data */ + /* Processor specific values for the Phdr p_type field. */ #define PT_ARM_EXIDX (PT_LOPROC + 1) /* ARM unwind segment. */ diff --git a/fs/Kconfig b/fs/Kconfig index b2aef67bb81..b42c5d513d8 100644 --- a/fs/Kconfig +++ b/fs/Kconfig @@ -94,6 +94,17 @@ config PSEUDOFS_ATTRIBUTES Enable support for attributes(e.g. mode, uid, gid and time) in the pseudo file system. +config FS_PIN + bool + default n + ---help--- + Selected by a file system that can pin a file's blocks in place and + hand out their media address, so that a module's read-only segment + can be executed where it already lies instead of being copied. + + The pin is held for as long as the module is loaded and given back + when it is unloaded. + config FS_PERMISSION bool "Enable UNIX Filesystem Permission Support" default n diff --git a/fs/xipfs/Kconfig b/fs/xipfs/Kconfig index fbdad9240a0..73351cf7ea2 100644 --- a/fs/xipfs/Kconfig +++ b/fs/xipfs/Kconfig @@ -7,6 +7,7 @@ config FS_XIPFS bool "XIPFS contiguous execute-in-place file system" default n depends on !DISABLE_MOUNTPOINT && MTD + select FS_PIN ---help--- Enable support for XIPFS, a file system that stores each file as a single physically contiguous, erase-block aligned extent on memory diff --git a/include/elf.h b/include/elf.h index a3d6dc8f927..f940dbabb67 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/elf.h b/include/nuttx/elf.h index abb3c2fa6dc..ab3a767d87d 100644 --- a/include/nuttx/elf.h +++ b/include/nuttx/elf.h @@ -37,6 +37,12 @@ #define ELF_PRARGSZ (80) /* Number of chars for args */ +/* An architecture that has no FDPIC ABI recognises no FDPIC object. */ + +#ifndef ELF_IS_FDPIC +# define ELF_IS_FDPIC(ehdr) false +#endif + /**************************************************************************** * Public Types ****************************************************************************/ diff --git a/include/nuttx/lib/elf.h b/include/nuttx/lib/elf.h index bcb8a039c80..1648da450e5 100644 --- a/include/nuttx/lib/elf.h +++ b/include/nuttx/lib/elf.h @@ -44,6 +44,19 @@ # define CONFIG_LIBC_ELF_MAXDEPEND 0 #endif +/* A compacting filesystem gives its media address with a pin that holds the + * blocks in place. The loader holds the pin through a file reference, + * because the unload runs on another task. + * + * That reference is taken with file_get() and file_dup2(), which are kernel + * side, so this is for the copy of libc that runs there. + */ + +#if defined(CONFIG_FS_PIN) && \ + (defined(CONFIG_BUILD_FLAT) || defined(__KERNEL__)) +# define HAVE_LIBC_ELF_PIN 1 +#endif + #ifndef CONFIG_LIBC_ELF_ALIGN_LOG2 # define CONFIG_LIBC_ELF_ALIGN_LOG2 2 #endif @@ -123,6 +136,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 +266,16 @@ struct mod_loadinfo_s * skip the copy. */ + /* True if e_ident[EI_OSABI] marked this an FDPIC object. */ + + bool fdpic; + +#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 7d5a7bcd671..fcf3227cf11 100644 --- a/libs/libc/elf/elf.h +++ b/libs/libc/elf/elf.h @@ -349,4 +349,26 @@ int libelf_addrenv_restore(FAR struct mod_loadinfo_s *loadinfo); void libelf_addrenv_free(FAR struct mod_loadinfo_s *loadinfo); #endif /* CONFIG_ARCH_ADDRENV */ + +#ifdef HAVE_LIBC_ELF_PIN + +/**************************************************************************** + * Name: libelf_pinrelease + * + * Description: + * Give back an XIP pin that the loader took, and the file that holds it. + * Does nothing if the loader took no pin. + * + * Input Parameters: + * pinfile - The held file. Cleared on return. + * + * Returned Value: + * None. + * + ****************************************************************************/ + +void libelf_pinrelease(FAR struct file **pinfile); + +#endif + #endif /* __LIBS_LIBC_LIBC_ELF_LIBC_ELF_H */ diff --git a/libs/libc/elf/elf_load.c b/libs/libc/elf/elf_load.c index e01e2fd7489..ceea2eb2d59 100644 --- a/libs/libc/elf/elf_load.c +++ b/libs/libc/elf/elf_load.c @@ -29,6 +29,7 @@ #include #include +#include #include #include #include @@ -40,6 +41,7 @@ #include #include +#include #include #include "libc.h" @@ -364,6 +366,13 @@ static inline int libelf_loadfile(FAR struct mod_loadinfo_s *loadinfo) { if (phdr->p_flags & PF_X) { + if (loadinfo->fdpic && loadinfo->xipbase != 0) + { + /* Mapped, not copied. */ + + continue; + } + ret = libelf_read(loadinfo, buffer_data_address(text), phdr->p_filesz, phdr->p_offset); @@ -539,10 +548,113 @@ skipload: return OK; } +/**************************************************************************** + * Name: libelf_xipacquire + * + * Description: + * Ask the filesystem for the address of this file on its media, so the + * read-only part of the object can run where it lies. Ask for a pin + * first: a compacting filesystem is not safe without one. Do not ask at + * all if this build cannot hold a pin. + * + * Returned Value: + * Zero if an address was obtained, a negated errno otherwise. Callers + * that can live without one may ignore the failure. + * + ****************************************************************************/ + +#ifdef HAVE_LIBC_ELF_PIN +static int libelf_pinhold(FAR struct mod_loadinfo_s *loadinfo) +{ + FAR struct file *filep; + int ret; + + /* The descriptor belongs to the task that called the loader, and the + * unload runs on another task. Hold the file instead. + */ + + loadinfo->pinfile = lib_zalloc(sizeof(struct file)); + if (loadinfo->pinfile == NULL) + { + return -ENOMEM; + } + + ret = file_get(loadinfo->filfd, &filep); + if (ret >= 0) + { + ret = file_dup2(filep, loadinfo->pinfile); + file_put(filep); + } + + if (ret < 0) + { + lib_free(loadinfo->pinfile); + loadinfo->pinfile = NULL; + } + + return ret; +} + +#endif + +static int libelf_xipacquire(FAR struct mod_loadinfo_s *loadinfo) +{ + uintptr_t base = 0; + +#ifdef HAVE_LIBC_ELF_PIN + if (ioctl(loadinfo->filfd, XIPFSIOC_PIN, (unsigned long)&base) >= 0) + { + int ret = libelf_pinhold(loadinfo); + + if (ret < 0) + { + berr("ERROR: Failed to hold the pinned file: %d\n", ret); + ioctl(loadinfo->filfd, XIPFSIOC_UNPIN, 0); + return ret; + } + + loadinfo->xipbase = base; + binfo("pinned xipbase %" PRIxPTR "\n", loadinfo->xipbase); + return OK; + } +#endif + + if (ioctl(loadinfo->filfd, FIOC_XIPBASE, (unsigned long)&base) >= 0) + { + loadinfo->xipbase = base; + binfo("can use xipbase %" PRIxPTR "\n", loadinfo->xipbase); + return OK; + } + + return -ENOTTY; +} + /**************************************************************************** * Public Functions ****************************************************************************/ +#ifdef HAVE_LIBC_ELF_PIN +/**************************************************************************** + * Name: libelf_pinrelease + * + * Description: + * Give back an XIP pin and the file it was held through, so the + * filesystem can reclaim the extent. + * + ****************************************************************************/ + +void libelf_pinrelease(FAR struct file **pinfile) +{ + if (*pinfile != NULL) + { + file_ioctl(*pinfile, XIPFSIOC_UNPIN, 0); + file_close(*pinfile); + lib_free(*pinfile); + *pinfile = NULL; + } +} +#endif + /**************************************************************************** * Name: libelf_load * @@ -559,6 +671,7 @@ skipload: int libelf_load(FAR struct mod_loadinfo_s *loadinfo) { int ret; + int i; binfo("loadinfo: %p\n", loadinfo); DEBUGASSERT(loadinfo && loadinfo->filfd >= 0); @@ -576,11 +689,7 @@ int libelf_load(FAR struct mod_loadinfo_s *loadinfo) if (loadinfo->gotindex >= 0) { binfo("GOT section found! index %d\n", loadinfo->gotindex); - if (ioctl(loadinfo->filfd, FIOC_XIPBASE, - (unsigned long)&loadinfo->xipbase) >= 0) - { - binfo("can use xipbase %zu\n", loadinfo->xipbase); - } + libelf_xipacquire(loadinfo); } /* Determine total size to allocate */ @@ -647,21 +756,96 @@ int libelf_load(FAR struct mod_loadinfo_s *loadinfo) } else if (loadinfo->ehdr.e_type == ET_DYN) { - loadinfo->textalloc = (uintptr_t)lib_memalign(loadinfo->textalign, - loadinfo->textsize + - loadinfo->datasize + - loadinfo->segpad); - - if (!loadinfo->textalloc) + if (loadinfo->fdpic) { - berr("ERROR: Failed to allocate memory for the module\n"); - ret = -ENOMEM; - goto errout_with_buffers; - } + /* The two segments are placed independently, thus only the + * writable segment is allocated, once per instance. + */ - loadinfo->datastart = loadinfo->textalloc + - loadinfo->textsize + - loadinfo->segpad; + if (loadinfo->xipbase != 0) + { + /* The text stays on the media. The media address is the base + * of the file, thus add the file offset of the segment. + */ + + for (i = 0; i < loadinfo->ehdr.e_phnum; i++) + { + FAR Elf_Phdr *phdr = &loadinfo->phdr[i]; + + if (phdr->p_type == PT_LOAD && + (phdr->p_flags & PF_X) != 0) + { + loadinfo->textalloc = loadinfo->xipbase + + phdr->p_offset; + break; + } + } + } + else if (loadinfo->textsize > 0) + { + /* The filesystem cannot show its media, thus copy the text + * to RAM. The instances no longer share it. + */ + +# if defined(CONFIG_ARCH_USE_TEXT_HEAP) && \ + defined(CONFIG_ARCH_USE_SEPARATED_SECTION) + loadinfo->textalloc = (uintptr_t) + up_textheap_memalign(".text", + loadinfo->textalign, + loadinfo->textsize); +# elif defined(CONFIG_ARCH_USE_TEXT_HEAP) + loadinfo->textalloc = (uintptr_t) + up_textheap_memalign(loadinfo->textalign, + loadinfo->textsize); +# else + loadinfo->textalloc = (uintptr_t) + lib_memalign(loadinfo->textalign, + loadinfo->textsize); +# endif + if (loadinfo->textalloc == 0) + { + berr("ERROR: Failed to allocate the module's text\n"); + ret = -ENOMEM; + goto errout_with_buffers; + } + } + + if (loadinfo->datasize > 0) + { + loadinfo->datastart = + (uintptr_t)lib_memalign(loadinfo->dataalign, + loadinfo->datasize); + if (!loadinfo->datastart) + { + berr("ERROR: Failed to allocate the module's data\n"); + ret = -ENOMEM; + goto errout_with_buffers; + } + } + } + else + { + /* Everything else keeps text and data adjacent: one allocation, + * data behind text. + */ + + loadinfo->textalloc = (uintptr_t) + lib_memalign(loadinfo->textalign, + loadinfo->textsize + + loadinfo->datasize + + loadinfo->segpad); + + if (!loadinfo->textalloc) + { + berr("ERROR: Failed to allocate memory for the module\n"); + ret = -ENOMEM; + goto errout_with_buffers; + } + + loadinfo->datastart = loadinfo->textalloc + + loadinfo->textsize + + loadinfo->segpad; + } } #endif /* CONFIG_LIBC_ELF_LOADTO_LMA */ @@ -732,11 +916,7 @@ int libelf_load_with_addrenv(FAR struct mod_loadinfo_s *loadinfo) if (loadinfo->gotindex >= 0) { 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 6e3e3afb041..1bc929027c8 100644 --- a/libs/libc/elf/elf_loadhdrs.c +++ b/libs/libc/elf/elf_loadhdrs.c @@ -29,6 +29,7 @@ #include #include #include +#include #include @@ -66,6 +67,23 @@ int libelf_loadhdrs(FAR struct mod_loadinfo_s *loadinfo) /* Verify that there are sections */ + /* The architecture knows how an FDPIC object announces itself. Ask it + * once. + */ + + loadinfo->fdpic = ELF_IS_FDPIC(&loadinfo->ehdr); + + /* A module is a shared object. An FDPIC object that is anything else + * would be placed through the wrong path. + */ + + if (loadinfo->fdpic && loadinfo->ehdr.e_type != ET_DYN) + { + berr("ERROR: FDPIC object is not a shared object: e_type=%u\n", + loadinfo->ehdr.e_type); + return -ENOEXEC; + } + if (loadinfo->ehdr.e_shnum < 1) { berr("ERROR: No sections(?)\n"); diff --git a/libs/libc/elf/elf_unload.c b/libs/libc/elf/elf_unload.c index c1c7609bf2f..9754a4da00f 100644 --- a/libs/libc/elf/elf_unload.c +++ b/libs/libc/elf/elf_unload.c @@ -59,6 +59,17 @@ int libelf_unload(FAR struct mod_loadinfo_s *loadinfo) libelf_freebuffers(loadinfo); +#ifdef HAVE_LIBC_ELF_PIN + /* Give the pin back if the loader took one, so the filesystem can + * reclaim the extent. + */ + + if (loadinfo->pinfile != NULL) + { + libelf_pinrelease(&loadinfo->pinfile); + } +#endif + #ifdef CONFIG_ARCH_ADDRENV if (loadinfo->addrenv != NULL) { @@ -68,9 +79,35 @@ int libelf_unload(FAR struct mod_loadinfo_s *loadinfo) #endif /* Release memory holding the relocated ELF image */ - /* ET_DYN has a single allocation so we only free textalloc */ + /* An FDPIC object placed its two segments separately. Free each one. If + * the text stayed on the media, it was never allocated, thus leave it. + */ - if (loadinfo->ehdr.e_type != ET_DYN) + if (loadinfo->fdpic) + { + if (loadinfo->textalloc != 0 && loadinfo->xipbase == 0) + { +#ifdef CONFIG_ARCH_USE_TEXT_HEAP + up_textheap_free((FAR void *)loadinfo->textalloc); +#else + lib_free((FAR void *)loadinfo->textalloc); +#endif + } + + if (loadinfo->datastart != 0) + { + lib_free((FAR void *)loadinfo->datastart); + loadinfo->datastart = 0; + } + + loadinfo->textalloc = 0; + loadinfo->textsize = 0; + loadinfo->datasize = 0; + } + + /* Any other ET_DYN has a single allocation so we only free textalloc */ + + else if (loadinfo->ehdr.e_type != ET_DYN) { #ifdef CONFIG_ARCH_USE_SEPARATED_SECTION int i;