From 74836bcaf095e983c7818138c24d1b3afa3a5618 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Mon, 3 Aug 2026 08:58:41 +0200 Subject: [PATCH] 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 | 62 +++++++++++++++ libs/libc/elf/elf_bind.c | 64 +++++++++++++++- libs/libc/machine/arm/armv7-m/arch_elf.c | 96 +++++++++++++++++++++++- libs/libc/machine/arm/armv8-m/arch_elf.c | 96 +++++++++++++++++++++++- 4 files changed, 314 insertions(+), 4 deletions(-) diff --git a/arch/arm/include/elf.h b/arch/arm/include/elf.h index 460cb34ad4efb..b0bcf2c676cf5 100644 --- a/arch/arm/include/elf.h +++ b/arch/arm/include/elf.h @@ -264,10 +264,72 @@ #define DT_ARM_PREEMPTMAP 0x70000002 #define DT_ARM_RESERVED2 0x70000003 +/* Loader state the FDPIC relocations need: the object's data base, and the + * descriptor pool cursor, which must survive from one relocation to the + * next. It arrives through the arch_data channel. + */ + +#define ARCH_ELFDATA 1 + +#define ARCH_ELFDATA_INIT(d, l) \ + do \ + { \ + (d)->fdpic = (l)->fdpic; \ + (d)->gotbase = (l)->gotbase; \ + (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) + +/* Which relocation table is being walked. 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. + */ + +#define ARCH_ELFDATA_SET_PLTREL(d, v) (d)->pltrel = (v) + /**************************************************************************** * Public Types ****************************************************************************/ +#ifndef __ASSEMBLY__ + +/* A function descriptor is what an FDPIC function pointer is. Its shape is + * common, so struct fdpic_desc_s from include/nuttx/fdpic.h serves; only a + * pointer to one is kept here, thus the tag is enough. + */ + +struct fdpic_desc_s; + +struct arch_elfdata_s +{ + uint8_t fdpic; /* The object is an FDPIC one */ + uintptr_t gotbase; /* DT_PLTGOT: this object's data base */ + uint16_t ndesc; /* Capacity, in descriptors */ + uint16_t usedesc; /* Next free slot */ + + /* The pool the descriptors are taken from */ + + FAR struct fdpic_desc_s *descpool; + + 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 4e0727222dd46..fae5b5f3cec1e 100644 --- a/libs/libc/elf/elf_bind.c +++ b/libs/libc/elf/elf_bind.c @@ -58,6 +58,25 @@ # define ARCH_ELFDATA_PARM NULL #endif +/* Move loader state in and out of the arch_data block, and say which + * relocation table is being walked. Nothing for an architecture whose + * relocations do not need any of it. + */ + +#if defined(ARCH_ELFDATA) && defined(ARCH_ELFDATA_SET_PLTREL) +# define ARCH_ELFDATA_PLTREL(v) ARCH_ELFDATA_SET_PLTREL(&arch_data, v) +#else +# define ARCH_ELFDATA_PLTREL(v) +#endif + +#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 +739,8 @@ static int libelf_relocatedyn(FAR struct module_s *modp, * for it names this base. */ - loadinfo->gotbase = dyn[i].d_un.d_ptr; + loadinfo->gotbase = libelf_addr(loadinfo, + dyn[i].d_un.d_ptr); break; /* The constructor and destructor tables. Section headers are @@ -778,6 +798,13 @@ static int libelf_relocatedyn(FAR struct module_s *modp, } } + /* After the loop, because DT_PLTGOT is read there. Both relocation + * tables are walked under this one arch_data, so the pool cursor + * survives from one to the next. + */ + + ARCH_ELFDATA_SETUP(loadinfo); + symhdr = &loadinfo->shdr[loadinfo->dsymtabidx]; sym = lib_malloc(symhdr->sh_size); if (!sym) @@ -815,6 +842,10 @@ static int libelf_relocatedyn(FAR struct module_s *modp, ret = OK; lrelent = reldata.relsz[idx_rel] / reldata.relentsz[idx_rel]; + /* Say which table this is, for an architecture that cares. */ + + ARCH_ELFDATA_PLTREL(idx_rel == I_PLT); + for (i = 0; i < lrelent; i++) { /* Process each relocation entry @@ -909,7 +940,30 @@ static int libelf_relocatedyn(FAR struct module_s *modp, addr += rela->r_addend; } - *(FAR uintptr_t *)addr = (uintptr_t)ep; + /* An import may be a descriptor under FDPIC, which is + * built rather than assigned, so the relocation type + * decides what to write. Everything else stores the + * resolved address, which R_ARM_JUMP_SLOT and + * R_ARM_GLOB_DAT do, so one path serves both. + */ + + 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 if (loadinfo->fdpic) { @@ -975,6 +1029,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 f167b8c3daf55..e0468c71edef6 100644 --- a/libs/libc/machine/arm/armv7-m/arch_elf.c +++ b/libs/libc/machine/arm/armv7-m/arch_elf.c @@ -32,6 +32,7 @@ #include #include +#include /**************************************************************************** * Public Functions @@ -126,7 +127,8 @@ int up_relocate(const Elf32_Rel *rel, const Elf32_Sym *sym, uintptr_t addr, relotype = ELF32_R_TYPE(rel->r_info); if (sym == NULL && relotype != R_ARM_NONE && relotype != R_ARM_V4BX && - relotype != R_ARM_RELATIVE && relotype != R_ARM_JUMP_SLOT) + relotype != R_ARM_RELATIVE && relotype != R_ARM_JUMP_SLOT && + relotype != R_ARM_GLOB_DAT) { return -EINVAL; } @@ -175,6 +177,97 @@ int up_relocate(const Elf32_Rel *rel, const Elf32_Sym *sym, uintptr_t addr, } break; + case R_ARM_FUNCDESC_VALUE: + { + /* The target is a descriptor: entry point and data base. The + * addend sits in the word that becomes the entry point and + * carries the Thumb bit, so it must be kept. The base written is + * this object's own, which is what makes a callback work. + */ + + struct fdpic_desc_s *desc = + (struct fdpic_desc_s *)addr; + arch_elfdata_t *data = (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 its PLT stub address, not an + * addend. Overwrite it, do not add to it. + */ + + desc->entry = sym->st_value; + } + else + { + desc->entry = sym->st_value + desc->entry; + } + + desc->got = data->gotbase; + } + 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. + */ + + struct fdpic_desc_s *desc; + arch_elfdata_t *data = (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 = 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->gotbase; + + *(uint32_t *)addr = (uint32_t)(uintptr_t)desc; + } + break; + case R_ARM_ABS32: case R_ARM_TARGET1: /* New ABI: TARGET1 always treated as ABS32 */ { @@ -501,6 +594,7 @@ int up_relocate(const Elf32_Rel *rel, const Elf32_Sym *sym, uintptr_t addr, break; case R_ARM_RELATIVE: + case R_ARM_GLOB_DAT: case R_ARM_JUMP_SLOT: { *(uint32_t *)addr = (uint32_t)sym->st_value; diff --git a/libs/libc/machine/arm/armv8-m/arch_elf.c b/libs/libc/machine/arm/armv8-m/arch_elf.c index 27a6e673f22b9..a0e0ec998c3e2 100644 --- a/libs/libc/machine/arm/armv8-m/arch_elf.c +++ b/libs/libc/machine/arm/armv8-m/arch_elf.c @@ -32,6 +32,7 @@ #include #include +#include /**************************************************************************** * Public Functions @@ -126,7 +127,8 @@ int up_relocate(const Elf32_Rel *rel, const Elf32_Sym *sym, uintptr_t addr, relotype = ELF32_R_TYPE(rel->r_info); if (sym == NULL && relotype != R_ARM_NONE && relotype != R_ARM_V4BX && - relotype != R_ARM_RELATIVE && relotype != R_ARM_JUMP_SLOT) + relotype != R_ARM_RELATIVE && relotype != R_ARM_JUMP_SLOT && + relotype != R_ARM_GLOB_DAT) { return -EINVAL; } @@ -175,6 +177,97 @@ int up_relocate(const Elf32_Rel *rel, const Elf32_Sym *sym, uintptr_t addr, } break; + case R_ARM_FUNCDESC_VALUE: + { + /* The target is a descriptor: entry point and data base. The + * addend sits in the word that becomes the entry point and + * carries the Thumb bit, so it must be kept. The base written is + * this object's own, which is what makes a callback work. + */ + + struct fdpic_desc_s *desc = + (struct fdpic_desc_s *)addr; + arch_elfdata_t *data = (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 its PLT stub address, not an + * addend. Overwrite it, do not add to it. + */ + + desc->entry = sym->st_value; + } + else + { + desc->entry = sym->st_value + desc->entry; + } + + desc->got = data->gotbase; + } + 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. + */ + + struct fdpic_desc_s *desc; + arch_elfdata_t *data = (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 = 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->gotbase; + + *(uint32_t *)addr = (uint32_t)(uintptr_t)desc; + } + break; + case R_ARM_ABS32: case R_ARM_TARGET1: /* New ABI: TARGET1 always treated as ABS32 */ { @@ -501,6 +594,7 @@ int up_relocate(const Elf32_Rel *rel, const Elf32_Sym *sym, uintptr_t addr, break; case R_ARM_RELATIVE: + case R_ARM_GLOB_DAT: case R_ARM_JUMP_SLOT: { *(uint32_t *)addr = (uint32_t)sym->st_value;