run-static.cpp   [plain text]



#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <mach-o/loader.h>
#include <signal.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

#include <algorithm>

#include <TargetConditionals.h>

#include "ClosureFileSystemPhysical.h"
#include "MachOAnalyzer.h"
#include "MachOFile.h"

#include "../testing/test-cases/kernel-test-runner.h"

const bool isLoggingEnabled = false;

int entryFunc(const TestRunnerFunctions* funcs);
typedef __typeof(&entryFunc) EntryFuncTy;

TestRunnerFunctions testFuncs = {
    .version        = 1,
    .mhs            = { nullptr, nullptr, nullptr, nullptr },
    .basePointers   = { nullptr, nullptr, nullptr, nullptr },
    .printf         = &::printf,
    .exit           = &::exit,
    .testPass       = &_PASS,
    .testFail       = &_FAIL,
    .testLog        = &_LOG,
    .testTimeout    = &_TIMEOUT,
};

struct LoadedMachO {
    const dyld3::MachOAnalyzer* ma              = nullptr;
    // base pointer is the same as 'ma' when the binary has __TEXT first,
    // but will point at where we mapped __DATA if building a reverse auxKC.
    const void*                 basePointer     = nullptr;
};

LoadedMachO loadPath(const char* binaryPath) {
    __block Diagnostics diag;
    dyld3::closure::FileSystemPhysical fileSystem;
    dyld3::closure::LoadedFileInfo info;
    char realerPath[MAXPATHLEN];
    __block bool printedError = false;
    if (!fileSystem.loadFile(binaryPath, info, realerPath, ^(const char* format, ...) {
        fprintf(stderr, "run-static: ");
        va_list list;
        va_start(list, format);
        vfprintf(stderr, format, list);
        va_end(list);
        printedError = true;
    })) {
        if (!printedError )
            fprintf(stderr, "run-static: %s: file not found\n", binaryPath);
        exit(1);
    }

    const char* currentArchName = dyld3::MachOFile::currentArchName();
    const dyld3::GradedArchs& currentArchs = dyld3::GradedArchs::forName(currentArchName);
    __block const dyld3::MachOFile* mf = nullptr;
    __block uint64_t sliceOffset = 0;
    if ( dyld3::FatFile::isFatFile(info.fileContent) ) {
        const dyld3::FatFile* ff = (dyld3::FatFile*)info.fileContent;
        ff->forEachSlice(diag, info.fileContentLen, ^(uint32_t sliceCpuType, uint32_t sliceCpuSubType,
                                                      const void* sliceStart, uint64_t sliceSize, bool& stop) {
            const dyld3::MachOFile* sliceMF = (dyld3::MachOFile*)sliceStart;
            if ( currentArchs.grade(sliceMF->cputype, sliceMF->cpusubtype, false) != 0 ) {
                mf = sliceMF;
                sliceOffset = (uint64_t)mf - (uint64_t)ff;
                stop = true;
                return;
            }
        });

        if ( diag.hasError() ) {
            fprintf(stderr, "Error: %s\n", diag.errorMessage());
            return { nullptr, nullptr };
        }

        if ( mf == nullptr ) {
            fprintf(stderr, "Could not use binary '%s' because it does not contain a slice compatible with host '%s'\n",
                    binaryPath, currentArchName);
            return { nullptr, nullptr };
        }
    } else {
        mf = (dyld3::MachOFile*)info.fileContent;
        if ( !mf->isMachO(diag, info.sliceLen) ) {
            fprintf(stderr, "Could not use binary '%s' because '%s'\n", binaryPath, diag.errorMessage());
            return { nullptr, nullptr };
        }

        if ( currentArchs.grade(mf->cputype, mf->cpusubtype, false) == 0 ) {
            fprintf(stderr, "Could not use binary '%s' because 'incompatible arch'\n", binaryPath);
            return { nullptr, nullptr };
        }
    }

    if ( !mf->isFileSet() ) {
        fprintf(stderr, "Could not use binary '%s' because 'it is not a static executable'\n", binaryPath);
        return { nullptr, nullptr };
    }

    uint64_t mappedSize = ((dyld3::MachOAnalyzer*)mf)->mappedSize();
    vm_address_t mappedAddr;
    if ( ::vm_allocate(mach_task_self(), &mappedAddr, (size_t)mappedSize, VM_FLAGS_ANYWHERE) != 0 ) {
        fprintf(stderr, "Could not use binary '%s' because 'vm allocation failure'\n", binaryPath);
        return { nullptr, nullptr };
    }

    int fd = open(binaryPath, O_RDONLY);
    if ( fd == 0 ) {
        fprintf(stderr, "Could not open binary '%s' because '%s'\n", binaryPath, strerror(errno));
        return { nullptr, nullptr };
    }

    __block uint64_t baseAddress = ~0ULL;
    __block uint64_t textSegVMAddr = ~0ULL;
    mf->forEachSegment(^(const dyld3::MachOAnalyzer::SegmentInfo& info, bool& stop) {
        baseAddress = std::min(baseAddress, info.vmAddr);
        if ( strcmp(info.segName, "__TEXT") == 0 ) {
            textSegVMAddr = info.vmAddr;
        }
    });

    uint64_t loadAddress = (uint64_t)mappedAddr;
    if ( isLoggingEnabled ) {
        fprintf(stderr, "Mapping binary built at 0x%llx to 0x%llx\n", baseAddress, loadAddress);
    }
    mf->forEachSegment(^(const dyld3::MachOFile::SegmentInfo &info, bool &stop) {
        uint64_t requestedLoadAddress = info.vmAddr - baseAddress + loadAddress;
        if ( isLoggingEnabled )
            fprintf(stderr, "Mapping %p: %s with perms %d\n", (void*)requestedLoadAddress, info.segName, info.protections);
        if ( info.vmSize == 0 )
            return;
        size_t readBytes = pread(fd, (void*)requestedLoadAddress, (uintptr_t)info.fileSize, sliceOffset + info.fileOffset);
        if ( readBytes != info.fileSize ) {
            fprintf(stderr, "Didn't read enough bytes\n");
            exit(1);
        }
        // __DATA_CONST is read-only when we actually run live, but this test runner fixes up __DATA_CONST after this vm_protect
        // For now just don't make __DATA_CONST read only
        uint32_t protections = info.protections;
        if ( !strcmp(info.segName, "__DATA_CONST") )
            protections = VM_PROT_READ | VM_PROT_WRITE;
        const bool setCurrentPermissions = false;
        kern_return_t r = vm_protect(mach_task_self(), (vm_address_t)requestedLoadAddress, (uintptr_t)info.vmSize, setCurrentPermissions, protections);
        if ( r != KERN_SUCCESS ) {
            diag.error("vm_protect didn't work because %d", r);
            stop = true;
            return;
        }
    });

    if ( diag.hasError() ) {
        fprintf(stderr, "Error: %s\n", diag.errorMessage());
        return { nullptr, nullptr };
    }

    if ( textSegVMAddr != baseAddress ) {
        // __DATA is first.  ma should still point to __TEXT
        const dyld3::MachOAnalyzer* ma = (const dyld3::MachOAnalyzer*)(mappedAddr + textSegVMAddr - baseAddress);
        if ( !ma->validMachOForArchAndPlatform(diag, (size_t)mappedSize, binaryPath, currentArchs, dyld3::Platform::unknown, false) ) {
            fprintf(stderr, "Error: %s\n", diag.errorMessage());
            exit(1);
        }
        return { ma, (const void*)mappedAddr };
    }

    // __TEXT is first, so ma and base address are the same
    const dyld3::MachOAnalyzer* ma = (const dyld3::MachOAnalyzer*)mappedAddr;
    if ( !ma->validMachOForArchAndPlatform(diag, (size_t)mappedSize, binaryPath, currentArchs, dyld3::Platform::unknown, false) ) {
        fprintf(stderr, "Error: %s\n", diag.errorMessage());
        exit(1);
    }
    return { ma, (const void*)mappedAddr };
}

int main(int argc, const char * argv[]) {
    bool unsupported = false;
#if TARGET_OS_WATCH
    // HACK: Watch archs are not supported right now, so just return
    unsupported = true;
#endif
    if ( unsupported ) {
        funcs = &testFuncs;
        PASS("Success");
    }

    if ( (argc < 2) || (argc > 5) ) {
        fprintf(stderr, "Usage: run-static *path to static binary* [- - *path to auc kc*]\n");
        return 1;
    }

    for (unsigned i = 1; i != argc; ++i) {
        if ( !strcmp(argv[i], "-") )
            continue;
        LoadedMachO macho = loadPath(argv[i]);
        if ( macho.ma == nullptr )
            return 1;
        testFuncs.mhs[i - 1] = macho.ma;
        testFuncs.basePointers[i - 1] = macho.basePointer;
    }

    uint64_t entryOffset    = 0;
    bool usesCRT            = false;
    const dyld3::MachOAnalyzer* ma = (const dyld3::MachOAnalyzer*)testFuncs.mhs[0];
    if ( !ma->getEntry(entryOffset, usesCRT) ) {
        fprintf(stderr, "Could not use binary '%s' because 'no entry defined'\n", argv[1]);
        return 1;
    }

    EntryFuncTy entryFunc = (EntryFuncTy)((uint8_t*)testFuncs.mhs[0] + entryOffset);
#if __has_feature(ptrauth_calls)
    entryFunc = (EntryFuncTy)__builtin_ptrauth_sign_unauthenticated((void*)entryFunc, 0, 0);
#endif
    fprintf(stderr, "Entering static binary at %p\n", entryFunc);
    //kill(getpid(), SIGSTOP);
    int returnCode = entryFunc(&testFuncs);
    if ( returnCode != 0 ) {
        fprintf(stderr, "Binary '%s' returned non-zero value %d\n", argv[1], returnCode);
        return returnCode;
    }
    return 0;
}