Add support for user callbacks (#691)

This PR is my attempt to add support for user callbacks in the emulator.
User callbacks allow the emulator to call guest callbacks from syscalls,
and when the callback finishes running, control returns to the syscall
through the completion method. I've also added a test and implemented
the NtUserEnumDisplayMonitors syscall.

One thing to note is that this implementation isn't faithful to how the
Windows kernel does it, since the kernel uses the KernelCallbackTable
and the `ntdll!KiUserCallbackDispatch` method, and this implementation
currently just calls the callback directly.
This commit is contained in:
Maurice Heumann
2026-01-10 13:59:24 +01:00
committed by GitHub
13 changed files with 421 additions and 18 deletions

View File

@@ -449,6 +449,66 @@ void process_context::setup(x86_64_emulator& emu, memory_manager& memory, regist
});
}
void process_context::setup_callback_hook(windows_emulator& win_emu, memory_manager& memory)
{
uint64_t sentinel_addr = this->callback_sentinel_addr;
if (!sentinel_addr)
{
using sentinel_type = std::array<uint8_t, 2>;
constexpr sentinel_type sentinel_opcodes{0x90, 0xC3}; // NOP, RET
auto sentinel_obj = this->base_allocator.reserve_page_aligned<sentinel_type>();
sentinel_addr = sentinel_obj.value();
this->callback_sentinel_addr = sentinel_addr;
win_emu.emu().write_memory(sentinel_addr, sentinel_opcodes.data(), sentinel_opcodes.size());
const auto sentinel_aligned_length = page_align_up(sentinel_addr + sentinel_opcodes.size()) - sentinel_addr;
memory.protect_memory(sentinel_addr, static_cast<size_t>(sentinel_aligned_length), memory_permission::all);
}
auto& emu = win_emu.emu();
emu.hook_memory_execution(sentinel_addr, [&](uint64_t) {
auto* t = this->active_thread;
if (!t || t->callback_stack.empty())
{
return;
}
const auto frame = t->callback_stack.back();
t->callback_stack.pop_back();
const auto callbacks_before = t->callback_stack.size();
const uint64_t guest_result = emu.reg(x86_register::rax);
emu.reg(x86_register::rip, frame.rip);
emu.reg(x86_register::rsp, frame.rsp);
emu.reg(x86_register::r10, frame.r10);
emu.reg(x86_register::rcx, frame.rcx);
emu.reg(x86_register::rdx, frame.rdx);
emu.reg(x86_register::r8, frame.r8);
emu.reg(x86_register::r9, frame.r9);
win_emu.dispatcher.dispatch_completion(win_emu, frame.handler_id, guest_result);
uint64_t target_rip = emu.reg(x86_register::rip);
emu.reg(x86_register::rip, this->callback_sentinel_addr + 1);
const bool new_callback_dispatched = t->callback_stack.size() > callbacks_before;
if (!new_callback_dispatched)
{
// Move past the syscall instruction
target_rip += 2;
}
const uint64_t ret_stack_ptr = frame.rsp - sizeof(emulator_pointer);
emu.write_memory(ret_stack_ptr, &target_rip, sizeof(target_rip));
emu.reg(x86_register::rsp, ret_stack_ptr);
});
}
void process_context::serialize(utils::buffer_serializer& buffer) const
{
buffer.write(this->shared_section_address);
@@ -496,6 +556,8 @@ void process_context::serialize(utils::buffer_serializer& buffer) const
buffer.write(this->threads);
buffer.write(this->threads.find_handle(this->active_thread).bits);
buffer.write(this->callback_sentinel_addr);
}
void process_context::deserialize(utils::buffer_deserializer& buffer)
@@ -551,6 +613,8 @@ void process_context::deserialize(utils::buffer_deserializer& buffer)
buffer.read(this->threads);
this->active_thread = this->threads.get(buffer.read<uint64_t>());
buffer.read(this->callback_sentinel_addr);
}
generic_handle_store* process_context::get_handle_store(const handle handle)