nuttx/sched/semaphore/sem_post.c
Justin Hammond 31fbb99218
Some checks are pending
MemBrowse Memory Report / changes-filter (push) Waiting to run
MemBrowse Memory Report / load-targets (push) Waiting to run
MemBrowse Memory Report / identical (push) Blocked by required conditions
MemBrowse Memory Report / analyze (push) Blocked by required conditions
sched/semaphore: Keep a negative task id out of the mutex holder.
A mutex records its holder as a task id in the low 31 bits of a word
whose top bit means "someone is blocked on this".  The id was stored
without masking, so an id with its top bit set became a holder with the
blocking bit raised.

Task ids are normally small and positive, but not always.
nxsched_gettid() reports -ESRCH for a context that no longer maps to a
running task, and there is a window where that is exactly what the
running context is: nxtask_exit() marks the next task ready to run
while the dying task is still executing on its own stack, and only then
releases the TCB.  Freeing the group inside that release takes and
drops the group's mutexes, so the lock stores 0xfffffffd and the unlock
compares 0x7ffffffd, which are not equal.

With assertions enabled the unlock trips its holder check, and every
exit of a process that frees memory panics.  In a kernel build that is
every exit, so no program could be run twice, and running one at all
took the shell down with it.  Without assertions the failure is silent:
the accidental blocking bit sends the unlock looking for a waiter that
never existed.

Encode the id the same way everywhere it is stored or compared, so that
a lock and an unlock from one context agree whatever the id's sign.
The masked forms of -1 and -2 would alias the "no holder" and "reset"
values, but nxsched_gettid() yields only valid ids and -ESRCH.

mm_lock() already sidesteps this window with a note that gettid() may
return -ESRCH during a context switch; this gives the generic mutex the
same footing rather than a second special case.

Test case, on the EIC7700 EVB, which is a kernel build with assertions:

  nsh> hello
  Hello, World!!

Before, that printed and then panicked in sem_post, taking the shell
with it, every time.  After, five runs in a row complete and the shell
survives.  ps over telnet still completes.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Justin Hammond <justin@dynam.ac>
2026-08-09 10:05:30 +08:00

270 lines
8.5 KiB
C

/****************************************************************************
* sched/semaphore/sem_post.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 <limits.h>
#include <errno.h>
#include <sched.h>
#include <nuttx/irq.h>
#include <nuttx/arch.h>
#include "sched/sched.h"
#include "semaphore/semaphore.h"
/****************************************************************************
* Public Functions
****************************************************************************/
/****************************************************************************
* Name: nxsem_post_slow
*
* Description:
* When a kernel thread has finished with a semaphore, it will call
* nxsem_post(). This function unlocks the semaphore referenced by sem
* by performing the semaphore unlock operation on that semaphore.
*
* If the semaphore value resulting from this operation is positive, then
* no tasks were blocked waiting for the semaphore to become unlocked; the
* semaphore is simply incremented.
*
* If the value of the semaphore resulting from this operation is zero,
* then one of the tasks blocked waiting for the semaphore shall be
* allowed to return successfully from its call to nxsem_wait().
*
* Input Parameters:
* sem - Semaphore descriptor
*
* Returned Value:
* This is an internal OS interface and should not be used by applications.
* It follows the NuttX internal error return policy: Zero (OK) is
* returned on success. A negated errno value is returned on failure.
*
* Assumptions:
* This function may be called from an interrupt handler.
*
****************************************************************************/
int nxsem_post_slow(FAR sem_t *sem)
{
FAR struct tcb_s *stcb = NULL;
irqstate_t flags;
#if defined(CONFIG_PRIORITY_INHERITANCE) || defined(CONFIG_PRIORITY_PROTECT)
uint8_t proto;
#endif
bool blocking = false;
bool mutex = NXSEM_IS_MUTEX(sem);
uint32_t mholder = NXSEM_NO_MHOLDER;
int ret = OK;
/* The following operations must be performed with interrupts
* disabled because sem_post() may be called from an interrupt
* handler.
*/
flags = enter_critical_section();
if (mutex)
{
/* Mutex post from interrupt context is not allowed */
DEBUGASSERT(!up_interrupt_context());
/* Lock the mutex for us by setting the blocking bit */
mholder = atomic_fetch_or(NXSEM_MHOLDER(sem), NXSEM_MBLOCKING_BIT);
/* Mutex post from another thread is not allowed, unless
* called from nxsem_reset. The comparison uses the same encoding
* as the lock side so that a context whose id is -ESRCH, which is
* what a task being torn down reports, still matches its own lock.
*/
DEBUGASSERT(mholder == (NXSEM_MBLOCKING_BIT | NXSEM_MRESET) ||
(mholder & (~NXSEM_MBLOCKING_BIT)) ==
NXSEM_MAKE_MHOLDER(nxsched_gettid()));
blocking = NXSEM_MBLOCKING(mholder);
if (!blocking)
{
if (mholder != NXSEM_MRESET)
{
mholder = NXSEM_NO_MHOLDER;
}
atomic_set(NXSEM_MHOLDER(sem), mholder);
}
}
else
{
int32_t sem_count;
/* Check the maximum allowable value */
sem_count = atomic_read(NXSEM_COUNT(sem));
do
{
#ifdef CONFIG_CUSTOM_SEMAPHORE_MAXVALUE
if (sem_count >= sem->maxvalue)
#else
if (sem_count >= SEM_VALUE_MAX)
#endif
{
ret = -EOVERFLOW;
break;
}
}
while (!atomic_try_cmpxchg_release(NXSEM_COUNT(sem), &sem_count,
sem_count + 1));
blocking = sem_count < 0;
}
if (ret == OK)
{
/* Perform the semaphore unlock operation, releasing this task as a
* holder then also incrementing the count on the semaphore.
*
* NOTE: When semaphores are used for signaling purposes, the holder
* of the semaphore may not be this thread! In this case,
* nxsem_release_holder() will do nothing.
*
* In the case of a mutex this could be simply resolved since there is
* only one holder but for the case of counting semaphores, there may
* be many holders and if the holder is not this thread, then it is
* not possible to know which thread/holder should be released.
*
* For this reason, it is recommended that priority inheritance be
* disabled via nxsem_set_protocol(SEM_PRIO_NONE) when the semaphore is
* initialized if the semaphore is to used for signaling purposes.
*/
if (!mutex || blocking)
{
nxsem_release_holder(sem);
}
#if defined(CONFIG_PRIORITY_INHERITANCE) || defined(CONFIG_PRIORITY_PROTECT)
/* Don't let any unblocked tasks run until we complete any priority
* restoration steps. Interrupts are disabled, but we do not want
* the head of the ready-to-run list to be modified yet.
*
* NOTE: If this sched_lock is called from an interrupt handler, it
* will do nothing.
*/
proto = sem->flags & SEM_PRIO_MASK;
if (proto != SEM_PRIO_NONE)
{
sched_lock();
}
#endif
/* If the result of semaphore unlock is non-positive, then
* there must be some task waiting for the semaphore.
*/
if (blocking)
{
/* Check if there are any tasks in the waiting for semaphore
* task list that are waiting for this semaphore. This is a
* prioritized list so the first one we encounter is the one
* that we want.
*/
stcb = (FAR struct tcb_s *)dq_remfirst(SEM_WAITLIST(sem));
if (stcb != NULL)
{
FAR struct tcb_s *rtcb = this_task();
/* The task will be the new holder of the semaphore when
* it is awakened.
*/
if (mutex)
{
uint32_t blocking_bit = dq_empty(SEM_WAITLIST(sem)) ?
0 : NXSEM_MBLOCKING_BIT;
atomic_set(NXSEM_MHOLDER(sem),
((uint32_t)stcb->pid) | blocking_bit);
}
else
{
nxsem_add_holder_tcb(stcb, sem);
}
/* Stop the watchdog timer */
wd_cancel(&stcb->waitdog);
/* Indicate that the wait is over. */
stcb->waitobj = NULL;
/* Add the task to ready-to-run task list and
* perform the context switch if one is needed
*/
if (nxsched_add_readytorun(stcb))
{
up_switch_context(this_task(), rtcb);
}
}
}
/* Check if we need to drop the priority of any threads holding
* this semaphore. The priority could have been boosted while they
* held the semaphore.
*/
#if defined(CONFIG_PRIORITY_INHERITANCE) || defined(CONFIG_PRIORITY_PROTECT)
if (proto != SEM_PRIO_NONE)
{
if (proto == SEM_PRIO_INHERIT)
{
# ifdef CONFIG_PRIORITY_INHERITANCE
nxsem_restore_baseprio(stcb, sem);
# endif
}
else if (proto == SEM_PRIO_PROTECT)
{
# ifdef CONFIG_PRIORITY_PROTECT
nxsem_protect_post(sem);
# endif
}
sched_unlock();
}
#endif
}
/* Interrupts may now be enabled. */
leave_critical_section(flags);
return ret;
}