binfmt/fdpic: Run constructors, and bind the PLT relocation table.

Two gaps that both fail quietly.

DT_INIT_ARRAY and DT_FINI_ARRAY were ignored entirely.  A C++ module with
any global object therefore loaded, resolved every symbol and ran, with all
of its globals left as .bss reading back zero -- no fault, no log, just
wrong answers.  Both arrays are now walked per object and in dependency
order: an object joins the load's list before the DT_NEEDED walk appends its
own dependencies, so walking that list backwards constructs a library before
the module that needs it, and destruction mirrors it.

Constructors run in whichever task called the loader, before the module's
own task exists, so the FDPIC register does not already hold the object's
data base the way it does once the module is running.  fdpic_callfn()
installs it around each call.  That is safe only because the firmware
reserves the register; being preempted mid-constructor is harmless, since
the register is part of the saved context.

DT_JMPREL was not parsed at all.  Which table an imported function's
descriptor lands in is a linker decision -- -z now puts it in DT_REL, and
without it the same entry goes to DT_JMPREL -- so a module linked the second
way loaded cleanly and then branched to an unrelocated address on its first
call into the firmware.  The symptom is an INVSTATE UsageFault escalated to
a HardFault: no console, no crash dump.  There is no lazy resolver here, so
an unwalked table is not deferred work; both are now bound eagerly, and
nothing is lost by that because a module carries a handful of relocations.

The two tables are not walked identically, which is the part worth
remembering.  In DT_REL the word being overwritten is the addend, and
dropping it breaks a static function reached through its section symbol.  In
DT_JMPREL that same word is the lazy-binding bootstrap -- the address of the
entry's own PLT resolution stub, with a GOT half of -1 -- and adding it to
the resolved symbol value produces an arbitrary address that faults exactly
like the bug this change fixes.  An eager binder overwrites the descriptor
outright.

The descriptor pool is sized from relsize + pltrelsz rather than relsize
alone, so an R_ARM_FUNCDESC in the PLT table cannot run off the end of the
allocation.  GNU ld does not appear to emit that combination -- an
address-taken function is not a call and so never becomes a PLT relocation
-- but the failure it would cause is heap corruption, and the guard is two
additions.

An object declaring RELA PLT relocations is refused rather than misread:
nothing here reads RELA, whose entries are twelve bytes rather than eight.

Assisted-by: Claude Code:claude-opus-4-8
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
This commit is contained in:
Marco Casaroli 2026-07-23 13:26:25 +02:00
parent ea78802f8d
commit d5b4edd761
2 changed files with 366 additions and 17 deletions

View file

@ -102,6 +102,17 @@
#define FDPIC_THUMB_BIT 1
/* Calling into a module from the loader -- which is what running its
* constructors amounts to -- needs the object's data base installed in the
* FDPIC register first, and that takes a little assembly. Where it cannot
* be done, a module carrying constructors is refused rather than run with
* whatever the register happened to hold.
*/
#if defined(CONFIG_ARCH_ARM) && defined(__thumb__)
# define FDPIC_HAVE_CALLFN 1
#endif
/****************************************************************************
* Private Function Prototypes
****************************************************************************/
@ -266,6 +277,7 @@ static int fdpic_readdynamic(FAR struct fdpic_loadinfo_s *loadinfo,
FAR const Elf32_Phdr *phdr)
{
FAR Elf32_Dyn *dyn;
uint32_t pltreltype = DT_REL;
size_t ndyn;
size_t i;
int ret;
@ -316,6 +328,34 @@ static int fdpic_readdynamic(FAR struct fdpic_loadinfo_s *loadinfo,
loadinfo->gotaddr = dyn[i].d_un.d_ptr;
break;
case DT_JMPREL:
loadinfo->pltrelvaddr = dyn[i].d_un.d_ptr;
break;
case DT_PLTRELSZ:
loadinfo->pltrelsize = dyn[i].d_un.d_val;
break;
case DT_PLTREL:
pltreltype = dyn[i].d_un.d_val;
break;
case DT_INIT_ARRAY:
loadinfo->initvaddr = dyn[i].d_un.d_ptr;
break;
case DT_INIT_ARRAYSZ:
loadinfo->initsize = dyn[i].d_un.d_val;
break;
case DT_FINI_ARRAY:
loadinfo->finivaddr = dyn[i].d_un.d_ptr;
break;
case DT_FINI_ARRAYSZ:
loadinfo->finisize = dyn[i].d_un.d_val;
break;
case DT_NEEDED:
/* An offset into DT_STRTAB. The string itself lives in the
* read-only segment, which is not mapped yet, so only the
@ -342,6 +382,19 @@ static int fdpic_readdynamic(FAR struct fdpic_loadinfo_s *loadinfo,
kmm_free(dyn);
/* DT_PLTREL says which format the PLT table is in. Nothing anywhere in
* this loader reads RELA, so an object claiming that layout must be
* refused rather than walked as if it were REL: the entries are 12 bytes,
* not 8, and misreading them would apply garbage relocations.
*/
if (loadinfo->pltrelsize != 0 && pltreltype != DT_REL)
{
berr("ERROR: %s has RELA PLT relocations, which are not supported\n",
loadinfo->name);
return -ENOEXEC;
}
if (loadinfo->gotaddr == 0)
{
/* DT_PLTGOT is only emitted when the object has a PLT, which a leaf
@ -495,9 +548,15 @@ static int fdpic_load(FAR struct fdpic_loadinfo_s *loadinfo)
* the relocations ask us to manufacture. Bounding that by the total
* relocation count costs a few bytes and avoids a second pass over the
* relocation table.
*
* Both tables have to be counted. An R_ARM_FUNCDESC in the PLT table
* draws from the same pool, so sizing this from DT_RELSZ alone would let
* a module carrying DT_JMPREL entries run off the end of the allocation
* and corrupt the heap.
*/
loadinfo->ndesc = loadinfo->relsize / sizeof(Elf32_Rel);
loadinfo->ndesc = (loadinfo->relsize + loadinfo->pltrelsize) /
sizeof(Elf32_Rel);
loadinfo->dataalloc = loadinfo->datamemsz +
(size_t)loadinfo->ndesc *
sizeof(struct fdpic_desc_s);
@ -902,35 +961,41 @@ static int fdpic_symvalue(FAR struct fdpic_loadinfo_s *obj,
}
/****************************************************************************
* Name: fdpic_bind
* Name: fdpic_reltable
*
* Description:
* Apply the module's dynamic relocations.
* Apply one REL relocation table.
*
* Input Parameters:
* pltrel - This is the DT_JMPREL table, so the words being overwritten
* are not addends. See the addend handling below.
*
****************************************************************************/
static int fdpic_bind(FAR struct fdpic_loadinfo_s *loadinfo,
FAR struct fdpic_loadinfo_s *head,
FAR const struct symtab_s *exports, int nexports)
static int fdpic_reltable(FAR struct fdpic_loadinfo_s *loadinfo,
FAR struct fdpic_loadinfo_s *head,
FAR const struct symtab_s *exports, int nexports,
uintptr_t relvaddr, size_t relsize,
bool pltrel, FAR const char *what)
{
FAR const Elf32_Rel *rels;
size_t nrels;
size_t i;
int ret;
if (loadinfo->relsize == 0)
if (relsize == 0)
{
return OK;
}
rels = (FAR const Elf32_Rel *)fdpic_addr(loadinfo, loadinfo->relvaddr);
rels = (FAR const Elf32_Rel *)fdpic_addr(loadinfo, relvaddr);
if (rels == NULL)
{
berr("ERROR: Relocations outside any segment\n");
berr("ERROR: %s of %s is outside any segment\n", what, loadinfo->name);
return -ENOEXEC;
}
nrels = loadinfo->relsize / sizeof(Elf32_Rel);
nrels = relsize / sizeof(Elf32_Rel);
for (i = 0; i < nrels; i++)
{
@ -1015,9 +1080,20 @@ static int fdpic_bind(FAR struct fdpic_loadinfo_s *loadinfo,
* Thumb bit -- carried entirely by the addend. Dropping it
* yields an even address and the core faults trying to
* execute it as ARM code.
*
* Except in the DT_JMPREL table, where that word is not an
* addend at all. A lazy descriptor is pre-loaded with the
* address of its PLT resolution stub and a GOT half of -1,
* for a resolver to overwrite on first call. Adding that
* stub address to the symbol value produces an arbitrary
* address, and the module faults on its first call exactly as
* if the relocation had never been applied -- which is a
* remarkably good imitation of the bug this table support was
* added to fix. An eager binder overwrites the descriptor
* outright.
*/
addend = desc->entry;
addend = pltrel ? 0 : desc->entry;
desc->entry = value + addend;
@ -1060,7 +1136,7 @@ static int fdpic_bind(FAR struct fdpic_loadinfo_s *loadinfo,
desc = (FAR struct fdpic_desc_s *)loadinfo->descpool +
loadinfo->usedesc++;
desc->entry = value + *where; /* addend, as above */
desc->entry = value + (pltrel ? 0 : *where); /* as above */
desc->got = (owner != NULL) ? owner->gotaddr
: loadinfo->gotaddr;
@ -1074,12 +1150,244 @@ static int fdpic_bind(FAR struct fdpic_loadinfo_s *loadinfo,
}
}
binfo("fdpic: applied %zu relocations, %u descriptors created\n",
nrels, loadinfo->usedesc);
binfo("fdpic: %s: applied %zu %s relocations, %u descriptors created\n",
loadinfo->name, nrels, what, loadinfo->usedesc);
return OK;
}
/****************************************************************************
* Name: fdpic_bind
*
* Description:
* Apply the module's dynamic relocations.
*
* Both relocation tables are walked, and both eagerly. Which one an
* imported function's descriptor lands in is purely a linker decision --
* -z now puts it in DT_REL, and without it the same entry goes to
* DT_JMPREL for a lazy resolver to fill in later. There is no resolver
* here, so a table left unwalked is not deferred work, it is a descriptor
* that stays unrelocated until the module branches through it. Binding
* both is what makes the layout stop mattering.
*
* Nothing is lost by binding the PLT table early: a module carries a
* handful of relocations, so there is no load time worth deferring.
*
* The two tables are not handled identically, though. A lazy descriptor
* arrives pre-loaded with its resolution stub rather than with an addend,
* so the PLT table is bound with that word ignored.
*
****************************************************************************/
static int fdpic_bind(FAR struct fdpic_loadinfo_s *loadinfo,
FAR struct fdpic_loadinfo_s *head,
FAR const struct symtab_s *exports, int nexports)
{
int ret;
ret = fdpic_reltable(loadinfo, head, exports, nexports,
loadinfo->relvaddr, loadinfo->relsize,
false, "DT_REL");
if (ret < 0)
{
return ret;
}
return fdpic_reltable(loadinfo, head, exports, nexports,
loadinfo->pltrelvaddr, loadinfo->pltrelsize,
true, "DT_JMPREL");
}
/****************************************************************************
* Name: fdpic_callfn
*
* Description:
* Call a function in a loaded object with that object's data base in the
* FDPIC register.
*
* A module's own code normally runs with the register already correct,
* because the scheduler installs it from the task's D-Space on every
* switch. Constructors do not get that: they run here, in whichever task
* called the loader, before the module's task exists. So the register has
* to be installed by hand around the call, and the function entered
* directly rather than through a descriptor.
*
* The base firmware is built with the register reserved, so nothing of the
* loader's own is being displaced; saving and restoring it covers the case
* where the loader was itself called from module code. Being preempted
* in the middle is harmless -- the register is part of the saved context,
* so it travels with whichever task is holding it.
*
****************************************************************************/
#ifdef FDPIC_HAVE_CALLFN
static void fdpic_callfn(uintptr_t entry, uintptr_t got)
{
__asm__ __volatile__
(
"mov r4, r9\n" /* Save whatever the caller had there */
"mov r9, %1\n" /* This object's data base */
"blx %0\n"
"mov r9, r4\n"
:
: "r" (entry), "r" (got)
: "r0", "r1", "r2", "r3", "r4", "r12", "lr", "cc", "memory"
);
}
#endif
/****************************************************************************
* Name: fdpic_callarray
*
* Description:
* Run one of the DT_INIT_ARRAY / DT_FINI_ARRAY tables of an object.
*
* The array lives in the writable segment and each entry carries an
* R_ARM_RELATIVE relocation, so by the time this runs the entries are
* already run-time code addresses with their Thumb bit intact. Nothing
* further has to be translated.
*
* Input Parameters:
* reverse - Walk the array backwards. Destructors run in the opposite
* order to constructors, which is the only reason the two
* directions exist.
*
****************************************************************************/
static int fdpic_callarray(FAR struct fdpic_loadinfo_s *obj,
uintptr_t vaddr, size_t size, bool reverse,
FAR const char *what)
{
FAR const uintptr_t *array;
#ifdef FDPIC_HAVE_CALLFN
size_t n;
size_t i;
#endif
if (vaddr == 0 || size == 0)
{
return OK;
}
array = (FAR const uintptr_t *)fdpic_addr(obj, vaddr);
if (array == NULL)
{
berr("ERROR: %s of %s is outside any segment\n", what, obj->name);
return -ENOEXEC;
}
#ifndef FDPIC_HAVE_CALLFN
berr("ERROR: %s has a %s, which this architecture cannot run\n",
obj->name, what);
return -ENOSYS;
#else
n = size / sizeof(uintptr_t);
binfo("fdpic: %s: %zu entries in %s\n", obj->name, n, what);
for (i = 0; i < n; i++)
{
uintptr_t entry = array[reverse ? n - 1 - i : i];
uintptr_t code = entry & ~FDPIC_THUMB_BIT;
/* 0 and ~0 are the conventional "no function here" fillers */
if (entry == 0 || entry == (uintptr_t)-1)
{
continue;
}
/* The entry must point into this object's text. Left unchecked, a
* relocation that did not happen -- the failure this loader is most
* prone to -- becomes a branch to an arbitrary address, which on this
* class of target means a HardFault with no console and no clue.
*/
if (code < obj->textaddr || code >= obj->textaddr + obj->textsize)
{
berr("ERROR: %s entry %zu of %s is %08lx, outside its text\n",
what, i, obj->name, (unsigned long)entry);
return -ENOEXEC;
}
fdpic_callfn(entry, obj->gotaddr);
}
return OK;
#endif
}
/****************************************************************************
* Name: fdpic_runinit
*
* Description:
* Run every object's constructors, dependencies first.
*
* An object joins the list before the DT_NEEDED walk appends its own
* dependencies, so a library always sits behind whatever needed it.
* Walking the list backwards therefore constructs a library before the
* object that uses it, which is the ordering that matters: a module's
* constructor may well touch a library object that has to exist already.
*
****************************************************************************/
static int fdpic_runinit(FAR struct fdpic_loadinfo_s *head)
{
FAR struct fdpic_loadinfo_s *done = NULL;
FAR struct fdpic_loadinfo_s *obj;
int ret;
for (; ; )
{
/* The object just ahead of the one done last time. The list is
* singly linked and short -- a handful of entries at most -- so
* rescanning it beats carrying a back pointer around.
*/
for (obj = head; obj != NULL && obj->flink != done; obj = obj->flink)
{
}
if (obj == NULL)
{
return OK;
}
ret = fdpic_callarray(obj, obj->initvaddr, obj->initsize, false,
"DT_INIT_ARRAY");
if (ret < 0)
{
return ret;
}
done = obj;
}
}
/****************************************************************************
* Name: fdpic_runfini
*
* Description:
* Run every object's destructors, in the mirror of the construction
* order: the module first, then the libraries it was built on.
*
* Errors are not propagated. This runs on the teardown path, where there
* is nothing left to abandon and reporting a failure would only turn an
* incomplete teardown into a leaked one.
*
****************************************************************************/
static void fdpic_runfini(FAR struct fdpic_loadinfo_s *head)
{
FAR struct fdpic_loadinfo_s *obj;
for (obj = head; obj != NULL; obj = obj->flink)
{
fdpic_callarray(obj, obj->finivaddr, obj->finisize, true,
"DT_FINI_ARRAY");
}
}
/****************************************************************************
* Name: fdpic_loadbinary
****************************************************************************/
@ -1130,6 +1438,24 @@ static int fdpic_loadbinary(FAR struct binary_s *binp,
}
}
/* Constructors, now that every object can reach its own data.
*
* fdpic_release() can undo a load but cannot undo a constructor, so this
* sits ahead of the D-Space allocation rather than after it: the only way
* left to discard a load with constructors already run is a constructor
* list that fails partway through, and such a module is unusable anyway.
* Destructors are not run for the entries that did succeed -- a table this
* loader has just found malformed is not one to branch into on the way
* out.
*/
ret = fdpic_runinit(head);
if (ret < 0)
{
berr("ERROR: Constructors of %s failed: %d\n", main_obj->name, ret);
goto errout;
}
binp->entrypt = (main_t)main_obj->entry;
binp->mapsize = 0;
binp->stacksize = CONFIG_FDPIC_STACKSIZE;
@ -1172,14 +1498,16 @@ errout:
* Name: fdpic_unloadbinary
*
* Description:
* Release the module. Unmapping the text drops the filesystem's pin on
* it, which is what allows a compacting filesystem to move those blocks
* again once no instance is executing from them.
* Release the module. Destructors run first, while the writable segment
* they operate on is still there. Unmapping the text then drops the
* filesystem's pin on it, which is what allows a compacting filesystem to
* move those blocks again once no instance is executing from them.
*
****************************************************************************/
static int fdpic_unloadbinary(FAR struct binary_s *binp)
{
fdpic_runfini(binp->mapped);
fdpic_release(binp->mapped);
binp->mapped = NULL;
return OK;

View file

@ -138,6 +138,27 @@ struct fdpic_loadinfo_s
uintptr_t relvaddr;
size_t relsize;
/* The PLT relocation table. Separate from DT_REL only by where the
* linker chose to put an entry: without -z now the imported function
* descriptors land here instead, and the loader binds both tables
* eagerly rather than requiring one particular layout.
*/
uintptr_t pltrelvaddr;
size_t pltrelsize;
/* Constructors and destructors. Both arrays live in the writable
* segment and their entries arrive as link-time addresses carrying an
* R_ARM_RELATIVE relocation, so they are real code addresses -- Thumb
* bit included -- only after fdpic_bind() has run.
*/
uintptr_t initvaddr;
size_t initsize;
uintptr_t finivaddr;
size_t finisize;
uintptr_t symtabvaddr;
uintptr_t strtabvaddr;
};