Add support for user_object/user_handle_table (#677)

Fixes #641

This PR is my attempt to add support for user32 objects and the user32
handle table. I also added a test, but as expected, it fails on Windows
2022. I’ll try to fix that another day, but feel free to review the code
😄
This commit is contained in:
Maurice Heumann
2026-01-06 10:21:58 +01:00
committed by GitHub
12 changed files with 747 additions and 15 deletions

View File

@@ -17,6 +17,7 @@
#include "unicode.hpp"
#include "status.hpp"
#include "process.hpp"
#include "user.hpp"
#include "kernel_mapped.hpp"
#include "memory.hpp"
#include "file_management.hpp"

View File

@@ -50,6 +50,14 @@ typedef union _LARGE_INTEGER
using BYTE = std::uint8_t;
#define CHAR BYTE
typedef struct _RECT
{
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT;
#endif
using WORD = std::uint16_t;

View File

@@ -0,0 +1,117 @@
#pragma once
#include <cstdint>
// NOLINTBEGIN(modernize-use-using,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
struct USER_SERVERINFO
{
DWORD dwSRVIFlags;
uint64_t cHandleEntries;
uint8_t unknown[0x1000];
};
struct USER_DISPINFO
{
DWORD dwMonitorCount;
EMULATOR_CAST(uint64_t, USER_MONITOR*) pPrimaryMonitor;
uint8_t unknown[0xFF];
};
struct USER_HANDLEENTRY
{
uint64_t pHead;
uint64_t pOwner;
uint64_t unknown;
uint8_t bType;
uint8_t bFlags;
uint16_t wUniq;
};
static_assert(sizeof(USER_HANDLEENTRY) == 0x20);
struct USER_SHAREDINFO
{
uint64_t psi;
uint64_t aheList;
uint32_t HeEntrySize;
uint64_t pDispInfo;
uint8_t unknown[0xFF];
};
struct USER_THROBJHEAD
{
struct
{
uint64_t h;
uint32_t cLockObj;
} h;
uint64_t pti;
};
struct USER_THRDESKHEAD
{
USER_THROBJHEAD h;
uint64_t rpdesk;
uint64_t pSelf;
};
enum USER_HANDLETYPE : uint8_t
{
TYPE_FREE = 0,
TYPE_WINDOW = 1,
TYPE_MENU = 2,
TYPE_CURSOR = 3,
TYPE_SETWINDOWPOS = 4,
TYPE_HOOK = 5,
TYPE_CLIPDATA = 6,
TYPE_CALLPROC = 7,
TYPE_ACCELTABLE = 8,
TYPE_DDEACCESS = 9,
TYPE_DDECONV = 10,
TYPE_DDEXACT = 11,
TYPE_MONITOR = 12,
TYPE_KBDLAYOUT = 13,
TYPE_KBDFILE = 14,
TYPE_WINEVENTHOOK = 15,
TYPE_TIMER = 16,
TYPE_INPUTCONTEXT = 17,
TYPE_HIDDATA = 18,
TYPE_DEVICEINFO = 19,
TYPE_TOUCHINPUTINFO = 20,
TYPE_GESTUREINFOOBJ = 21,
TYPE_CTYPES = 22,
TYPE_GENERIC = 255
};
struct USER_MONITOR
{
EMULATOR_CAST(uint64_t, HMONITOR) hmon;
uint8_t unknown1[0x14];
RECT rcMonitor;
RECT rcWork;
union
{
struct
{
uint16_t monitorDpi;
uint16_t nativeDpi;
} b26;
struct
{
uint32_t unknown1;
uint16_t monitorDpi;
uint16_t nativeDpi;
uint16_t cachedDpi;
uint16_t unknown2;
RECT rcMonitorDpiAware;
} b20;
};
uint8_t unknown4[0xFF];
};
struct USER_WINDOW
{
uint8_t unknown[0xFF];
};
// NOLINTEND(modernize-use-using,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)

View File

@@ -160,6 +160,36 @@ namespace utils
this->write_span(std::span(vec));
}
void write_vector(const std::vector<bool>& vec)
{
this->write(static_cast<uint64_t>(vec.size()));
uint8_t byte = 0;
uint8_t bit_index = 0;
for (const bool b : vec)
{
if (b)
{
byte |= (1u << bit_index);
}
++bit_index;
if (bit_index == 8)
{
this->write<uint8_t>(byte);
byte = 0;
bit_index = 0;
}
}
if (bit_index != 0)
{
this->write<uint8_t>(byte);
}
}
template <typename T>
void write_list(const std::list<T>& vec)
{
@@ -385,6 +415,25 @@ namespace utils
}
}
void read_vector(std::vector<bool>& result)
{
const auto bit_count = this->read<uint64_t>();
result.clear();
result.reserve(static_cast<size_t>(bit_count));
const auto size = (bit_count + 7) / 8;
for (uint64_t i = 0; i < size; ++i)
{
const auto byte = this->read<uint8_t>();
for (uint8_t bit = 0; bit < 8 && result.size() < bit_count; ++bit)
{
result.push_back((byte >> bit) & 1u);
}
}
}
template <typename T>
std::vector<T> read_vector()
{

View File

@@ -509,6 +509,41 @@ namespace
return true;
}
bool test_monitor_info()
{
const POINT pt = {0, 0};
const auto hMonitor = MonitorFromPoint(pt, MONITOR_DEFAULTTOPRIMARY);
if (!hMonitor)
{
return false;
}
MONITORINFOEXA mi;
mi.cbSize = sizeof(mi);
if (!GetMonitorInfoA(hMonitor, &mi))
{
return false;
}
if (std::string_view(mi.szDevice) != R"(\\.\DISPLAY1)")
{
return false;
}
if (mi.rcMonitor.left != 0 || mi.rcMonitor.top != 0 || mi.rcMonitor.right != 1920 || mi.rcMonitor.bottom != 1080)
{
return false;
}
if (!(mi.dwFlags & MONITORINFOF_PRIMARY))
{
return false;
}
return true;
}
bool test_time_zone()
{
DYNAMIC_TIME_ZONE_INFORMATION current_dtzi = {};
@@ -908,6 +943,7 @@ int main(const int argc, const char* argv[])
RUN_TEST(test_working_directory, "Working Directory")
RUN_TEST(test_registry, "Registry")
RUN_TEST(test_system_info, "System Info")
RUN_TEST(test_monitor_info, "Monitor Info")
RUN_TEST(test_time_zone, "Time Zone")
RUN_TEST(test_threads, "Threads")
RUN_TEST(test_threads_winapi, "Threads WinAPI")

View File

@@ -21,6 +21,7 @@ struct handle_types
token,
window,
timer,
monitor,
};
};

View File

@@ -9,7 +9,13 @@ namespace
{
NTSTATUS handle_request(windows_emulator& win_emu, const lpc_request_context& c) override
{
// TODO: Fix this. This is broken and wrong.
uint32_t server_dll_index{};
win_emu.memory.read_memory(c.recv_buffer + 0x18, &server_dll_index, sizeof(server_dll_index));
if (server_dll_index != 3)
{
return STATUS_NOT_SUPPORTED;
}
try
{
@@ -17,8 +23,13 @@ namespace
const auto dest = data.read();
const auto base = dest.Base;
const auto value = base + 0x10;
win_emu.emu().write_memory(base + 8, &value, sizeof(value));
const emulator_object<USER_SHAREDINFO> shared_obj{win_emu.emu(), base + 8};
shared_obj.access([&](USER_SHAREDINFO& shared) {
shared.psi = win_emu.process.user_handles.get_server_info().value();
shared.aheList = win_emu.process.user_handles.get_handle_table().value();
shared.HeEntrySize = sizeof(USER_HANDLEENTRY);
shared.pDispInfo = win_emu.process.user_handles.get_display_info().value();
});
}
catch (...)
{

View File

@@ -176,12 +176,40 @@ namespace
return env_map;
}
uint32_t read_windows_build(registry_manager& registry)
{
const auto key = registry.get_key({R"(\Registry\Machine\Software\Microsoft\Windows NT\CurrentVersion)"});
if (!key)
{
return 0;
}
for (size_t i = 0; const auto value = registry.get_value(*key, i); ++i)
{
if (value->type != REG_SZ)
{
continue;
}
if (value->name == "CurrentBuildNumber" || value->name == "CurrentBuild")
{
const auto* s = reinterpret_cast<const char16_t*>(value->data.data());
return static_cast<uint32_t>(std::strtoul(u16_to_u8(s).c_str(), nullptr, 10));
}
}
return 0;
}
}
void process_context::setup(x86_64_emulator& emu, memory_manager& memory, registry_manager& registry,
const application_settings& app_settings, const mapped_module& executable, const mapped_module& ntdll,
const apiset::container& apiset_container, const mapped_module* ntdll32)
{
this->windows_build_number = read_windows_build(registry);
setup_gdt(emu, memory);
this->kusd.setup();
@@ -391,6 +419,34 @@ void process_context::setup(x86_64_emulator& emu, memory_manager& memory, regist
this->instrumentation_callback = 0;
this->default_register_set = emu.save_registers();
this->user_handles.setup();
auto [h, monitor_obj] = this->user_handles.allocate_object<USER_MONITOR>(handle_types::monitor);
this->default_monitor_handle = h;
monitor_obj.access([&](USER_MONITOR& monitor) {
monitor.hmon = h.bits;
monitor.rcMonitor = {.left = 0, .top = 0, .right = 1920, .bottom = 1080};
monitor.rcWork = monitor.rcMonitor;
if (this->is_older_windows_build())
{
monitor.b20.monitorDpi = 96;
monitor.b20.nativeDpi = monitor.b20.monitorDpi;
monitor.b20.cachedDpi = monitor.b20.monitorDpi;
monitor.b20.rcMonitorDpiAware = monitor.rcMonitor;
}
else
{
monitor.b26.monitorDpi = 96;
monitor.b26.nativeDpi = monitor.b26.monitorDpi;
}
});
const auto user_display_info = this->user_handles.get_display_info();
user_display_info.access([&](USER_DISPINFO& display_info) {
display_info.dwMonitorCount = 1;
display_info.pPrimaryMonitor = monitor_obj.value();
});
}
void process_context::serialize(utils::buffer_serializer& buffer) const
@@ -408,6 +464,7 @@ void process_context::serialize(utils::buffer_serializer& buffer) const
buffer.write(this->kusd);
buffer.write(this->is_wow64_process);
buffer.write(this->windows_build_number);
buffer.write(this->ntdll_image_base);
buffer.write(this->ldr_initialize_thunk);
buffer.write(this->rtl_user_thread_start);
@@ -416,6 +473,8 @@ void process_context::serialize(utils::buffer_serializer& buffer) const
buffer.write(this->ki_user_exception_dispatcher);
buffer.write(this->instrumentation_callback);
buffer.write(this->user_handles);
buffer.write(this->default_monitor_handle);
buffer.write(this->events);
buffer.write(this->files);
buffer.write(this->sections);
@@ -454,6 +513,7 @@ void process_context::deserialize(utils::buffer_deserializer& buffer)
buffer.read(this->kusd);
buffer.read(this->is_wow64_process);
buffer.read(this->windows_build_number);
buffer.read(this->ntdll_image_base);
buffer.read(this->ldr_initialize_thunk);
buffer.read(this->rtl_user_thread_start);
@@ -462,6 +522,8 @@ void process_context::deserialize(utils::buffer_deserializer& buffer)
buffer.read(this->ki_user_exception_dispatcher);
buffer.read(this->instrumentation_callback);
buffer.read(this->user_handles);
buffer.read(this->default_monitor_handle);
buffer.read(this->events);
buffer.read(this->files);
buffer.read(this->sections);

View File

@@ -14,6 +14,7 @@
#include "windows_objects.hpp"
#include "emulator_thread.hpp"
#include "port.hpp"
#include "user_handle_table.hpp"
#include "apiset/apiset.hpp"
@@ -66,7 +67,8 @@ struct process_context
base_allocator(emu),
peb64(emu),
process_params64(emu),
kusd(memory, clock)
kusd(memory, clock),
user_handles(memory)
{
}
@@ -86,10 +88,17 @@ struct process_context
void serialize(utils::buffer_serializer& buffer) const;
void deserialize(utils::buffer_deserializer& buffer);
generic_handle_store* get_handle_store(handle handle);
// WOW64 support flag - set during process setup based on executable architecture
bool is_wow64_process{false};
generic_handle_store* get_handle_store(handle handle);
uint32_t windows_build_number{0};
bool is_older_windows_build() const
{
return windows_build_number < 26040;
}
callbacks* callbacks_{};
@@ -118,6 +127,8 @@ struct process_context
std::optional<emulator_object<RTL_USER_PROCESS_PARAMETERS32>> process_params32;
std::optional<uint64_t> rtl_user_thread_start32{};
user_handle_table user_handles;
handle default_monitor_handle{};
handle_store<handle_types::event, event> events{};
handle_store<handle_types::file, file> files{};
handle_store<handle_types::section, section> sections{};
@@ -125,7 +136,7 @@ struct process_context
handle_store<handle_types::semaphore, semaphore> semaphores{};
handle_store<handle_types::port, port_container> ports{};
handle_store<handle_types::mutant, mutant> mutants{};
handle_store<handle_types::window, window> windows{};
user_handle_store<handle_types::window, window> windows{user_handles};
handle_store<handle_types::timer, timer> timers{};
handle_store<handle_types::registry, registry_key, 2> registry_keys{};
std::map<uint16_t, atom_entry> atoms{};

View File

@@ -466,7 +466,7 @@ namespace syscalls
return STATUS_INVALID_HANDLE;
}
if (auto* e = c.win_emu.process.events.get(event))
if (auto* e = c.proc.events.get(event))
{
e->signaled = false;
}
@@ -834,7 +834,7 @@ namespace syscalls
const hwnd /*parent*/, const hmenu /*menu*/, const hinstance /*instance*/, const pointer /*l_param*/,
const DWORD /*flags*/, const pointer /*acbi_buffer*/)
{
window win{};
auto [handle, win] = c.proc.windows.create(c.win_emu.memory);
win.x = x;
win.y = y;
win.width = width;
@@ -843,7 +843,7 @@ namespace syscalls
win.class_name = read_large_string(class_name);
win.name = read_large_string(window_name);
return c.proc.windows.store(std::move(win)).bits;
return handle.bits;
}
BOOL handle_NtUserDestroyWindow(const syscall_context& c, const hwnd window)
@@ -1030,6 +1030,19 @@ namespace syscalls
{
return STATUS_NOT_SUPPORTED;
}
BOOL handle_NtUserGetHDevName(const syscall_context& c, handle hdev, emulator_pointer device_name)
{
if (hdev != c.proc.default_monitor_handle)
{
return FALSE;
}
const std::u16string name = u"\\\\.\\DISPLAY1";
c.emu.write_memory(device_name, name.c_str(), (name.size() + 1) * sizeof(char16_t));
return TRUE;
}
}
void syscall_dispatcher::add_handlers(std::map<std::string, syscall_handler>& handler_mapping)
@@ -1243,6 +1256,7 @@ void syscall_dispatcher::add_handlers(std::map<std::string, syscall_handler>& ha
add_handler(NtSetInformationDebugObject);
add_handler(NtRemoveProcessDebug);
add_handler(NtNotifyChangeDirectoryFileEx);
add_handler(NtUserGetHDevName);
#undef add_handler
}

View File

@@ -0,0 +1,394 @@
#pragma once
#include "emulator_utils.hpp"
#include "handles.hpp"
class user_handle_table
{
public:
static constexpr uint32_t MAX_HANDLES = 0xFFFF;
user_handle_table(memory_manager& memory)
: memory_(&memory)
{
}
void setup()
{
used_indices_.resize(MAX_HANDLES, false);
const auto server_info_size = static_cast<size_t>(page_align_up(sizeof(USER_SERVERINFO)));
server_info_addr_ = memory_->allocate_memory(server_info_size, memory_permission::read);
const auto display_info_size = static_cast<size_t>(page_align_up(sizeof(USER_DISPINFO)));
display_info_addr_ = memory_->allocate_memory(display_info_size, memory_permission::read);
const emulator_object<USER_SERVERINFO> srv_obj(*memory_, server_info_addr_);
srv_obj.access([&](USER_SERVERINFO& srv) {
srv.cHandleEntries = MAX_HANDLES - 1; //
});
const auto handle_table_size = static_cast<size_t>(page_align_up(sizeof(USER_HANDLEENTRY) * MAX_HANDLES));
handle_table_addr_ = memory_->allocate_memory(handle_table_size, memory_permission::read);
}
emulator_object<USER_SHAREDINFO> get_server_info() const
{
return {*memory_, server_info_addr_};
}
emulator_object<USER_HANDLEENTRY> get_handle_table() const
{
return {*memory_, handle_table_addr_};
}
emulator_object<USER_DISPINFO> get_display_info() const
{
return {*memory_, display_info_addr_};
}
template <typename T>
std::pair<handle, emulator_object<T>> allocate_object(handle_types::type type)
{
const auto index = find_free_index();
const auto alloc_size = static_cast<size_t>(page_align_up(sizeof(T)));
const auto alloc_ptr = memory_->allocate_memory(alloc_size, memory_permission::read);
const emulator_object<T> alloc_obj(*memory_, alloc_ptr);
const emulator_object<USER_HANDLEENTRY> handle_table_obj(*memory_, handle_table_addr_);
handle_table_obj.access(
[&](USER_HANDLEENTRY& entry) {
entry.pHead = alloc_ptr;
entry.bType = get_native_type(type);
entry.wUniq = static_cast<uint16_t>(type << 7);
},
index);
used_indices_[index] = true;
return {make_handle(index, type, false), alloc_obj};
}
void free_index(uint32_t index)
{
if (index >= used_indices_.size() || !used_indices_[index])
{
return;
}
used_indices_[index] = false;
const emulator_object<USER_HANDLEENTRY> handle_table_obj(*memory_, handle_table_addr_);
handle_table_obj.access(
[&](USER_HANDLEENTRY& entry) {
memory_->release_memory(entry.pHead, 0);
entry = {};
},
index);
}
void serialize(utils::buffer_serializer& buffer) const
{
buffer.write(server_info_addr_);
buffer.write(handle_table_addr_);
buffer.write(display_info_addr_);
buffer.write_vector(used_indices_);
}
void deserialize(utils::buffer_deserializer& buffer)
{
buffer.read(server_info_addr_);
buffer.read(handle_table_addr_);
buffer.read(display_info_addr_);
buffer.read_vector(used_indices_);
}
private:
uint32_t find_free_index() const
{
for (uint32_t i = 1; i < used_indices_.size(); ++i)
{
if (!used_indices_[i])
{
return i;
}
}
throw std::runtime_error("No more user handles available");
}
static uint8_t get_native_type(handle_types::type type)
{
switch (type)
{
case handle_types::type::window:
return TYPE_WINDOW;
case handle_types::type::monitor:
return TYPE_MONITOR;
default:
throw std::runtime_error("Unhandled handle type!");
}
}
uint64_t server_info_addr_{};
uint64_t handle_table_addr_{};
uint64_t display_info_addr_{};
std::vector<bool> used_indices_{};
memory_manager* memory_{};
};
template <handle_types::type Type, typename T>
requires(utils::Serializable<T> && std::is_base_of_v<ref_counted_object, T>)
class user_handle_store : public generic_handle_store
{
public:
using index_type = uint32_t;
using value_map = std::map<index_type, T>;
explicit user_handle_store(user_handle_table& table)
: table_(&table)
{
}
std::pair<handle, T&> create(memory_interface& memory)
{
if (this->block_mutation_)
{
throw std::runtime_error("Mutation of user object store is blocked!");
}
auto [h, guest_obj] = table_->allocate_object<typename T::guest_type>(Type);
T new_obj(memory);
new_obj.guest = std::move(guest_obj);
const auto index = static_cast<uint32_t>(h.value.id);
const auto it = this->store_.emplace(index, std::move(new_obj)).first;
return {h, it->second};
}
bool block_mutation(bool blocked)
{
std::swap(this->block_mutation_, blocked);
return blocked;
}
handle make_handle(const index_type index) const
{
handle h{};
h.bits = 0;
h.value.is_pseudo = false;
h.value.type = Type;
h.value.id = index;
return h;
}
T* get_by_index(const uint32_t index)
{
const auto it = this->store_.find(index);
if (it == this->store_.end())
{
return nullptr;
}
return &it->second;
}
T* get(const handle_value h)
{
if (h.type != Type || h.is_pseudo)
{
return nullptr;
}
return this->get_by_index(static_cast<uint32_t>(h.id));
}
T* get(const handle h)
{
return this->get(h.value);
}
T* get(const uint64_t h)
{
handle hh{};
hh.bits = h;
return this->get(hh);
}
size_t size() const
{
return this->store_.size();
}
std::optional<handle> duplicate(const handle h) override
{
auto* entry = this->get(h);
if (!entry)
{
return std::nullopt;
}
++entry->ref_count;
return h;
}
std::pair<typename value_map::iterator, bool> erase(const typename value_map::iterator& entry)
{
if (this->block_mutation_)
{
throw std::runtime_error("Mutation of handle store is blocked!");
}
if (entry == this->store_.end())
{
return {entry, false};
}
if constexpr (handle_detail::has_deleter_function<T>())
{
if (!T::deleter(entry->second))
{
return {entry, true};
}
}
auto new_iter = this->store_.erase(entry);
return {new_iter, true};
}
bool erase(const handle_value h)
{
if (this->block_mutation_)
{
throw std::runtime_error("Mutation of user object store is blocked!");
}
if (h.type != Type || h.is_pseudo)
{
return false;
}
const auto index = static_cast<uint32_t>(h.id);
const auto entry = this->store_.find(index);
if (entry == this->store_.end())
{
return false;
}
if constexpr (handle_detail::has_deleter_function<T>())
{
if (!T::deleter(entry->second))
{
return false;
}
}
table_->free_index(index);
this->store_.erase(entry);
return true;
}
bool erase(const handle h) override
{
return this->erase(h.value);
}
bool erase(const uint64_t h)
{
handle hh{};
hh.bits = h;
return this->erase(hh);
}
bool erase(const T& value)
{
const auto entry = this->find(value);
if (entry == this->store_.end())
{
return false;
}
return this->erase(make_handle(entry->first));
}
typename value_map::iterator find(const T& value)
{
auto i = this->store_.begin();
for (; i != this->store_.end(); ++i)
{
if (&i->second == &value)
{
break;
}
}
return i;
}
typename value_map::const_iterator find(const T& value) const
{
auto i = this->store_.begin();
for (; i != this->store_.end(); ++i)
{
if (&i->second == &value)
{
break;
}
}
return i;
}
handle find_handle(const T& value) const
{
const auto entry = this->find(value);
if (entry == this->end())
{
return {};
}
return this->make_handle(entry->first);
}
handle find_handle(const T* value) const
{
if (!value)
{
return {};
}
return this->find_handle(*value);
}
typename value_map::iterator begin()
{
return this->store_.begin();
}
typename value_map::const_iterator begin() const
{
return this->store_.begin();
}
typename value_map::iterator end()
{
return this->store_.end();
}
typename value_map::const_iterator end() const
{
return this->store_.end();
}
void serialize(utils::buffer_serializer& buffer) const
{
buffer.write(this->block_mutation_);
buffer.write_map(this->store_);
}
void deserialize(utils::buffer_deserializer& buffer)
{
buffer.read(this->block_mutation_);
buffer.read_map(this->store_);
}
private:
user_handle_table* table_;
bool block_mutation_{false};
value_map store_{};
};

View File

@@ -54,19 +54,47 @@ struct event : ref_counted_object
}
};
struct window : ref_counted_object
template <typename GuestType>
struct user_object : ref_counted_object
{
using guest_type = GuestType;
emulator_object<GuestType> guest;
user_object(memory_interface& memory)
: guest(memory)
{
}
void serialize_object(utils::buffer_serializer& buffer) const override
{
buffer.write(this->guest);
}
void deserialize_object(utils::buffer_deserializer& buffer) override
{
buffer.read(this->guest);
}
};
struct window : user_object<USER_WINDOW>
{
uint32_t thread_id{};
std::u16string name{};
std::u16string class_name{};
int32_t width;
int32_t height;
int32_t x;
int32_t y;
int32_t width{};
int32_t height{};
int32_t x{};
int32_t y{};
std::unordered_map<std::u16string, uint64_t> props;
window(memory_interface& memory)
: user_object(memory)
{
}
void serialize_object(utils::buffer_serializer& buffer) const override
{
user_object::serialize_object(buffer);
buffer.write(this->thread_id);
buffer.write(this->name);
buffer.write(this->class_name);
@@ -79,6 +107,7 @@ struct window : ref_counted_object
void deserialize_object(utils::buffer_deserializer& buffer) override
{
user_object::deserialize_object(buffer);
buffer.read(this->thread_id);
buffer.read(this->name);
buffer.read(this->class_name);
@@ -186,7 +215,6 @@ struct file : ref_counted_object
utils::file_handle handle{};
std::u16string name{};
std::optional<file_enumeration_state> enumeration_state{};
std::optional<std::u16string> deferred_rename;
bool is_file() const
{