asahi: Add wrap library

Add a library that wraps the key IOKit entrypoints used in the macOS
UABI for AGX. Our wrapped routines print information about the kernel
calls made and dump work submitted to the GPU using agxdecode. This code
has two major use cases:

1. Debugging Mesa, particularly around the undocumented macOS
   user-kernel interface. Logs from Mesa may compared to Metal to check
   that the UABI is being used correcrly.

2. Reverse-engineering the hardware, using this as glue to get at the
   "interesting" GPU memory.

Signed-off-by: Alyssa Rosenzweig <alyssa@rosenzweig.io>
Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/16512>
This commit is contained in:
Alyssa Rosenzweig 2021-07-10 12:30:40 -04:00
parent d814711155
commit 1b7304f44b
3 changed files with 497 additions and 0 deletions

View File

@ -0,0 +1,37 @@
/*
* Copyright (C) 2010 Apple Inc. 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 APPLE INC. AND ITS 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 APPLE INC. OR ITS 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.
*/
/*
* Macro found in WebKit source code:
*
* https://chromium.googlesource.com/chromium/blink.git/+/8ced00ca9f81cd8403584ae9123d9f3bb696c86a/Source/WebKit2/PluginProcess/mac/PluginProcessShim.mm
*/
#ifndef DYLD_INTERPOSE
#define DYLD_INTERPOSE(_replacement,_replacee) \
__attribute__((used)) static struct{ const void* replacement; const void* replacee; } _interpose_##_replacee \
__attribute__ ((section ("__DATA,__interpose"))) = { (const void*)(unsigned long)&_replacement, (const void*)(unsigned long)&_replacee };
#endif

View File

@ -83,3 +83,16 @@ if with_tests
protocol : gtest_test_protocol,
)
endif
if dep_iokit.found()
libasahi_wrap = shared_library(
'wrap',
'wrap.c',
include_directories : [inc_include, inc_src, inc_mapi, inc_mesa, inc_gallium, inc_gallium_aux, inc_asahi],
dependencies : [idep_mesautil, dep_iokit],
c_args : [no_override_init_args, '-Wno-missing-prototypes'],
gnu_symbol_visibility : 'hidden',
build_by_default : with_tools.contains('asahi'),
link_with: libasahi_decode,
)
endif

447
src/asahi/lib/wrap.c Normal file
View File

@ -0,0 +1,447 @@
/*
* Copyright (C) 2021-2022 Alyssa Rosenzweig <alyssa@rosenzweig.io>
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice (including the next
* paragraph) shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <dlfcn.h>
#include <assert.h>
#include <inttypes.h>
#include <mach/mach.h>
#include <IOKit/IOKitLib.h>
#include "util/compiler.h"
#include "io.h"
#include "decode.h"
#include "util.h"
#include "hexdump.h"
#include "dyld_interpose.h"
/*
* Wrap IOKit entrypoints to intercept communication between the AGX kernel
* extension and userspace clients. IOKit prototypes are public from the IOKit
* source release.
*/
mach_port_t metal_connection = 0;
kern_return_t
wrap_Method(mach_port_t connection, uint32_t selector, const uint64_t* input,
uint32_t inputCnt, const void *inputStruct, size_t inputStructCnt,
uint64_t *output, uint32_t *outputCnt, void *outputStruct,
size_t *outputStructCntP)
{
/* Heuristic guess which connection is Metal, skip over I/O from everything
* else. This is technically wrong but it works in practice, and reduces the
* surface area we need to wrap.
*/
if (selector == AGX_SELECTOR_SET_API) {
metal_connection = connection;
} else if (metal_connection != connection) {
return IOConnectCallMethod(connection, selector, input, inputCnt,
inputStruct, inputStructCnt, output, outputCnt,
outputStruct, outputStructCntP);
}
printf("Selector %u, %X, %X\n", selector, connection, metal_connection);
/* Check the arguments make sense */
assert((input != NULL) == (inputCnt != 0));
assert((inputStruct != NULL) == (inputStructCnt != 0));
assert((output != NULL) == (outputCnt != 0));
assert((outputStruct != NULL) == (outputStructCntP != 0));
/* Dump inputs */
switch (selector) {
case AGX_SELECTOR_SET_API:
assert(input == NULL && output == NULL && outputStruct == NULL);
assert(inputStruct != NULL && inputStructCnt == 16);
assert(((uint8_t *) inputStruct)[15] == 0x0);
printf("%X: SET_API(%s)\n", connection, (const char *) inputStruct);
break;
case AGX_SELECTOR_ALLOCATE_MEM: {
const struct agx_allocate_resource_req *req = inputStruct;
struct agx_allocate_resource_req *req2 = (void *) inputStruct;
req2->mode = (req->mode & 0x800) | 0x430;
bool suballocated = req->mode & 0x800;
printf("Resource allocation:\n");
printf(" Mode: 0x%X%s\n", req->mode & ~0x800,
suballocated ? " (suballocated) " : "");
printf(" CPU fixed: 0x%" PRIx64 "\n", req->cpu_fixed);
printf(" CPU fixed (parent): 0x%" PRIx64 "\n", req->cpu_fixed_parent);
printf(" Size: 0x%X\n", req->size);
printf(" Flags: 0x%X\n", req->flags);
if (suballocated) {
printf(" Parent: %u\n", req->parent);
} else {
assert(req->parent == 0);
}
for (unsigned i = 0; i < ARRAY_SIZE(req->unk0); ++i) {
if (req->unk0[i])
printf(" UNK%u: 0x%X\n", 0 + i, req->unk0[i]);
}
for (unsigned i = 0; i < ARRAY_SIZE(req->unk6); ++i) {
if (req->unk6[i])
printf(" UNK%u: 0x%X\n", 6 + i, req->unk6[i]);
}
if (req->unk17)
printf(" UNK17: 0x%X\n", req->unk17);
if (req->unk19)
printf(" UNK19: 0x%X\n", req->unk19);
for (unsigned i = 0; i < ARRAY_SIZE(req->unk21); ++i) {
if (req->unk21[i])
printf(" UNK%u: 0x%X\n", 21 + i, req->unk21[i]);
}
break;
}
case AGX_SELECTOR_SUBMIT_COMMAND_BUFFERS:
assert(output == NULL && outputStruct == NULL);
assert(inputStructCnt == sizeof(struct agx_submit_cmdbuf_req));
assert(inputCnt == 1);
printf("%X: SUBMIT_COMMAND_BUFFERS command queue id:%llx %p\n",
connection, input[0], inputStruct);
const struct agx_submit_cmdbuf_req *req = inputStruct;
agxdecode_cmdstream(req->command_buffer_shmem_id,
req->segment_list_shmem_id, true);
if (getenv("ASAHI_DUMP"))
agxdecode_dump_mappings(req->segment_list_shmem_id);
agxdecode_next_frame();
FALLTHROUGH;
default:
printf("%X: call %s (out %p, %zu)", connection,
wrap_selector_name(selector), outputStructCntP,
outputStructCntP ? *outputStructCntP : 0);
for (uint64_t u = 0; u < inputCnt; ++u)
printf(" %llx", input[u]);
if(inputStructCnt) {
printf(", struct:\n");
hexdump(stdout, inputStruct, inputStructCnt, true);
} else {
printf("\n");
}
break;
}
/* Invoke the real method */
kern_return_t ret =
IOConnectCallMethod(connection, selector, input, inputCnt, inputStruct,
inputStructCnt, output, outputCnt, outputStruct,
outputStructCntP);
if (ret != 0)
printf("return %u\n", ret);
/* Track allocations for later analysis (dumping, disassembly, etc) */
switch (selector) {
case AGX_SELECTOR_CREATE_SHMEM: {
assert(inputCnt == 2);
assert((*outputStructCntP) == 0x10);
uint64_t *inp = (uint64_t *) input;
uint8_t type = inp[1];
assert(type <= 2);
if (type == 2)
printf("(cmdbuf with error reporting)\n");
uint64_t *ptr = (uint64_t *) outputStruct;
uint32_t *words = (uint32_t *) (ptr + 1);
agxdecode_track_alloc(&(struct agx_bo) {
.handle = words[1],
.ptr.cpu = (void *) *ptr,
.size = words[0],
.type = inp[1] ? AGX_ALLOC_CMDBUF : AGX_ALLOC_MEMMAP
});
break;
}
case AGX_SELECTOR_ALLOCATE_MEM: {
assert((*outputStructCntP) == 0x50);
const struct agx_allocate_resource_req *req = inputStruct;
struct agx_allocate_resource_resp *resp = outputStruct;
if (resp->cpu && req->cpu_fixed)
assert(resp->cpu == req->cpu_fixed);
printf("Response:\n");
printf(" GPU VA: 0x%" PRIx64 "\n", resp->gpu_va);
printf(" CPU VA: 0x%" PRIx64 "\n", resp->cpu);
printf(" Handle: %u\n", resp->handle);
printf(" Root size: 0x%" PRIx64 "\n", resp->root_size);
printf(" Suballocation size: 0x%" PRIx64 "\n", resp->sub_size);
printf(" GUID: 0x%X\n", resp->guid);
for (unsigned i = 0; i < ARRAY_SIZE(resp->unk4); ++i) {
if (resp->unk4[i])
printf(" UNK%u: 0x%X\n", 4 + i, resp->unk4[i]);
}
for (unsigned i = 0; i < ARRAY_SIZE(resp->unk11); ++i) {
if (resp->unk11[i])
printf(" UNK%u: 0x%X\n", 11 + i, resp->unk11[i]);
}
if (req->parent)
assert(resp->sub_size <= resp->root_size);
else
assert(resp->sub_size == resp->root_size);
agxdecode_track_alloc(&(struct agx_bo) {
.type = AGX_ALLOC_REGULAR,
.size = resp->sub_size,
.handle = resp->handle,
.ptr.gpu = resp->gpu_va,
.ptr.cpu = (void *) resp->cpu,
});
break;
}
case AGX_SELECTOR_FREE_MEM: {
assert(inputCnt == 1);
assert(inputStruct == NULL);
assert(output == NULL);
assert(outputStruct == NULL);
agxdecode_track_free(&(struct agx_bo) {
.type = AGX_ALLOC_REGULAR,
.handle = input[0]
});
break;
}
default:
/* Dump the outputs */
if(outputCnt) {
printf("%u scalars: ", *outputCnt);
for (uint64_t u = 0; u < *outputCnt; ++u)
printf("%llx ", output[u]);
printf("\n");
}
if(outputStructCntP) {
printf(" struct\n");
hexdump(stdout, outputStruct, *outputStructCntP, true);
if (selector == 2) {
/* Dump linked buffer as well */
void **o = outputStruct;
hexdump(stdout, *o, 64, true);
}
}
printf("\n");
break;
}
return ret;
}
kern_return_t
wrap_AsyncMethod(mach_port_t connection, uint32_t selector,
mach_port_t wakePort, uint64_t *reference,
uint32_t referenceCnt, const uint64_t *input,
uint32_t inputCnt, const void *inputStruct,
size_t inputStructCnt, uint64_t *output, uint32_t *outputCnt,
void *outputStruct, size_t *outputStructCntP)
{
/* Check the arguments make sense */
assert((input != NULL) == (inputCnt != 0));
assert((inputStruct != NULL) == (inputStructCnt != 0));
assert((output != NULL) == (outputCnt != 0));
assert((outputStruct != NULL) == (outputStructCntP != 0));
printf("%X: call %X, wake port %X (out %p, %zu)", connection, selector,
wakePort, outputStructCntP, outputStructCntP ? *outputStructCntP : 0);
for (uint64_t u = 0; u < inputCnt; ++u)
printf(" %llx", input[u]);
if(inputStructCnt) {
printf(", struct:\n");
hexdump(stdout, inputStruct, inputStructCnt, true);
} else {
printf("\n");
}
printf(", references: ");
for (unsigned i = 0; i < referenceCnt; ++i)
printf(" %llx", reference[i]);
printf("\n");
kern_return_t ret = IOConnectCallAsyncMethod(connection, selector, wakePort,
reference, referenceCnt, input, inputCnt, inputStruct, inputStructCnt,
output, outputCnt, outputStruct, outputStructCntP);
printf("return %u", ret);
if(outputCnt) {
printf("%u scalars: ", *outputCnt);
for (uint64_t u = 0; u < *outputCnt; ++u)
printf("%llx ", output[u]);
printf("\n");
}
if(outputStructCntP) {
printf(" struct\n");
hexdump(stdout, outputStruct, *outputStructCntP, true);
if (selector == 2) {
/* Dump linked buffer as well */
void **o = outputStruct;
hexdump(stdout, *o, 64, true);
}
}
printf("\n");
return ret;
}
kern_return_t
wrap_StructMethod(mach_port_t connection, uint32_t selector,
const void *inputStruct, size_t inputStructCnt,
void *outputStruct, size_t *outputStructCntP)
{
return wrap_Method(connection, selector, NULL, 0, inputStruct,
inputStructCnt, NULL, NULL, outputStruct,
outputStructCntP);
}
kern_return_t
wrap_AsyncStructMethod(mach_port_t connection, uint32_t selector,
mach_port_t wakePort, uint64_t *reference,
uint32_t referenceCnt, const void *inputStruct,
size_t inputStructCnt, void *outputStruct,
size_t *outputStructCnt)
{
return wrap_AsyncMethod(connection, selector, wakePort, reference,
referenceCnt, NULL, 0, inputStruct, inputStructCnt,
NULL, NULL, outputStruct, outputStructCnt);
}
kern_return_t
wrap_ScalarMethod(mach_port_t connection, uint32_t selector,
const uint64_t *input, uint32_t inputCnt, uint64_t *output,
uint32_t *outputCnt)
{
return wrap_Method(connection, selector, input, inputCnt, NULL, 0, output,
outputCnt, NULL, NULL);
}
kern_return_t
wrap_AsyncScalarMethod(mach_port_t connection, uint32_t selector,
mach_port_t wakePort, uint64_t *reference,
uint32_t referenceCnt, const uint64_t *input,
uint32_t inputCnt, uint64_t *output, uint32_t *outputCnt)
{
return wrap_AsyncMethod(connection, selector, wakePort, reference,
referenceCnt, input, inputCnt, NULL, 0, output,
outputCnt, NULL, NULL);
}
mach_port_t
wrap_DataQueueAllocateNotificationPort()
{
mach_port_t ret = IODataQueueAllocateNotificationPort();
printf("Allocated notif port %X\n", ret);
return ret;
}
kern_return_t
wrap_SetNotificationPort(io_connect_t connect, uint32_t type,
mach_port_t port, uintptr_t reference)
{
printf("Set noficiation port connect=%X, type=%X, port=%X, reference=%"
PRIx64"\n", connect, type, port, (uint64_t) reference);
return IOConnectSetNotificationPort(connect, type, port, reference);
}
IOReturn
wrap_DataQueueWaitForAvailableData(IODataQueueMemory *dataQueue,
mach_port_t notificationPort)
{
printf("Waiting for data queue at notif port %X\n", notificationPort);
IOReturn ret = IODataQueueWaitForAvailableData(dataQueue, notificationPort);
printf("ret=%X\n", ret);
return ret;
}
IODataQueueEntry *
wrap_DataQueuePeek(IODataQueueMemory *dataQueue)
{
printf("Peeking data queue\n");
return IODataQueuePeek(dataQueue);
}
IOReturn
wrap_DataQueueDequeue(IODataQueueMemory *dataQueue, void *data, uint32_t *dataSize)
{
printf("Dequeueing (dataQueue=%p, data=%p, buffer %u)\n", dataQueue, data, *dataSize);
IOReturn ret = IODataQueueDequeue(dataQueue, data, dataSize);
printf("Return \"%s\", got %u bytes\n", mach_error_string(ret), *dataSize);
uint8_t *data8 = data;
for (unsigned i = 0; i < *dataSize; ++i) {
printf("%02X ", data8[i]);
}
printf("\n");
return ret;
}
DYLD_INTERPOSE(wrap_Method, IOConnectCallMethod);
DYLD_INTERPOSE(wrap_AsyncMethod, IOConnectCallAsyncMethod);
DYLD_INTERPOSE(wrap_StructMethod, IOConnectCallStructMethod);
DYLD_INTERPOSE(wrap_AsyncStructMethod, IOConnectCallAsyncStructMethod);
DYLD_INTERPOSE(wrap_ScalarMethod, IOConnectCallScalarMethod);
DYLD_INTERPOSE(wrap_AsyncScalarMethod, IOConnectCallAsyncScalarMethod);
DYLD_INTERPOSE(wrap_SetNotificationPort, IOConnectSetNotificationPort);
DYLD_INTERPOSE(wrap_DataQueueAllocateNotificationPort, IODataQueueAllocateNotificationPort);
DYLD_INTERPOSE(wrap_DataQueueWaitForAvailableData, IODataQueueWaitForAvailableData);
DYLD_INTERPOSE(wrap_DataQueuePeek, IODataQueuePeek);
DYLD_INTERPOSE(wrap_DataQueueDequeue, IODataQueueDequeue);