HardenedBSD/sys/cddl/dev/kinst/trampoline.c
Christos Margiolis 5b701ed19c kinst: start moving towards per-probe trampolines
Using per-CPU and per-thread trampolines is expensive and error-prone,
since we're rewriting the same memory blocks constantly. Per-probe
trampolines solve this problem by giving each probe its own block of
executable memory, which more or less remains the same after the initial
write.

What this patch does, is get rid of the initialization code which
allocates a trampoline for each thread, and instead let each port of
kinst allocate a trampoline for each new probe created. It also sets up
the infrastructure needed to support the new trampoline scheme.

This change is not currently supported on amd64, as the amd64 port needs
further changes to work, so this is a temporary/gradual patch to fix the
riscv and arm64 ports.

Reviewed by:	markj
Approved by:	markj (mentor)
Sponsored by:	The FreeBSD Foundation
Differential Revision:	https://reviews.freebsd.org/D40962
2023-07-19 17:57:21 +03:00

355 lines
8.3 KiB
C

/*
* SPDX-License-Identifier: CDDL 1.0
*
* Copyright (c) 2022 Christos Margiolis <christos@FreeBSD.org>
* Copyright (c) 2022 Mark Johnston <markj@FreeBSD.org>
* Copyright (c) 2023 The FreeBSD Foundation
*
* Portions of this software were developed by Christos Margiolis
* <christos@FreeBSD.org> under sponsorship from the FreeBSD Foundation.
*/
#include <sys/param.h>
#include <sys/bitset.h>
#include <sys/cred.h>
#include <sys/eventhandler.h>
#include <sys/kernel.h>
#include <sys/lock.h>
#include <sys/malloc.h>
#include <sys/proc.h>
#include <sys/queue.h>
#include <sys/sx.h>
#include <vm/vm.h>
#include <vm/vm_param.h>
#include <vm/pmap.h>
#include <vm/vm_map.h>
#include <vm/vm_kern.h>
#include <vm/vm_object.h>
#include <cddl/dev/dtrace/dtrace_cddl.h>
#include "kinst.h"
#include "kinst_isa.h"
#define KINST_TRAMP_FILL_PATTERN ((kinst_patchval_t []){KINST_PATCHVAL})
#define KINST_TRAMP_FILL_SIZE sizeof(kinst_patchval_t)
#define KINST_TRAMPCHUNK_SIZE PAGE_SIZE
#define KINST_TRAMPS_PER_CHUNK (KINST_TRAMPCHUNK_SIZE / KINST_TRAMP_SIZE)
struct trampchunk {
TAILQ_ENTRY(trampchunk) next;
uint8_t *addr;
/* 0 -> allocated, 1 -> free */
BITSET_DEFINE(, KINST_TRAMPS_PER_CHUNK) free;
};
static TAILQ_HEAD(, trampchunk) kinst_trampchunks =
TAILQ_HEAD_INITIALIZER(kinst_trampchunks);
static struct sx kinst_tramp_sx;
SX_SYSINIT(kinst_tramp_sx, &kinst_tramp_sx, "kinst tramp");
#ifdef __amd64__
static eventhandler_tag kinst_thread_ctor_handler;
static eventhandler_tag kinst_thread_dtor_handler;
#endif
/*
* Fill the trampolines with KINST_TRAMP_FILL_PATTERN so that the kernel will
* crash cleanly if things somehow go wrong.
*/
static void
kinst_trampoline_fill(uint8_t *addr, int size)
{
int i;
for (i = 0; i < size; i += KINST_TRAMP_FILL_SIZE) {
memcpy(&addr[i], KINST_TRAMP_FILL_PATTERN,
KINST_TRAMP_FILL_SIZE);
}
}
static struct trampchunk *
kinst_trampchunk_alloc(void)
{
struct trampchunk *chunk;
vm_offset_t trampaddr;
int error __diagused;
sx_assert(&kinst_tramp_sx, SX_XLOCKED);
#ifdef __amd64__
/*
* To simplify population of trampolines, we follow the amd64 kernel's
* code model and allocate them above KERNBASE, i.e., in the top 2GB of
* the kernel's virtual address space (not the case for other
* platforms).
*/
trampaddr = KERNBASE;
#else
trampaddr = VM_MIN_KERNEL_ADDRESS;
#endif
/*
* Allocate virtual memory for the trampoline chunk. The returned
* address is saved in "trampaddr". Trampolines must be executable so
* max_prot must include VM_PROT_EXECUTE.
*/
error = vm_map_find(kernel_map, NULL, 0, &trampaddr,
KINST_TRAMPCHUNK_SIZE, 0, VMFS_ANY_SPACE, VM_PROT_ALL, VM_PROT_ALL,
0);
if (error != KERN_SUCCESS) {
KINST_LOG("trampoline chunk allocation failed: %d", error);
return (NULL);
}
error = kmem_back(kernel_object, trampaddr, KINST_TRAMPCHUNK_SIZE,
M_WAITOK | M_EXEC);
KASSERT(error == KERN_SUCCESS, ("kmem_back failed: %d", error));
kinst_trampoline_fill((uint8_t *)trampaddr, KINST_TRAMPCHUNK_SIZE);
/* Allocate a tracker for this chunk. */
chunk = malloc(sizeof(*chunk), M_KINST, M_WAITOK);
chunk->addr = (void *)trampaddr;
BIT_FILL(KINST_TRAMPS_PER_CHUNK, &chunk->free);
TAILQ_INSERT_HEAD(&kinst_trampchunks, chunk, next);
return (chunk);
}
static void
kinst_trampchunk_free(struct trampchunk *chunk)
{
sx_assert(&kinst_tramp_sx, SX_XLOCKED);
TAILQ_REMOVE(&kinst_trampchunks, chunk, next);
kmem_unback(kernel_object, (vm_offset_t)chunk->addr,
KINST_TRAMPCHUNK_SIZE);
(void)vm_map_remove(kernel_map, (vm_offset_t)chunk->addr,
(vm_offset_t)(chunk->addr + KINST_TRAMPCHUNK_SIZE));
free(chunk, M_KINST);
}
static uint8_t *
kinst_trampoline_alloc_locked(int how)
{
struct trampchunk *chunk;
uint8_t *tramp;
int off;
sx_assert(&kinst_tramp_sx, SX_XLOCKED);
TAILQ_FOREACH(chunk, &kinst_trampchunks, next) {
/* All trampolines from this chunk are already allocated. */
if ((off = BIT_FFS(KINST_TRAMPS_PER_CHUNK, &chunk->free)) == 0)
continue;
/* BIT_FFS() returns indices starting at 1 instead of 0. */
off--;
break;
}
if (chunk == NULL) {
if ((how & M_NOWAIT) != 0)
return (NULL);
if ((chunk = kinst_trampchunk_alloc()) == NULL) {
#ifdef __amd64__
/*
* We didn't find any free trampoline in the current
* list, allocate a new one. If that fails the
* provider will no longer be reliable, so try to warn
* the user.
*/
static bool once = true;
if (once) {
once = false;
KINST_LOG(
"kinst: failed to allocate trampoline, "
"probes may not fire");
}
#endif
return (NULL);
}
off = 0;
}
BIT_CLR(KINST_TRAMPS_PER_CHUNK, off, &chunk->free);
tramp = chunk->addr + off * KINST_TRAMP_SIZE;
return (tramp);
}
uint8_t *
kinst_trampoline_alloc(int how)
{
uint8_t *tramp;
sx_xlock(&kinst_tramp_sx);
tramp = kinst_trampoline_alloc_locked(how);
sx_xunlock(&kinst_tramp_sx);
return (tramp);
}
static void
kinst_trampoline_dealloc_locked(uint8_t *tramp, bool freechunks)
{
struct trampchunk *chunk;
int off;
sx_assert(&kinst_tramp_sx, SX_XLOCKED);
if (tramp == NULL)
return;
TAILQ_FOREACH(chunk, &kinst_trampchunks, next) {
for (off = 0; off < KINST_TRAMPS_PER_CHUNK; off++) {
if (chunk->addr + off * KINST_TRAMP_SIZE == tramp) {
kinst_trampoline_fill(tramp, KINST_TRAMP_SIZE);
BIT_SET(KINST_TRAMPS_PER_CHUNK, off,
&chunk->free);
if (freechunks &&
BIT_ISFULLSET(KINST_TRAMPS_PER_CHUNK,
&chunk->free))
kinst_trampchunk_free(chunk);
return;
}
}
}
panic("%s: did not find trampoline chunk for %p", __func__, tramp);
}
void
kinst_trampoline_dealloc(uint8_t *tramp)
{
sx_xlock(&kinst_tramp_sx);
kinst_trampoline_dealloc_locked(tramp, true);
sx_xunlock(&kinst_tramp_sx);
}
#ifdef __amd64__
static void
kinst_thread_ctor(void *arg __unused, struct thread *td)
{
td->t_kinst_tramp = kinst_trampoline_alloc(M_WAITOK);
}
static void
kinst_thread_dtor(void *arg __unused, struct thread *td)
{
void *tramp;
tramp = td->t_kinst_tramp;
td->t_kinst_tramp = NULL;
/*
* This assumes that the thread_dtor event permits sleeping, which
* appears to be true for the time being.
*/
kinst_trampoline_dealloc(tramp);
}
#endif
int
kinst_trampoline_init(void)
{
#ifdef __amd64__
struct proc *p;
struct thread *td;
void *tramp;
int error;
kinst_thread_ctor_handler = EVENTHANDLER_REGISTER(thread_ctor,
kinst_thread_ctor, NULL, EVENTHANDLER_PRI_ANY);
kinst_thread_dtor_handler = EVENTHANDLER_REGISTER(thread_dtor,
kinst_thread_dtor, NULL, EVENTHANDLER_PRI_ANY);
error = 0;
tramp = NULL;
sx_slock(&allproc_lock);
sx_xlock(&kinst_tramp_sx);
FOREACH_PROC_IN_SYSTEM(p) {
retry:
PROC_LOCK(p);
FOREACH_THREAD_IN_PROC(p, td) {
if (td->t_kinst_tramp != NULL)
continue;
if (tramp == NULL) {
/*
* Try to allocate a trampoline without dropping
* the process lock. If all chunks are fully
* utilized, we must release the lock and try
* again.
*/
tramp = kinst_trampoline_alloc_locked(M_NOWAIT);
if (tramp == NULL) {
PROC_UNLOCK(p);
tramp = kinst_trampoline_alloc_locked(
M_WAITOK);
if (tramp == NULL) {
/*
* Let the unload handler clean
* up.
*/
error = ENOMEM;
goto out;
} else
goto retry;
}
}
td->t_kinst_tramp = tramp;
tramp = NULL;
}
PROC_UNLOCK(p);
}
out:
sx_xunlock(&kinst_tramp_sx);
sx_sunlock(&allproc_lock);
#else
int error = 0;
sx_xlock(&kinst_tramp_sx);
TAILQ_INIT(&kinst_trampchunks);
sx_xunlock(&kinst_tramp_sx);
#endif
return (error);
}
int
kinst_trampoline_deinit(void)
{
#ifdef __amd64__
struct trampchunk *chunk, *tmp;
struct proc *p;
struct thread *td;
EVENTHANDLER_DEREGISTER(thread_ctor, kinst_thread_ctor_handler);
EVENTHANDLER_DEREGISTER(thread_dtor, kinst_thread_dtor_handler);
sx_slock(&allproc_lock);
sx_xlock(&kinst_tramp_sx);
FOREACH_PROC_IN_SYSTEM(p) {
PROC_LOCK(p);
FOREACH_THREAD_IN_PROC(p, td) {
kinst_trampoline_dealloc_locked(td->t_kinst_tramp,
false);
td->t_kinst_tramp = NULL;
}
PROC_UNLOCK(p);
}
sx_sunlock(&allproc_lock);
TAILQ_FOREACH_SAFE(chunk, &kinst_trampchunks, next, tmp)
kinst_trampchunk_free(chunk);
sx_xunlock(&kinst_tramp_sx);
#else
struct trampchunk *chunk, *tmp;
sx_xlock(&kinst_tramp_sx);
TAILQ_FOREACH_SAFE(chunk, &kinst_trampchunks, next, tmp)
kinst_trampchunk_free(chunk);
sx_xunlock(&kinst_tramp_sx);
#endif
return (0);
}