libc, sched: Resolve FDPIC descriptors at module callback entry points.

A module's function pointer is the address of a two-word descriptor in its
writable segment, not a code address.  Firmware that accepts one, stores it,
and later branches to it therefore jumps into the module's RAM data.  There
is no diagnostic: the board takes a HardFault with a dead console.

qsort and bsearch have resolved the descriptor since FDPIC support landed.
Nothing else did, and nothing noticed, because qsort's comparison function
was the only callback anything exercised.  That left every other entry point
looking perfectly usable -- a module could call one, it would return success,
and the fault arrived later from somewhere else.  Resolution is opt-in per
call site via fdpic_callback(), and this covers the rest of them:
pthread_create, signal, task_create, pthread_once, task_spawn, scandir and
sigaction.

Resolution happens once, in the innermost routine the paths share, for the
reason qsort already documents: resolving twice would treat a code address as
a descriptor.  task_create_with_stack resolves and task_create forwards to
it; nxsig_action() resolves for both signal() and a direct sigaction(), on
the local copy it already makes, so the caller's const struct is untouched
and the SA_SIGINFO form is covered through the union.

Two entry points need care beyond that pattern:

signal() has to exclude the dispositions by hand.  SIG_IGN, SIG_DFL, SIG_HOLD
and SIG_ERR are the integers 0, 1, 2 and -1 rather than addresses, and
fdpic_callback() declines to dereference NULL and nothing else -- handing it
SIG_ERR would read through (void *)-1.

scandir takes two pointers.  Its filter is called by scandir itself and is
resolved here; its comparison function is handed to qsort(), whose own entry
point resolves it, so resolving it here as well would resolve twice.  The
test passes both at once, which is what exercises that distinction.

Resolving the code address is sufficient at all of these.  A thread or task
created from a module inherits its D-Space -- nxtask_dup_dspace() runs before
up_initial_state() installs it in the FDPIC register -- and a signal handler
or pthread_once init routine runs in a context that already holds the
module's data base.

Verified on an RP2350 with a module that hands its own function to each entry
point.  Each assertion was also checked against a deliberate breakage:
removing the scandir filter resolution, and adding a second resolution of the
comparison function, each take the board down with a HardFault rather than
printing a FAIL line -- which is how a broken callback resolution manifests on
Cortex-M, and confirms the assertions test what they name.

mq_notify and timer_create with SIGEV_THREAD remain, and are harder: their
callback runs on a work-queue worker that carries no module data base at all.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
This commit is contained in:
Marco Casaroli 2026-07-30 13:42:32 +02:00
parent d5b4edd761
commit b7faaed58f
7 changed files with 123 additions and 2 deletions

View file

@ -31,6 +31,10 @@
#include <errno.h>
#include <stdlib.h>
#ifdef CONFIG_FDPIC
# include <nuttx/binfmt/fdpic.h>
#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_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);

View file

@ -30,6 +30,10 @@
#include <nuttx/pthread.h>
#ifdef CONFIG_FDPIC
# include <nuttx/binfmt/fdpic.h>
#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_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);
}

View file

@ -33,6 +33,10 @@
#include <nuttx/mutex.h>
#include <nuttx/debug.h>
#ifdef CONFIG_FDPIC
# include <nuttx/binfmt/fdpic.h>
#endif
/****************************************************************************
* Public Functions
****************************************************************************/
@ -73,6 +77,21 @@ int pthread_once(FAR pthread_once_t *once_control,
return EINVAL;
}
#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.
*
* 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);

View file

@ -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;

View file

@ -38,6 +38,10 @@
#include <nuttx/signal.h>
#include <nuttx/spinlock.h>
#ifdef CONFIG_FDPIC
# include <nuttx/binfmt/fdpic.h>
#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_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.

View file

@ -37,6 +37,10 @@
#include <nuttx/kthread.h>
#include <nuttx/fs/fs.h>
#ifdef CONFIG_FDPIC
# include <nuttx/binfmt/fdpic.h>
#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_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);

View file

@ -38,6 +38,10 @@
#include <nuttx/kthread.h>
#include <nuttx/spawn.h>
#ifdef CONFIG_FDPIC
# include <nuttx/binfmt/fdpic.h>
#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_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);