Stopped pthread_self() faulting when the caller is not a pthread (#627)

Nothing prevents an application mixing tx_thread_create() with the POSIX layer,
and a thread created that way has no POSIX control block. posix_thread2tcb()
returns NULL for it, which posix_thread2tid() then read through:

    p_tcb = posix_thread2tcb(thread_ptr);
    thread_ID = p_tcb->pthreadID;

pthread_self() went on to compound it, reading the signal fields of a POSIX_TCB
out of a thread that is only a TX_THREAD:

    if (((POSIX_TCB *) thread_ptr) -> signals.signal_handler)

The first is a null dereference and the second runs off the end of the control
block into whatever the linker put there. Under qemu-system-riscv32 the first one
lands first: mcause=0x5, a load access fault, with mtval=0xb4 for the offset of
pthreadID.

Have posix_thread2tid() report zero for a thread with no control block, which is
what px_pth_join.c already does for the same call, and have pthread_self() skip
the signal check unless the ID says the caller really is a pthread. Zero cannot
collide with a real ID because px_pth_create.c uses the address of the control
block as the ID.

This also covers the case where there is no current thread at all, from an ISR or
before the scheduler starts: tx_thread_identify() returns NULL, and the same
zero comes back instead of a fault.

Add posix_pthread_self_test, which asks both kinds of thread for their ID: a
pthread, which has to report what pthread_create() returned, and a plain ThreadX
thread, which has to report zero. Reverting either half of the fix turns the test
into the load access fault above.

Verified with riscv64-unknown-elf and qemu-system-riscv32: 4 tests across the
default build, 4 of 4 passing.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
This commit is contained in:
Frédéric Desbiens
2026-08-16 18:45:16 -04:00
committed by GitHub
parent f184531e7f
commit 2f6945475b
4 changed files with 203 additions and 4 deletions
@@ -19,6 +19,7 @@ set(posix_test_cases
${SOURCE_DIR}/posix_mq_basic_test.c
${SOURCE_DIR}/posix_mq_send_abort_test.c
${SOURCE_DIR}/posix_pthread_basic_test.c
${SOURCE_DIR}/posix_pthread_self_test.c
)
set(WEAK_DEFAULTS_SOURCE ${CMAKE_CURRENT_LIST_DIR}/../../../shared/regression/testcontrol_weak_defaults.c)
@@ -0,0 +1,177 @@
/***************************************************************************/
/* Copyright (c) 2026 Eclipse ThreadX contributors */
/* */
/* This program and the accompanying materials are made available under */
/* the terms of the MIT License which is available at */
/* https://opensource.org/licenses/MIT. */
/* */
/* AI Disclosure: This file was largely AI-generated by Claude Code */
/* (Opus 5). The AI-generated portions may be considered public domain */
/* (CC0-1.0) and not subject to the project's licence. The human */
/* contributor has reviewed and verified that the code is correct. */
/* */
/* SPDX-License-Identifier: MIT and CC0-1.0 */
/***************************************************************************/
/* This test covers pthread_self() being called from a thread that is not a
pthread.
Nothing stops an application mixing tx_thread_create() with the POSIX layer,
and a ThreadX thread created that way has no POSIX control block.
posix_thread2tcb() returns NULL for it, and pthread_self() used to read the
pthread ID through that null pointer, then read the signal fields of a
POSIX_TCB out of a plain TX_THREAD that never had any.
The test calls pthread_self() from both kinds of thread: a real pthread,
which must get the ID pthread_create() handed back, and a plain ThreadX
thread, which must get zero rather than a fault or a value read out of
whatever follows its control block. */
#include <stdio.h>
#include "tx_api.h"
#include "pthread.h"
#define TEST_STACK_BYTES 4096
static ULONG posix_region[192 * 1024 / sizeof(ULONG)];
static pthread_t worker;
static pthread_attr_t worker_attr;
/* A plain ThreadX thread, deliberately not created through pthread_create(). */
static TX_THREAD plain_thread;
static volatile INT worker_done;
static volatile INT plain_done;
static pthread_t worker_reported_id;
static pthread_t plain_reported_id;
extern void test_control_return(UINT status);
static VOID *worker_entry(VOID *input);
static VOID plain_entry(ULONG input);
static VOID checker_entry(ULONG input);
static TX_THREAD checker_thread;
/* Define what the initial system looks like. */
#ifdef CTEST
void test_application_define(void *first_unused_memory)
#else
void posix_pthread_self_application_define(void *first_unused_memory)
#endif
{
struct sched_param param;
VOID *storage_ptr;
CHAR *pointer;
pointer = (CHAR *) first_unused_memory;
storage_ptr = posix_initialize(posix_region);
/* A genuine pthread. */
pthread_attr_init(&worker_attr);
param.sched_priority = 15;
pthread_attr_setschedparam(&worker_attr, &param);
pthread_attr_setstackaddr(&worker_attr, storage_ptr);
if (pthread_create(&worker, &worker_attr, worker_entry, NULL) != OK)
{
printf("Running POSIX Pthread Self Test.................................. ERROR #1\n");
test_control_return(1);
}
/* A ThreadX thread that the POSIX layer knows nothing about. */
if (tx_thread_create(&plain_thread, "plain threadx thread", plain_entry, 0,
pointer, TEST_STACK_BYTES,
20, 20, TX_NO_TIME_SLICE, TX_AUTO_START) != TX_SUCCESS)
{
printf("Running POSIX Pthread Self Test.................................. ERROR #2\n");
test_control_return(1);
}
pointer = pointer + TEST_STACK_BYTES;
/* Runs last and inspects what the other two reported. */
if (tx_thread_create(&checker_thread, "checker", checker_entry, 0,
pointer, TEST_STACK_BYTES,
25, 25, TX_NO_TIME_SLICE, TX_AUTO_START) != TX_SUCCESS)
{
printf("Running POSIX Pthread Self Test.................................. ERROR #3\n");
test_control_return(1);
}
}
static VOID *worker_entry(VOID *input)
{
(void) input;
worker_reported_id = pthread_self();
worker_done = 1;
return (NULL);
}
/* The case that used to fault: a ThreadX thread asking for its pthread ID. */
static VOID plain_entry(ULONG input)
{
(void) input;
plain_reported_id = pthread_self();
plain_done = 1;
}
static VOID checker_entry(ULONG input)
{
(void) input;
printf("Running POSIX Pthread Self Test.................................. ");
/* Both threads run above this one, so they have finished by now. */
if ((worker_done == 0) || (plain_done == 0))
{
printf("ERROR #4\n");
test_control_return(1);
}
/* A pthread must report the ID pthread_create() produced. */
if (worker_reported_id != worker)
{
printf("ERROR #5\n");
test_control_return(1);
}
/* A pthread ID is the address of its control block, so a real one is never
zero. */
if (worker_reported_id == (pthread_t) 0)
{
printf("ERROR #6\n");
test_control_return(1);
}
/* The plain ThreadX thread has no pthread ID, and must be told so rather
than handed something read out of memory that follows its TX_THREAD. */
if (plain_reported_id != (pthread_t) 0)
{
printf("ERROR #7\n");
test_control_return(1);
}
printf("SUCCESS!\n");
test_control_return(0);
}
@@ -550,6 +550,18 @@ POSIX_TCB *p_tcb;
/* Get the TCB for this pthread */
p_tcb = posix_thread2tcb(thread_ptr);
/* A running ThreadX thread that was not created through pthread_create()
has no TCB in the pool, and posix_thread2tcb() returns NULL for it. It
has no pthread ID either, so report zero rather than reading through the
null pointer. Zero can never collide with a real ID, because
px_pth_create.c uses the address of the TCB as the ID. */
if (!p_tcb)
{
return((pthread_t) 0);
}
thread_ID = p_tcb->pthreadID;
/* All done. */
@@ -74,12 +74,21 @@ pthread_t thread_ID;
thread_ID = posix_thread2tid(thread_ptr);
/* Determine if this thread is actually the signal thread helper. */
if (((POSIX_TCB *) thread_ptr) -> signals.signal_handler)
/* Only a pthread has the signal fields read below: they belong to the
POSIX_TCB that surrounds the thread, and reading them through anything
else runs off the end of a plain TX_THREAD. A zero ID means the caller
is not a pthread, either because it is a ThreadX thread created directly
or because there is no current thread at all, so leave it alone. */
if (thread_ID != (pthread_t) 0)
{
/* Yes, override the thread_ID with the non-signal thread ID. */
thread_ID = (pthread_t) ((POSIX_TCB *) thread_ptr) -> signals.base_thread_ptr;
/* Determine if this thread is actually the signal thread helper. */
if (((POSIX_TCB *) thread_ptr) -> signals.signal_handler)
{
/* Yes, override the thread_ID with the non-signal thread ID. */
thread_ID = (pthread_t) ((POSIX_TCB *) thread_ptr) -> signals.base_thread_ptr;
}
}