blob: 4d95a44e67567a69ee024ee7c2e024ce9ab733fc [file] [log] [blame]
// Copyright 2017 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
// +build
#include <algorithm>
#include <errno.h>
#include <limits.h>
#include <signal.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <atomic>
#include <optional>
#if !GOOS_windows
#include <unistd.h>
#endif
#include "defs.h"
#include "pkg/flatrpc/flatrpc.h"
#if defined(__GNUC__)
#define SYSCALLAPI
#define NORETURN __attribute__((noreturn))
#define PRINTF(fmt, args) __attribute__((format(printf, fmt, args)))
#else
// Assuming windows/cl.
#define SYSCALLAPI WINAPI
#define NORETURN __declspec(noreturn)
#define PRINTF(fmt, args)
#define __thread __declspec(thread)
#endif
#ifndef GIT_REVISION
#define GIT_REVISION "unknown"
#endif
#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
#ifndef __has_feature
#define __has_feature(x) 0
#endif
#if defined(__SANITIZE_ADDRESS__) || __has_feature(address_sanitizer)
constexpr bool kAddressSanitizer = true;
#else
constexpr bool kAddressSanitizer = false;
#endif
// uint64 is impossible to printf without using the clumsy and verbose "%" PRId64.
// So we define and use uint64. Note: pkg/csource does s/uint64/uint64/.
// Also define uint32/16/8 for consistency.
typedef unsigned long long uint64;
typedef unsigned int uint32;
typedef unsigned short uint16;
typedef unsigned char uint8;
// Note: zircon max fd is 256.
// Some common_OS.h files know about this constant for RLIMIT_NOFILE.
const int kMaxFd = 250;
const int kFdLimit = 256;
const int kMaxThreads = 32;
const int kInPipeFd = kMaxFd - 1; // remapped from stdin
const int kOutPipeFd = kMaxFd - 2; // remapped from stdout
const int kCoverFd = kOutPipeFd - kMaxThreads;
const int kExtraCoverFd = kCoverFd - 1;
const int kMaxArgs = 9;
const int kCoverSize = 512 << 10;
const int kFailStatus = 67;
// Two approaches of dealing with kcov memory.
const int kCoverOptimizedCount = 8; // the max number of kcov instances
const int kCoverOptimizedPreMmap = 3; // this many will be mmapped inside main(), others - when needed.
const int kCoverDefaultCount = 6; // the max number of kcov instances when delayed kcov mmap is not available
// Logical error (e.g. invalid input program), use as an assert() alternative.
// If such error happens 10+ times in a row, it will be detected as a bug by the runner process.
// The runner will fail and syz-manager will create a bug for this.
// Note: err is used for bug deduplication, thus distinction between err (constant message)
// and msg (varying part).
static NORETURN void fail(const char* err);
static NORETURN PRINTF(2, 3) void failmsg(const char* err, const char* msg, ...);
// Just exit (e.g. due to temporal ENOMEM error).
static NORETURN PRINTF(1, 2) void exitf(const char* msg, ...);
static NORETURN void doexit(int status);
#if !GOOS_fuchsia
static NORETURN void doexit_thread(int status);
#endif
// Print debug output that is visible when running syz-manager/execprog with -debug flag.
// Debug output is supposed to be relatively high-level (syscalls executed, return values, timing, etc)
// and is intended mostly for end users. If you need to debug lower-level details, use debug_verbose
// function and temporary enable it in your build by changing #if 0 below.
// This function does not add \n at the end of msg as opposed to the previous functions.
static PRINTF(1, 2) void debug(const char* msg, ...);
void debug_dump_data(const char* data, int length);
#if 0
#define debug_verbose(...) debug(__VA_ARGS__)
#else
#define debug_verbose(...) (void)0
#endif
static void receive_execute();
static void reply_execute(uint32 status);
static void receive_handshake();
#if SYZ_EXECUTOR_USES_FORK_SERVER
static void SnapshotPrepareParent();
// Allocating (and forking) virtual memory for each executed process is expensive, so we only mmap
// the amount we might possibly need for the specific received prog.
const int kMaxOutputComparisons = 14 << 20; // executions with comparsions enabled are usually < 1% of all executions
const int kMaxOutputCoverage = 6 << 20; // coverage is needed in ~ up to 1/3 of all executions (depending on corpus rotation)
const int kMaxOutputSignal = 4 << 20;
const int kMinOutput = 256 << 10; // if we don't need to send signal, the output is rather short.
const int kInitialOutput = kMinOutput; // the minimal size to be allocated in the parent process
const int kMaxOutput = kMaxOutputComparisons;
#else
// We don't fork and allocate the memory only once, so prepare for the worst case.
const int kInitialOutput = 14 << 20;
const int kMaxOutput = kInitialOutput;
#endif
// For use with flatrpc bit flags.
template <typename T>
bool IsSet(T flags, T f)
{
return (flags & f) != T::NONE;
}
// TODO: allocate a smaller amount of memory in the parent once we merge the patches that enable
// prog execution with neither signal nor coverage. Likely 64kb will be enough in that case.
const uint32 kMaxCalls = 64;
struct alignas(8) OutputData {
std::atomic<uint32> size;
std::atomic<uint32> consumed;
std::atomic<uint32> completed;
std::atomic<uint32> num_calls;
std::atomic<flatbuffers::Offset<flatbuffers::Vector<uint8_t>>> result_offset;
struct {
// Call index in the test program (they may be out-of-order is some syscalls block).
int index;
// Offset of the CallInfo object in the output region.
flatbuffers::Offset<rpc::CallInfoRaw> offset;
} calls[kMaxCalls];
void Reset()
{
size.store(0, std::memory_order_relaxed);
consumed.store(0, std::memory_order_relaxed);
completed.store(0, std::memory_order_relaxed);
num_calls.store(0, std::memory_order_relaxed);
result_offset.store(0, std::memory_order_relaxed);
}
};
// ShmemAllocator/ShmemBuilder help to construct flatbuffers ExecResult reply message in shared memory.
//
// To avoid copying the reply (in particular coverage/signal/comparisons which may be large), the child
// process starts forming CallInfo objects as it handles completion of syscalls, then the top-most runner
// process uses these CallInfo to form an array of them, and adds ProgInfo object with a reference to the array.
// In order to make this possible, OutputData object is placed at the beginning of the shared memory region,
// and it records metadata required to start serialization in one process and continue later in another process.
//
// OutputData::size is the size of the whole shmem region that the child uses (it different size when coverage/
// comparisons are requested). Note that flatbuffers serialization happens from the end of the buffer backwards.
// OutputData::consumed records currently consumed amount memory in the shmem region so that the parent process
// can continue from that point.
// OutputData::completed records number of completed calls (entries in OutputData::calls arrays).
// Flatbuffers identifies everything using offsets in the buffer, OutputData::calls::offset records this offset
// for the call object so that we can use it in the parent process to construct the array of calls.
//
// FlatBufferBuilder generally grows the underlying buffer incrementally as necessary and copying data
// (std::vector style). We cannot do this in the shared memory since we have only a single region.
// To allow serialization into the shared memory region, ShmemBuilder passes initial buffer size which is equal
// to the overall shmem region size (minus OutputData header size) to FlatBufferBuilder, and the custom
// ShmemAllocator allocator. As the result, FlatBufferBuilder does exactly one allocation request
// to ShmemAllocator and never reallocates (if we overflow the buffer and FlatBufferBuilder does another request,
// ShmemAllocator will fail).
class ShmemAllocator : public flatbuffers::Allocator
{
public:
ShmemAllocator(void* buf, size_t size)
: buf_(buf),
size_(size)
{
}
private:
void* buf_;
size_t size_;
bool allocated_ = false;
uint8_t* allocate(size_t size) override
{
if (allocated_ || size != size_)
failmsg("bad allocate request", "allocated=%d size=%zu/%zu", allocated_, size_, size);
allocated_ = true;
return static_cast<uint8_t*>(buf_);
}
void deallocate(uint8_t* p, size_t size) override
{
if (!allocated_ || buf_ != p || size_ != size)
failmsg("bad deallocate request", "allocated=%d buf=%p/%p size=%zu/%zu",
allocated_, buf_, p, size_, size);
allocated_ = false;
}
uint8_t* reallocate_downward(uint8_t* old_p, size_t old_size,
size_t new_size, size_t in_use_back,
size_t in_use_front) override
{
fail("can't reallocate");
}
};
class ShmemBuilder : ShmemAllocator, public flatbuffers::FlatBufferBuilder
{
public:
ShmemBuilder(OutputData* data, size_t size, bool store_size)
: ShmemAllocator(data + 1, size - sizeof(*data)),
FlatBufferBuilder(size - sizeof(*data), this)
{
if (store_size)
data->size.store(size, std::memory_order_relaxed);
size_t consumed = data->consumed.load(std::memory_order_relaxed);
if (consumed >= size - sizeof(*data))
failmsg("ShmemBuilder: too large output offset", "size=%zd consumed=%zd", size, consumed);
if (consumed)
FlatBufferBuilder::buf_.make_space(consumed);
}
};
const int kInFd = 3;
const int kOutFd = 4;
const int kMaxSignalFd = 5;
const int kCoverFilterFd = 6;
static OutputData* output_data;
static std::optional<ShmemBuilder> output_builder;
static uint32 output_size;
static void mmap_output(uint32 size);
static uint32 hash(uint32 a);
static bool dedup(uint8 index, uint64 sig);
static uint64 start_time_ms = 0;
static bool flag_debug;
static bool flag_snapshot;
static bool flag_coverage;
static bool flag_read_only_coverage;
static bool flag_sandbox_none;
static bool flag_sandbox_setuid;
static bool flag_sandbox_namespace;
static bool flag_sandbox_android;
static bool flag_extra_coverage;
static bool flag_net_injection;
static bool flag_net_devices;
static bool flag_net_reset;
static bool flag_cgroups;
static bool flag_close_fds;
static bool flag_devlink_pci;
static bool flag_nic_vf;
static bool flag_vhci_injection;
static bool flag_wifi;
static bool flag_delay_kcov_mmap;
static bool flag_collect_cover;
static bool flag_collect_signal;
static bool flag_dedup_cover;
static bool flag_threaded;
// If true, then executor should write the comparisons data to fuzzer.
static bool flag_comparisons;
static uint64 request_id;
static rpc::RequestType request_type;
static uint64 all_call_signal;
static bool all_extra_signal;
// Tunable timeouts, received with execute_req.
static uint64 syscall_timeout_ms;
static uint64 program_timeout_ms;
static uint64 slowdown_scale;
// Can be used to disginguish whether we're at the initialization stage
// or we already execute programs.
static bool in_execute_one = false;
#define SYZ_EXECUTOR 1
#include "common.h"
const size_t kMaxInput = 4 << 20; // keep in sync with prog.ExecBufferSize
const size_t kMaxCommands = 1000; // prog package knows about this constant (prog.execMaxCommands)
const uint64 instr_eof = -1;
const uint64 instr_copyin = -2;
const uint64 instr_copyout = -3;
const uint64 instr_setprops = -4;
const uint64 arg_const = 0;
const uint64 arg_addr32 = 1;
const uint64 arg_addr64 = 2;
const uint64 arg_result = 3;
const uint64 arg_data = 4;
const uint64 arg_csum = 5;
const uint64 binary_format_native = 0;
const uint64 binary_format_bigendian = 1;
const uint64 binary_format_strdec = 2;
const uint64 binary_format_strhex = 3;
const uint64 binary_format_stroct = 4;
const uint64 no_copyout = -1;
static int running;
static uint32 completed;
static bool is_kernel_64_bit;
static bool use_cover_edges;
static uint8* input_data;
// Checksum kinds.
static const uint64 arg_csum_inet = 0;
// Checksum chunk kinds.
static const uint64 arg_csum_chunk_data = 0;
static const uint64 arg_csum_chunk_const = 1;
typedef intptr_t(SYSCALLAPI* syscall_t)(intptr_t, intptr_t, intptr_t, intptr_t, intptr_t, intptr_t, intptr_t, intptr_t, intptr_t);
struct call_t {
const char* name;
int sys_nr;
call_attrs_t attrs;
syscall_t call;
};
struct cover_t {
int fd;
uint32 size;
// mmap_alloc_ptr is the internal pointer to KCOV mapping, possibly with guard pages.
// It is only used to allocate/deallocate the buffer of mmap_alloc_size.
char* mmap_alloc_ptr;
uint32 mmap_alloc_size;
// data is the pointer to the kcov buffer containing the recorded PCs.
// data may differ from mmap_alloc_ptr.
char* data;
// data_size is set by cover_open(). This is the requested kcov buffer size.
uint32 data_size;
// data_end is simply data + data_size.
char* data_end;
// Currently collecting comparisons.
bool collect_comps;
// Note: On everything but darwin the first value in data is the count of
// recorded PCs, followed by the PCs. We therefore set data_offset to the
// size of one PC.
// On darwin data points to an instance of the ksancov_trace struct. Here we
// set data_offset to the offset between data and the structs 'pcs' member,
// which contains the PCs.
intptr_t data_offset;
// Note: On everything but darwin this is 0, as the PCs contained in data
// are already correct. XNUs KSANCOV API, however, chose to always squeeze
// PCs into 32 bit. To make the recorded PC fit, KSANCOV substracts a fixed
// offset (VM_MIN_KERNEL_ADDRESS for AMD64) and then truncates the result to
// uint32_t. We get this from the 'offset' member in ksancov_trace.
intptr_t pc_offset;
// The coverage buffer has overflowed and we have truncated coverage.
bool overflow;
// True if cover_enable() was called for this object.
bool enabled;
};
struct thread_t {
int id;
bool created;
event_t ready;
event_t done;
uint8* copyout_pos;
uint64 copyout_index;
bool executing;
int call_index;
int call_num;
int num_args;
intptr_t args[kMaxArgs];
call_props_t call_props;
intptr_t res;
uint32 reserrno;
bool fault_injected;
cover_t cov;
bool soft_fail_state;
};
static thread_t threads[kMaxThreads];
static thread_t* last_scheduled;
// Threads use this variable to access information about themselves.
static __thread struct thread_t* current_thread;
static cover_t extra_cov;
struct res_t {
bool executed;
uint64 val;
};
static res_t results[kMaxCommands];
const uint64 kInMagic = 0xbadc0ffeebadface;
struct handshake_req {
uint64 magic;
bool use_cover_edges;
bool is_kernel_64_bit;
rpc::ExecEnv flags;
uint64 pid;
uint64 sandbox_arg;
uint64 syscall_timeout_ms;
uint64 program_timeout_ms;
uint64 slowdown_scale;
};
struct execute_req {
uint64 magic;
uint64 id;
rpc::RequestType type;
uint64 exec_flags;
uint64 all_call_signal;
bool all_extra_signal;
};
struct execute_reply {
uint32 magic;
uint32 done;
uint32 status;
};
enum {
KCOV_CMP_CONST = 1,
KCOV_CMP_SIZE1 = 0,
KCOV_CMP_SIZE2 = 2,
KCOV_CMP_SIZE4 = 4,
KCOV_CMP_SIZE8 = 6,
KCOV_CMP_SIZE_MASK = 6,
};
struct kcov_comparison_t {
// Note: comparisons are always 64-bits regardless of kernel bitness.
uint64 type;
uint64 arg1;
uint64 arg2;
uint64 pc;
};
typedef char kcov_comparison_size[sizeof(kcov_comparison_t) == 4 * sizeof(uint64) ? 1 : -1];
struct feature_t {
rpc::Feature id;
const char* (*setup)();
};
static thread_t* schedule_call(int call_index, int call_num, uint64 copyout_index, uint64 num_args, uint64* args, uint8* pos, call_props_t call_props);
static void handle_completion(thread_t* th);
static void copyout_call_results(thread_t* th);
static void write_call_output(thread_t* th, bool finished);
static void write_extra_output();
static void execute_call(thread_t* th);
static void thread_create(thread_t* th, int id, bool need_coverage);
static void thread_mmap_cover(thread_t* th);
static void* worker_thread(void* arg);
static uint64 read_input(uint8** input_posp, bool peek = false);
static uint64 read_arg(uint8** input_posp);
static uint64 read_const_arg(uint8** input_posp, uint64* size_p, uint64* bf, uint64* bf_off_p, uint64* bf_len_p);
static uint64 read_result(uint8** input_posp);
static uint64 swap(uint64 v, uint64 size, uint64 bf);
static void copyin(char* addr, uint64 val, uint64 size, uint64 bf, uint64 bf_off, uint64 bf_len);
static bool copyout(char* addr, uint64 size, uint64* res);
static void setup_control_pipes();
static bool coverage_filter(uint64 pc);
static rpc::ComparisonRaw convert(const kcov_comparison_t& cmp);
static flatbuffers::span<uint8_t> finish_output(OutputData* output, int proc_id, uint64 req_id, uint32 num_calls,
uint64 elapsed, uint64 freshness, uint32 status, bool hanged,
const std::vector<uint8_t>* process_output);
static void parse_execute(const execute_req& req);
static void parse_handshake(const handshake_req& req);
static void mmap_input();
#include "syscalls.h"
#if GOOS_linux
#ifndef MAP_FIXED_NOREPLACE
#define MAP_FIXED_NOREPLACE 0x100000
#endif
#define MAP_FIXED_EXCLUSIVE MAP_FIXED_NOREPLACE
#elif GOOS_freebsd
#define MAP_FIXED_EXCLUSIVE (MAP_FIXED | MAP_EXCL)
#else
#define MAP_FIXED_EXCLUSIVE MAP_FIXED // The check is not supported.
#endif
#if GOOS_linux
#include "executor_linux.h"
#elif GOOS_fuchsia
#include "executor_fuchsia.h"
#elif GOOS_freebsd || GOOS_netbsd || GOOS_openbsd
#include "executor_bsd.h"
#elif GOOS_darwin
#include "executor_darwin.h"
#elif GOOS_windows
#include "executor_windows.h"
#elif GOOS_test
#include "executor_test.h"
#else
#error "unknown OS"
#endif
class CoverAccessScope final
{
public:
CoverAccessScope(cover_t* cov)
: cov_(cov)
{
// CoverAccessScope must not be used recursively b/c on Linux pkeys protection is global,
// so cover_protect for one cov overrides previous cover_unprotect for another cov.
if (used_)
fail("recursion in CoverAccessScope");
used_ = true;
if (flag_coverage)
cover_unprotect(cov_);
}
~CoverAccessScope()
{
if (flag_coverage)
cover_protect(cov_);
used_ = false;
}
private:
cover_t* const cov_;
static bool used_;
CoverAccessScope(const CoverAccessScope&) = delete;
CoverAccessScope& operator=(const CoverAccessScope&) = delete;
};
bool CoverAccessScope::used_;
#if !SYZ_HAVE_FEATURES
static feature_t features[] = {};
#endif
#include "shmem.h"
#include "conn.h"
#include "cover_filter.h"
#include "files.h"
#include "subprocess.h"
#include "snapshot.h"
#include "executor_runner.h"
#include "test.h"
static std::optional<CoverFilter> max_signal;
static std::optional<CoverFilter> cover_filter;
#if SYZ_HAVE_SANDBOX_ANDROID
static uint64 sandbox_arg = 0;
#endif
int main(int argc, char** argv)
{
if (argc == 1) {
fprintf(stderr, "no command");
return 1;
}
if (strcmp(argv[1], "runner") == 0) {
runner(argv, argc);
fail("runner returned");
}
if (strcmp(argv[1], "leak") == 0) {
#if SYZ_HAVE_LEAK_CHECK
check_leaks(argv + 2, argc - 2);
#else
fail("leak checking is not implemented");
#endif
return 0;
}
if (strcmp(argv[1], "test") == 0)
return run_tests(argc == 3 ? argv[2] : nullptr);
if (strcmp(argv[1], "exec") != 0) {
fprintf(stderr, "unknown command");
return 1;
}
start_time_ms = current_time_ms();
os_init(argc, argv, (char*)SYZ_DATA_OFFSET, SYZ_NUM_PAGES * SYZ_PAGE_SIZE);
use_temporary_dir();
install_segv_handler();
current_thread = &threads[0];
if (argc > 2 && strcmp(argv[2], "snapshot") == 0) {
SnapshotSetup(argv, argc);
} else {
mmap_input();
mmap_output(kInitialOutput);
// Prevent test programs to mess with these fds.
// Due to races in collider mode, a program can e.g. ftruncate one of these fds,
// which will cause fuzzer to crash.
close(kInFd);
#if !SYZ_EXECUTOR_USES_FORK_SERVER
// For SYZ_EXECUTOR_USES_FORK_SERVER, close(kOutFd) is invoked in the forked child,
// after the program has been received.
close(kOutFd);
#endif
if (fcntl(kMaxSignalFd, F_GETFD) != -1) {
// Use random addresses for coverage filters to not collide with output_data.
max_signal.emplace(kMaxSignalFd, reinterpret_cast<void*>(0x110c230000ull));
close(kMaxSignalFd);
}
if (fcntl(kCoverFilterFd, F_GETFD) != -1) {
cover_filter.emplace(kCoverFilterFd, reinterpret_cast<void*>(0x110f230000ull));
close(kCoverFilterFd);
}
setup_control_pipes();
receive_handshake();
#if !SYZ_EXECUTOR_USES_FORK_SERVER
// We receive/reply handshake when fork server is disabled just to simplify runner logic.
// It's a bit suboptimal, but no fork server is much slower anyway.
reply_execute(0);
receive_execute();
#endif
}
if (flag_coverage) {
int create_count = kCoverDefaultCount, mmap_count = create_count;
if (flag_delay_kcov_mmap) {
create_count = kCoverOptimizedCount;
mmap_count = kCoverOptimizedPreMmap;
}
if (create_count > kMaxThreads)
create_count = kMaxThreads;
for (int i = 0; i < create_count; i++) {
threads[i].cov.fd = kCoverFd + i;
cover_open(&threads[i].cov, false);
if (i < mmap_count) {
// Pre-mmap coverage collection for some threads. This should be enough for almost
// all programs, for the remaning few ones coverage will be set up when it's needed.
thread_mmap_cover(&threads[i]);
}
}
extra_cov.fd = kExtraCoverFd;
cover_open(&extra_cov, true);
cover_mmap(&extra_cov);
cover_protect(&extra_cov);
if (flag_extra_coverage) {
// Don't enable comps because we don't use them in the fuzzer yet.
cover_enable(&extra_cov, false, true);
}
}
int status = 0;
if (flag_sandbox_none)
status = do_sandbox_none();
#if SYZ_HAVE_SANDBOX_SETUID
else if (flag_sandbox_setuid)
status = do_sandbox_setuid();
#endif
#if SYZ_HAVE_SANDBOX_NAMESPACE
else if (flag_sandbox_namespace)
status = do_sandbox_namespace();
#endif
#if SYZ_HAVE_SANDBOX_ANDROID
else if (flag_sandbox_android)
status = do_sandbox_android(sandbox_arg);
#endif
else
fail("unknown sandbox type");
#if SYZ_EXECUTOR_USES_FORK_SERVER
fprintf(stderr, "loop exited with status %d\n", status);
// If an external sandbox process wraps executor, the out pipe will be closed
// before the sandbox process exits this will make ipc package kill the sandbox.
// As the result sandbox process will exit with exit status 9 instead of the executor
// exit status (notably kFailStatus). So we duplicate the exit status on the pipe.
reply_execute(status);
doexit(status);
// Unreachable.
return 1;
#else
reply_execute(status);
return status;
#endif
}
static uint32* input_base_address()
{
if (kAddressSanitizer) {
// ASan conflicts with -static, so we end up having a dynamically linked syz-executor binary.
// It's often the case that the libraries are mapped shortly after 0x7f0000000000, so we cannot
// blindly set some HighMemory address and hope it's free.
// Since we only run relatively safe (or fake) syscalls under tests, it should be fine to
// just use whatever address mmap() returns us.
return 0;
}
// It's the first time we map output region - generate its location.
// The output region is the only thing in executor process for which consistency matters.
// If it is corrupted ipc package will fail to parse its contents and panic.
// But fuzzer constantly invents new ways of how to corrupt the region,
// so we map the region at a (hopefully) hard to guess address with random offset,
// surrounded by unmapped pages.
// The address chosen must also work on 32-bit kernels with 1GB user address space.
const uint64 kOutputBase = 0x1b2bc20000ull;
return (uint32*)(kOutputBase + (1 << 20) * (getpid() % 128));
}
static void mmap_input()
{
uint32* mmap_at = input_base_address();
int flags = MAP_SHARED;
if (mmap_at != 0)
// If we map at a specific address, ensure it's not overlapping with anything else.
flags = flags | MAP_FIXED_EXCLUSIVE;
void* result = mmap(mmap_at, kMaxInput, PROT_READ, flags, kInFd, 0);
if (result == MAP_FAILED)
fail("mmap of input file failed");
input_data = static_cast<uint8*>(result);
}
static uint32* output_base_address()
{
if (kAddressSanitizer) {
// See the comment in input_base_address();
return 0;
}
if (output_data != NULL) {
// If output_data was already mapped, use the old base address
// since we could be extending the area from a different pid:
// realloc_output_data() may be called from a fork, which would cause
// input_base_address() to return a different address.
return (uint32*)output_data;
}
// Leave some unmmapped area after the input data.
return input_base_address() + kMaxInput + SYZ_PAGE_SIZE;
}
// This method can be invoked as many times as one likes - MMAP_FIXED can overwrite the previous
// mapping without any problems. The only precondition - kOutFd must not be closed.
static void mmap_output(uint32 size)
{
if (size <= output_size)
return;
if (size % SYZ_PAGE_SIZE != 0)
failmsg("trying to mmap output area that is not divisible by page size", "page=%d,area=%d", SYZ_PAGE_SIZE, size);
uint32* mmap_at = output_base_address();
int flags = MAP_SHARED;
if (mmap_at == NULL) {
// We map at an address chosen by the kernel, so if there was any previous mapping, just unmap it.
if (output_data != NULL) {
int ret = munmap(output_data, output_size);
if (ret != 0)
fail("munmap failed");
output_size = 0;
}
} else {
// We are possibly expanding the mmapped region. Adjust the parameters to avoid mmapping already
// mmapped area as much as possible.
// There exists a mremap call that could have helped, but it's purely Linux-specific.
mmap_at = (uint32*)((char*)(mmap_at) + output_size);
// Ensure we don't overwrite anything.
flags = flags | MAP_FIXED_EXCLUSIVE;
}
void* result = mmap(mmap_at, size - output_size, PROT_READ | PROT_WRITE, flags, kOutFd, output_size);
if (result == MAP_FAILED || (mmap_at && result != mmap_at))
failmsg("mmap of output file failed", "want %p, got %p", mmap_at, result);
if (output_size == 0)
output_data = static_cast<OutputData*>(result);
output_size = size;
}
void setup_control_pipes()
{
if (dup2(0, kInPipeFd) < 0)
fail("dup2(0, kInPipeFd) failed");
if (dup2(1, kOutPipeFd) < 0)
fail("dup2(1, kOutPipeFd) failed");
if (dup2(2, 1) < 0)
fail("dup2(2, 1) failed");
// We used to close(0), but now we dup stderr to stdin to keep fd numbers
// stable across executor and C programs generated by pkg/csource.
if (dup2(2, 0) < 0)
fail("dup2(2, 0) failed");
}
void receive_handshake()
{
handshake_req req = {};
ssize_t n = read(kInPipeFd, &req, sizeof(req));
if (n != sizeof(req))
failmsg("handshake read failed", "read=%zu", n);
parse_handshake(req);
}
void parse_handshake(const handshake_req& req)
{
if (req.magic != kInMagic)
failmsg("bad handshake magic", "magic=0x%llx", req.magic);
#if SYZ_HAVE_SANDBOX_ANDROID
sandbox_arg = req.sandbox_arg;
#endif
is_kernel_64_bit = req.is_kernel_64_bit;
use_cover_edges = req.use_cover_edges;
procid = req.pid;
syscall_timeout_ms = req.syscall_timeout_ms;
program_timeout_ms = req.program_timeout_ms;
slowdown_scale = req.slowdown_scale;
flag_debug = (bool)(req.flags & rpc::ExecEnv::Debug);
flag_coverage = (bool)(req.flags & rpc::ExecEnv::Signal);
flag_read_only_coverage = (bool)(req.flags & rpc::ExecEnv::ReadOnlyCoverage);
flag_sandbox_none = (bool)(req.flags & rpc::ExecEnv::SandboxNone);
flag_sandbox_setuid = (bool)(req.flags & rpc::ExecEnv::SandboxSetuid);
flag_sandbox_namespace = (bool)(req.flags & rpc::ExecEnv::SandboxNamespace);
flag_sandbox_android = (bool)(req.flags & rpc::ExecEnv::SandboxAndroid);
flag_extra_coverage = (bool)(req.flags & rpc::ExecEnv::ExtraCover);
flag_net_injection = (bool)(req.flags & rpc::ExecEnv::EnableTun);
flag_net_devices = (bool)(req.flags & rpc::ExecEnv::EnableNetDev);
flag_net_reset = (bool)(req.flags & rpc::ExecEnv::EnableNetReset);
flag_cgroups = (bool)(req.flags & rpc::ExecEnv::EnableCgroups);
flag_close_fds = (bool)(req.flags & rpc::ExecEnv::EnableCloseFds);
flag_devlink_pci = (bool)(req.flags & rpc::ExecEnv::EnableDevlinkPCI);
flag_vhci_injection = (bool)(req.flags & rpc::ExecEnv::EnableVhciInjection);
flag_wifi = (bool)(req.flags & rpc::ExecEnv::EnableWifi);
flag_delay_kcov_mmap = (bool)(req.flags & rpc::ExecEnv::DelayKcovMmap);
flag_nic_vf = (bool)(req.flags & rpc::ExecEnv::EnableNicVF);
}
void receive_execute()
{
execute_req req = {};
ssize_t n = 0;
while ((n = read(kInPipeFd, &req, sizeof(req))) == -1 && errno == EINTR)
;
if (n != (ssize_t)sizeof(req))
failmsg("control pipe read failed", "read=%zd want=%zd", n, sizeof(req));
parse_execute(req);
}
void parse_execute(const execute_req& req)
{
request_id = req.id;
request_type = req.type;
flag_collect_signal = req.exec_flags & (uint64)rpc::ExecFlag::CollectSignal;
flag_collect_cover = req.exec_flags & (uint64)rpc::ExecFlag::CollectCover;
flag_dedup_cover = req.exec_flags & (uint64)rpc::ExecFlag::DedupCover;
flag_comparisons = req.exec_flags & (uint64)rpc::ExecFlag::CollectComps;
flag_threaded = req.exec_flags & (uint64)rpc::ExecFlag::Threaded;
all_call_signal = req.all_call_signal;
all_extra_signal = req.all_extra_signal;
debug("[%llums] exec opts: reqid=%llu type=%llu procid=%llu threaded=%d cover=%d comps=%d dedup=%d signal=%d "
" sandbox=%d/%d/%d/%d timeouts=%llu/%llu/%llu kernel_64_bit=%d\n",
current_time_ms() - start_time_ms, request_id, (uint64)request_type, procid, flag_threaded, flag_collect_cover,
flag_comparisons, flag_dedup_cover, flag_collect_signal, flag_sandbox_none, flag_sandbox_setuid,
flag_sandbox_namespace, flag_sandbox_android, syscall_timeout_ms, program_timeout_ms, slowdown_scale,
is_kernel_64_bit);
if (syscall_timeout_ms == 0 || program_timeout_ms <= syscall_timeout_ms || slowdown_scale == 0)
failmsg("bad timeouts", "syscall=%llu, program=%llu, scale=%llu",
syscall_timeout_ms, program_timeout_ms, slowdown_scale);
}
bool cover_collection_required()
{
return flag_coverage && (flag_collect_signal || flag_collect_cover || flag_comparisons);
}
void reply_execute(uint32 status)
{
if (flag_snapshot)
SnapshotDone(status == kFailStatus);
if (write(kOutPipeFd, &status, sizeof(status)) != sizeof(status))
fail("control pipe write failed");
}
void realloc_output_data()
{
#if SYZ_EXECUTOR_USES_FORK_SERVER
if (flag_comparisons)
mmap_output(kMaxOutputComparisons);
else if (flag_collect_cover)
mmap_output(kMaxOutputCoverage);
else if (flag_collect_signal)
mmap_output(kMaxOutputSignal);
if (close(kOutFd) < 0)
fail("failed to close kOutFd");
#endif
}
void execute_glob()
{
const char* pattern = (const char*)input_data;
const auto& files = Glob(pattern);
size_t size = 0;
for (const auto& file : files)
size += file.size() + 1;
mmap_output(kMaxOutput);
ShmemBuilder fbb(output_data, kMaxOutput, true);
uint8_t* pos = nullptr;
auto off = fbb.CreateUninitializedVector(size, &pos);
for (const auto& file : files) {
memcpy(pos, file.c_str(), file.size() + 1);
pos += file.size() + 1;
}
output_data->consumed.store(fbb.GetSize(), std::memory_order_release);
output_data->result_offset.store(off, std::memory_order_release);
}
// execute_one executes program stored in input_data.
void execute_one()
{
if (request_type == rpc::RequestType::Glob) {
execute_glob();
return;
}
if (request_type != rpc::RequestType::Program)
failmsg("bad request type", "type=%llu", (uint64)request_type);
in_execute_one = true;
#if GOOS_linux
char buf[64];
// Linux TASK_COMM_LEN is only 16, so the name needs to be compact.
snprintf(buf, sizeof(buf), "syz.%llu.%llu", procid, request_id);
prctl(PR_SET_NAME, buf);
#endif
if (flag_snapshot)
SnapshotStart();
else
realloc_output_data();
// Output buffer may be pkey-protected in snapshot mode, so don't write the output size
// (it's fixed and known anyway).
output_builder.emplace(output_data, output_size, !flag_snapshot);
uint64 start = current_time_ms();
uint8* input_pos = input_data;
if (cover_collection_required()) {
if (!flag_threaded)
cover_enable(&threads[0].cov, flag_comparisons, false);
if (flag_extra_coverage)
cover_reset(&extra_cov);
}
int call_index = 0;
uint64 prog_extra_timeout = 0;
uint64 prog_extra_cover_timeout = 0;
call_props_t call_props;
memset(&call_props, 0, sizeof(call_props));
read_input(&input_pos); // total number of calls
for (;;) {
uint64 call_num = read_input(&input_pos);
if (call_num == instr_eof)
break;
if (call_num == instr_copyin) {
char* addr = (char*)(read_input(&input_pos) + SYZ_DATA_OFFSET);
uint64 typ = read_input(&input_pos);
switch (typ) {
case arg_const: {
uint64 size, bf, bf_off, bf_len;
uint64 arg = read_const_arg(&input_pos, &size, &bf, &bf_off, &bf_len);
copyin(addr, arg, size, bf, bf_off, bf_len);
break;
}
case arg_addr32:
case arg_addr64: {
uint64 val = read_input(&input_pos) + SYZ_DATA_OFFSET;
if (typ == arg_addr32)
NONFAILING(*(uint32*)addr = val);
else
NONFAILING(*(uint64*)addr = val);
break;
}
case arg_result: {
uint64 meta = read_input(&input_pos);
uint64 size = meta & 0xff;
uint64 bf = meta >> 8;
uint64 val = read_result(&input_pos);
copyin(addr, val, size, bf, 0, 0);
break;
}
case arg_data: {
uint64 size = read_input(&input_pos);
size &= ~(1ull << 63); // readable flag
if (input_pos + size > input_data + kMaxInput)
fail("data arg overflow");
NONFAILING(memcpy(addr, input_pos, size));
input_pos += size;
break;
}
case arg_csum: {
debug_verbose("checksum found at %p\n", addr);
uint64 size = read_input(&input_pos);
char* csum_addr = addr;
uint64 csum_kind = read_input(&input_pos);
switch (csum_kind) {
case arg_csum_inet: {
if (size != 2)
failmsg("bag inet checksum size", "size=%llu", size);
debug_verbose("calculating checksum for %p\n", csum_addr);
struct csum_inet csum;
csum_inet_init(&csum);
uint64 chunks_num = read_input(&input_pos);
uint64 chunk;
for (chunk = 0; chunk < chunks_num; chunk++) {
uint64 chunk_kind = read_input(&input_pos);
uint64 chunk_value = read_input(&input_pos);
uint64 chunk_size = read_input(&input_pos);
switch (chunk_kind) {
case arg_csum_chunk_data:
chunk_value += SYZ_DATA_OFFSET;
debug_verbose("#%lld: data chunk, addr: %llx, size: %llu\n",
chunk, chunk_value, chunk_size);
NONFAILING(csum_inet_update(&csum, (const uint8*)chunk_value, chunk_size));
break;
case arg_csum_chunk_const:
if (chunk_size != 2 && chunk_size != 4 && chunk_size != 8)
failmsg("bad checksum const chunk size", "size=%lld", chunk_size);
// Here we assume that const values come to us big endian.
debug_verbose("#%lld: const chunk, value: %llx, size: %llu\n",
chunk, chunk_value, chunk_size);
csum_inet_update(&csum, (const uint8*)&chunk_value, chunk_size);
break;
default:
failmsg("bad checksum chunk kind", "kind=%llu", chunk_kind);
}
}
uint16 csum_value = csum_inet_digest(&csum);
debug_verbose("writing inet checksum %hx to %p\n", csum_value, csum_addr);
copyin(csum_addr, csum_value, 2, binary_format_native, 0, 0);
break;
}
default:
failmsg("bad checksum kind", "kind=%llu", csum_kind);
}
break;
}
default:
failmsg("bad argument type", "type=%llu", typ);
}
continue;
}
if (call_num == instr_copyout) {
read_input(&input_pos); // index
read_input(&input_pos); // addr
read_input(&input_pos); // size
// The copyout will happen when/if the call completes.
continue;
}
if (call_num == instr_setprops) {
read_call_props_t(call_props, read_input(&input_pos, false));
continue;
}
// Normal syscall.
if (call_num >= ARRAY_SIZE(syscalls))
failmsg("invalid syscall number", "call_num=%llu", call_num);
const call_t* call = &syscalls[call_num];
if (prog_extra_timeout < call->attrs.prog_timeout)
prog_extra_timeout = call->attrs.prog_timeout * slowdown_scale;
if (call->attrs.remote_cover)
prog_extra_cover_timeout = 500 * slowdown_scale; // 500 ms
uint64 copyout_index = read_input(&input_pos);
uint64 num_args = read_input(&input_pos);
if (num_args > kMaxArgs)
failmsg("command has bad number of arguments", "args=%llu", num_args);
uint64 args[kMaxArgs] = {};
for (uint64 i = 0; i < num_args; i++)
args[i] = read_arg(&input_pos);
for (uint64 i = num_args; i < kMaxArgs; i++)
args[i] = 0;
thread_t* th = schedule_call(call_index++, call_num, copyout_index,
num_args, args, input_pos, call_props);
if (call_props.async && flag_threaded) {
// Don't wait for an async call to finish. We'll wait at the end.
// If we're not in the threaded mode, just ignore the async flag - during repro simplification syzkaller
// will anyway try to make it non-threaded.
} else if (flag_threaded) {
// Wait for call completion.
uint64 timeout_ms = syscall_timeout_ms + call->attrs.timeout * slowdown_scale;
// This is because of printing pre/post call. Ideally we print everything in the main thread
// and then remove this (would also avoid intermixed output).
if (flag_debug && timeout_ms < 1000)
timeout_ms = 1000;
if (event_timedwait(&th->done, timeout_ms))
handle_completion(th);
// Check if any of previous calls have completed.
for (int i = 0; i < kMaxThreads; i++) {
th = &threads[i];
if (th->executing && event_isset(&th->done))
handle_completion(th);
}
} else {
// Execute directly.
if (th != &threads[0])
fail("using non-main thread in non-thread mode");
event_reset(&th->ready);
execute_call(th);
event_set(&th->done);
handle_completion(th);
}
memset(&call_props, 0, sizeof(call_props));
}
if (running > 0) {
// Give unfinished syscalls some additional time.
last_scheduled = 0;
uint64 wait_start = current_time_ms();
uint64 wait_end = wait_start + 2 * syscall_timeout_ms;
wait_end = std::max(wait_end, start + program_timeout_ms / 6);
wait_end = std::max(wait_end, wait_start + prog_extra_timeout);
while (running > 0 && current_time_ms() <= wait_end) {
sleep_ms(1 * slowdown_scale);
for (int i = 0; i < kMaxThreads; i++) {
thread_t* th = &threads[i];
if (th->executing && event_isset(&th->done))
handle_completion(th);
}
}
// Write output coverage for unfinished calls.
if (running > 0) {
for (int i = 0; i < kMaxThreads; i++) {
thread_t* th = &threads[i];
if (th->executing) {
if (cover_collection_required())
cover_collect(&th->cov);
write_call_output(th, false);
}
}
}
}
#if SYZ_HAVE_CLOSE_FDS
close_fds();
#endif
write_extra_output();
if (flag_extra_coverage) {
// Check for new extra coverage in small intervals to avoid situation
// that we were killed on timeout before we write any.
// Check for extra coverage is very cheap, effectively a memory load.
const uint64 kSleepMs = 100;
for (uint64 i = 0; i < prog_extra_cover_timeout / kSleepMs &&
output_data->completed.load(std::memory_order_relaxed) < kMaxCalls;
i++) {
sleep_ms(kSleepMs);
write_extra_output();
}
}
}
thread_t* schedule_call(int call_index, int call_num, uint64 copyout_index, uint64 num_args, uint64* args, uint8* pos, call_props_t call_props)
{
// Find a spare thread to execute the call.
int i = 0;
for (; i < kMaxThreads; i++) {
thread_t* th = &threads[i];
if (!th->created)
thread_create(th, i, cover_collection_required());
if (event_isset(&th->done)) {
if (th->executing)
handle_completion(th);
break;
}
}
if (i == kMaxThreads)
exitf("out of threads");
thread_t* th = &threads[i];
if (event_isset(&th->ready) || !event_isset(&th->done) || th->executing)
exitf("bad thread state in schedule: ready=%d done=%d executing=%d",
event_isset(&th->ready), event_isset(&th->done), th->executing);
last_scheduled = th;
th->copyout_pos = pos;
th->copyout_index = copyout_index;
event_reset(&th->done);
// We do this both right before execute_syscall in the thread and here because:
// the former is useful to reset all unrelated coverage from our syscalls (e.g. futex in event_wait),
// while the reset here is useful to avoid the following scenario that the fuzzer was able to trigger.
// If the test program contains seccomp syscall that kills the worker thread on the next syscall,
// then it won't receive this next syscall and won't do cover_reset. If we are collecting comparions
// then we've already transformed comparison data from the previous syscall into rpc::ComparisonRaw
// in write_comparisons. That data is still in the buffer. The first word of rpc::ComparisonRaw is PC
// which overlaps with comparison type in kernel exposed records. As the result write_comparisons
// that will try to write out data from unfinished syscalls will see these rpc::ComparisonRaw records,
// mis-interpret PC as type, and fail as: SYZFAIL: invalid kcov comp type (type=ffffffff8100b4e0).
if (flag_coverage)
cover_reset(&th->cov);
th->executing = true;
th->call_index = call_index;
th->call_num = call_num;
th->num_args = num_args;
th->call_props = call_props;
for (int i = 0; i < kMaxArgs; i++)
th->args[i] = args[i];
event_set(&th->ready);
running++;
return th;
}
template <typename cover_data_t>
uint32 write_signal(flatbuffers::FlatBufferBuilder& fbb, int index, cover_t* cov, bool all)
{
// Write out feedback signals.
// Currently it is code edges computed as xor of two subsequent basic block PCs.
fbb.StartVector(0, sizeof(uint64));
cover_data_t* cover_data = (cover_data_t*)(cov->data + cov->data_offset);
if ((char*)(cover_data + cov->size) > cov->data_end)
failmsg("too much cover", "cov=%u", cov->size);
uint32 nsig = 0;
cover_data_t prev_pc = 0;
bool prev_filter = true;
for (uint32 i = 0; i < cov->size; i++) {
cover_data_t pc = cover_data[i] + cov->pc_offset;
uint64 sig = pc;
if (use_cover_edges) {
// Only hash the lower 12 bits so the hash is independent of any module offsets.
const uint64 mask = (1 << 12) - 1;
sig ^= hash(prev_pc & mask) & mask;
}
bool filter = coverage_filter(pc);
// Ignore the edge only if both current and previous PCs are filtered out
// to capture all incoming and outcoming edges into the interesting code.
bool ignore = !filter && !prev_filter;
prev_pc = pc;
prev_filter = filter;
if (ignore || dedup(index, sig))
continue;
if (!all && max_signal && max_signal->Contains(sig))
continue;
fbb.PushElement(uint64(sig));
nsig++;
}
return fbb.EndVector(nsig);
}
template <typename cover_data_t>
uint32 write_cover(flatbuffers::FlatBufferBuilder& fbb, cover_t* cov)
{
uint32 cover_size = cov->size;
cover_data_t* cover_data = (cover_data_t*)(cov->data + cov->data_offset);
if (flag_dedup_cover) {
cover_data_t* end = cover_data + cover_size;
std::sort(cover_data, end);
cover_size = std::unique(cover_data, end) - cover_data;
}
fbb.StartVector(cover_size, sizeof(uint64));
// Flatbuffer arrays are written backwards, so reverse the order on our side as well.
for (uint32 i = 0; i < cover_size; i++)
fbb.PushElement(uint64(cover_data[cover_size - i - 1] + cov->pc_offset));
return fbb.EndVector(cover_size);
}
uint32 write_comparisons(flatbuffers::FlatBufferBuilder& fbb, cover_t* cov)
{
// Collect only the comparisons
uint64 ncomps = *(uint64_t*)cov->data;
kcov_comparison_t* cov_start = (kcov_comparison_t*)(cov->data + sizeof(uint64));
if ((char*)(cov_start + ncomps) > cov->data_end)
failmsg("too many comparisons", "ncomps=%llu", ncomps);
cov->overflow = ((char*)(cov_start + ncomps + 1) > cov->data_end);
rpc::ComparisonRaw* start = (rpc::ComparisonRaw*)cov_start;
rpc::ComparisonRaw* end = start;
// We will convert kcov_comparison_t to ComparisonRaw inplace.
static_assert(sizeof(kcov_comparison_t) >= sizeof(rpc::ComparisonRaw));
for (uint32 i = 0; i < ncomps; i++) {
auto raw = convert(cov_start[i]);
if (!raw.pc())
continue;
*end++ = raw;
}
std::sort(start, end, [](rpc::ComparisonRaw a, rpc::ComparisonRaw b) -> bool {
if (a.pc() != b.pc())
return a.pc() < b.pc();
if (a.op1() != b.op1())
return a.op1() < b.op1();
return a.op2() < b.op2();
});
ncomps = std::unique(start, end, [](rpc::ComparisonRaw a, rpc::ComparisonRaw b) -> bool {
return a.pc() == b.pc() && a.op1() == b.op1() && a.op2() == b.op2();
}) -
start;
return fbb.CreateVectorOfStructs(start, ncomps).o;
}
bool coverage_filter(uint64 pc)
{
if (!cover_filter)
return true;
return cover_filter->Contains(pc);
}
void handle_completion(thread_t* th)
{
if (event_isset(&th->ready) || !event_isset(&th->done) || !th->executing)
exitf("bad thread state in completion: ready=%d done=%d executing=%d",
event_isset(&th->ready), event_isset(&th->done), th->executing);
if (th->res != (intptr_t)-1)
copyout_call_results(th);
write_call_output(th, true);
write_extra_output();
th->executing = false;
running--;
if (running < 0) {
// This fires periodically for the past 2 years (see issue #502).
fprintf(stderr, "running=%d completed=%d flag_threaded=%d current=%d\n",
running, completed, flag_threaded, th->id);
for (int i = 0; i < kMaxThreads; i++) {
thread_t* th1 = &threads[i];
fprintf(stderr, "th #%2d: created=%d executing=%d"
" ready=%d done=%d call_index=%d res=%lld reserrno=%d\n",
i, th1->created, th1->executing,
event_isset(&th1->ready), event_isset(&th1->done),
th1->call_index, (uint64)th1->res, th1->reserrno);
}
exitf("negative running");
}
}
void copyout_call_results(thread_t* th)
{
if (th->copyout_index != no_copyout) {
if (th->copyout_index >= kMaxCommands)
failmsg("result overflows kMaxCommands", "index=%lld", th->copyout_index);
results[th->copyout_index].executed = true;
results[th->copyout_index].val = th->res;
}
for (bool done = false; !done;) {
uint64 instr = read_input(&th->copyout_pos);
switch (instr) {
case instr_copyout: {
uint64 index = read_input(&th->copyout_pos);
if (index >= kMaxCommands)
failmsg("result overflows kMaxCommands", "index=%lld", index);
char* addr = (char*)(read_input(&th->copyout_pos) + SYZ_DATA_OFFSET);
uint64 size = read_input(&th->copyout_pos);
uint64 val = 0;
if (copyout(addr, size, &val)) {
results[index].executed = true;
results[index].val = val;
}
debug_verbose("copyout 0x%llx from %p\n", val, addr);
break;
}
default:
done = true;
break;
}
}
}
void write_output(int index, cover_t* cov, rpc::CallFlag flags, uint32 error, bool all_signal)
{
CoverAccessScope scope(cov);
auto& fbb = *output_builder;
const uint32 start_size = output_builder->GetSize();
(void)start_size;
uint32 signal_off = 0;
uint32 cover_off = 0;
uint32 comps_off = 0;
if (flag_comparisons) {
comps_off = write_comparisons(fbb, cov);
} else {
if (flag_collect_signal) {
if (is_kernel_64_bit)
signal_off = write_signal<uint64>(fbb, index, cov, all_signal);
else
signal_off = write_signal<uint32>(fbb, index, cov, all_signal);
}
if (flag_collect_cover) {
if (is_kernel_64_bit)
cover_off = write_cover<uint64>(fbb, cov);
else
cover_off = write_cover<uint32>(fbb, cov);
}
}
rpc::CallInfoRawBuilder builder(*output_builder);
if (cov->overflow)
flags |= rpc::CallFlag::CoverageOverflow;
builder.add_flags(flags);
builder.add_error(error);
if (signal_off)
builder.add_signal(signal_off);
if (cover_off)
builder.add_cover(cover_off);
if (comps_off)
builder.add_comps(comps_off);
auto off = builder.Finish();
uint32 slot = output_data->completed.load(std::memory_order_relaxed);
if (slot >= kMaxCalls)
failmsg("too many calls in output", "slot=%d", slot);
auto& call = output_data->calls[slot];
call.index = index;
call.offset = off;
output_data->consumed.store(output_builder->GetSize(), std::memory_order_release);
output_data->completed.store(slot + 1, std::memory_order_release);
debug_verbose("out #%u: index=%u errno=%d flags=0x%x total_size=%u\n",
slot + 1, index, error, static_cast<unsigned>(flags), call.data_size - start_size);
}
void write_call_output(thread_t* th, bool finished)
{
uint32 reserrno = ENOSYS;
rpc::CallFlag flags = rpc::CallFlag::Executed;
if (finished && th != last_scheduled)
flags |= rpc::CallFlag::Blocked;
if (finished) {
reserrno = th->res != -1 ? 0 : th->reserrno;
flags |= rpc::CallFlag::Finished;
if (th->fault_injected)
flags |= rpc::CallFlag::FaultInjected;
}
bool all_signal = th->call_index < 64 ? (all_call_signal & (1ull << th->call_index)) : false;
write_output(th->call_index, &th->cov, flags, reserrno, all_signal);
}
void write_extra_output()
{
if (!cover_collection_required() || !flag_extra_coverage || flag_comparisons)
return;
cover_collect(&extra_cov);
if (!extra_cov.size)
return;
write_output(-1, &extra_cov, rpc::CallFlag::NONE, 997, all_extra_signal);
cover_reset(&extra_cov);
}
flatbuffers::span<uint8_t> finish_output(OutputData* output, int proc_id, uint64 req_id, uint32 num_calls, uint64 elapsed,
uint64 freshness, uint32 status, bool hanged, const std::vector<uint8_t>* process_output)
{
// In snapshot mode the output size is fixed and output_size is always initialized, so use it.
int out_size = flag_snapshot ? output_size : output->size.load(std::memory_order_relaxed) ?
: kMaxOutput;
uint32 completed = output->completed.load(std::memory_order_relaxed);
completed = std::min(completed, kMaxCalls);
debug("handle completion: completed=%u output_size=%u\n", completed, out_size);
ShmemBuilder fbb(output, out_size, false);
auto empty_call = rpc::CreateCallInfoRawDirect(fbb, rpc::CallFlag::NONE, 998);
std::vector<flatbuffers::Offset<rpc::CallInfoRaw>> calls(num_calls, empty_call);
std::vector<flatbuffers::Offset<rpc::CallInfoRaw>> extra;
for (uint32_t i = 0; i < completed; i++) {
const auto& call = output->calls[i];
if (call.index == -1) {
extra.push_back(call.offset);
continue;
}
if (call.index < 0 || call.index >= static_cast<int>(num_calls) || call.offset.o > kMaxOutput) {
debug("bad call index/offset: proc=%d req=%llu call=%d/%d completed=%d offset=%u",
proc_id, req_id, call.index, num_calls,
completed, call.offset.o);
continue;
}
calls[call.index] = call.offset;
}
auto prog_info_off = rpc::CreateProgInfoRawDirect(fbb, &calls, &extra, 0, elapsed, freshness);
flatbuffers::Offset<flatbuffers::String> error_off = 0;
if (status == kFailStatus)
error_off = fbb.CreateString("process failed");
// If the request wrote binary result (currently glob requests do this), use it instead of the output.
auto output_off = output->result_offset.load(std::memory_order_relaxed);
if (output_off.IsNull() && process_output)
output_off = fbb.CreateVector(*process_output);
auto exec_off = rpc::CreateExecResultRaw(fbb, req_id, proc_id, output_off, hanged, error_off, prog_info_off);
auto msg_off = rpc::CreateExecutorMessageRaw(fbb, rpc::ExecutorMessagesRaw::ExecResult,
flatbuffers::Offset<void>(exec_off.o));
fbb.FinishSizePrefixed(msg_off);
return fbb.GetBufferSpan();
}
void thread_create(thread_t* th, int id, bool need_coverage)
{
th->created = true;
th->id = id;
th->executing = false;
// Lazily set up coverage collection.
// It is assumed that actually it's already initialized - with a few rare exceptions.
if (need_coverage) {
if (!th->cov.fd)
exitf("out of opened kcov threads");
thread_mmap_cover(th);
}
event_init(&th->ready);
event_init(&th->done);
event_set(&th->done);
if (flag_threaded)
thread_start(worker_thread, th);
}
void thread_mmap_cover(thread_t* th)
{
if (th->cov.data != NULL)
return;
cover_mmap(&th->cov);
cover_protect(&th->cov);
}
void* worker_thread(void* arg)
{
thread_t* th = (thread_t*)arg;
current_thread = th;
for (bool first = true;; first = false) {
event_wait(&th->ready);
event_reset(&th->ready);
// Setup coverage only after receiving the first ready event
// because in snapshot mode we don't know coverage mode for precreated threads.
if (first && cover_collection_required())
cover_enable(&th->cov, flag_comparisons, false);
execute_call(th);
event_set(&th->done);
}
return 0;
}
void execute_call(thread_t* th)
{
const call_t* call = &syscalls[th->call_num];
debug("#%d [%llums] -> %s(",
th->id, current_time_ms() - start_time_ms, call->name);
for (int i = 0; i < th->num_args; i++) {
if (i != 0)
debug(", ");
debug("0x%llx", (uint64)th->args[i]);
}
debug(")\n");
int fail_fd = -1;
th->soft_fail_state = false;
if (th->call_props.fail_nth > 0) {
if (th->call_props.rerun > 0)
fail("both fault injection and rerun are enabled for the same call");
fail_fd = inject_fault(th->call_props.fail_nth);
th->soft_fail_state = true;
}
if (flag_coverage)
cover_reset(&th->cov);
// For pseudo-syscalls and user-space functions NONFAILING can abort before assigning to th->res.
// Arrange for res = -1 and errno = EFAULT result for such case.
th->res = -1;
errno = EFAULT;
NONFAILING(th->res = execute_syscall(call, th->args));
th->reserrno = errno;
// Our pseudo-syscalls may misbehave.
if ((th->res == -1 && th->reserrno == 0) || call->attrs.ignore_return)
th->reserrno = EINVAL;
// Reset the flag before the first possible fail().
th->soft_fail_state = false;
if (flag_coverage)
cover_collect(&th->cov);
th->fault_injected = false;
if (th->call_props.fail_nth > 0)
th->fault_injected = fault_injected(fail_fd);
// If required, run the syscall some more times.
// But let's still return res, errno and coverage from the first execution.
for (int i = 0; i < th->call_props.rerun; i++)
NONFAILING(execute_syscall(call, th->args));
debug("#%d [%llums] <- %s=0x%llx",
th->id, current_time_ms() - start_time_ms, call->name, (uint64)th->res);
if (th->res == (intptr_t)-1)
debug(" errno=%d", th->reserrno);
if (flag_coverage)
debug(" cover=%u", th->cov.size);
if (th->call_props.fail_nth > 0)
debug(" fault=%d", th->fault_injected);
if (th->call_props.rerun > 0)
debug(" rerun=%d", th->call_props.rerun);
debug("\n");
}
static uint32 hash(uint32 a)
{
// For test OS we disable hashing for determinism and testability.
#if !GOOS_test
a = (a ^ 61) ^ (a >> 16);
a = a + (a << 3);
a = a ^ (a >> 4);
a = a * 0x27d4eb2d;
a = a ^ (a >> 15);
#endif
return a;
}
const uint32 dedup_table_size = 8 << 10;
uint64 dedup_table_sig[dedup_table_size];
uint8 dedup_table_index[dedup_table_size];
// Poorman's best-effort hashmap-based deduplication.
static bool dedup(uint8 index, uint64 sig)
{
for (uint32 i = 0; i < 4; i++) {
uint32 pos = (sig + i) % dedup_table_size;
if (dedup_table_sig[pos] == sig && dedup_table_index[pos] == index)
return true;
if (dedup_table_sig[pos] == 0 || dedup_table_index[pos] != index) {
dedup_table_index[pos] = index;
dedup_table_sig[pos] = sig;
return false;
}
}
uint32 pos = sig % dedup_table_size;
dedup_table_sig[pos] = sig;
dedup_table_index[pos] = index;
return false;
}
template <typename T>
void copyin_int(char* addr, uint64 val, uint64 bf, uint64 bf_off, uint64 bf_len)
{
if (bf_off == 0 && bf_len == 0) {
*(T*)addr = swap(val, sizeof(T), bf);
return;
}
T x = swap(*(T*)addr, sizeof(T), bf);
debug_verbose("copyin_int<%zu>: old x=0x%llx\n", sizeof(T), (uint64)x);
#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
const uint64 shift = sizeof(T) * CHAR_BIT - bf_off - bf_len;
#else
const uint64 shift = bf_off;
#endif
x = (x & ~BITMASK(shift, bf_len)) | ((val << shift) & BITMASK(shift, bf_len));
debug_verbose("copyin_int<%zu>: x=0x%llx\n", sizeof(T), (uint64)x);
*(T*)addr = swap(x, sizeof(T), bf);
}
void copyin(char* addr, uint64 val, uint64 size, uint64 bf, uint64 bf_off, uint64 bf_len)
{
debug_verbose("copyin: addr=%p val=0x%llx size=%llu bf=%llu bf_off=%llu bf_len=%llu\n",
addr, val, size, bf, bf_off, bf_len);
if (bf != binary_format_native && bf != binary_format_bigendian && (bf_off != 0 || bf_len != 0))
failmsg("bitmask for string format", "off=%llu, len=%llu", bf_off, bf_len);
switch (bf) {
case binary_format_native:
case binary_format_bigendian:
NONFAILING(switch (size) {
case 1:
copyin_int<uint8>(addr, val, bf, bf_off, bf_len);
break;
case 2:
copyin_int<uint16>(addr, val, bf, bf_off, bf_len);
break;
case 4:
copyin_int<uint32>(addr, val, bf, bf_off, bf_len);
break;
case 8:
copyin_int<uint64>(addr, val, bf, bf_off, bf_len);
break;
default:
failmsg("copyin: bad argument size", "size=%llu", size);
});
break;
case binary_format_strdec:
if (size != 20)
failmsg("bad strdec size", "size=%llu", size);
NONFAILING(sprintf((char*)addr, "%020llu", val));
break;
case binary_format_strhex:
if (size != 18)
failmsg("bad strhex size", "size=%llu", size);
NONFAILING(sprintf((char*)addr, "0x%016llx", val));
break;
case binary_format_stroct:
if (size != 23)
failmsg("bad stroct size", "size=%llu", size);
NONFAILING(sprintf((char*)addr, "%023llo", val));
break;
default:
failmsg("unknown binary format", "format=%llu", bf);
}
}
bool copyout(char* addr, uint64 size, uint64* res)
{
return NONFAILING(
switch (size) {
case 1:
*res = *(uint8*)addr;
break;
case 2:
*res = *(uint16*)addr;
break;
case 4:
*res = *(uint32*)addr;
break;
case 8:
*res = *(uint64*)addr;
break;
default:
failmsg("copyout: bad argument size", "size=%llu", size);
});
}
uint64 read_arg(uint8** input_posp)
{
uint64 typ = read_input(input_posp);
switch (typ) {
case arg_const: {
uint64 size, bf, bf_off, bf_len;
uint64 val = read_const_arg(input_posp, &size, &bf, &bf_off, &bf_len);
if (bf != binary_format_native && bf != binary_format_bigendian)
failmsg("bad argument binary format", "format=%llu", bf);
if (bf_off != 0 || bf_len != 0)
failmsg("bad argument bitfield", "off=%llu, len=%llu", bf_off, bf_len);
return swap(val, size, bf);
}
case arg_addr32:
case arg_addr64: {
return read_input(input_posp) + SYZ_DATA_OFFSET;
}
case arg_result: {
uint64 meta = read_input(input_posp);
uint64 bf = meta >> 8;
if (bf != binary_format_native)
failmsg("bad result argument format", "format=%llu", bf);
return read_result(input_posp);
}
default:
failmsg("bad argument type", "type=%llu", typ);
}
}
uint64 swap(uint64 v, uint64 size, uint64 bf)
{
if (bf == binary_format_native)
return v;
if (bf != binary_format_bigendian)
failmsg("bad binary format in swap", "format=%llu", bf);
switch (size) {
case 2:
return htobe16(v);
case 4:
return htobe32(v);
case 8:
return htobe64(v);
default:
failmsg("bad big-endian int size", "size=%llu", size);
}
}
uint64 read_const_arg(uint8** input_posp, uint64* size_p, uint64* bf_p, uint64* bf_off_p, uint64* bf_len_p)
{
uint64 meta = read_input(input_posp);
uint64 val = read_input(input_posp);
*size_p = meta & 0xff;
uint64 bf = (meta >> 8) & 0xff;
*bf_off_p = (meta >> 16) & 0xff;
*bf_len_p = (meta >> 24) & 0xff;
uint64 pid_stride = meta >> 32;
val += pid_stride * procid;
*bf_p = bf;
return val;
}
uint64 read_result(uint8** input_posp)
{
uint64 idx = read_input(input_posp);
uint64 op_div = read_input(input_posp);
uint64 op_add = read_input(input_posp);
uint64 arg = read_input(input_posp);
if (idx >= kMaxCommands)
failmsg("command refers to bad result", "result=%lld", idx);
if (results[idx].executed) {
arg = results[idx].val;
if (op_div != 0)
arg = arg / op_div;
arg += op_add;
}
return arg;
}
uint64 read_input(uint8** input_posp, bool peek)
{
uint64 v = 0;
unsigned shift = 0;
uint8* input_pos = *input_posp;
for (int i = 0;; i++, shift += 7) {
const int maxLen = 10;
if (i == maxLen)
failmsg("varint overflow", "pos=%zu", (size_t)(*input_posp - input_data));
if (input_pos >= input_data + kMaxInput)
failmsg("input command overflows input", "pos=%p: [%p:%p)",
input_pos, input_data, input_data + kMaxInput);
uint8 b = *input_pos++;
v |= uint64(b & 0x7f) << shift;
if (b < 0x80) {
if (i == maxLen - 1 && b > 1)
failmsg("varint overflow", "pos=%zu", (size_t)(*input_posp - input_data));
break;
}
}
if (v & 1)
v = ~(v >> 1);
else
v = v >> 1;
if (!peek)
*input_posp = input_pos;
return v;
}
rpc::ComparisonRaw convert(const kcov_comparison_t& cmp)
{
if (cmp.type > (KCOV_CMP_CONST | KCOV_CMP_SIZE_MASK))
failmsg("invalid kcov comp type", "type=%llx", cmp.type);
uint64 arg1 = cmp.arg1;
uint64 arg2 = cmp.arg2;
// Comparisons with 0 are not interesting, fuzzer should be able to guess 0's without help.
if (arg1 == 0 && (arg2 == 0 || (cmp.type & KCOV_CMP_CONST)))
return {};
// Successful comparison is not interesting.
if (arg1 == arg2)
return {};
// This can be a pointer (assuming 64-bit kernel).
// First of all, we want avert fuzzer from our output region.
// Without this fuzzer manages to discover and corrupt it.
uint64 out_start = (uint64)output_data;
uint64 out_end = out_start + output_size;
if (arg1 >= out_start && arg1 <= out_end)
return {};
if (arg2 >= out_start && arg2 <= out_end)
return {};
if (!coverage_filter(cmp.pc))
return {};
// KCOV converts all arguments of size x first to uintx_t and then to uint64.
// We want to properly extend signed values, e.g we want int8 c = 0xfe to be represented
// as 0xfffffffffffffffe. Note that uint8 c = 0xfe will be represented the same way.
// This is ok because during hints processing we will anyways try the value 0x00000000000000fe.
switch (cmp.type & KCOV_CMP_SIZE_MASK) {
case KCOV_CMP_SIZE1:
arg1 = (uint64)(long long)(signed char)arg1;
arg2 = (uint64)(long long)(signed char)arg2;
break;
case KCOV_CMP_SIZE2:
arg1 = (uint64)(long long)(short)arg1;
arg2 = (uint64)(long long)(short)arg2;
break;
case KCOV_CMP_SIZE4:
arg1 = (uint64)(long long)(int)arg1;
arg2 = (uint64)(long long)(int)arg2;
break;
}
// Prog package expects operands in the opposite order (first operand may come from the input,
// the second operand was computed in the kernel), so swap operands.
return {cmp.pc, arg2, arg1, !!(cmp.type & KCOV_CMP_CONST)};
}
void failmsg(const char* err, const char* msg, ...)
{
int e = errno;
fprintf(stderr, "SYZFAIL: %s\n", err);
if (msg) {
va_list args;
va_start(args, msg);
vfprintf(stderr, msg, args);
va_end(args);
}
fprintf(stderr, " (errno %d: %s)\n", e, strerror(e));
// fail()'s are often used during the validation of kernel reactions to queries
// that were issued by pseudo syscalls implementations. As fault injection may
// cause the kernel not to succeed in handling these queries (e.g. socket writes
// or reads may fail), this could ultimately lead to unwanted "lost connection to
// test machine" crashes.
// In order to avoid this and, on the other hand, to still have the ability to
// signal a disastrous situation, the exit code of this function depends on the
// current context.
// All fail() invocations during system call execution with enabled fault injection
// lead to termination with zero exit code. In all other cases, the exit code is
// kFailStatus.
if (current_thread && current_thread->soft_fail_state)
doexit(0);
doexit(kFailStatus);
}
void fail(const char* err)
{
failmsg(err, 0);
}
void exitf(const char* msg, ...)
{
int e = errno;
va_list args;
va_start(args, msg);
vfprintf(stderr, msg, args);
va_end(args);
fprintf(stderr, " (errno %d)\n", e);
doexit(1);
}
void debug(const char* msg, ...)
{
if (!flag_debug)
return;
int err = errno;
va_list args;
va_start(args, msg);
vfprintf(stderr, msg, args);
va_end(args);
fflush(stderr);
errno = err;
}
void debug_dump_data(const char* data, int length)
{
if (!flag_debug)
return;
int i = 0;
for (; i < length; i++) {
debug("%02x ", data[i] & 0xff);
if (i % 16 == 15)
debug("\n");
}
if (i % 16 != 0)
debug("\n");
}