Files

451 lines
18 KiB
C

/* SPDX-License-Identifier: GPL-3.0-or-later */
#define ND_MAX_TRACE_CALLS 64U
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-function"
#include "remote_trace_core.inc"
#pragma clang diagnostic pop
#include "remote_trace.h"
#include <sys/time.h>
#define ND_MAX_MAPPING (1024U * 1024U)
#define ND_MAX_TRACE_IO 512U
#define ND_TOUCH_STRIDE 4096U
#define ND_MAX_TOUCHES (ND_TRACE_MAX_INITIALIZED_MAPPING / ND_TOUCH_STRIDE)
_Static_assert(SYS_gettimeofday == 116, "profiled gettimeofday syscall");
_Static_assert(sizeof(struct timeval) == 16, "target timeval ABI");
_Static_assert(offsetof(struct timeval, tv_sec) == 0, "target timeval seconds ABI");
_Static_assert(offsetof(struct timeval, tv_usec) == 8, "target timeval microseconds ABI");
_Static_assert(ND_MAX_TOUCHES + 8U <= ND_MAX_TRACE_CALLS, "initialization and cleanup budget");
struct trace_io {
uint64_t address;
size_t requested;
int writing, result, local_error, error, success;
};
struct nd_trace {
struct report r;
struct remote_call operations[ND_MAX_TRACE_CALLS];
struct trace_io io[ND_MAX_TRACE_IO];
size_t operation_count, io_count, bytes_written, bytes_read;
size_t bytes_write_completed, bytes_read_attempted, io_rejected;
uint64_t owned_mapping;
size_t owned_size, initialized_pages;
int initialization_attempted, initialization_complete;
int ready, closed, retained, close_result;
};
struct nd_trace *nd_trace_begin(pid_t expected_pid) {
struct nd_trace *t = calloc(1, sizeof(*t));
if (!t)
return NULL;
struct report *r = &t->r;
r->target = r->before_target = r->final_target = -1;
r->probe_pid = getpid();
r->firmware = kernel_get_fw_version();
if (monotonic_ns(&r->started_ns)) {
r->failure = "clock failed";
return t;
}
if (r->firmware != UINT32_C(0x13200006) || expected_pid <= 0) {
r->failure = "unsupported firmware or missing expected PID";
return t;
}
if (credential_setup(&r->credentials)) {
r->failure = "credential setup failed";
return t;
}
r->target = find_shellui(&r->process_bytes, &r->process_records);
r->before_target = find_shellui(NULL, NULL);
if (r->target != expected_pid || r->before_target != expected_pid) {
r->failure = "ShellUI identity changed before attach";
return t;
}
uint64_t started;
if (monotonic_ns(&started)) {
r->failure = "attach clock failed";
return t;
}
r->attach = trace_call(PT_ATTACH, r->target, NULL, 0);
r->accepted = call_ok(&r->attach);
if (!r->accepted) {
r->failure = "attach rejected";
return t;
}
wait_for_stop(r->target, &r->attach_wait, started);
note_stop(r, &r->attach_wait);
if (!r->target_stopped || r->attach_wait.signal != SIGSTOP) {
r->failure = "attach stop not confirmed";
return t;
}
struct ptrace_lwpinfo info = {0};
r->initial_lwp = trace_call(PT_LWPINFO, r->target, &info, sizeof(info));
if (!call_ok(&r->initial_lwp) || info.pl_lwpid <= 0) {
r->failure = "initial LWP unavailable";
return t;
}
r->lwp = info.pl_lwpid;
r->save_regs = trace_call(PT_GETREGS, r->lwp, &r->original, 0);
if (!call_ok(&r->save_regs)) {
r->failure = "register save failed";
return t;
}
r->regs_saved = 1;
r->preexisting_tf = !!(r->original.r_rflags & 0x100);
if (r->preexisting_tf) {
r->failure = "preexisting single-step flag";
return t;
}
const uint32_t handles[2] = {1, 0x2001};
for (unsigned i = 0; i < 2 && r->entry <= 0; ++i) {
errno = 0;
r->entry = kernel_dynlib_resolve(r->target, handles[i], "HoLVWNanBBc");
r->resolve_error[i] = r->entry > 0 ? 0 : saved_error();
if (r->entry > 0)
r->resolve_handle = handles[i];
}
if (r->entry <= 0 || (uint64_t)r->entry > USER_END - sizeof(r->gate_bytes) ||
kernel_proc_copyout(r->target, r->entry, r->gate_bytes, sizeof(r->gate_bytes))) {
r->failure = "syscall gate unavailable";
return t;
}
const uint8_t expected[12] = {0x48, 0xc7, 0xc0, 0x14, 0, 0, 0, 0x49, 0x89, 0xca, 0x0f, 0x05};
r->gate_valid = memcmp(r->gate_bytes, expected, sizeof(expected)) == 0;
if (!r->gate_valid) {
r->failure = "syscall gate bytes changed";
return t;
}
struct remote_call *c = &t->operations[t->operation_count++];
c->name = "getpid";
c->number = SYS_getpid;
if (!execute_remote(r, c) || c->remote_carry || c->remote_rax != (uint64_t)r->target) {
if (!r->failure)
r->failure = "remote getpid not confirmed";
return t;
}
r->getpid_matches = 1;
t->ready = 1;
return t;
}
int nd_trace_ready(const struct nd_trace *t) {
return t && t->ready && !t->closed && !t->retained && !t->r.operator_recovery_required &&
t->r.target_stopped && !t->r.regs_dirty;
}
static struct remote_call *operation(struct nd_trace *t, const char *name, uint64_t number) {
if (!nd_trace_ready(t) || t->operation_count >= ND_MAX_TRACE_CALLS)
return NULL;
struct remote_call *c = &t->operations[t->operation_count++];
c->name = name;
c->number = number;
return c;
}
static int range_ok(uint64_t address, size_t size) {
return address >= MAP_BYTES && size && size <= ND_MAX_MAPPING && address <= USER_END - size;
}
static int run(struct nd_trace *t, struct remote_call *c) {
if (!c)
return -1;
if (!execute_remote(&t->r, c)) {
t->ready = 0;
if (!t->r.failure)
t->r.failure = "remote operation not confirmed";
return -1;
}
/* A proven syscall error still leaves a stopped, restored tracee. Allow
* rollback of earlier unpublished mappings in that case. */
if (c->remote_carry) {
t->r.failure = "remote syscall returned an error";
return -1;
}
return 0;
}
int nd_trace_map(struct nd_trace *t, size_t size, uint64_t *address) {
if (!t || t->owned_mapping || !address || !size || size > ND_TRACE_MAX_INITIALIZED_MAPPING ||
size % MAP_BYTES)
return -1;
*address = 0;
struct remote_call *c = operation(t, "mmap-rw", SYS_mmap);
if (!c)
return -1;
c->args[1] = size;
c->args[2] = PROT_READ | PROT_WRITE;
c->args[3] = MAP_PRIVATE | MAP_ANONYMOUS;
c->args[4] = UINT64_MAX;
if (run(t, c) || !range_ok(c->remote_rax, size) || c->remote_rax % MAP_BYTES)
return -1;
*address = c->remote_rax;
t->owned_mapping = *address;
t->owned_size = size;
return 0;
}
int nd_trace_initialize_mapping(struct nd_trace *t, uint64_t address, size_t size) {
if (!nd_trace_ready(t) || t->initialization_attempted || address != t->owned_mapping ||
size != t->owned_size || !size || size > ND_TRACE_MAX_INITIALIZED_MAPPING ||
size % ND_TOUCH_STRIDE ||
t->operation_count + size / ND_TOUCH_STRIDE + 4U > ND_MAX_TRACE_CALLS) {
errno = EINVAL;
return -1;
}
t->initialization_attempted = 1;
for (size_t offset = 0; offset < size; offset += ND_TOUCH_STRIDE) {
struct remote_call *c =
operation(t, "gettimeofday-initialize-private-page", SYS_gettimeofday);
if (!c)
return -1;
c->args[0] = address + offset; /* timezone=NULL; never touch existing objects. */
if (run(t, c) || c->remote_rax != 0) {
if (!t->r.failure)
t->r.failure = "mapping initialization syscall failed";
return -1;
}
struct timeval value = {0};
if (nd_trace_read(t, address + offset, &value, sizeof(value)) || value.tv_sec <= 0 ||
value.tv_usec < 0 || value.tv_usec >= 1000000) {
if (!t->r.failure)
t->r.failure = "mapping initialization readback invalid";
return -1;
}
++t->initialized_pages;
}
t->initialization_complete = 1;
return 0;
}
int nd_trace_protect(struct nd_trace *t, uint64_t address, size_t size, int prot) {
if (!range_ok(address, size) || address % MAP_BYTES || size % MAP_BYTES ||
(prot != PROT_READ && prot != (PROT_READ | PROT_EXEC)))
return -1;
struct remote_call *c = operation(t, "mprotect", SYS_mprotect);
if (!c)
return -1;
c->args[0] = address;
c->args[1] = size;
c->args[2] = (uint64_t)prot;
return run(t, c) || c->remote_rax != 0 ? -1 : 0;
}
int nd_trace_unmap(struct nd_trace *t, uint64_t address, size_t size) {
if (!range_ok(address, size) || address % MAP_BYTES || size % MAP_BYTES)
return -1;
struct remote_call *c = operation(t, "munmap-unpublished", SYS_munmap);
if (!c)
return -1;
c->args[0] = address;
c->args[1] = size;
if (run(t, c) || c->remote_rax != 0)
return -1;
if (address == t->owned_mapping && size == t->owned_size)
t->owned_mapping = 0;
return 0;
}
static int memory_io(struct nd_trace *t, uint64_t address, void *data, size_t size, int writing) {
if (!nd_trace_ready(t) || !data || !range_ok(address, size) ||
(writing && (!t->owned_mapping || !t->initialization_complete))) {
if (t)
++t->io_rejected;
errno = EINVAL;
return -1;
}
if (t->io_count == ND_MAX_TRACE_IO) {
++t->io_rejected;
if (!t->r.failure)
t->r.failure = "memory I/O evidence capacity exhausted";
errno = ENOSPC;
return -1;
}
struct trace_io *io = &t->io[t->io_count++];
io->address = address;
io->requested = size;
io->writing = writing;
if (writing)
t->bytes_written += size;
else
t->bytes_read_attempted += size;
/* Fresh mapping writes are permitted only after normal kernel-to-user
* gettimeofday writes materialized every 4-KiB subpage. Existing executor
* publication/rollback remains the caller's guarded aligned 8-byte write. */
errno = 0;
io->result = writing ? kernel_proc_copyin(t->r.target, data, (intptr_t)address, size)
: kernel_proc_copyout(t->r.target, (intptr_t)address, data, size);
io->local_error = errno;
io->success = io->result == 0;
if (io->success) {
if (writing)
t->bytes_write_completed += size;
else
t->bytes_read += size;
return 0;
}
io->error = io->local_error > 0 ? io->local_error : EIO;
if (!t->r.failure)
t->r.failure = writing ? "kernel_proc_copyin failed" : "kernel_proc_copyout failed";
/* Failed/partial I/O does not imply that the tracee resumed. Keep the
* stopped scope available for the caller's existing rollback/cleanup. */
errno = io->error;
return -1;
}
int nd_trace_read(struct nd_trace *t, uint64_t address, void *data, size_t size) {
return memory_io(t, address, data, size, 0);
}
int nd_trace_write(struct nd_trace *t, uint64_t address, const void *data, size_t size) {
return memory_io(t, address, (void *)data, size, 1);
}
void nd_trace_retain(struct nd_trace *t, const char *reason) {
if (!t || t->retained)
return;
struct report *r = &t->r;
/* Set the latch before cleanup: no later close can enter PT_DETACH. */
t->ready = 0;
t->retained = 1;
t->close_result = -1;
r->detach_skipped_unsafe = 1;
r->operator_recovery_required = 1;
r->failure = reason ? reason : "caller requires tracer retention";
if (!t->closed) {
if (r->regs_dirty) {
if (!r->target_stopped)
recover_stop(r);
restore_registers(r);
}
r->final_target = find_shellui(NULL, NULL);
credential_restore(&r->credentials);
t->closed = 1;
}
}
int nd_trace_close(struct nd_trace *t) {
if (!t)
return -1;
struct report *r = &t->r;
if (t->retained)
return -1;
if (r->operator_recovery_required) {
nd_trace_retain(t, r->failure);
return -1;
}
if (t->closed)
return t->close_result;
t->ready = 0;
if (r->regs_dirty) {
if (!r->target_stopped)
recover_stop(r);
restore_registers(r);
}
if (r->accepted) {
if (r->regs_dirty) {
r->detach_skipped_unsafe = 1;
r->operator_recovery_required = 1;
r->failure = "registers not restored; tracer must remain alive";
} else {
r->detach = trace_call(PT_DETACH, r->target, NULL, (uint64_t)r->detach_signal);
r->detach_ok = call_ok(&r->detach);
if (r->detach_ok)
r->target_stopped = 0;
else {
r->operator_recovery_required = 1;
r->failure = "detach unconfirmed; tracer must remain alive";
}
}
}
r->final_target = find_shellui(NULL, NULL);
credential_restore(&r->credentials);
t->closed = 1;
const int identity_changed = r->target > 0 && r->final_target != r->target;
if (identity_changed && !r->failure)
r->failure = "ShellUI identity changed during trace scope";
t->close_result = r->operator_recovery_required ||
(r->credentials.saved && !r->credentials.restored) || identity_changed
? -1
: 0;
return t->close_result;
}
void nd_trace_json(FILE *f, const struct nd_trace *t) {
if (!t) {
fputs("null", f);
return;
}
const struct report *r = &t->r;
fprintf(
f,
"{\"pid\":%d,\"finalPid\":%d,\"firmware\":\"0x%08x\",\"failure\":\"%s\","
"\"ready\":%s,\"closed\":%s,\"retained\":%s,\"targetStopped\":%s,\"lwp\":%d,\"gate\":\"0x%"
"llx\","
"\"registersRestored\":%s,\"registersDirty\":%s,\"detachOk\":%s,\"detachSignal\":%d,"
"\"operatorRecoveryRequired\":%s,\"credentialsRestored\":%s,\"bytesWriteAttempted\":%zu,"
"\"bytesRead\":%zu,\"attach\":",
r->target, r->final_target, r->firmware, r->failure ? r->failure : "",
t->ready ? "true" : "false", t->closed ? "true" : "false", t->retained ? "true" : "false",
r->target_stopped ? "true" : "false", r->lwp, (unsigned long long)r->entry,
r->regs_restored ? "true" : "false", r->regs_dirty ? "true" : "false",
r->detach_ok ? "true" : "false", r->detach_signal,
r->operator_recovery_required ? "true" : "false",
r->credentials.restored ? "true" : "false", t->bytes_written, t->bytes_read);
write_call(f, &r->attach);
fputs(",\"attachWait\":", f);
write_wait(f, &r->attach_wait);
fputs(",\"detach\":", f);
write_call(f, &r->detach);
fprintf(f,
",\"detachSkippedUnsafe\":%s,\"unattributedStepTrap\":%s,\"capabilityMode\":\"fullcaps_"
"ff_debugger_authid_4800000000010003\","
"\"credentialScope\":{\"saved\":%s,\"setupError\":%d,\"restoreCapsError\":%d,"
"\"restoreAttrsError\":%d,"
"\"restoreAuthError\":%d,\"verifyCapsError\":%d,\"verifyAttrsError\":%d,"
"\"verifyAuthError\":%d},"
"\"registerRestores\":[",
r->detach_skipped_unsafe ? "true" : "false",
r->unattributed_step_trap ? "true" : "false", r->credentials.saved ? "true" : "false",
r->credentials.setup_error, r->credentials.restore_caps_error,
r->credentials.restore_attrs_error, r->credentials.restore_auth_error,
r->credentials.verify_caps_error, r->credentials.verify_attrs_error,
r->credentials.verify_auth_error);
for (size_t i = 0; i < r->restore_count; ++i) {
if (i)
fputc(',', f);
fputs("{\"set\":", f);
write_call(f, &r->restores[i].set);
fputs(",\"readback\":", f);
write_call(f, &r->restores[i].get);
fprintf(f, ",\"matches\":%s}", r->restores[i].matches ? "true" : "false");
}
fprintf(f, "],\"recoveryAttempted\":%s,\"recoveryOk\":%s,\"recoverySignal\":",
r->recovery_attempted ? "true" : "false", r->recovery_ok ? "true" : "false");
write_call(f, &r->recovery_signal);
fputs(",\"recoveryWait\":", f);
write_wait(f, &r->recovery_wait);
fputs(",\"operations\":[", f);
for (size_t i = 0; i < t->operation_count; ++i) {
if (i)
fputc(',', f);
write_remote(f, &t->operations[i]);
}
fprintf(f,
"],\"bytesWriteReportedSuccess\":%zu,\"bytesReadAttempted\":%zu,"
"\"ioRejected\":%zu,\"mappingInitialization\":{\"attempted\":%s,\"complete\":%s,"
"\"mappingBytes\":%zu,\"stride\":%u,\"initializedPages\":%zu,\"maximumPages\":%u},"
"\"memoryIO\":[",
t->bytes_write_completed, t->bytes_read_attempted, t->io_rejected,
t->initialization_attempted ? "true" : "false",
t->initialization_complete ? "true" : "false", t->owned_size, ND_TOUCH_STRIDE,
t->initialized_pages, ND_MAX_TOUCHES);
for (size_t i = 0; i < t->io_count; ++i) {
const struct trace_io *io = &t->io[i];
if (i)
fputc(',', f);
fprintf(f,
"{\"backend\":\"%s\",\"operation\":\"%s\",\"address\":\"0x%llx\","
"\"bytesRequested\":%zu,\"bytesCompleted\":null,\"lengthSource\":\"SDK status "
"only; no transfer count\","
"\"bytesReportedSuccess\":%zu,\"success\":%s,\"result\":%d,\"localErrno\":%d,"
"\"effectiveErrno\":%d}",
io->writing ? "kernel_proc_copyin" : "kernel_proc_copyout",
io->writing ? "write" : "read", (unsigned long long)io->address, io->requested,
io->success ? io->requested : 0, io->success ? "true" : "false", io->result,
io->local_error, io->error);
}
fputs("]}", f);
}
void nd_trace_destroy(struct nd_trace *t) {
if (t && t->closed && !t->retained && !t->r.operator_recovery_required)
free(t);
}