Files
raven/native/nt/sync_api.cpp
T

343 lines
14 KiB
C++

#include "kernel_context.h"
#include <algorithm>
#include <chrono>
#include <cstring>
#include <deque>
#include <map>
#include <memory>
namespace raven {
namespace {
// CRITICAL_SECTION (x64): debug info, lock count, recursion count, owning thread, semaphore, spin count.
constexpr uint64_t lock_count = 8, recursion_count = 12, owning_thread = 16, spin_count = 32;
struct Waiter {
bool woken = false;
};
}
void register_sync_api(ApiRegistry &registry, KernelContext &context)
{
auto &threads = context.threads;
const auto me = [&threads] { return uint64_t(threads.current_id()); };
const auto initialize = [](ApiCall &call) {
std::vector<uint8_t> bytes(40);
const int32_t unlocked = -1;
std::memcpy(bytes.data() + lock_count, &unlocked, 4);
call.write(call[0], bytes);
call.ret(1);
};
registry.add("InitializeCriticalSection", 1, initialize);
registry.add("InitializeCriticalSectionAndSpinCount", 2, [initialize](ApiCall &call) {
initialize(call);
call.store<uint64_t>(call[0] + spin_count, call[1] & 0x00ffffff);
});
registry.add("InitializeCriticalSectionEx", 3, [initialize](ApiCall &call) {
if (call.u32(2) & ~0x03000000U) {
call.fail(87);
return;
}
initialize(call);
call.store<uint64_t>(call[0] + spin_count, call[1] & 0x00ffffff);
});
registry.add("DeleteCriticalSection", 1, [](ApiCall &call) {
call.store<uint64_t>(call[0] + owning_thread, 0);
call.store<int32_t>(call[0] + lock_count, -1);
call.store<int32_t>(call[0] + recursion_count, 0);
});
// A section is free when nothing owns it; an owner nests, anyone else waits for the owner to leave.
const auto take = [me](ApiCall &call) {
const auto owner = call.load<uint64_t>(call[0] + owning_thread);
const auto recursion = call.load<int32_t>(call[0] + recursion_count);
if (recursion > 0 && owner == me()) {
call.store<int32_t>(call[0] + recursion_count, recursion + 1);
call.store<int32_t>(call[0] + lock_count, call.load<int32_t>(call[0] + lock_count) + 1);
return true;
}
if (recursion > 0 && owner)
return false;
call.store<uint64_t>(call[0] + owning_thread, me());
call.store<int32_t>(call[0] + recursion_count, 1);
call.store<int32_t>(call[0] + lock_count, 0);
return true;
};
registry.add("EnterCriticalSection", 1, [take, &context, &threads](ApiCall &call) {
const uint64_t section = call[0];
while (!take(call)) {
threads.block([&context, section] {
return context.process.read_memory(section + recursion_count, 4) == std::vector<uint8_t>(4, 0) ||
context.process.read_memory(section + owning_thread, 8) == std::vector<uint8_t>(8, 0);
}, std::nullopt);
}
});
registry.add("TryEnterCriticalSection", 1, [take](ApiCall &call) { call.ret(take(call)); });
registry.add("LeaveCriticalSection", 1, [](ApiCall &call) {
const auto recursion = call.load<int32_t>(call[0] + recursion_count);
if (recursion <= 0)
return;
call.store<int32_t>(call[0] + recursion_count, recursion - 1);
if (recursion == 1) {
call.store<uint64_t>(call[0] + owning_thread, 0);
call.store<int32_t>(call[0] + lock_count, -1);
} else {
call.store<int32_t>(call[0] + lock_count, call.load<int32_t>(call[0] + lock_count) - 1);
}
});
// SRW locks are one pointer-sized word: 1 is exclusive, shared holders count in steps of 2.
const auto acquire_exclusive = [&context, &threads](ApiCall &call) {
const uint64_t lock = call[0];
const size_t width = call.pointer_width();
while (call.load_pointer(lock))
threads.block([&context, lock, width] { return context.process.read_memory(lock, width) == std::vector<uint8_t>(width, 0); }, std::nullopt);
call.store_pointer(lock, 1);
};
const auto acquire_shared = [&context, &threads](ApiCall &call) {
const uint64_t lock = call[0];
while (call.load_pointer(lock) & 1)
threads.block([&context, lock] { return !(context.process.read_memory(lock, 1)[0] & 1); }, std::nullopt);
call.store_pointer(lock, call.load_pointer(lock) + 2);
};
registry.add("InitializeSRWLock", 1, [](ApiCall &call) { call.store_pointer(call[0], 0); });
registry.add("AcquireSRWLockExclusive", 1, acquire_exclusive);
// The guest component takes a free lock itself and asks here, under the ntdll names, only for one that is held.
registry.add("RtlAcquireSRWLockExclusive", 1, acquire_exclusive);
registry.add("TryAcquireSRWLockExclusive", 1, [](ApiCall &call) {
const bool free = !call.load_pointer(call[0]);
if (free)
call.store_pointer(call[0], 1);
call.ret(free);
});
registry.add("ReleaseSRWLockExclusive", 1, [](ApiCall &call) { call.store_pointer(call[0], 0); });
registry.add("AcquireSRWLockShared", 1, acquire_shared);
registry.add("RtlAcquireSRWLockShared", 1, acquire_shared);
registry.add("TryAcquireSRWLockShared", 1, [](ApiCall &call) {
const auto value = call.load_pointer(call[0]);
if (!(value & 1))
call.store_pointer(call[0], value + 2);
call.ret(!(value & 1));
});
registry.add("ReleaseSRWLockShared", 1, [](ApiCall &call) {
const auto value = call.load_pointer(call[0]);
if (value >= 2)
call.store_pointer(call[0], value - 2);
});
// Condition variables keep their waiters host-side, keyed by the variable's address. The variable's word holds how many sleep on it,
// so a guest can see that a wake has nobody to wake without asking.
auto variables = std::make_shared<std::map<uint64_t, std::vector<std::shared_ptr<Waiter>>>>();
const auto sleep_on = [&threads, variables](ApiCall &call, std::function<void()> release, std::function<void()> reacquire, uint32_t milliseconds) {
auto waiter = std::make_shared<Waiter>();
(*variables)[call[0]].push_back(waiter);
call.store_pointer(call[0], (*variables)[call[0]].size());
release();
std::optional<std::chrono::steady_clock::time_point> wake;
if (milliseconds != UINT32_MAX)
wake = std::chrono::steady_clock::now() + std::chrono::milliseconds(milliseconds);
threads.block([waiter] { return waiter->woken; }, wake);
auto &list = (*variables)[call[0]];
list.erase(std::remove(list.begin(), list.end(), waiter), list.end());
call.store_pointer(call[0], list.size());
reacquire();
if (!waiter->woken) {
call.fail(1460);
return;
}
call.ret(1);
};
registry.add("InitializeConditionVariable", 1, [](ApiCall &call) { call.store_pointer(call[0], 0); });
registry.add("SleepConditionVariableSRW", 4, [sleep_on, acquire_exclusive, acquire_shared](ApiCall &call) {
const bool shared = call.u32(3) & 1;
const uint64_t lock = call[1];
auto sub = ApiCall(call.process, call.environment, {lock});
sleep_on(call, [&] { if (shared) { const auto v = call.load_pointer(lock); call.store_pointer(lock, v >= 2 ? v - 2 : 0); } else call.store_pointer(lock, 0); },
[&] { if (shared) acquire_shared(sub); else acquire_exclusive(sub); }, call.u32(2));
});
registry.add("SleepConditionVariableCS", 3, [sleep_on, take, &threads, &context](ApiCall &call) {
const uint64_t section = call[1];
if (call.pointer_width() == 4 || context.modules.all().contains("raven-rtl.dll")) {
// A 32-bit section is not laid out in guest memory, and a 64-bit one belongs to the guest RTL when it is loaded: leaving and
// entering it goes through the program's own calls.
const auto leave = context.modules.resolve({"kernel32.dll", "LeaveCriticalSection", 0, false, 0});
const auto enter = context.modules.resolve({"kernel32.dll", "EnterCriticalSection", 0, false, 0});
const uint64_t arguments[] = {section};
sleep_on(call, [&] { context.call(leave, arguments); }, [&] { context.call(enter, arguments); }, call.u32(2));
return;
}
auto sub = ApiCall(call.process, call.environment, {section});
int32_t saved = 0;
sleep_on(call, [&] {
saved = sub.load<int32_t>(section + recursion_count);
sub.store<uint64_t>(section + owning_thread, 0);
sub.store<int32_t>(section + recursion_count, 0);
sub.store<int32_t>(section + lock_count, -1);
}, [&] {
while (!take(sub))
threads.block([&context, section] { return context.process.read_memory(section + recursion_count, 4) == std::vector<uint8_t>(4, 0); }, std::nullopt);
sub.store<int32_t>(section + recursion_count, saved);
}, call.u32(2));
});
const auto wake = [variables](ApiCall &call, bool all) {
auto &list = (*variables)[call[0]];
for (auto &waiter : list)
if (!waiter->woken) {
waiter->woken = true;
if (!all)
break;
}
};
registry.add("WakeConditionVariable", 1, [wake](ApiCall &call) { wake(call, false); });
registry.add("WakeAllConditionVariable", 1, [wake](ApiCall &call) { wake(call, true); });
registry.add("RtlWakeConditionVariable", 1, [wake](ApiCall &call) { wake(call, false); });
registry.add("RtlWakeAllConditionVariable", 1, [wake](ApiCall &call) { wake(call, true); });
// INIT_ONCE is one word: 0 untouched, 1 running, otherwise the context pointer with low bits 2.
registry.add("InitOnceInitialize", 1, [](ApiCall &call) { call.store_pointer(call[0], 0); });
registry.add("InitOnceBeginInitialize", 4, [&context, &threads](ApiCall &call) {
const uint32_t flags = call.u32(1);
if (flags & ~3U) {
call.fail(87);
return;
}
const uint64_t once = call[0];
auto state = call.load_pointer(once);
if ((state & 3) == 1 && !(flags & 1)) {
threads.block([&context, once] { return (context.process.read_memory(once, 1)[0] & 3) != 1; }, std::nullopt);
state = call.load_pointer(once);
}
if ((state & 3) == 2) {
call.store<int32_t>(call[2], 0);
if (call[3]) call.store_pointer(call[3], state & ~3ULL);
call.ret(1);
return;
}
if (flags & 1) {
call.fail(1627);
return;
}
call.store_pointer(once, 1);
call.store<int32_t>(call[2], 1);
call.ret(1);
});
registry.add("InitOnceComplete", 3, [](ApiCall &call) {
const uint32_t flags = call.u32(1);
if (call.load_pointer(call[0]) != 1 || (flags & 4 && call[2])) {
call.fail(87);
return;
}
call.store_pointer(call[0], flags & 4 ? 0 : (call[2] & ~3ULL) | 2);
call.ret(1);
});
registry.add("InitOnceExecuteOnce", 4, [&](ApiCall &call) {
const uint64_t once = call[0];
auto state = call.load_pointer(once);
if ((state & 3) == 1) {
threads.block([&context, once] { return (context.process.read_memory(once, 1)[0] & 3) != 1; }, std::nullopt);
state = call.load_pointer(once);
}
if ((state & 3) == 2) {
if (call[3]) call.store_pointer(call[3], state & ~3ULL);
call.ret(1);
return;
}
call.store_pointer(once, 1);
const auto scratch = context.allocator.allocate(16);
if (!scratch) {
call.fail(8);
return;
}
const uint64_t arguments[] = {once, call[2], scratch};
const auto result = context.call(call[1], arguments);
if (result.stop != NativeStop::returned)
throw result;
const auto produced = call.load_pointer(scratch);
context.allocator.release(scratch);
if (!uint32_t(result.value)) {
call.store_pointer(once, 0);
call.fail(1627);
return;
}
call.store_pointer(once, (produced & ~3ULL) | 2);
if (call[3]) call.store_pointer(call[3], produced);
call.ret(1);
});
// WaitOnAddress watches memory directly; wakeups are implicit because waiting threads re-check the value.
registry.add("WaitOnAddress", 4, [&context, &threads](ApiCall &call) {
const size_t size = call[2];
if (size != 1 && size != 2 && size != 4 && size != 8) {
call.fail(87);
return;
}
const uint64_t address = call[0];
const auto expected = call.read(call[1], size);
std::optional<std::chrono::steady_clock::time_point> wake;
if (call.u32(3) != UINT32_MAX)
wake = std::chrono::steady_clock::now() + std::chrono::milliseconds(call.u32(3));
const bool changed = threads.block([&context, address, size, expected] { return context.process.read_memory(address, size) != expected; }, wake);
if (!changed) {
call.fail(1460);
return;
}
call.ret(1);
});
registry.add("WakeByAddressSingle", 1, [](ApiCall &) {});
registry.add("WakeByAddressAll", 1, [](ApiCall &) {});
// Keyed events: a release meets one wait on the same key, whichever comes first waiting for the other. Only the process-wide keyed
// event (a null handle) exists, which is what critical sections and slim locks use; a key is an address.
struct KeyedParty {
bool met = false;
};
auto keyed = std::make_shared<std::map<std::pair<bool, uint64_t>, std::deque<std::shared_ptr<KeyedParty>>>>();
const auto rendezvous = [&threads, keyed](ApiCall &call, bool releasing) {
constexpr uint32_t success = 0, timeout = 0x102, invalid_handle = 0xc0000008;
if (call[0]) {
call.ret(invalid_handle);
return;
}
const uint64_t key = call[1];
// One who arrived on the other side first is met at once.
auto &others = (*keyed)[{!releasing, key}];
if (!others.empty()) {
others.front()->met = true;
others.pop_front();
call.ret(success);
return;
}
// The timeout: none waits for ever, a negative one is relative and a positive one absolute, both in 100 ns units.
std::optional<std::chrono::steady_clock::time_point> wake;
if (call[3]) {
const int64_t value = call.load<int64_t>(call[3]);
auto delay = std::chrono::nanoseconds(0);
if (value < 0) {
delay = std::chrono::nanoseconds(-value * 100);
} else if (value > 0) {
const auto since_epoch = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::system_clock::now().time_since_epoch());
const int64_t now = since_epoch.count() / 100 + int64_t(filetime_epoch_offset);
delay = std::chrono::nanoseconds(std::max<int64_t>(0, value - now) * 100);
}
wake = std::chrono::steady_clock::now() + delay;
}
auto party = std::make_shared<KeyedParty>();
auto &mine = (*keyed)[{releasing, key}];
mine.push_back(party);
threads.block([party] { return party->met; }, wake);
if (!party->met) {
auto &list = (*keyed)[{releasing, key}];
list.erase(std::remove(list.begin(), list.end(), party), list.end());
call.ret(timeout);
return;
}
call.ret(success);
};
registry.add("NtWaitForKeyedEvent", 4, [rendezvous](ApiCall &call) { rendezvous(call, false); });
registry.add("NtReleaseKeyedEvent", 4, [rendezvous](ApiCall &call) { rendezvous(call, true); });
}
}