diff --git a/common/platform/config.c b/common/platform/config.c index 7461430..4f948d0 100644 --- a/common/platform/config.c +++ b/common/platform/config.c @@ -44,6 +44,7 @@ void platform_config_load(struct platform_config *cfg, const wchar_t *filename) vfs_config_load(&cfg->vfs, filename); system_config_load(&cfg->system, filename); openssl_config_load(&cfg->openssl, filename); + ewf_config_load(&cfg->ewf, filename); } void amvideo_config_load(struct amvideo_config *cfg, const wchar_t *filename) @@ -374,3 +375,11 @@ void openssl_config_load(struct openssl_config *cfg, const wchar_t *filename) cfg->enable = GetPrivateProfileIntW(L"openssl", L"enable", 1, filename); cfg->override = GetPrivateProfileIntW(L"openssl", L"override", 0, filename); } + +void ewf_config_load(struct ewf_config* cfg, const wchar_t* filename) { + assert(cfg != NULL); + assert(filename != NULL); + + cfg->enable = GetPrivateProfileIntW(L"ewf", L"enable", 0, filename); + cfg->full = GetPrivateProfileIntW(L"ewf", L"full", 0, filename); +} diff --git a/common/platform/config.h b/common/platform/config.h index efad5a3..db6aad8 100644 --- a/common/platform/config.h +++ b/common/platform/config.h @@ -38,3 +38,4 @@ void pcbid_config_load(struct pcbid_config *cfg, const wchar_t *filename); void vfs_config_load(struct vfs_config *cfg, const wchar_t *filename); void system_config_load(struct system_config *cfg, const wchar_t *filename); void openssl_config_load(struct openssl_config *cfg, const wchar_t *filename); +void ewf_config_load(struct ewf_config *cfg, const wchar_t *filename); diff --git a/common/platform/ewf.c b/common/platform/ewf.c new file mode 100644 index 0000000..800e3cd --- /dev/null +++ b/common/platform/ewf.c @@ -0,0 +1,558 @@ +#include +#include + +#include +#include +#include +#include +#include + +#include "hook/iohook.h" +#include "hook/procaddr.h" + +#include "platform/ewf.h" + +#include + +#include "hooklib/path.h" +#include "util/dprintf.h" +#include "util/dump.h" + +/* EWF hook */ + +const static struct ewf_config* ewf_config; +static struct ewf_virtual_file virtual_file_table[EWF_MAX_VIRTUAL_FILES] = {0}; +static struct ewf_real_handle handle_table[EWF_MAX_HANDLES] = {0}; +static CRITICAL_SECTION file_table_lock; +static CRITICAL_SECTION handle_table_lock; +static wchar_t windows_directory[MAX_PATH]; + +static const wchar_t* default_drive = L"C:\\"; +static const wchar_t default_paths[][MAX_PATH] = { + L"alib.conf", + L"cacert.pem", + L"first_ar.conf", + L"last_pras.log", + L"last_shime.log", + L"play_history.csv" +}; + +/* Helper functions */ + +static HRESULT atow(const char* string, wchar_t** result) { + if (string == NULL) { + *result = NULL; + return S_FALSE; + } + const size_t n = strlen(string); + wchar_t* widestring = malloc((n + 1) * sizeof(wchar_t)); + if (widestring == NULL) { + return E_OUTOFMEMORY; + } + mbstowcs_s(NULL, widestring, n + 1, string, n); + + *result = widestring; + return S_OK; +} + +static inline bool wprefix(const wchar_t* pre, const wchar_t* str) { + return wcsncmp(pre, str, wcslen(pre)) == 0; +} + +static inline bool wsuffix(const wchar_t* suffix, const wchar_t* str) { + if (str == NULL || suffix == NULL) { + return false; + } + const size_t str_len = wcslen(str); + const size_t suf_len = wcslen(suffix); + if (suf_len > str_len) { + return false; + } + return wcsncmp(str + str_len - suf_len, suffix, suf_len) == 0; +} + + +static BOOL ewf_needs_virtualization(const wchar_t* path) { + if (path == NULL) { + return FALSE; + } + if (ewf_config->full) { + if (wprefix(default_drive, path) && !wprefix(windows_directory, path)) { + return TRUE; + } + } else { + for (int i = 0; i < _countof(default_paths); i++) { + if (wsuffix(default_paths[i], path)) { + return TRUE; + } + } + } + return FALSE; +} + +static struct ewf_virtual_file* ewf_get_virtual_file(HANDLE handle) { + struct ewf_virtual_file* ret = NULL; + EnterCriticalSection(&file_table_lock); + for (int i = 0; i < EWF_MAX_VIRTUAL_FILES; i++) { + if (virtual_file_table[i].virtual_handle == handle) { + ret = &virtual_file_table[i]; + break; + } + } + LeaveCriticalSection(&file_table_lock); + return ret; +} + +static struct ewf_virtual_file* ewf_find_virtual_file(const wchar_t* path) { + if (path == NULL) { + return NULL; + } + HANDLE ret = NULL; + EnterCriticalSection(&file_table_lock); + for (int i = 0; i < EWF_MAX_VIRTUAL_FILES; i++) { + if (virtual_file_table[i].virtual_handle != NULL && wcscmp(virtual_file_table[i].path, path) == 0) { + ret = &virtual_file_table[i]; + break; + } + } + LeaveCriticalSection(&file_table_lock); + return ret; +} + +static struct ewf_virtual_file* ewf_create_virtual_file(const wchar_t* path) { + assert(path != NULL); + struct ewf_virtual_file* ret = NULL; + EnterCriticalSection(&file_table_lock); + for (int i = 0; i < EWF_MAX_VIRTUAL_FILES; i++) { + if (virtual_file_table[i].virtual_handle == NULL) { + struct ewf_virtual_file* h = &virtual_file_table[i]; + HRESULT hr = iohook_open_nul_fd(&h->virtual_handle); + if (!SUCCEEDED(hr)) { + dprintf("EWF: Could not create handle: Failed to get NUL handle: %lx", hr); + break; + } + dprintf("EWF: Created virtual file: %ls\n", path); + wcscpy_s(h->path, MAX_PATH, path); + h->length = 0; + h->data = NULL; + ret = h; + break; + } + } + if (ret == NULL) { + dprintf("EWF: Could not create handle: Too many virtualized files\n"); + } + LeaveCriticalSection(&file_table_lock); + return ret; +} + +static BOOL ewf_delete_virtual_file(HANDLE virtual_handle) { + if (virtual_handle == NULL) { + return TRUE; + } + struct ewf_virtual_file* match = NULL; + EnterCriticalSection(&file_table_lock); + for (int i = 0; i < EWF_MAX_VIRTUAL_FILES; i++) { + if (virtual_file_table[i].virtual_handle == virtual_handle) { + match = &virtual_file_table[i]; + break; + } + } + LeaveCriticalSection(&file_table_lock); + if (match) { + match->virtual_handle = NULL; +#if LOG_EWF + dprintf("EWF: Deleted file: %ls\n", match->path); +#endif + return CloseHandle(virtual_handle); + } else { + SetLastError(ERROR_INVALID_HANDLE); + return FALSE; + } +} + +static struct ewf_real_handle* ewf_open_virtual_file(HANDLE virtual_handle) { + struct ewf_virtual_file* file = ewf_get_virtual_file(virtual_handle); + struct ewf_real_handle* h = NULL; + if (file == NULL) { + dprintf("EWF: open failed: invalid handle\n"); + return NULL; + } + EnterCriticalSection(&handle_table_lock); + for (int i = 0; i < EWF_MAX_HANDLES; i++) { + if (handle_table[i].real_handle == NULL) { + h = &handle_table[i]; + HRESULT hr = iohook_open_nul_fd(&h->real_handle); + if (!SUCCEEDED(hr)) { + dprintf("EWF: Could not create handle: Failed to get NUL handle: %lx", hr); + break; + } + h->virtual_file = file; + h->offset = 0; +#if LOG_EWF + dprintf("EWF: Virtual file opened: %ls\n", file->path); +#endif + break; + } + } + LeaveCriticalSection(&handle_table_lock); + if (h == NULL) { + dprintf("EWF: Could not create handle: Too many open files\n"); + } + return h; +} + +static struct ewf_real_handle* ewf_get_real_handle(HANDLE real_handle) { + if (real_handle == NULL) { + return NULL; + } + struct ewf_real_handle* match = NULL; + EnterCriticalSection(&handle_table_lock); + for (int i = 0; i < EWF_MAX_HANDLES; i++) { + if (handle_table[i].real_handle == real_handle) { + match = &handle_table[i]; + break; + } + } + LeaveCriticalSection(&handle_table_lock); + return match; +} + +static BOOL ewf_close_virtual_file(HANDLE real_handle) { + const struct ewf_virtual_file* match = NULL; + EnterCriticalSection(&handle_table_lock); + for (int i = 0; i < EWF_MAX_HANDLES; i++) { + if (handle_table[i].real_handle == real_handle) { + match = handle_table[i].virtual_file; + handle_table[i].real_handle = NULL; + break; + } + } + LeaveCriticalSection(&handle_table_lock); + if (match != NULL) { +#if LOG_EWF + dprintf("EWF: Virtual file closed: %ls\n", match->path); +#endif + return CloseHandle(real_handle); + } else { + SetLastError(ERROR_INVALID_HANDLE); + return FALSE; + } +} + +/* Hooks */ + +static int __cdecl hook_stat64i32( + const char* path, + struct _stat64i32* buffer +); + +static int (__cdecl *next_stat64i32)( + const char* path, + struct _stat64i32* buffer); + +static const struct hook_symbol ewf_hook_syms[] = { + { + .name = "__imp__stat64i32", + .patch = hook_stat64i32, + .link = (void **) &next_stat64i32, + }, + { + .name = "_stat64i32", + .patch = hook_stat64i32, + .link = (void **) &next_stat64i32, + } +}; + +/* EWF hook main functions */ + +static HRESULT ewf_handle_irp(struct irp* irp); + +static HRESULT ewf_handle_open(struct irp* irp); + +static HRESULT ewf_handle_close(const struct irp* irp, const struct ewf_real_handle* file); + +static HRESULT ewf_handle_read(struct irp* irp, struct ewf_real_handle* handle); + +static HRESULT ewf_handle_write(const struct irp* irp, const struct ewf_real_handle* file); + +void ewf_hook_insert_hooks(HMODULE target) { + hook_table_apply( + target, + "msvcr110d.dll", + ewf_hook_syms, + _countof(ewf_hook_syms)); + hook_table_apply( + target, + "msvcr110.dll", + ewf_hook_syms, + _countof(ewf_hook_syms)); +} + +HRESULT ewf_hook_init(const struct ewf_config* config) { + assert(config != NULL); + + ewf_config = config; + + if (!config->enable) { + return S_FALSE; + } + + GetWindowsDirectoryW(windows_directory, MAX_PATH); + + if (config->full) { + wchar_t executable_path[MAX_PATH]; + GetModuleFileNameW(NULL, executable_path, MAX_PATH); + if (wprefix(default_drive, executable_path)) { + dprintf("FATAL: EWF full virtualization cannot be enabled with the game executable being located on %ls\n", + default_drive); + return E_FAIL; + } + } + + dprintf("EWF: Virtualizing disk I/O to %s\n", + config->full ? "the entirety of the C:\\ drive" : "the ALPB billing directory"); + + InitializeCriticalSection(&handle_table_lock); + InitializeCriticalSection(&file_table_lock); + + ewf_hook_insert_hooks(NULL); + + return iohook_push_handler(ewf_handle_irp); +} + +static HRESULT ewf_handle_irp(struct irp* irp) { + assert(irp != NULL); + + struct ewf_real_handle* h = NULL; + + if (irp->op == IRP_OP_OPEN) { + if (!ewf_needs_virtualization(irp->open_filename)) { + return iohook_invoke_next(irp); + } + } else { + h = ewf_get_real_handle(irp->fd); + if (h == NULL) { + return iohook_invoke_next(irp); + } + } + + + switch (irp->op) { + case IRP_OP_OPEN: return ewf_handle_open(irp); + case IRP_OP_WRITE: return ewf_handle_write(irp, h); + case IRP_OP_READ: return ewf_handle_read(irp, h); + case IRP_OP_CLOSE: return ewf_handle_close(irp, h); + default: return HRESULT_FROM_WIN32(ERROR_INVALID_FUNCTION); + } +} + +static HRESULT ewf_handle_open(struct irp* irp) { + assert(irp != NULL); + + if (irp->ovl != NULL) { + dprintf("EWF: async file operations not supported\n"); + return E_NOTIMPL; + } + + struct ewf_virtual_file* f = ewf_find_virtual_file(irp->open_filename); + + if (irp->open_creation == CREATE_NEW) { + if (f != NULL) { + return HRESULT_FROM_WIN32(ERROR_FILE_EXISTS); + } else { + f = ewf_create_virtual_file(irp->open_filename); + } + } else if (irp->open_creation == CREATE_ALWAYS) { + if (f != NULL) { + SetLastError(ERROR_ALREADY_EXISTS); +#if LOG_EWF + dprintf("EWF: File was truncated\n"); +#endif + f->length = 0; + free(f->data); + } else { + f = ewf_create_virtual_file(irp->open_filename); + } + } else if (irp->open_creation == OPEN_EXISTING) { + if (f == NULL) { + return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); + } + } else if (irp->open_creation == OPEN_ALWAYS) { + if (f != NULL) { + SetLastError(ERROR_ALREADY_EXISTS); + } else { + f = ewf_create_virtual_file(irp->open_filename); + } + } else if (irp->open_creation == TRUNCATE_EXISTING) { + if (f != NULL) { +#if LOG_EWF + dprintf("EWF: File was truncated\n"); +#endif + f->length = 0; + free(f->data); + } else { + return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); + } + } else { + return E_INVALIDARG; + } + + const struct ewf_real_handle* h = ewf_open_virtual_file(f->virtual_handle); + irp->fd = h->real_handle; + return S_OK; +} + +static HRESULT ewf_handle_close(const struct irp* irp, const struct ewf_real_handle* file) { + assert(irp != NULL); + assert(file != NULL); + + return ewf_close_virtual_file(file->real_handle) ? S_OK : E_FAIL; +} + +static HRESULT ewf_handle_read(struct irp* irp, struct ewf_real_handle* handle) { + assert(irp != NULL); + assert(handle != NULL); + + const struct ewf_virtual_file* f = handle->virtual_file; + size_t pos = handle->offset; + size_t to_read = irp->read.nbytes; + size_t max = f->length; + + if (pos > max || pos < 0) { + dprintf("EWF: Out-of-bounds read from %ls\n", f->path); + return HRESULT_FROM_WIN32(ERROR_HANDLE_EOF); + } + if (max == 0 || pos >= max || to_read == 0) { +#if LOG_EWF + dprintf("EWF: Zero read of %d at %d/%d from %ls\n", (int) to_read, (int) pos, (int) max, f->path); +#endif + irp->read.pos = 0; + return S_FALSE; + } + + if (pos + to_read > max) { + to_read = max - pos; + } + + memcpy(irp->read.bytes, f->data, to_read); + irp->read.pos = to_read; + handle->offset += to_read; + +#if LOG_EWF + dprintf("EWF: Read %d/%d bytes (offset %d, max %d) from %ls\n", (int) to_read, (int) irp->read.nbytes, (int) pos, + (int) f->length, f->path); + dump_iobuf(&irp->read); +#endif + + return S_OK; +} + +static HRESULT ewf_handle_write(const struct irp* irp, const struct ewf_real_handle* file) { + assert(irp != NULL); + assert(file != NULL); + + struct ewf_virtual_file* f = file->virtual_file; + const size_t n = irp->write.nbytes; + const void* data = irp->write.bytes; + + if (f->length == 0) { + const size_t initial_buf = max(n, EWF_DEFAULT_FILE_BUFFER_SIZE); + f->data = malloc(initial_buf); + if (f->data == NULL) { + return E_OUTOFMEMORY; + } + f->alloc_length = initial_buf; + } else if (f->length + n > f->alloc_length) { + void* ptr = realloc(f->data, f->alloc_length + n); + if (ptr == NULL) { + return E_OUTOFMEMORY; + } + f->data = ptr; + f->alloc_length = f->alloc_length + n; + } + + if (memcpy_s((char *) f->data + f->length, f->alloc_length - f->length, data, n) != 0) { + dprintf("EWF: Failed to copy %d bytes at offset %d (max %d)\n", (int) n, (int) f->length, + (int) f->alloc_length); + return E_NOT_SUFFICIENT_BUFFER; + } + f->length += n; + +#if LOG_EWF + dprintf("EWF: Write %d bytes to %ls\n", (int) n, f->path); + dump_const_iobuf(&irp->write); + dprintf("EWF: File content (%d, allocated %d)\n", (int) f->length, (int) f->alloc_length); + dump(f->data, f->length); +#endif + + return S_OK; +} + +// MSVC implementation detail. amdaemon depends on this succeeding, or you will be spammed with "cannot get accounting info" +static int __cdecl hook_stat64i32( + const char* path, + struct _stat64i32* buffer +) { + if (buffer == NULL || path == NULL) { + _set_errno(EINVAL); + return -1; + } + + wchar_t* wpath = NULL; + wchar_t* trans; + BOOL ok; + + HRESULT result = atow(path, &wpath); + if (!SUCCEEDED(result)) { + _set_errno(ENOMEM); + return -1; + } + + ok = path_transform_w(&trans, wpath); + + if (!ok) { + _set_errno(EINVAL); + return -1; + } + + wchar_t* target_path = trans ? trans : wpath; + + if (!ewf_needs_virtualization(target_path)) { + free(trans); + free(wpath); +#if LOG_EWF + dprintf("EWF: stat64i32: ignore: %s\n", path); +#endif + return next_stat64i32(path, buffer); + } + + const struct ewf_virtual_file* f = ewf_find_virtual_file(target_path); + if (f == NULL) { + free(trans); + free(wpath); + dprintf("EWF: stat64i32: File not found: %s\n", path); + _set_errno(ENOENT); + return -1; + } + + buffer->st_gid = 0; + buffer->st_atime = time(NULL); + buffer->st_ctime = time(NULL); + buffer->st_dev = 0; + buffer->st_ino = 0; + buffer->st_mode = _S_IFREG; + buffer->st_mtime = time(NULL); + buffer->st_nlink = 1; + buffer->st_rdev = 0; + buffer->st_size = (_off_t) f->length; + buffer->st_uid = 0; + +#if LOG_EWF + dprintf("EWF: stat64i32: file length of %s: %d\n", path, (int) f->length); +#endif + free(trans); + free(wpath); + + return 0; +} diff --git a/common/platform/ewf.h b/common/platform/ewf.h new file mode 100644 index 0000000..2d7fc63 --- /dev/null +++ b/common/platform/ewf.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include +#include + +#define EWF_MAX_VIRTUAL_FILES 255 +#define EWF_MAX_HANDLES 50000 +#define EWF_DEFAULT_FILE_BUFFER_SIZE 1024 + +struct ewf_config { + bool enable; + bool full; +}; + +// A virtual file that we are storing in memory. +struct ewf_virtual_file { + // The handle that we use internally. This is NULL for a handle that doesn't exist and non-NULL for a handle that maps to a virtual file. + HANDLE virtual_handle; + // The path that is being virtualized (ex. C:\sample.txt). + wchar_t path[MAX_PATH]; + // The current length of the virtual file. + size_t length; + // The current length of the allocated data buffer. + size_t alloc_length; + // Pointer to some buffer holding file data. This is guaranteed to be at least length bytes in size. If length is zero, this may be NULL. + void* data; +}; + +// An open handle to a ewf_virtual_file. +struct ewf_real_handle { + // The "real" handle passed to the application as a result of CreateFile, etc. + HANDLE real_handle; + // The virtual file the real handle points to. + struct ewf_virtual_file* virtual_file; + // The current read offset. + size_t offset; +}; + +HRESULT ewf_hook_init(const struct ewf_config* config); diff --git a/common/platform/meson.build b/common/platform/meson.build index 0e51ddd..8f90208 100644 --- a/common/platform/meson.build +++ b/common/platform/meson.build @@ -4,6 +4,7 @@ platform_lib = static_library( implicit_include_directories : false, dependencies : [ capnhook.get_variable('hook_dep'), + capnhook.get_variable('hooklib_dep'), shlwapi_lib, bcrypt_lib, ], @@ -18,6 +19,8 @@ platform_lib = static_library( 'dns.h', 'epay.c', 'epay.h', + 'ewf.c', + 'ewf.h', 'hwmon.c', 'hwmon.h', 'hwreset.c', diff --git a/common/platform/platform.c b/common/platform/platform.c index d1f380d..2558809 100644 --- a/common/platform/platform.c +++ b/common/platform/platform.c @@ -101,5 +101,11 @@ HRESULT platform_hook_init( return hr; } + hr = ewf_hook_init(&cfg->ewf); + + if (FAILED(hr)) { + return hr; + } + return S_OK; } diff --git a/common/platform/platform.h b/common/platform/platform.h index dc8dfc2..0f01eaf 100644 --- a/common/platform/platform.h +++ b/common/platform/platform.h @@ -2,6 +2,7 @@ #include +#include "ewf.h" #include "platform/amvideo.h" #include "platform/clock.h" #include "platform/dns.h" @@ -30,6 +31,7 @@ struct platform_config { struct vfs_config vfs; struct system_config system; struct openssl_config openssl; + struct ewf_config ewf; }; HRESULT platform_hook_init( diff --git a/common/platform/vfs.c b/common/platform/vfs.c index 372add1..22f0583 100644 --- a/common/platform/vfs.c +++ b/common/platform/vfs.c @@ -14,6 +14,7 @@ #include "platform/vfs.h" +#include "ewf.h" #include "util/dprintf.h" static void vfs_fixup_path(wchar_t *path, size_t max_count); diff --git a/doc/config/common.md b/doc/config/common.md index 1be9b7c..3e9361e 100644 --- a/doc/config/common.md +++ b/doc/config/common.md @@ -694,3 +694,31 @@ Default: `0` Enables the override to always hook the OpenSSL env variable. By default the hook is only applied to Intel CPUs with the SHA extension present. + +## `[ewf]` + +Configure Enhanced Write Filter / Unified Write Filter emulation. + +### `enable` + +Default: `0` + +Enables the EWF/UWF hook. This will cause anything the game writes to the following paths to become virtualized to +reduce disk I/O: + +* C:\Users\AppUser\temp\alib.conf +* C:\Users\AppUser\temp\cacert.pem +* C:\Users\AppUser\temp\first_ar.conf +* C:\Users\AppUser\temp\last_pras.log +* C:\Users\AppUser\temp\last_shime.log +* C:\Users\AppUser\temp\play_history.csv +* and their equivalent Windows XP variants + +### `full` + +Default: `0` + +Enabling full virtualization will virtualize ALL writes to the C:\ drive. Using this setting is highly discouraged +unless you know what you're doing! + +The Windows directory is always excluded from virtualization. diff --git a/meson.build b/meson.build index 76e9577..b4376bb 100644 --- a/meson.build +++ b/meson.build @@ -85,6 +85,9 @@ endif if get_option('log_all') or get_option('log_vfs') add_project_arguments('-DLOG_VFS', language: 'c') endif +if get_option('log_all') or get_option('log_ewf') + add_project_arguments('-DLOG_EWF', language : 'c') +endif shlwapi_lib = cc.find_library('shlwapi') dinput8_lib = cc.find_library('dinput8') diff --git a/meson_options.txt b/meson_options.txt index b6989f4..7cce000 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -78,3 +78,8 @@ option('log_vfs', value : false, description : 'Enable debug logging for file system redirections' ) +option('log_ewf', + type : 'boolean', + value : false, + description : 'Enable debug logging for file system virtualization' +)