arch/x86: Add support for catching null dereferences through debug regs

This commit adds support for catching null dereferences and execution
through x86's debug registers. This is particularly useful when running
32-bit coreboot as paging is not enabled to catch these through page
faults. This commit adds three new configs to support this feature:
DEBUG_HW_BREAKPOINTS, DEBUG_NULL_DEREF_BREAKPOINTS and
DEBUG_NULL_DEREF_HALT.

BUG=b:223902046
TEST=Ran on nipperkin device, verifying that HW breakpoints work as
expected.

Change-Id: I113590689046a13c2a552741bbfe7668a834354a
Signed-off-by: Robert Zieba <robertzieba@google.com>
Reviewed-on: https://review.coreboot.org/c/coreboot/+/63657
Tested-by: build bot (Jenkins) <no-reply@coreboot.org>
Reviewed-by: Raul Rangel <rrangel@chromium.org>
This commit is contained in:
Robert Zieba 2022-04-14 10:36:15 -06:00 committed by Nick Vaccaro
parent 4be0f4bf99
commit 3f01cd1453
7 changed files with 486 additions and 2 deletions

View File

@ -320,6 +320,38 @@ config MEMLAYOUT_LD_FILE
string
default "src/arch/x86/memlayout.ld"
config DEBUG_HW_BREAKPOINTS
bool
default y
help
Enable support for hardware data and instruction breakpoints through
the x86 debug registers
config DEBUG_HW_BREAKPOINTS_IN_ALL_STAGES
bool
default y
depends on DEBUG_HW_BREAKPOINTS && IDT_IN_EVERY_STAGE
config DEBUG_NULL_DEREF_BREAKPOINTS
bool
default y
depends on DEBUG_HW_BREAKPOINTS
help
Enable support for catching null dereferences and instruction execution
config DEBUG_NULL_DEREF_BREAKPOINTS_IN_ALL_STAGES
bool
default y
depends on DEBUG_NULL_DEREF_BREAKPOINTS && DEBUG_HW_BREAKPOINTS_IN_ALL_STAGES
config DEBUG_NULL_DEREF_HALT
bool
default n
depends on DEBUG_NULL_DEREF_BREAKPOINTS
help
When enabled null dereferences and instruction fetches will halt execution.
Otherwise an error will be printed.
# Some EC need an "EC firmware pointer" (a data structure hinting the address
# of its firmware blobs) being put at a fixed position. Its space
# (__section__(".ecfw_ptr")) should be reserved if it lies in the range of a

View File

@ -78,6 +78,7 @@ endef
ifeq ($(CONFIG_ARCH_BOOTBLOCK_X86_32)$(CONFIG_ARCH_BOOTBLOCK_X86_64),y)
bootblock-y += boot.c
bootblock-$(CONFIG_DEBUG_HW_BREAKPOINTS_IN_ALL_STAGES) += breakpoint.c
bootblock-y += post.c
bootblock-y += cpu_common.c
bootblock-$(CONFIG_IDT_IN_EVERY_STAGE) += exception.c
@ -87,6 +88,7 @@ bootblock-y += memset.c
bootblock-y += memmove.c
bootblock-$(CONFIG_COLLECT_TIMESTAMPS_TSC) += timestamp.c
bootblock-$(CONFIG_X86_TOP4G_BOOTMEDIA_MAP) += mmap_boot.c
bootblock-$(CONFIG_DEBUG_NULL_DEREF_BREAKPOINTS_IN_ALL_STAGES) += null_breakpoint.c
bootblock-$(CONFIG_BOOTBLOCK_NORMAL) += bootblock_normal.c
bootblock-y += gdt_init.S
bootblock-y += id.S
@ -122,6 +124,7 @@ ifeq ($(CONFIG_ARCH_VERSTAGE_X86_32)$(CONFIG_ARCH_VERSTAGE_X86_64),y)
verstage-$(CONFIG_VBOOT_SEPARATE_VERSTAGE) += assembly_entry.S
verstage-y += boot.c
verstage-$(CONFIG_DEBUG_HW_BREAKPOINTS_IN_ALL_STAGES) += breakpoint.c
verstage-y += post.c
verstage-$(CONFIG_VBOOT_SEPARATE_VERSTAGE) += gdt_init.S
verstage-$(CONFIG_IDT_IN_EVERY_STAGE) += exception.c
@ -133,6 +136,7 @@ verstage-y += memset.c
verstage-y += memcpy.c
verstage-y += memmove.c
verstage-$(CONFIG_X86_TOP4G_BOOTMEDIA_MAP) += mmap_boot.c
verstage-$(CONFIG_DEBUG_NULL_DEREF_BREAKPOINTS_IN_ALL_STAGES) += null_breakpoint.c
# If verstage is a separate stage it means there's no need
# for a chipset-specific car_stage_entry() so use the generic one
# which just calls verstage().
@ -158,6 +162,7 @@ ifeq ($(CONFIG_ARCH_ROMSTAGE_X86_32)$(CONFIG_ARCH_ROMSTAGE_X86_64),y)
romstage-y += assembly_entry.S
romstage-y += boot.c
romstage-$(CONFIG_DEBUG_HW_BREAKPOINTS_IN_ALL_STAGES) += breakpoint.c
romstage-y += post.c
romstage-y += gdt_init.S
romstage-y += cpu_common.c
@ -167,6 +172,7 @@ romstage-y += memcpy.c
romstage-y += memmove.c
romstage-y += memset.c
romstage-$(CONFIG_X86_TOP4G_BOOTMEDIA_MAP) += mmap_boot.c
romstage-$(CONFIG_DEBUG_NULL_DEREF_BREAKPOINTS_IN_ALL_STAGES) += null_breakpoint.c
romstage-y += postcar_loader.c
romstage-$(CONFIG_COLLECT_TIMESTAMPS_TSC) += timestamp.c
romstage-$(CONFIG_HAVE_CF9_RESET) += cf9_reset.c
@ -199,6 +205,7 @@ endif
postcar-generic-ccopts += -D__POSTCAR__
postcar-y += boot.c
postcar-$(CONFIG_DEBUG_HW_BREAKPOINTS_IN_ALL_STAGES) += breakpoint.c
postcar-y += post.c
postcar-y += gdt_init.S
postcar-y += cpu_common.c
@ -209,6 +216,7 @@ postcar-y += memcpy.c
postcar-y += memmove.c
postcar-y += memset.c
postcar-$(CONFIG_X86_TOP4G_BOOTMEDIA_MAP) += mmap_boot.c
postcar-$(CONFIG_DEBUG_NULL_DEREF_BREAKPOINTS_IN_ALL_STAGES) += null_breakpoint.c
postcar-y += postcar.c
postcar-$(CONFIG_COLLECT_TIMESTAMPS_TSC) += timestamp.c
postcar-$(CONFIG_HAVE_CF9_RESET) += cf9_reset.c
@ -243,6 +251,7 @@ ramstage-y += c_start.S
ramstage-y += c_exit.S
ramstage-y += cpu.c
ramstage-y += cpu_common.c
ramstage-$(CONFIG_DEBUG_HW_BREAKPOINTS) += breakpoint.c
ramstage-y += ebda.c
ramstage-y += exception.c
ramstage-y += idt.S
@ -252,6 +261,7 @@ ramstage-y += memmove.c
ramstage-y += memset.c
ramstage-$(CONFIG_X86_TOP4G_BOOTMEDIA_MAP) += mmap_boot.c
ramstage-$(CONFIG_GENERATE_MP_TABLE) += mpspec.c
ramstage-$(CONFIG_DEBUG_NULL_DEREF_BREAKPOINTS) += null_breakpoint.c
ramstage-$(CONFIG_GENERATE_PIRQ_TABLE) += pirq_routing.c
ramstage-y += rdrand.c
ramstage-$(CONFIG_GENERATE_SMBIOS_TABLES) += smbios.c
@ -306,11 +316,13 @@ $(objgenerated)/ramstage.o: $$(ramstage-objs) $(COMPILER_RT_ramstage) $$(ramstag
endif # CONFIG_ARCH_RAMSTAGE_X86_32 / CONFIG_ARCH_RAMSTAGE_X86_64
smm-$(CONFIG_DEBUG_HW_BREAKPOINTS_IN_ALL_STAGES) += breakpoint.c
smm-$(CONFIG_IDT_IN_EVERY_STAGE) += exception.c
smm-$(CONFIG_IDT_IN_EVERY_STAGE) += idt.S
smm-y += memcpy.c
smm-y += memmove.c
smm-y += memset.c
smm-$(CONFIG_X86_TOP4G_BOOTMEDIA_MAP) += mmap_boot.c
smm-$(CONFIG_DEBUG_NULL_DEREF_BREAKPOINTS_IN_ALL_STAGES) += null_breakpoint.c
smm-srcs += $(wildcard src/mainboard/$(MAINBOARDDIR)/smihandler.c)

300
src/arch/x86/breakpoint.c Normal file
View File

@ -0,0 +1,300 @@
/* SPDX-License-Identifier: GPL-2.0-only */
#include <arch/registers.h>
#include <arch/breakpoint.h>
#include <console/console.h>
#include <stdint.h>
#include <types.h>
#define DEBUG_REGISTER_COUNT 4
/* Each enable field is 2 bits and starts at bit 0 */
#define DEBUG_CTRL_ENABLE_SHIFT(index) (2 * (index))
#define DEBUG_CTRL_ENABLE_MASK(index) (0x3 << DEBUG_CTRL_ENABLE_SHIFT(index))
#define DEBUG_CTRL_ENABLE(index, enable) ((enable) << DEBUG_CTRL_ENABLE_SHIFT(index))
/* Each breakpoint has a length and type, each is two bits and start at bit 16 */
#define DEBUG_CTRL_LT_SHIFT(index) (4 * (index) + 16)
#define DEBUG_CTRL_LT_MASK(index) (0xf << DEBUG_CTRL_LT_SHIFT(index))
#define DEBUG_CTRL_LT(index, len, type) ((((len) << 2 | (type))) << DEBUG_CTRL_LT_SHIFT(index))
/* Each field is one bit, starting at bit 0 */
#define DEBUG_STATUS_BP_HIT_MASK(index) (1 << (index))
#define DEBUG_STATUS_GET_BP_HIT(index, value) \
(((value) & DEBUG_STATUS_BP_HIT_MASK(index)) >> (index))
/* Breakpoint lengths values */
#define DEBUG_CTRL_LEN_1 0x0
#define DEBUG_CTRL_LEN_2 0x1
#define DEBUG_CTRL_LEN_8 0x2
#define DEBUG_CTRL_LEN_4 0x3
/* Breakpoint enable values */
#define DEBUG_CTRL_ENABLE_LOCAL 0x1
#define DEBUG_CTRL_ENABLE_GLOBAL 0x2
/* eflags/rflags bit to continue execution after hitting an instruction breakpoint */
#define FLAGS_RESUME (1 << 16)
struct breakpoint {
bool allocated;
enum breakpoint_type type;
breakpoint_handler handler;
};
static struct breakpoint breakpoints[DEBUG_REGISTER_COUNT];
static inline bool debug_write_addr_reg(int index, uintptr_t value)
{
switch (index) {
case 0:
asm("mov %0, %%dr0" ::"r"(value));
break;
case 1:
asm("mov %0, %%dr1" ::"r"(value));
break;
case 2:
asm("mov %0, %%dr2" ::"r"(value));
break;
case 3:
asm("mov %0, %%dr3" ::"r"(value));
break;
default:
return false;
}
return true;
}
static inline uintptr_t debug_read_status(void)
{
uintptr_t ret = 0;
asm("mov %%dr6, %0" : "=r"(ret));
return ret;
}
static inline void debug_write_status(uintptr_t value)
{
asm("mov %0, %%dr6" ::"r"(value));
}
static inline uintptr_t debug_read_control(void)
{
uintptr_t ret = 0;
asm("mov %%dr7, %0" : "=r"(ret));
return ret;
}
static inline void debug_write_control(uintptr_t value)
{
asm("mov %0, %%dr7" ::"r"(value));
}
static enum breakpoint_result allocate_breakpoint(struct breakpoint_handle *out_handle,
enum breakpoint_type type)
{
for (int i = 0; i < DEBUG_REGISTER_COUNT; i++) {
if (breakpoints[i].allocated)
continue;
breakpoints[i].allocated = true;
breakpoints[i].handler = NULL;
breakpoints[i].type = type;
out_handle->bp = i;
return BREAKPOINT_RES_OK;
}
return BREAKPOINT_RES_NONE_AVAILABLE;
}
static enum breakpoint_result validate_handle(struct breakpoint_handle handle)
{
int bp = handle.bp;
if (bp < 0 || bp >= DEBUG_REGISTER_COUNT || !breakpoints[bp].allocated)
return BREAKPOINT_RES_INVALID_HANDLE;
return BREAKPOINT_RES_OK;
}
enum breakpoint_result breakpoint_create_instruction(struct breakpoint_handle *out_handle,
void *virt_addr)
{
enum breakpoint_result res =
allocate_breakpoint(out_handle, BREAKPOINT_TYPE_INSTRUCTION);
if (res != BREAKPOINT_RES_OK)
return res;
int bp = out_handle->bp;
if (!debug_write_addr_reg(bp, (uintptr_t)virt_addr))
return BREAKPOINT_RES_INVALID_HANDLE;
uintptr_t control = debug_read_control();
control &= ~DEBUG_CTRL_LT_MASK(bp);
control |= DEBUG_CTRL_LT(bp, DEBUG_CTRL_LEN_1, BREAKPOINT_TYPE_INSTRUCTION);
debug_write_control(control);
return BREAKPOINT_RES_OK;
}
enum breakpoint_result breakpoint_create_data(struct breakpoint_handle *out_handle,
void *virt_addr, size_t len, bool write_only)
{
uintptr_t len_value = 0;
switch (len) {
case 1:
len_value = DEBUG_CTRL_LEN_1;
break;
case 2:
len_value = DEBUG_CTRL_LEN_2;
break;
case 4:
len_value = DEBUG_CTRL_LEN_4;
break;
case 8:
/* Only supported on 64-bit CPUs */
if (!ENV_X86_64)
return BREAKPOINT_RES_INVALID_LENGTH;
len_value = DEBUG_CTRL_LEN_8;
break;
default:
return BREAKPOINT_RES_INVALID_LENGTH;
}
enum breakpoint_type type =
write_only ? BREAKPOINT_TYPE_DATA_WRITE : BREAKPOINT_TYPE_DATA_RW;
enum breakpoint_result res = allocate_breakpoint(out_handle, type);
if (res != BREAKPOINT_RES_OK)
return res;
int bp = out_handle->bp;
if (!debug_write_addr_reg(bp, (uintptr_t)virt_addr))
return BREAKPOINT_RES_INVALID_HANDLE;
uintptr_t control = debug_read_control();
control &= ~DEBUG_CTRL_LT_MASK(bp);
control |= DEBUG_CTRL_LT(bp, len_value, type);
debug_write_control(control);
return BREAKPOINT_RES_OK;
}
enum breakpoint_result breakpoint_remove(struct breakpoint_handle handle)
{
enum breakpoint_result res = validate_handle(handle);
if (res != BREAKPOINT_RES_OK)
return res;
breakpoint_enable(handle, false);
int bp = handle.bp;
breakpoints[bp].allocated = false;
return BREAKPOINT_RES_OK;
}
enum breakpoint_result breakpoint_enable(struct breakpoint_handle handle, bool enabled)
{
enum breakpoint_result res = validate_handle(handle);
if (res != BREAKPOINT_RES_OK)
return res;
uintptr_t control = debug_read_control();
int bp = handle.bp;
control &= ~DEBUG_CTRL_ENABLE_MASK(bp);
if (enabled)
control |= DEBUG_CTRL_ENABLE(bp, DEBUG_CTRL_ENABLE_GLOBAL);
debug_write_control(control);
return BREAKPOINT_RES_OK;
}
enum breakpoint_result breakpoint_get_type(struct breakpoint_handle handle,
enum breakpoint_type *type)
{
enum breakpoint_result res = validate_handle(handle);
if (res != BREAKPOINT_RES_OK)
return res;
*type = breakpoints[handle.bp].type;
return BREAKPOINT_RES_OK;
}
enum breakpoint_result breakpoint_set_handler(struct breakpoint_handle handle,
breakpoint_handler handler)
{
enum breakpoint_result res = validate_handle(handle);
if (res != BREAKPOINT_RES_OK)
return res;
breakpoints[handle.bp].handler = handler;
return BREAKPOINT_RES_OK;
}
static enum breakpoint_result is_breakpoint_hit(struct breakpoint_handle handle, bool *out_hit)
{
enum breakpoint_result res = validate_handle(handle);
if (res != BREAKPOINT_RES_OK)
return res;
uintptr_t status = debug_read_status();
*out_hit = DEBUG_STATUS_GET_BP_HIT(handle.bp, status);
return BREAKPOINT_RES_OK;
}
int breakpoint_dispatch_handler(struct eregs *info)
{
bool instr_bp_hit = 0;
for (int i = 0; i < DEBUG_REGISTER_COUNT; i++) {
struct breakpoint_handle handle = { i };
bool hit = false;
enum breakpoint_type type;
if (is_breakpoint_hit(handle, &hit) != BREAKPOINT_RES_OK || !hit)
continue;
if (breakpoint_get_type(handle, &type) != BREAKPOINT_RES_OK)
continue;
instr_bp_hit |= type == BREAKPOINT_TYPE_INSTRUCTION;
/* Call the breakpoint handler. */
if (breakpoints[handle.bp].handler) {
int ret = breakpoints[handle.bp].handler(handle, info);
/* A non-zero return value indicates a fatal error. */
if (ret)
return ret;
}
}
/* Clear hit breakpoints. */
uintptr_t status = debug_read_status();
for (int i = 0; i < DEBUG_REGISTER_COUNT; i++) {
status &= ~DEBUG_STATUS_BP_HIT_MASK(i);
}
debug_write_status(status);
if (instr_bp_hit) {
/* Set the resume flag so the same breakpoint won't be hit immediately. */
#if ENV_X86_64
info->rflags |= FLAGS_RESUME;
#else
info->eflags |= FLAGS_RESUME;
#endif
}
return 0;
}

View File

@ -1,7 +1,9 @@
/* SPDX-License-Identifier: GPL-2.0-only */
#include <arch/cpu.h>
#include <arch/breakpoint.h>
#include <arch/null_breakpoint.h>
#include <arch/exception.h>
#include <arch/registers.h>
#include <commonlib/helpers.h>
#include <console/console.h>
#include <console/streams.h>
@ -371,7 +373,7 @@ static void put_packet(char *buffer)
}
#endif /* CONFIG_GDB_STUB */
#include <arch/registers.h>
#define DEBUG_VECTOR 1
void x86_exception(struct eregs *info);
@ -488,6 +490,11 @@ void x86_exception(struct eregs *info)
int logical_processor = 0;
u32 apic_id = CONFIG(SMP) ? lapicid() : 0;
if (info->vector == DEBUG_VECTOR) {
if (breakpoint_dispatch_handler(info) == 0)
return;
}
#if ENV_RAMSTAGE
logical_processor = cpu_index();
#endif
@ -657,4 +664,6 @@ asmlinkage void exception_init(void)
}
load_idt(idt, sizeof(idt));
null_breakpoint_init();
}

View File

@ -0,0 +1,58 @@
/* SPDX-License-Identifier: GPL-2.0-only */
#ifndef _BREAKPOINT_H_
#define _BREAKPOINT_H_
#include <arch/registers.h>
#include <types.h>
#if CONFIG(DEBUG_HW_BREAKPOINTS) && \
(CONFIG(DEBUG_HW_BREAKPOINTS_IN_ALL_STAGES) || ENV_RAMSTAGE)
struct breakpoint_handle {
int bp;
};
typedef int (*breakpoint_handler)(struct breakpoint_handle, struct eregs *info);
enum breakpoint_result {
BREAKPOINT_RES_OK = 0,
BREAKPOINT_RES_NONE_AVAILABLE = -1,
BREAKPOINT_RES_INVALID_HANDLE = -2,
BREAKPOINT_RES_INVALID_LENGTH = -3
};
enum breakpoint_type {
BREAKPOINT_TYPE_INSTRUCTION = 0x0,
BREAKPOINT_TYPE_DATA_WRITE = 0x1,
BREAKPOINT_TYPE_IO = 0x2,
BREAKPOINT_TYPE_DATA_RW = 0x3,
};
/* Creates an instruction breakpoint at the given address. */
enum breakpoint_result breakpoint_create_instruction(struct breakpoint_handle *out_handle,
void *virt_addr);
/* Creates a data breakpoint at the given address for len bytes. */
enum breakpoint_result breakpoint_create_data(struct breakpoint_handle *out_handle,
void *virt_addr, size_t len, bool write_only);
/* Removes a given breakpoint. */
enum breakpoint_result breakpoint_remove(struct breakpoint_handle handle);
/* Enables or disables a given breakpoint. */
enum breakpoint_result breakpoint_enable(struct breakpoint_handle handle, bool enabled);
/* Returns the type of a breakpoint. */
enum breakpoint_result breakpoint_get_type(struct breakpoint_handle handle,
enum breakpoint_type *type);
/*
* Sets a handler function to be called when the breakpoint is hit. The handler should return 0
* to continue or any other value to halt execution as a fatal error.
*/
enum breakpoint_result breakpoint_set_handler(struct breakpoint_handle handle,
breakpoint_handler handler);
/* Called by x86_exception to dispatch breakpoint exceptions to the correct handler. */
int breakpoint_dispatch_handler(struct eregs *info);
#else
static inline int breakpoint_dispatch_handler(struct eregs *info)
{
/* Not implemented */
return 0;
}
#endif
#endif /* _BREAKPOINT_H_ */

View File

@ -0,0 +1,16 @@
/* SPDX-License-Identifier: GPL-2.0-only */
#ifndef _NULL_BREAKPOINT_H_
#define _NULL_BREAKPOINT_H_
#if CONFIG(DEBUG_NULL_DEREF_BREAKPOINTS) && \
(CONFIG(DEBUG_NULL_DEREF_BREAKPOINTS_IN_ALL_STAGES) || ENV_RAMSTAGE)
/* Places data and instructions breakpoints at address zero. */
void null_breakpoint_init(void);
#else
static inline void null_breakpoint_init(void)
{
/* Not implemented */
}
#endif
#endif /* _NULL_BREAKPOINT_H_ */

View File

@ -0,0 +1,57 @@
/* SPDX-License-Identifier: GPL-2.0-only */
#include <arch/breakpoint.h>
#include <arch/null_breakpoint.h>
#include <console/console.h>
#include <stdint.h>
static struct breakpoint_handle null_deref_bp;
static struct breakpoint_handle null_fetch_bp;
static int handle_fetch_breakpoint(struct breakpoint_handle handle, struct eregs *regs)
{
printk(BIOS_ERR, "Instruction fetch from address zero\n");
return CONFIG(DEBUG_NULL_DEREF_HALT);
}
static int handle_deref_breakpoint(struct breakpoint_handle handle, struct eregs *regs)
{
#if ENV_X86_64
printk(BIOS_ERR, "Null dereference at rip: 0x%llx \n", regs->rip);
#else
printk(BIOS_ERR, "Null dereference at eip: 0x%x \n", regs->eip);
#endif
return CONFIG(DEBUG_NULL_DEREF_HALT);
}
static void create_deref_breakpoint(void)
{
enum breakpoint_result res =
breakpoint_create_data(&null_deref_bp, NULL, sizeof(uintptr_t), false);
if (res != BREAKPOINT_RES_OK) {
printk(BIOS_ERR, "Failed to create NULL dereference breakpoint\n");
return;
}
breakpoint_set_handler(null_deref_bp, &handle_deref_breakpoint);
breakpoint_enable(null_deref_bp, true);
}
static void create_instruction_breakpoint(void)
{
enum breakpoint_result res = breakpoint_create_instruction(&null_fetch_bp, NULL);
if (res != BREAKPOINT_RES_OK) {
printk(BIOS_ERR, "Failed to create address zero instruction fetch breakpoint\n");
return;
}
breakpoint_set_handler(null_fetch_bp, &handle_fetch_breakpoint);
breakpoint_enable(null_fetch_bp, true);
}
void null_breakpoint_init(void)
{
create_deref_breakpoint();
create_instruction_breakpoint();
}