Files
rtems/cpukit/libdebugger/rtems-debugger-target.c
Kinsey Moore 3d8b56f10c cpukit/libdebugger: Prevent hang on memory access
When memory is accessed by the remote debugging client, the access is
sandboxed with setjmp/longjmp and appropriate exception handlers to
prevent the attempted access from causing a failure of the debugger or
otherwise altering execution. The existing implementation works as
expected when the context executing the memory access and the exception
context resulting from a failed access do not share a stack.

In the case of AArch64, a failed access when the debugger is already in
exception context causes a re-entry into exception context where the
machine state is pushed onto the same stack that was in use where the
exception occurred. When setjmp is called inside a stack frame and the
exception occurs outside that stack frame, the stack frame is unwound
before the exception occurs and the exception entry overwrites the area
previously occupied by the stack frame housing the setjmp and corrupting
the link register that is stored there. After restoration of state using
longjmp(), this corrupted link register information is loaded from the
stack frame and undesired behavior occurs.

In the instance of this bug that was encountered, the corrupted link
register contained an unaligned pointer which caused an unending cascade
of prefetch abort exceptions presenting as a hard hang.

Closes #5273
2025-06-18 13:53:35 -05:00

615 lines
18 KiB
C

/*
* Copyright (c) 2016-2019 Chris Johns <chrisj@rtems.org>.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
#define TARGET_DEBUG 0
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <errno.h>
#include <inttypes.h>
#include <stdlib.h>
#include <rtems.h>
#include <rtems/score/threadimpl.h>
#include "rtems-debugger-target.h"
#include "rtems-debugger-threads.h"
/**
* Exception local stack frame data to synchronise with the debugger
* server's events loop processor.
*/
typedef struct {
rtems_chain_node node;
rtems_id id;
CPU_Exception_frame* frame;
rtems_rx_cond cond;
} rtems_debugger_exception;
#if TARGET_DEBUG
#include <rtems/bspIo.h>
static void target_printk(const char* format, ...) RTEMS_PRINTFLIKE(1, 2);
static void
target_printk(const char* format, ...)
{
rtems_interrupt_lock_context lock_context;
va_list ap;
va_start(ap, format);
rtems_debugger_printk_lock(&lock_context);
vprintk(format, ap);
rtems_debugger_printk_unlock(&lock_context);
va_end(ap);
}
#else
#define target_printk(_fmt, ...)
#endif
int
rtems_debugger_target_create(void)
{
if (rtems_debugger->target == NULL) {
rtems_debugger_target* target;
int r;
target = calloc(1, sizeof(rtems_debugger_target));
if (target == NULL) {
errno = ENOMEM;
return -1;
}
r = rtems_debugger_target_configure(target);
if (r < 0) {
free(target);
return -1;
}
if (target->breakpoint_size > RTEMS_DEBUGGER_TARGET_SWBREAK_MAX_SIZE) {
free(target);
rtems_debugger_printf("error: rtems-db: target: breakpoint size too big\n");
return -1;
}
r = rtems_debugger_block_create(&target->swbreaks,
RTEMS_DEBUGGER_TARGET_SWBREAK_NUM,
sizeof(rtems_debugger_target_swbreak));
if (r < 0) {
free(target);
return -1;
}
rtems_debugger->target = target;
}
return 0;
}
int
rtems_debugger_target_destroy(void)
{
if (rtems_debugger->target != NULL) {
rtems_debugger_target* target = rtems_debugger->target;
rtems_debugger_target_swbreak_remove();
rtems_debugger_target_disable();
rtems_debugger_block_destroy(&target->swbreaks);
free(target);
rtems_debugger->target = NULL;
}
return 0;
}
uint32_t
rtems_debugger_target_capabilities(void)
{
if (rtems_debugger->target != NULL)
return rtems_debugger->target->capabilities;
return 0;
}
size_t
rtems_debugger_target_reg_num(void)
{
rtems_debugger_target* target = rtems_debugger->target;
if (target != NULL)
return target->reg_num;
return 0;
}
size_t
rtems_debugger_target_reg_size(size_t reg)
{
rtems_debugger_target* target = rtems_debugger->target;
if (target != NULL && reg < target->reg_num)
return target->reg_offset[reg + 1] - target->reg_offset[reg];
return 0;
}
size_t
rtems_debugger_target_reg_offset(size_t reg)
{
rtems_debugger_target* target = rtems_debugger->target;
if (target != NULL && reg < target->reg_num)
return target->reg_offset[reg];
return 0;
}
size_t
rtems_debugger_target_reg_table_size(void)
{
rtems_debugger_target* target = rtems_debugger->target;
if (target != NULL)
return target->reg_offset[target->reg_num];
return 0;
}
bool
rtems_debugger_target_swbreak_is_configured( uintptr_t addr )
{
size_t i;
rtems_debugger_target_swbreak *swbreaks;
rtems_debugger_target *target = rtems_debugger->target;
if ( target == NULL ) {
return false;
}
swbreaks = target->swbreaks.block;
if ( swbreaks == NULL ) {
return false;
}
for ( i = 0; i < target->swbreaks.level; ++i ) {
if ( (uintptr_t) swbreaks[ i ].address == addr ) {
return true;
}
}
return false;
}
int
rtems_debugger_target_swbreak_control(bool insert, uintptr_t addr, DB_UINT kind)
{
rtems_debugger_target* target = rtems_debugger->target;
rtems_debugger_target_swbreak* swbreaks;
size_t swbreak_size;
uint8_t* loc = (void*) addr;
size_t i;
int r;
if (target == NULL || target->swbreaks.block == NULL ||
kind != target->breakpoint_size) {
errno = EIO;
return -1;
}
swbreaks = target->swbreaks.block;
swbreak_size =
sizeof(rtems_debugger_target_swbreak) + target->breakpoint_size;
for (i = 0; i < target->swbreaks.level; ++i) {
if (loc == swbreaks[i].address) {
size_t remaining;
if (!insert) {
if (target->code_writer != NULL) {
r = target->code_writer(loc,
&swbreaks[i].contents[0],
target->breakpoint_size);
if (r < 0) {
return r;
}
} else {
if (target->breakpoint_size > 4) {
memcpy(loc, &swbreaks[i].contents[0], target->breakpoint_size);
} else {
switch (target->breakpoint_size) {
case 4:
loc[3] = swbreaks[i].contents[3];
case 3:
loc[2] = swbreaks[i].contents[2];
case 2:
loc[1] = swbreaks[i].contents[1];
case 1:
loc[0] = swbreaks[i].contents[0];
break;
}
}
rtems_debugger_target_cache_sync(&swbreaks[i]);
}
--target->swbreaks.level;
remaining = (target->swbreaks.level - i) * swbreak_size;
memmove(&swbreaks[i], &swbreaks[i + 1], remaining);
}
return 0;
}
}
if (!insert)
return 0;
r = rtems_debugger_block_resize(&target->swbreaks);
if (r < 0)
return -1;
swbreaks = target->swbreaks.block;
swbreaks[target->swbreaks.level].address = loc;
if (target->breakpoint_size > 4)
memcpy(&swbreaks[target->swbreaks.level].contents[0],
loc,
target->breakpoint_size);
else {
uint8_t* contents = &swbreaks[target->swbreaks.level].contents[0];
switch (target->breakpoint_size) {
case 4:
contents[3] = loc[3];
case 3:
contents[2] = loc[2];
case 2:
contents[1] = loc[1];
case 1:
contents[0] = loc[0];
break;
}
}
++target->swbreaks.level;
return 0;
}
int
rtems_debugger_target_swbreak_insert(void)
{
rtems_debugger_target* target = rtems_debugger->target;
int r = -1;
if (target != NULL && target->swbreaks.block != NULL) {
rtems_debugger_target_swbreak* swbreaks = target->swbreaks.block;
size_t i;
r = 0;
for (i = 0; i < target->swbreaks.level; ++i) {
uint8_t* loc = swbreaks[i].address;
if (rtems_debugger_verbose())
rtems_debugger_printf("rtems-db: bp: in: %p\n", swbreaks[i].address);
if (target->code_writer != NULL) {
r = target->code_writer(loc,
&target->breakpoint[0],
target->breakpoint_size);
if (r < 0) {
return r;
}
} else {
if (target->breakpoint_size > 4) {
memcpy(loc, &target->breakpoint[0], target->breakpoint_size);
} else {
if (rtems_debugger_verbose())
rtems_debugger_printf("rtems-db: bp: in: %p %p %d %d %d\n",
loc, &target->breakpoint[0],
(int) target->breakpoint_size,
(int) i, (int) target->swbreaks.level);
switch (target->breakpoint_size) {
case 4:
loc[3] = target->breakpoint[3];
case 3:
loc[2] = target->breakpoint[2];
case 2:
loc[1] = target->breakpoint[1];
case 1:
loc[0] = target->breakpoint[0];
break;
}
}
r = rtems_debugger_target_cache_sync(&swbreaks[i]);
}
}
}
return r;
}
int
rtems_debugger_target_swbreak_remove(void)
{
rtems_debugger_target* target = rtems_debugger->target;
int r = -1;
if (target != NULL && target->swbreaks.block != NULL) {
rtems_debugger_target* target = rtems_debugger->target;
rtems_debugger_target_swbreak* swbreaks = target->swbreaks.block;
size_t i;
r = 0;
for (i = 0; i < target->swbreaks.level; ++i) {
uint8_t* loc = swbreaks[i].address;
uint8_t* contents = &swbreaks[i].contents[0];
if (rtems_debugger_verbose())
rtems_debugger_printf("rtems-db: bp: out: %p\n", swbreaks[i].address);
if (target->code_writer != NULL) {
r = target->code_writer(loc, contents, target->breakpoint_size);
if (r < 0) {
return r;
}
} else {
if (target->breakpoint_size > 4) {
memcpy(loc, contents, target->breakpoint_size);
} else {
switch (target->breakpoint_size) {
case 4:
loc[3] = contents[3];
case 3:
loc[2] = contents[2];
case 2:
loc[1] = contents[1];
case 1:
loc[0] = contents[0];
break;
}
}
r = rtems_debugger_target_cache_sync(&swbreaks[i]);
}
}
}
return r;
}
static rtems_debugger_target_exc_action
rtems_debugger_soft_step_and_continue(CPU_Exception_frame* frame)
{
uintptr_t break_address;
rtems_debugger_target *target = rtems_debugger->target;
Thread_Control *thread = _Thread_Get_executing();
const rtems_id tid = thread->Object.id;
rtems_debugger_thread fake_debugger_thread;
/*
* If this was a hwbreak, cascade. If this is a swbreak replace the contents
* of the instruction, step then return the swbreak's contents.
*/
if ((target->capabilities & RTEMS_DEBUGGER_TARGET_CAP_SWBREAK) == 0) {
target_printk("rtems-db: exception in an interrupt, cascading\n");
rtems_debugger_unlock();
return rtems_debugger_target_exc_cascade;
}
break_address = rtems_debugger_target_frame_pc( frame );
if ( rtems_debugger_target_swbreak_is_configured( break_address ) == false ) {
target_printk("rtems-db: exception in an interrupt, cascading\n");
rtems_debugger_unlock();
return rtems_debugger_target_exc_cascade;
}
/*
* Remove the current breakpoint
*/
rtems_debugger_target_swbreak_control(
false,
break_address,
target->breakpoint_size
);
/*
* Save the thread ID and break address to recover after stepping
*/
target->step_tid = tid;
target->step_bp_address = break_address;
/*
* Populate the fake rtems_debugger_thread
*/
fake_debugger_thread.flags |= RTEMS_DEBUGGER_THREAD_FLAG_STEP;
fake_debugger_thread.frame = frame;
target_printk("rtems-db: stepping to the next instruction\n");
rtems_debugger_target_thread_stepping(&fake_debugger_thread);
/*
* rtems_debugger_unlock() not called until the step is resolved
*/
return rtems_debugger_target_exc_step;
}
rtems_debugger_target_exc_action
rtems_debugger_target_exception(CPU_Exception_frame* frame)
{
rtems_debugger_target *target = rtems_debugger->target;
Thread_Control* thread = _Thread_Get_executing();
const rtems_id tid = thread->Object.id;
/*
* Resolve outstanding step+continue
*/
if (target->step_bp_address != 0 && target->step_tid == tid) {
rtems_debugger_target_swbreak_control(
true,
target->step_bp_address,
rtems_debugger->target->breakpoint_size
);
target->step_bp_address = 0;
target->step_tid = 0;
/*
* Release the debugger lock now that the step+continue is complete
*/
target_printk("rtems-db: resuming after step\n");
rtems_debugger_unlock();
return rtems_debugger_target_exc_consumed;
}
rtems_debugger_lock();
if (!rtems_interrupt_is_in_progress()) {
rtems_debugger_threads* threads = rtems_debugger->threads;
rtems_id* excludes;
uintptr_t pc;
const rtems_debugger_thread_stepper* stepper;
rtems_debugger_exception target_exception;
size_t i;
target_printk("[} tid:%08" PRIx32 ": thread:%08" PRIxPTR
" frame:%08" PRIxPTR "\n",
tid, (intptr_t) thread, (intptr_t) frame);
/*
* If the thread is in the debugger recover. If the access is from gdb
* continue else shutdown and let the user know.
*/
if (tid == rtems_debugger->server_task ||
tid == rtems_debugger->events_task) {
bool memory_access = rtems_debugger_target_is_memory_access();
rtems_debugger_unlock();
/*
* Has GDB has asked us to write to an address?
*/
if (memory_access) {
target_printk("[} server fault: memory access\n");
longjmp(rtems_debugger->target->access_return, -1);
}
rtems_debugger_printf("rtems-db: server exception (report)\n");
rtems_debugger_target_exception_print(frame);
rtems_debugger_server_crash();
return rtems_debugger_target_exc_cascade;
}
/*
* See if the thread is excluded.
*/
excludes = rtems_debugger_thread_excludes(threads);
for (i = 0; i < threads->excludes.level; ++i) {
if (tid == excludes[i]) {
/*
* We do nothing with this condition and cascade the exception.
*
* @todo: if this is a hwbreak carry on, if this is a swbreak replace
* the contents of the instruction, step then return the
* swbreak's contents.
*/
target_printk("[} tid:%08" PRIx32 ": excluded\n", tid);
rtems_debugger_unlock();
return rtems_debugger_target_exc_cascade;
}
}
/*
* See if the thread is inside the stepping a range.
*/
pc = rtems_debugger_target_frame_pc(frame);
stepper = rtems_debugger_thread_is_stepping(tid, pc);
if (stepper != NULL) {
stepper->thread->frame = frame;
rtems_debugger_target_thread_stepping(stepper->thread);
target_printk("[} tid:%08" PRIx32 ": stepping\n", tid);
rtems_debugger_unlock();
return rtems_debugger_target_exc_step;
}
target_printk("[} tid:%08" PRIx32 ": suspending\n", tid);
/*
* Initialise the target exception data and queue ready for the debugger
* server's event processor to handle.
*/
rtems_chain_initialize_node(&target_exception.node);
target_exception.frame = frame;
target_exception.id = tid;
_Condition_Initialize(&target_exception.cond);
rtems_chain_append_unprotected(&rtems_debugger->exception_threads,
&target_exception.node);
/*
* Signal the debug server's thread.
*/
rtems_debugger_server_events_signal();
/*
* Block on the exception thread's condition variable unlocking the
* debugger's mutex and letting the server's thread run.
*/
_Condition_Wait_recursive(&target_exception.cond, &rtems_debugger->lock);
/*
* Unlock the debugger's lock now the exception is resuming.
*/
rtems_debugger_unlock();
target_printk("[} tid:%08" PRIx32 ": resuming\n", tid);
return rtems_debugger_target_exc_consumed;
}
target_printk("[} tid:%08" PRIx32 ": exception in interrupt context\n", tid);
/*
* Soft_step_and_continue releases the debugger lock
*/
return rtems_debugger_soft_step_and_continue(frame);
}
void
rtems_debugger_target_exception_thread(rtems_debugger_thread* thread)
{
rtems_chain_node* node;
thread->frame = NULL;
thread->flags &= ~RTEMS_DEBUGGER_THREAD_FLAG_EXCEPTION;
for (node = rtems_chain_first(&rtems_debugger->exception_threads);
!rtems_chain_is_tail(&rtems_debugger->exception_threads, node);
node = rtems_chain_next(node)) {
rtems_debugger_exception* target_exception = (rtems_debugger_exception*) node;
if (target_exception->id == thread->id) {
thread->frame = target_exception->frame;
thread->flags |= RTEMS_DEBUGGER_THREAD_FLAG_EXCEPTION;
}
}
}
void
rtems_debugger_target_exception_thread_resume(rtems_debugger_thread* thread)
{
rtems_chain_node* node;
for (node = rtems_chain_first(&rtems_debugger->exception_threads);
!rtems_chain_is_tail(&rtems_debugger->exception_threads, node);
node = rtems_chain_next(node)) {
rtems_debugger_exception* target_exception = (rtems_debugger_exception*) node;
if (target_exception->id == thread->id) {
rtems_chain_extract(node);
thread->frame = NULL;
thread->flags &= ~RTEMS_DEBUGGER_THREAD_FLAG_EXCEPTION;
_Condition_Signal(&target_exception->cond);
break;
}
}
}
void
rtems_debugger_target_end_memory_access(void)
{
rtems_debugger->target->memory_access = false;
}
bool
rtems_debugger_target_is_memory_access(void)
{
return rtems_debugger->target->memory_access;
}