diff --git a/host/display.c b/host/display.c index 2bdf05ea4..42cea8a61 100644 --- a/host/display.c +++ b/host/display.c @@ -102,6 +102,13 @@ static LRESULT CALLBACK wndproc(HWND w, UINT msg, WPARAM wp, LPARAM lp) { switch (msg) { case WM_TIMER: return 0; + case WM_SYSCOMMAND: + if ((wp & 0xFFF0) == SC_SCREENSAVE && !(wp & SCF_ISSECURE) && + renderer && GetForegroundWindow() == w && !IsIconic(w)) { + BOOL secure = TRUE; /* unknown or protected policy keeps Windows' normal handling */ + if (SystemParametersInfoA(SPI_GETSCREENSAVESECURE, 0, &secure, 0) && !secure) return 0; + } + break; case WM_INPUT: { RAWINPUT ri; UINT size = sizeof ri; diff --git a/tests/run_all.py b/tests/run_all.py index e853b0b28..2bdcd1906 100644 --- a/tests/run_all.py +++ b/tests/run_all.py @@ -26,6 +26,7 @@ # Arguments some tests need. {name} is a tool or folder found below (find_tools); a check whose tools are missing is # skipped, or fails with --strict. Tests not listed run with no arguments and their own defaults. ARGS = { + "tests/test_screensaver.py": ["--cc", "gcc"], "tests/test_system_header_cache.py": ["--cc", "clang"], "tests/test_precompile_compute_guard.py": ["--cxx", "clang++"], "tests/test_input_background.py": ["--cc", "gcc"], diff --git a/tests/screensaver_test.c b/tests/screensaver_test.c new file mode 100644 index 000000000..599941f2c --- /dev/null +++ b/tests/screensaver_test.c @@ -0,0 +1,123 @@ +/* All APIs are mocks: no window, screen saver, security or power setting is accessed. */ +#include +#include +#include + +typedef void *HWND; +typedef void *HRAWINPUT; +typedef unsigned UINT; +typedef int BOOL; +typedef int32_t LONG; +typedef uintptr_t WPARAM; +typedef intptr_t LPARAM; +typedef intptr_t LRESULT; +#define CALLBACK +#define TRUE 1 +#define FALSE 0 +#define LOWORD(v) ((unsigned)(uintptr_t)(v) & 0xffff) +#define HIWORD(v) (((unsigned)(uintptr_t)(v) >> 16) & 0xffff) +enum { + WM_TIMER = 0x113, WM_SYSCOMMAND = 0x112, WM_INPUT = 0xff, WM_LBUTTONDOWN = 0x201, + WM_KEYDOWN = 0x100, WM_SYSKEYDOWN = 0x104, WM_SYSCHAR = 0x106, WM_APP = 0x8000, + WM_DPICHANGED = 0x2e0, WM_KILLFOCUS = 8, WM_CANCELMODE = 0x1f, WM_SETCURSOR = 0x20, + WM_MOVE = 3, WM_SIZE = 5, WM_PAINT = 0xf, WM_ERASEBKGND = 0x14, WM_CLOSE = 0x10, + VK_ESCAPE = 0x1b, VK_F11 = 0x7a, VK_RETURN = 0xd, HTCLIENT = 1, + SC_SCREENSAVE = 0xf140, SCF_ISSECURE = 1, SPI_GETSCREENSAVESECURE = 0x76, + SC_CLOSE = 0xf060, SC_MONITORPOWER = 0xf170, SC_MAXIMIZE = 0xf030, + SWP_NOZORDER = 4, SWP_NOACTIVATE = 0x10, RID_INPUT = 0x10000003, + RIM_TYPEMOUSE = 0, MOUSE_MOVE_ABSOLUTE = 1 +}; +typedef struct { LONG left, top, right, bottom; } RECT; +typedef struct { UINT dwType; } RAWINPUTHEADER; +typedef struct { + RAWINPUTHEADER header; + struct { struct { unsigned short usFlags; LONG lLastX, lLastY; } mouse; } data; +} RAWINPUT; + +#define CHECK(name, condition) do { if (!(condition)) { \ + fprintf(stderr, "FAIL: %s (line %d)\n", name, __LINE__); exit(1); } } while (0) +static HWND game = (HWND)(uintptr_t)1, foreground; +static int renderer, fullscreen, mouse_captured; +static volatile LONG mouse_dx, mouse_dy; +static int iconic, query_ok, query_writes, policy_secure, query_calls, forwarded, cases; +static UINT forwarded_msg; +static WPARAM forwarded_wp; +static LPARAM forwarded_lp; +static const LRESULT default_result = 1234; + +HWND GetForegroundWindow(void) { return foreground; } +BOOL IsIconic(HWND w) { CHECK("iconic query targets game", w == game); return iconic; } +BOOL SystemParametersInfoA(UINT action, UINT parameter, void *value, UINT flags) { + CHECK("only read the secure screen saver policy", action == SPI_GETSCREENSAVESECURE); + CHECK("no profile write/notification flags", parameter == 0 && flags == 0 && value != NULL); + query_calls++; + if (query_writes) *(BOOL *)value = policy_secure; + return query_ok; +} +static LRESULT DefWindowProcA(HWND w, UINT msg, WPARAM wp, LPARAM lp) { + CHECK("forward original window", w == game); + forwarded++; + forwarded_msg = msg; forwarded_wp = wp; forwarded_lp = lp; + return default_result; +} + +/* The complete procedure compiles, but these unrelated branches must never run in this fixture. */ +static void unexpected(void) { fputs("FAIL: unrelated window action executed\n", stderr); abort(); } +static UINT GetRawInputData(HRAWINPUT h, UINT command, void *data, UINT *size, UINT header) { + (void)h; (void)command; (void)data; (void)size; (void)header; unexpected(); return (UINT)-1; +} +static LONG InterlockedExchangeAdd(volatile LONG *target, LONG value) { + (void)target; (void)value; unexpected(); return 0; +} +static void mouse_capture(HWND w, int on) { (void)w; (void)on; unexpected(); } +static void set_fullscreen(HWND w, int on) { (void)w; (void)on; unexpected(); } +static void hos_vk_note_key_press(unsigned key) { (void)key; unexpected(); } +static BOOL SetWindowPos(HWND w, HWND after, int x, int y, int width, int height, UINT flags) { + (void)w; (void)after; (void)x; (void)y; (void)width; (void)height; (void)flags; unexpected(); return FALSE; +} +static void *SetCursor(void *cursor) { (void)cursor; unexpected(); return NULL; } +static void rx_resize(unsigned width, unsigned height) { (void)width; (void)height; unexpected(); } +static BOOL ValidateRect(HWND w, const RECT *rect) { (void)w; (void)rect; unexpected(); return FALSE; } +static void hos_log(const char *text) { (void)text; unexpected(); } +static void hos_log_flush(void) { unexpected(); } +static void ExitProcess(unsigned code) { (void)code; unexpected(); } + +/* ACTUAL_WNDPROC */ + +static void reset(void) { + renderer = 1; foreground = game; iconic = 0; + query_ok = 1; query_writes = 1; policy_secure = FALSE; + query_calls = forwarded = 0; +} +static void call(const char *name, UINT msg, WPARAM wp, int handled, int queries) { + const LPARAM lp = 0x5678; + const LRESULT result = wndproc(game, msg, wp, lp); + CHECK(name, result == (handled ? 0 : default_result)); + CHECK("forwarding count", forwarded == !handled); + CHECK("policy query count", query_calls == queries); + if (!handled) CHECK("forward original message and parameters", + forwarded_msg == msg && forwarded_wp == wp && forwarded_lp == lp); + cases++; +} +int main(void) { + reset(); call("foreground nonsecure saver is handled", WM_SYSCOMMAND, SC_SCREENSAVE, 1, 1); + reset(); call("secure request is forwarded", WM_SYSCOMMAND, SC_SCREENSAVE | SCF_ISSECURE, 0, 0); + reset(); call("secure request flag survives other low bits", WM_SYSCOMMAND, SC_SCREENSAVE | SCF_ISSECURE | 8, 0, 0); + reset(); policy_secure = TRUE; call("password policy is forwarded", WM_SYSCOMMAND, SC_SCREENSAVE, 0, 1); + reset(); policy_secure = 2; call("any true policy is forwarded", WM_SYSCOMMAND, SC_SCREENSAVE, 0, 1); + reset(); query_ok = 0; call("failed policy query is forwarded even if it wrote false", WM_SYSCOMMAND, SC_SCREENSAVE, 0, 1); + reset(); query_ok = 0; query_writes = 0; call("unknown policy is forwarded", WM_SYSCOMMAND, SC_SCREENSAVE, 0, 1); + reset(); query_writes = 0; call("unwritten policy stays secure", WM_SYSCOMMAND, SC_SCREENSAVE, 0, 1); + reset(); renderer = 0; call("no renderer is forwarded", WM_SYSCOMMAND, SC_SCREENSAVE, 0, 0); + reset(); foreground = (HWND)(uintptr_t)2; call("background game is forwarded", WM_SYSCOMMAND, SC_SCREENSAVE, 0, 0); + reset(); foreground = NULL; call("unknown foreground is forwarded", WM_SYSCOMMAND, SC_SCREENSAVE, 0, 0); + reset(); iconic = 1; call("minimized game is forwarded", WM_SYSCOMMAND, SC_SCREENSAVE, 0, 0); + reset(); call("close command is forwarded", WM_SYSCOMMAND, SC_CLOSE, 0, 0); + reset(); call("monitor power command is forwarded", WM_SYSCOMMAND, SC_MONITORPOWER, 0, 0); + reset(); call("maximize command is forwarded", WM_SYSCOMMAND, SC_MAXIMIZE, 0, 0); + reset(); call("unknown command is forwarded", WM_SYSCOMMAND, 0xf2e0, 0, 0); + reset(); call("zero command is forwarded", WM_SYSCOMMAND, 0, 0, 0); + reset(); call("unrelated message is forwarded", WM_APP + 99, 0xabc, 0, 0); + printf("PASS: %d actual-wndproc screensaver/secure-policy/forwarding cases; all APIs mocked\n", cases); + return 0; +} diff --git a/tests/test_screensaver.py b/tests/test_screensaver.py new file mode 100644 index 000000000..53c340d55 --- /dev/null +++ b/tests/test_screensaver.py @@ -0,0 +1,39 @@ +"""Compile the complete production window procedure with portable, authored Win32 API mocks.""" +import argparse +from pathlib import Path +import subprocess +import tempfile + + +ROOT = Path(__file__).resolve().parents[1] + + +def wndproc(source): + start = source.index("static LRESULT CALLBACK wndproc(") + end = source.index("{", start) + 1 + depth = 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[start:end] + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--cc", default="gcc") + parser.add_argument("--source", type=Path, default=ROOT / "host/display.c") + args = parser.parse_args() + procedure = wndproc(args.source.read_text(encoding="utf-8")) + template = (ROOT / "tests/screensaver_test.c").read_text(encoding="utf-8") + assert template.count("/* ACTUAL_WNDPROC */") == 1 + with tempfile.TemporaryDirectory(prefix="hookshot-screensaver-") as temp: + source, executable = Path(temp) / "fixture.c", Path(temp) / "fixture.exe" + source.write_text(template.replace("/* ACTUAL_WNDPROC */", procedure), encoding="utf-8") + priority = {"creationflags": getattr(subprocess, "BELOW_NORMAL_PRIORITY_CLASS", 0)} + subprocess.run([args.cc, "-std=c11", "-O0", "-Wall", "-Wextra", "-Werror", str(source), "-o", str(executable)], + check=True, **priority) + subprocess.run([str(executable)], check=True, **priority) + + +if __name__ == "__main__": + main()