nuttx/sched/timer/timer_create.c
Marco Casaroli ac7b5f5218 sched: Run a module's SIGEV_THREAD notification with its data base.
A SIGEV_THREAD notification -- from mq_notify() or timer_create() -- runs its
callback on a shared signal-notification work queue, not in the registering
task.  That worker carries no FDPIC data base, so a module's callback reaches
it with the wrong base and cannot touch its own globals.  This is unlike every
other callback entry point, where the callback runs in a task that inherited
the module's data space and resolving the code address is enough.

The base is knowable exactly once, at registration, when the call is still in
the module's own context: capture it there with fdpic_base() into the
persisted work structure (mq's ntwork, the timer's pt_work).  At send or
expiry the descriptor is resolved to its code address -- a plain memory read
that needs no base -- and stored in work->func.  The worker, seeing a non-zero
base, installs it in the FDPIC register around the call and restores it after;
a zero base, which is every non-module callback, takes the direct path
unchanged.

fdpic_base() exposes the test fdpic_callback() already makes internally --
whether the caller is a module -- for a site that has to decide before it
stores a pointer somewhere the register will no longer be correct.

fdpic_invoke() is the install-call-restore, in the same ARM-thumb inline asm
as the rest of fdpic.h.  It saves the register on the stack and keeps the push
8-byte aligned, and pins the argument in r0, so it asks the allocator for only
two free registers -- enough on builds that also reserve a frame pointer.  It
is safe against preemption: the FDPIC register is REG_PIC in the saved
context, preserved across a context switch, and base firmware reserves it so
no interrupt handler disturbs it.  A context switch or interrupt while the
callback runs therefore keeps the module's base.

Verified with CONFIG_SIG_EVTHREAD on two ARM cores: an RP2350 (Cortex-M33,
armv8-m) on hardware and mps2-an500 (Cortex-M7, armv7e-m) under QEMU.  On each
a module's mq_notify and timer_create SIGEV_THREAD callbacks run on the worker
and write a distinctive value into a module global, proving the base was
installed.  Removing just the register install makes the same callback
HardFault the board, confirming it is load-bearing.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
2026-08-01 12:23:27 +02:00

246 lines
8.2 KiB
C

/****************************************************************************
* sched/timer/timer_create.c
*
* 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.
*
****************************************************************************/
/****************************************************************************
* Included Files
****************************************************************************/
#include <nuttx/config.h>
#include <stdint.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <errno.h>
#include <nuttx/irq.h>
#include <nuttx/wdog.h>
#include <nuttx/kmalloc.h>
#include <nuttx/spinlock.h>
#if defined(CONFIG_FDPIC) && defined(CONFIG_SIG_EVTHREAD)
# include <nuttx/binfmt/fdpic.h>
#endif
#include "sched/sched.h"
#include "timer/timer.h"
#ifndef CONFIG_DISABLE_POSIX_TIMERS
/****************************************************************************
* Private Functions
****************************************************************************/
/****************************************************************************
* Name: timer_allocate
*
* Description:
* Allocate one POSIX timer and place it into the allocated timer list.
*
****************************************************************************/
static FAR struct posix_timer_s *timer_allocate(void)
{
FAR struct posix_timer_s *ret;
irqstate_t flags;
uint8_t pt_flags;
/* Try to get a preallocated timer from the free list */
#if CONFIG_PREALLOC_TIMERS > 0
flags = spin_lock_irqsave(&g_locktimers);
ret = (FAR struct posix_timer_s *)
sq_remfirst((FAR sq_queue_t *)&g_freetimers);
spin_unlock_irqrestore(&g_locktimers, flags);
/* Did we get one? */
if (ret)
{
pt_flags = PT_FLAGS_PREALLOCATED;
}
else
#endif
{
/* Allocate a new timer from the heap */
ret = (FAR struct posix_timer_s *)
kmm_malloc(sizeof(struct posix_timer_s));
pt_flags = 0u;
}
/* If we have a timer, then put it into the allocated timer list */
if (ret)
{
/* Initialize the timer structure */
memset(ret, 0, sizeof(struct posix_timer_s));
ret->pt_flags = pt_flags;
/* And add it to the end of the list of allocated timers */
flags = spin_lock_irqsave(&g_locktimers);
sq_addlast((FAR sq_entry_t *)ret, (FAR sq_queue_t *)&g_alloctimers);
spin_unlock_irqrestore(&g_locktimers, flags);
}
return ret;
}
/****************************************************************************
* Public Functions
****************************************************************************/
/****************************************************************************
* Name: timer_create
*
* Description:
* The timer_create() function creates per-thread timer using the
* specified clock, clock_id, as the timing base. The timer_create()
* function returns, in the location referenced by timerid, a timer ID of
* type timer_t used to identify the timer in timer requests. This timer
* ID is unique until the timer is deleted. The particular clock, clock_id,
* is defined in <time.h>. The timer whose ID is returned will be in a
* disarmed state upon return from timer_create().
*
* The evp argument, if non-NULL, points to a sigevent structure. This
* structure is allocated by the called and defines the asynchronous
* notification to occur. If the evp argument is NULL, the effect is as
* if the evp argument pointed to a sigevent structure with the
* sigev_notify member having the value SIGEV_SIGNAL, the sigev_signo
* having a default signal number, and the sigev_value member having the
* value of the timer ID.
*
* Each implementation defines a set of clocks that can be used as timing
* bases for per-thread timers.
*
* Input Parameters:
* clockid - Specifies the clock to use as the timing base.
* evp - Refers to a user allocated sigevent structure that defines the
* asynchronous notification. evp may be NULL (see above).
* timerid - The pre-thread timer created by the call to timer_create().
*
* Returned Value:
* If the call succeeds, timer_create() will return 0 (OK) and update the
* location referenced by timerid to a timer_t, which can be passed to the
* other per-thread timer calls. If an error occurs, the function will
* return a value of -1 (ERROR) and set errno to indicate the error.
*
* EAGAIN - The system lacks sufficient signal queuing resources to honor
* the request.
* EAGAIN - The calling process has already created all of the timers it
* is allowed by this implementation.
* EINVAL - The specified clock ID is not defined.
* ENOTSUP - The implementation does not support the creation of a timer
* attached to the CPU-time clock that is specified by clock_id and
* associated with a thread different thread invoking timer_create().
*
* Assumptions:
*
****************************************************************************/
int timer_create(clockid_t clockid, FAR struct sigevent *evp,
FAR timer_t *timerid)
{
FAR struct posix_timer_s *ret = NULL;
FAR struct tcb_s *tcb = this_task();
/* Sanity checks. */
if (timerid == NULL || (clockid != CLOCK_REALTIME &&
clockid != CLOCK_MONOTONIC && clockid != CLOCK_BOOTTIME) ||
(evp != NULL && evp->sigev_notify == SIGEV_SIGNAL &&
!GOOD_SIGNO(evp->sigev_signo)))
{
set_errno(EINVAL);
}
else
{
/* Allocate a timer instance to contain the watchdog */
ret = timer_allocate();
if (!ret)
{
set_errno(EAGAIN);
}
else
{
/* Initialize the timer instance */
ret->pt_clock = clockid;
ret->pt_crefs = 1u;
ret->pt_owner = tcb->pid;
ret->pt_delay = 0u;
ret->pt_expected = 0u;
/* Was a struct sigevent provided? */
if (evp)
{
/* Yes, copy the entire struct sigevent content */
memcpy(&ret->pt_event, evp, sizeof(struct sigevent));
#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
* 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
{
/* "If the evp argument is NULL, the effect is as if the evp
* argument pointed to a sigevent structure with the
* sigev_notify member having the value SIGEV_SIGNAL,
* the sigev_signo having a default signal number,
* and the sigev_value member having the value of the
* timer ID."
*/
ret->pt_event.sigev_notify = SIGEV_SIGNAL;
ret->pt_event.sigev_signo = SIGALRM;
ret->pt_event.sigev_value.sival_ptr = ret;
#ifdef CONFIG_SIG_EVTHREAD
ret->pt_event.sigev_notify_function = NULL;
ret->pt_event.sigev_notify_attributes = NULL;
#endif
}
/* Return the timer */
*timerid = ret;
}
}
return ret ? OK : ERROR;
}
#endif /* CONFIG_DISABLE_POSIX_TIMERS */