From 15ebc3d8eed96a75163c1898bac618ecd8d2c7b7 Mon Sep 17 00:00:00 2001 From: 125hz <133181124+125hz@users.noreply.github.com> Date: Tue, 3 Nov 2026 18:56:53 -0600 Subject: [PATCH] Social Club: the renderer bridge for helper build 2.4.0.296 Social Club updated to 2.4.0.296 on 2026-11-03 and its launcher no longer loaded its pages under Madeira: [sc-rph] recognised one helper build only (TimeDateStamp 0x6a86d563), so SocialClubHelper.exe ran unpatched under --single-process, the launcher's UI waited for a page that never became ready and gave up with a website-load-failed error. The launcher reinstalls the current client, so there is no going back to the old one. The patch is now a table of analysed builds (ios_sch_builds): per build its RVAs, the SHA-256 of every code range relied on, the code it installs, its hook sites and the .data ranges that must be zero. The first build's entry writes exactly the bytes it wrote before. 2.4.0.296's entry installs the same bridge: the GetRenderProcessHandler thunk, the renderer MessageLoop adapter with its task callbacks, the renderer drain and the IPC split (separate renderer manager, retained page channels, 64-slot bound). That build leaves 792 bytes past .text's VirtualSize (the first 3.2 KB), so its variant carries no diagnostic traces (RunState, IPC rebind, UI notifications), uses compact Win64 forms for the manager getter, destructor and direct-load hooks, and places the four 5-11 byte task callbacks and two 15-byte IPC stubs in int3 padding between functions (a RET before, the next .pdata function after, checked as all-int3 and by digest before anything is written). Where 2.4.0.296's code differs (browser-only handler registration in Initialize, Channel Response skipping a Connect to the current channel, the capture site's displaced instruction), the digests cover the new bytes and the stubs replay the new form. Build selection is by TimeDateStamp and SizeOfImage; a mismatch logs the stamp, the size and the reason and writes nothing. No helper bytes are in the source, only digests. MADEIRA_SC_RENDER_HANDLER=0, MADEIRA_SC_CEF=0 and MADEIRA_SC_IPC_SPLIT=0 apply to both builds. Tests: check-sc-render-loop, -ipc-split and -render-handler run once per build (sc-render-handler.S is the thunk's source); check-sc-runstate, -ui-close and -ipc-rebind confirm the 2.4.0.296 build has no trace. New check-sc-helper-binary.py maps a real helper the way the loader does (never runs it), checks every digest, and runs the guard and the patch on the copy, in both bases with the IPC split on and off, then checks that switches, another image name and one-byte changes in any window write nothing: MADEIRA_SC_HELPER_EXE names the helper; without it the test is skipped. The 2.4.0.296 helper passes it; the device run is still to come. Co-Authored-By: Claude Fable 5.1 --- build/ntdll-unix/virtual_ios.c | 660 ++++++++++++++++++++------ tests/host/check-sc-helper-binary.py | 414 ++++++++++++++++ tests/host/check-sc-ipc-rebind.py | 15 + tests/host/check-sc-ipc-split.py | 447 +++++++++++------ tests/host/check-sc-render-handler.py | 236 +++++---- tests/host/check-sc-render-loop.py | 489 ++++++++++++------- tests/host/check-sc-runstate.py | 13 + tests/host/check-sc-ui-close.py | 15 + tests/host/sc-ipc-split.S | 99 ++++ tests/host/sc-render-handler.S | 43 ++ tests/host/sc-render-loop.S | 20 +- 11 files changed, 1896 insertions(+), 555 deletions(-) create mode 100644 tests/host/check-sc-helper-binary.py create mode 100644 tests/host/sc-render-handler.S diff --git a/build/ntdll-unix/virtual_ios.c b/build/ntdll-unix/virtual_ios.c index dacaa96bda..06cd0d1d6d 100644 --- a/build/ntdll-unix/virtual_ios.c +++ b/build/ntdll-unix/virtual_ios.c @@ -15812,11 +15812,14 @@ static int ios_sc_current_is_helper(void) * and offline content" (SC_INIT_ERR_WEBSITE_FAILED_LOAD). A renderer process of * its own cannot exist here: layout 2 has room for one Chromium. * - * For the one helper build analysed (TimeDateStamp 0x6a86d563, SizeOfImage - * 0x244000) every byte this relies on is checked first. Then a 112-byte x64 - * thunk goes into the zero tail of .text (past SizeOfRawData, cleared by the - * mapper above) and the browser app's GetRenderProcessHandler vtable slot - * points to it. The thunk builds the helper's own renderer app once (its + * Two helper builds are analysed, each one ios_sch_build entry below (its + * RVAs, the SHA-256 of every code range relied on, its blobs and hooks): the + * first one (TimeDateStamp 0x6a86d563, SizeOfImage 0x244000, device-proven) + * and Social Club 2.4.0.296's (0x6ae5e38b, 0x26d000). For the build found + * every byte this relies on is checked first. Then a 112-byte x64 thunk goes + * into the zero tail of .text (past VirtualSize: zero in the file or cleared + * by the mapper above) and the browser app's GetRenderProcessHandler vtable + * slot points to it. The thunk builds the helper's own renderer app once (its * operator new and constructor), keeps one reference to it in the zero tail of * .data's last page, and returns that app's handler through the helper's own * getter (which adds the caller's reference): @@ -15839,7 +15842,19 @@ static int ios_sc_current_is_helper(void) * vtable slots, so the slots must be written in their current base. Read that * base from a slot instead of assuming it is ptr. New pointers in the unused * .data tail have no DIR64 entries: those must name the mapped view directly - * so the normal PE-to-JIT alias translation can resolve them. */ + * so the normal PE-to-JIT alias translation can resolve them. + * + * 2.4.0.296 leaves 792 bytes past .text's VirtualSize (the first build 3.2 KB) + * and no other zero run in .text; making another section executable would rest + * on untested FEX behaviour. So its variant carries only what the bridge needs: + * no RunState, IPC rebind or UI notification traces and no manager creation + * line in the helper's log. Its manager getter and destructor hooks are plain + * Win64 code (the getter is only ever CALLed, the destructor hook site has no + * live volatile register but RCX, both guarded). Its four task callbacks and + * two 15-byte IPC stubs sit in int3 padding between functions: a RET before, + * the next .pdata function right after, nothing referencing it, checked as + * all-int3 and by digest. That padding is .text, executed like the tail. The + * first build's bytes and hooks are unchanged. */ #define IOS_SCH_IMAGE_BASE 0x140000000ULL /* the helper's preferred base */ #define IOS_SCH_STAMP 0x6a86d563u #define IOS_SCH_SIZE 0x244000u @@ -15894,6 +15909,50 @@ static int ios_sc_current_is_helper(void) #define IOS_SCH_UI_COUNT 0x1d0e60u #define IOS_SCH_UI_DELEGATE_CALLER 0x320afu +/* Social Club 2.4.0.296: the same roles, moved; slots 3 / 4 as above. */ +#define IOS_SCH296_STAMP 0x6ae5e38bu +#define IOS_SCH296_SIZE 0x26d000u +#define IOS_SCH296_TEXT_VS 0x1a2ce8u /* .text at 0x1000, raw 0x1a2e00 */ +#define IOS_SCH296_TEXT_RAW 0x1a2e00u +#define IOS_SCH296_DATA 0x1f4000u /* .data: VirtualSize 0x436c */ +#define IOS_SCH296_DATA_VS 0x436cu +#define IOS_SCH296_THUNK 0x1a3cf0u /* tail 0x1a3ce8..0x1a4000 */ +#define IOS_SCH296_CACHE 0x1f8ff8u +#define IOS_SCH296_VT_BROWSER 0x1a5288u +#define IOS_SCH296_VT_RENDERER 0x1a5678u +#define IOS_SCH296_GET_HANDLER 0x5ae0u +#define IOS_SCH296_GET_NULL 0x4a90u +#define IOS_SCH296_VT_RPH 0x1a56a8u +#define IOS_SCH296_WEBKIT 0x8880u +#define IOS_SCH296_CURRENT_LOOP 0x196cd0u +#define IOS_SCH296_LOOP_CTOR 0x195630u +#define IOS_SCH296_LOOP_WORK 0x1960f0u +#define IOS_SCH296_LOOP_DELAYED 0x195f90u +#define IOS_SCH296_LOOP_IDLE 0x196070u +#define IOS_SCH296_POST_TASK 0x1a4b10u /* cef_post_delayed_task import slot */ +#define IOS_SCH296_LOOP_INIT 0x1a3d60u +#define IOS_SCH296_LOOP_PUMP 0x1a3dd0u +#define IOS_SCH296_TASK_ADD 0x196db4u /* int3 padding, each after a RET */ +#define IOS_SCH296_TASK_RELEASE 0x197184u +#define IOS_SCH296_TASK_ONE 0x1964f5u +#define IOS_SCH296_TASK_ANY 0x198cf5u +#define IOS_SCH296_TASK 0x1f8c00u +#define IOS_SCH296_RENDER_DRAIN_SITE 0x49d74u +#define IOS_SCH296_RENDER_DRAIN 0x1a3e81u +#define IOS_SCH296_IPC_CONNECT 0x4ba30u +#define IOS_SCH296_RENDER_CHANNEL_REPLY 0x491e1u +#define IOS_SCH296_RENDER_CHANNEL_KEEP 0x491e6u +#define IOS_SCH296_RENDER_CHANNEL_THUNK 0x1a3ea0u +#define IOS_SCH296_IPC_SPLIT 0x1a3eb9u +#define IOS_SCH296_IPC_CAPTURE 0x1a3f46u +#define IOS_SCH296_IPC_STORE 0x1a3f5cu +#define IOS_SCH296_IPC_ROLE 0x1a3f73u +#define IOS_SCH296_IPC_GLOBAL_LOOP 0x483e1u /* int3 padding after a RET */ +#define IOS_SCH296_IPC_INITIAL_CONNECT 0x195d61u /* int3 padding after a RET */ +#define IOS_SCH296_IPC_DESTROY 0x1a3f8fu +#define IOS_SCH296_IPC_DIRECT 0x1a3fe8u +#define IOS_SCH296_IPC_CONTEXT 0x1f8e50u + /* Source: tests/host/sc-renderer-channels.S. Preserve page channels within * the embedded client's 64-slot capacity; the original reply cleanup still runs. */ @@ -16096,7 +16155,7 @@ static const unsigned char ios_sch_ui_trace[190] = /* The helper code the thunk and the vtable change rely on (no relocations in * any of these ranges), as the SHA-256 of each range: the helper's own bytes * stay out of this source. */ -static const struct { unsigned int rva; unsigned char len; unsigned char sha256[32]; } ios_sch_code[] = +static const struct ios_sch_range { unsigned int rva; unsigned char len; unsigned char sha256[32]; } ios_sch_code[] = { { IOS_SCH_GET_HANDLER, 0x3b, /* App::GetRenderProcessHandler / GetBrowserProcessHandler */ { 0xbf,0xf1,0x79,0x08,0x92,0xbe,0x32,0xf0,0x8e,0x4a,0xd7,0x26,0x87,0x96,0x48,0x60, @@ -16245,73 +16304,447 @@ static const struct { unsigned int rva; unsigned char len; unsigned char sha256[ 0x2a,0x96,0x76,0x0e,0x75,0x4f,0x1a,0x27,0xe8,0xc0,0x91,0x06,0xb8,0xea,0xf4,0x85 } }, }; +/* What one analysed build gets. A blob lands in a zero .text tail (fill 0) + * or in int3 padding between functions (fill 0xcc) and is checked for that + * fill before anything is written. A hook is a CALL or JMP rel32 over guarded + * instructions, NOPs after it up to `span`. `ipc` marks the IPC split's parts, + * which env.MADEIRA_SC_IPC_SPLIT = 0 leaves out (their fill is checked anyway). + * The .data ranges hold the task, contexts and cache; they must be zero. */ +struct ios_sch_blob { unsigned int rva; const unsigned char *code; unsigned short len; + unsigned char fill, ipc; const char *what; }; +struct ios_sch_hook { unsigned int site, dest; unsigned char span, opcode, ipc; }; +struct ios_sch_zero { unsigned int rva, len; const char *what; }; +struct ios_sch_build +{ + unsigned int stamp, size, text_vs, text_raw, data, data_vs; + unsigned int vt_browser, vt_renderer, vt_rph, get_handler, get_null, webkit; + unsigned int thunk, loop_init, loop_pump, task_add, task_release, task_one, task_any, task; + const struct ios_sch_range *code; unsigned int code_count; + const struct ios_sch_blob *blob; unsigned int blob_count; + const struct ios_sch_hook *hook; unsigned int hook_count; + const struct ios_sch_zero *zero; unsigned int zero_count; + const char *extras; /* the log line's account of this build's other patches */ +}; + +static const struct ios_sch_blob ios_sch_blob[] = +{ + { IOS_SCH_RENDER_CHANNEL_THUNK, ios_sch_renderer_channels, sizeof(ios_sch_renderer_channels), 0, 1, + "renderer channel .text tail not zero" }, + { IOS_SCH_THUNK, ios_sch_thunk, sizeof(ios_sch_thunk), 0, 0, ".text tail not zero" }, + { IOS_SCH_LOOP_INIT, ios_sch_loop_thunk, sizeof(ios_sch_loop_thunk), 0, 0, "renderer loop .text tail not zero" }, + { IOS_SCH_RUN_INSTALL, ios_sch_run_trace, sizeof(ios_sch_run_trace), 0, 0, "RunState trace .text tail not zero" }, + { IOS_SCH_RENDER_DRAIN, ios_sch_render_drain, sizeof(ios_sch_render_drain), 0, 0, + "renderer drain .text tail not zero" }, + { IOS_SCH_IPC_TRACE, ios_sch_ipc_rebind, sizeof(ios_sch_ipc_rebind), 0, 0, "IPC trace .text tail not zero" }, + { IOS_SCH_IPC_SPLIT, ios_sch_ipc_split, sizeof(ios_sch_ipc_split), 0, 1, "IPC split .text tail not zero" }, + { IOS_SCH_UI_TRACE, ios_sch_ui_trace, sizeof(ios_sch_ui_trace), 0, 0, + "UI notification trace .text tail not zero" }, +}; + +/* Multiple pages share this renderer process. The helper's IPC client already + * owns an indexed set of connections; a Channel Response must add its channel + * without disconnecting the previous page's channel. Keep the browser + * transport, explicit disconnects and window-close notifications unchanged. + * The channel thunk also bounds the client's table; the following TEST + * overwrites its flags. The drain hook stays a CALL so the renderer + * dispatcher resumes after draining work. */ +static const struct ios_sch_hook ios_sch_hook[] = +{ + { IOS_SCH_UI_CTOR, IOS_SCH_UI_TRACE, 5, 0xe9, 0 }, + { IOS_SCH_IPC_CONNECT, IOS_SCH_IPC_TRACE, 5, 0xe9, 0 }, + { IOS_SCH_RENDER_DRAIN_SITE, IOS_SCH_RENDER_DRAIN, 5, 0xe8, 0 }, + { 0x4b290, IOS_SCH_IPC_SPLIT, 7, 0xe9, 1 }, + { 0x4ba50, IOS_SCH_IPC_CAPTURE, 5, 0xe9, 1 }, + { 0x4a5c3, IOS_SCH_IPC_STORE, 7, 0xe9, 1 }, + { 0x4baab, IOS_SCH_IPC_ROLE, 7, 0xe9, 1 }, + { 0x4d255, IOS_SCH_IPC_GLOBAL_LOOP, 5, 0xe8, 1 }, + { 0x4d2fb, IOS_SCH_IPC_INITIAL_CONNECT, 5, 0xe8, 1 }, + { 0x4a719, IOS_SCH_IPC_DESTROY, 7, 0xe9, 1 }, + { 0x4da6c, IOS_SCH_IPC_DIRECT, 7, 0xe9, 1 }, + { IOS_SCH_RENDER_CHANNEL_KEEP, IOS_SCH_RENDER_CHANNEL_THUNK, 7, 0xe9, 1 }, + { 0x174121, IOS_SCH_RUN_INSTALL, 7, 0xe9, 0 }, + { 0x17412b, IOS_SCH_RUN_RESTORE, 9, 0xe9, 0 }, +}; + +static const struct ios_sch_zero ios_sch_zero[] = +{ + { IOS_SCH_TASK, 64, "renderer task .data tail not zero" }, + { IOS_SCH_RUN_RING, IOS_SCH_RUN_RING_SIZE, "RunState trace .data tail not zero" }, + { IOS_SCH_IPC_COUNT, 4, "IPC trace counter not zero" }, + { IOS_SCH_IPC_CONTEXT, 16, "IPC split context not zero" }, + { IOS_SCH_UI_COUNT, 4, "UI notification trace counter not zero" }, + { IOS_SCH_CACHE, 8, ".data tail not zero" }, +}; + +/* 2.4.0.296's blobs. Sources: tests/host/sc-renderer-channels.S, + * sc-render-handler.S, sc-render-loop.S, sc-render-drain.S and sc-ipc-split.S + * assembled with --defsym SCH296=1; the host tests rebuild and compare them. + * The thunk, adapter, drain and channel code are the first build's with this + * build's displacements; the getter, destructor and direct-load hooks are + * the compact forms described above. */ +static const unsigned char ios_sch296_renderer_channels[25] = +{ + 0x83,0xb8,0x20,0x02,0x00,0x00,0x40,0x73,0x0b,0x45,0x31,0xc0,0x48,0x89,0xc1,0xe9, + 0x39,0x53,0xea,0xff,0xe9,0x41,0x53,0xea,0xff, +}; + +static const unsigned char ios_sch296_thunk[112] = +{ + 0x53,0x48,0x83,0xec,0x30,0x48,0x89,0xd3,0x48,0x8b,0x05,0xf9,0x52,0x05,0x00,0x48, + 0x85,0xc0,0x75,0x3f,0xb9,0x60,0x00,0x00,0x00,0xe8,0xaa,0xe5,0xfb,0xff,0x48,0x85, + 0xc0,0x74,0x44,0x48,0x89,0xc1,0xba,0x01,0x00,0x00,0x00,0xe8,0xa0,0x36,0xe6,0xff, + 0x48,0x89,0x44,0x24,0x20,0x48,0x8b,0x48,0x08,0x48,0x63,0x49,0x04,0x48,0x8d,0x4c, + 0x08,0x08,0x48,0x8b,0x11,0xff,0x12,0x48,0x8b,0x44,0x24,0x20,0x48,0x89,0x05,0xb5, + 0x52,0x05,0x00,0x48,0x89,0xc1,0x48,0x89,0xda,0xe8,0x92,0x1d,0xe6,0xff,0x48,0x89, + 0xd8,0x48,0x83,0xc4,0x30,0x5b,0xc3,0x48,0xc7,0x03,0x00,0x00,0x00,0x00,0xeb,0xee, +}; + +static const unsigned char ios_sch296_loop_thunk[289] = +{ + 0x53,0x48,0x83,0xec,0x30,0x48,0x89,0xcb,0xc7,0x44,0x24,0x20,0x00,0x00,0x00,0x00, + 0xe8,0x5b,0x2f,0xff,0xff,0x48,0x85,0xc0,0x75,0x28,0xb9,0x60,0x01,0x00,0x00,0xe8, + 0x34,0xe5,0xfb,0xff,0x48,0x85,0xc0,0x74,0x19,0x48,0x89,0xc1,0x31,0xd2,0xe8,0x9d, + 0x18,0xff,0xff,0x48,0x89,0x05,0x96,0x4e,0x05,0x00,0xc7,0x44,0x24,0x20,0x01,0x00, + 0x00,0x00,0x48,0x89,0xd9,0xe8,0xd6,0x4a,0xe6,0xff,0x83,0x7c,0x24,0x20,0x00,0x74, + 0x18,0xb9,0x06,0x00,0x00,0x00,0x48,0x8d,0x15,0x43,0x4e,0x05,0x00,0x41,0xb8,0x0a, + 0x00,0x00,0x00,0xff,0x15,0x47,0x0d,0x00,0x00,0x48,0x83,0xc4,0x30,0x5b,0xc3,0x90, + 0x53,0x56,0x48,0x83,0xec,0x58,0x48,0x89,0xcb,0x48,0x8b,0x4b,0x30,0x48,0x85,0xc9, + 0x0f,0x84,0x94,0x00,0x00,0x00,0x48,0x8b,0x81,0x28,0x01,0x00,0x00,0x48,0x89,0x44, + 0x24,0x30,0xba,0x01,0x00,0x00,0x00,0x48,0x85,0xc0,0x74,0x04,0x8b,0x10,0xff,0xc2, + 0x89,0x54,0x24,0x38,0xc7,0x44,0x24,0x3c,0x00,0x00,0x00,0x00,0x48,0xc7,0x44,0x24, + 0x40,0x00,0x00,0x00,0x00,0x48,0x8d,0x44,0x24,0x38,0x48,0x89,0x81,0x28,0x01,0x00, + 0x00,0xbe,0x20,0x00,0x00,0x00,0x48,0x8b,0x4b,0x30,0xe8,0xc1,0x22,0xff,0xff,0x88, + 0x44,0x24,0x28,0x48,0x8b,0x4b,0x30,0x48,0x8d,0x54,0x24,0x20,0xe8,0x4f,0x21,0xff, + 0xff,0x0a,0x44,0x24,0x28,0x74,0x06,0xff,0xce,0x75,0xdb,0xeb,0x09,0x48,0x8b,0x4b, + 0x30,0xe8,0x1a,0x22,0xff,0xff,0x48,0x8b,0x4b,0x30,0x48,0x8b,0x44,0x24,0x30,0x48, + 0x89,0x81,0x28,0x01,0x00,0x00,0xb9,0x06,0x00,0x00,0x00,0x48,0x89,0xda,0x41,0xb8, + 0x0a,0x00,0x00,0x00,0xff,0x15,0x96,0x0c,0x00,0x00,0x48,0x83,0xc4,0x58,0x5e,0x5b, + 0xc3, +}; + +static const unsigned char ios_sch296_task_add[5] = +{ + 0xf0,0xff,0x41,0x38,0xc3, +}; + +static const unsigned char ios_sch296_task_release[7] = +{ + 0xf0,0xff,0x49,0x38,0x31,0xc0,0xc3, +}; + +static const unsigned char ios_sch296_task_one[11] = +{ + 0x83,0x79,0x38,0x01,0x0f,0x94,0xc0,0x0f,0xb6,0xc0,0xc3, +}; + +static const unsigned char ios_sch296_task_any[11] = +{ + 0x83,0x79,0x38,0x00,0x0f,0x95,0xc0,0x0f,0xb6,0xc0,0xc3, +}; + +static const unsigned char ios_sch296_render_drain[31] = +{ + 0x48,0x83,0xec,0x28,0xe8,0x46,0x2e,0xff,0xff,0x48,0x85,0xc0,0x74,0x0c,0x48,0x89, + 0xc1,0x48,0x83,0xc4,0x28,0xe9,0x35,0x27,0xff,0xff,0x48,0x83,0xc4,0x28,0xc3, +}; + +static const unsigned char ios_sch296_ipc_split[303] = +{ + 0x48,0x83,0xec,0x28,0xb9,0x06,0x00,0x00,0x00,0xff,0x15,0x38,0x0c,0x00,0x00,0x85, + 0xc0,0x74,0x6e,0x48,0x8b,0x05,0x7d,0x4f,0x05,0x00,0x48,0x85,0xc0,0x75,0x69,0x48, + 0x83,0x3d,0x78,0x4f,0x05,0x00,0x00,0x74,0x58,0xb9,0x30,0x1a,0x00,0x00,0xe8,0xcc, + 0xe3,0xfb,0xff,0x48,0x85,0xc0,0x74,0x49,0x48,0x89,0x05,0x58,0x4f,0x05,0x00,0x48, + 0x89,0xc1,0xe8,0x20,0x70,0xea,0xff,0x48,0x8b,0x15,0x51,0x4f,0x05,0x00,0x48,0x89, + 0x54,0x24,0x20,0x48,0x8b,0x42,0x08,0x48,0x8d,0x4a,0x08,0x48,0x63,0x40,0x04,0x48, + 0x01,0xc1,0x48,0x8b,0x01,0xff,0x10,0x48,0x8d,0x54,0x24,0x20,0x48,0x8b,0x0d,0x24, + 0x4f,0x05,0x00,0xe8,0x0f,0x89,0xea,0xff,0x48,0x8b,0x05,0x18,0x4f,0x05,0x00,0xeb, + 0x07,0x48,0x8b,0x05,0x5f,0x1c,0x05,0x00,0x48,0x83,0xc4,0x28,0xc3,0x48,0x89,0x5c, + 0x24,0x10,0x50,0x48,0x8b,0x02,0x48,0x89,0x05,0x02,0x4f,0x05,0x00,0x58,0xe9,0xe9, + 0x88,0xea,0xff,0x9c,0x48,0x3b,0x1d,0xec,0x4e,0x05,0x00,0x74,0x07,0x48,0x89,0x1d, + 0x33,0x1c,0x05,0x00,0x9d,0xe9,0x77,0x71,0xea,0xff,0x9c,0x4c,0x3b,0x35,0xd5,0x4e, + 0x05,0x00,0x75,0x05,0xb8,0x01,0x00,0x00,0x00,0x41,0x89,0x86,0xf0,0x00,0x00,0x00, + 0x9d,0xe9,0x1e,0x89,0xea,0xff,0x48,0x3b,0x3d,0xba,0x4e,0x05,0x00,0x74,0x40,0x48, + 0x83,0x3d,0xb0,0x4e,0x05,0x00,0x00,0x74,0x2a,0x51,0x48,0x83,0xec,0x28,0x48,0x8b, + 0x0d,0xa2,0x4e,0x05,0x00,0x48,0x89,0x4c,0x24,0x20,0xe8,0x88,0x72,0xea,0xff,0x48, + 0x8b,0x4c,0x24,0x20,0xba,0x30,0x1a,0x00,0x00,0xe8,0x2d,0xe3,0xfb,0xff,0x48,0x83, + 0xc4,0x28,0x59,0x48,0x89,0x35,0xcd,0x1b,0x05,0x00,0xe9,0xa8,0x72,0xea,0xff,0x48, + 0xc7,0x05,0x6d,0x4e,0x05,0x00,0x00,0x00,0x00,0x00,0xe9,0x98,0x72,0xea,0xff, +}; + +static const unsigned char ios_sch296_ipc_direct[13] = +{ + 0xe8,0xe3,0x80,0xea,0xff,0x48,0x89,0xc3,0xe9,0x7c,0xa7,0xea,0xff, +}; + +static const unsigned char ios_sch296_ipc_global_loop[15] = +{ + 0x4c,0x3b,0x35,0x68,0x0a,0x1b,0x00,0x74,0x05,0xe9,0x51,0x67,0x0f,0x00,0xc3, +}; + +static const unsigned char ios_sch296_ipc_initial_connect[15] = +{ + 0x48,0x3b,0x0d,0xe8,0x30,0x06,0x00,0x74,0x05,0xe9,0xc1,0x5c,0xeb,0xff,0xc3, +}; + +/* 2.4.0.296's guarded code ranges, as SHA-256 like ios_sch_code. Each int3 + * padding range starts with the RET that ends the function before it. */ +static const struct ios_sch_range ios_sch296_code[] = +{ + { IOS_SCH296_GET_HANDLER, 0x3b, /* App::GetRenderProcessHandler / GetBrowserProcessHandler */ + { 0xbf,0xf1,0x79,0x08,0x92,0xbe,0x32,0xf0,0x8e,0x4a,0xd7,0x26,0x87,0x96,0x48,0x60, + 0xc0,0x66,0x80,0xfd,0x5e,0xb7,0x01,0x04,0x4b,0xc9,0xb4,0x42,0xaa,0x53,0xf8,0x55 } }, + { 0x73c0, 0x20, /* RendererApp::RendererApp(this, most_derived) */ + { 0x0a,0x62,0xa0,0x68,0x3c,0x4a,0x42,0x89,0xd8,0x54,0x5c,0xf5,0xe0,0xc2,0xa9,0xae, + 0xc4,0x66,0x78,0x00,0x52,0x87,0xfb,0xa5,0xad,0x89,0x73,0xe5,0xf1,0x81,0x06,0x7c } }, + { 0x1622b8, 0x20, /* operator new */ + { 0x82,0x73,0x33,0x2b,0xfc,0x01,0x08,0xf9,0xc9,0xf5,0xdc,0xad,0x01,0x3c,0x5e,0x5b, + 0x80,0x91,0x10,0xd7,0x60,0x4f,0x49,0x56,0x2f,0xd4,0xdd,0x3b,0xc6,0x79,0x89,0xe9 } }, + { IOS_SCH296_GET_NULL, 0xb, + { 0x7e,0x90,0xc2,0x24,0x72,0x8a,0x8e,0x94,0xca,0x0a,0x73,0x81,0xc4,0x4b,0x1d,0x32, + 0x26,0x75,0x55,0xc7,0x55,0xfa,0xb1,0x86,0x74,0x12,0x92,0xf9,0x99,0xf2,0x54,0x46 } }, + { 0x84101, 0x6, + { 0xe7,0x74,0x7f,0xe6,0xd8,0xbe,0x61,0x5e,0x01,0x5d,0x32,0x45,0xb7,0x45,0x60,0xf3, + 0xa9,0x11,0x67,0xf3,0xa3,0x66,0x67,0xd8,0xc7,0x68,0x7b,0xe3,0xd7,0xc8,0x17,0xd3 } }, + { IOS_SCH296_CURRENT_LOOP, 0x1f, /* MessageLoop::current (TLS initialization) */ + { 0x7d,0x89,0x6a,0x5d,0x31,0x10,0x95,0xf4,0x85,0xcb,0xea,0xd4,0x23,0x27,0x25,0xe8, + 0x5d,0x39,0xa9,0xdb,0xe6,0x76,0x04,0x33,0x0f,0xe2,0x3c,0xfb,0xcb,0xe8,0x18,0xae } }, + { 0x196d2f, 0x10, /* MessageLoop::current (TLS getter tail) */ + { 0xda,0x4f,0x32,0x91,0x57,0xe5,0xba,0x42,0x7a,0x4f,0x33,0x7b,0x86,0xbc,0x92,0x81, + 0x9a,0xee,0x49,0x51,0x9c,0x62,0x19,0x0f,0x20,0xde,0x6d,0x0b,0xf3,0xc6,0x37,0x5e } }, + { 0x19a510, 0x8, /* TLS slot getter */ + { 0xd8,0x85,0xa2,0x37,0xeb,0x42,0x98,0x91,0xee,0xb1,0x5b,0x84,0x45,0x82,0xc5,0x66, + 0xe1,0x48,0x40,0x57,0xe8,0xdc,0x7c,0x15,0xc2,0x04,0x9d,0x2e,0xa5,0x2e,0x1c,0xba } }, + { IOS_SCH296_LOOP_CTOR, 0x20, /* MessageLoop constructor */ + { 0xff,0xbd,0x43,0xc8,0x9c,0x07,0x9e,0x5f,0x94,0x6e,0xd3,0x47,0xb5,0x67,0x77,0xf1, + 0x64,0x1d,0xbf,0xf6,0xd5,0xc7,0xbf,0xc7,0xf6,0xcb,0xba,0x63,0xdf,0x23,0x32,0xe1 } }, + { 0x1957b9, 0x28, /* constructor installs this thread TLS loop; type 0 keeps the default pump */ + { 0x28,0xf1,0xce,0x34,0x53,0x8a,0x4e,0xc5,0xcb,0x02,0x13,0x88,0x84,0x7c,0x0e,0x82, + 0x18,0xd6,0x45,0x4a,0x83,0x22,0xcc,0x64,0x7b,0x1b,0x92,0xbd,0x45,0xe9,0xe3,0x3a } }, + { 0x195875, 0x18, /* constructor returns the loop */ + { 0x7f,0xaa,0x45,0x3c,0xb6,0x39,0xf5,0x0f,0x40,0x2f,0x90,0xf3,0x06,0x7c,0xfc,0x83, + 0x76,0xb2,0xea,0xc7,0xd9,0xf1,0xd3,0x4a,0x53,0x09,0xa4,0xc7,0xd8,0xe7,0x0c,0x96 } }, + { IOS_SCH296_LOOP_WORK, 0x20, /* DoWork */ + { 0xe3,0xa6,0x77,0x5f,0x43,0x26,0x0a,0x47,0x38,0xce,0xff,0xdb,0x25,0x7a,0x3b,0x65, + 0x0c,0xb8,0x19,0x5b,0x4d,0x71,0x30,0x8b,0x5b,0x46,0xe4,0x95,0x91,0x05,0x3f,0x80 } }, + { IOS_SCH296_LOOP_DELAYED, 0x20, /* DoDelayedWork */ + { 0xaa,0xac,0x3b,0x50,0x8e,0x12,0x3e,0x76,0x82,0xcc,0xbe,0x44,0xbf,0xa9,0xfb,0x7c, + 0xd8,0xac,0x2e,0xbc,0xd3,0x9a,0x12,0xda,0x84,0x1c,0x5c,0x4b,0xfb,0x8b,0x5e,0xb6 } }, + { IOS_SCH296_LOOP_IDLE, 0x20, /* DoIdleWork */ + { 0x71,0x8f,0x26,0xea,0x35,0x5f,0x85,0x94,0xae,0x33,0x4d,0x28,0x2b,0x28,0x31,0x56, + 0x12,0xde,0xca,0xaa,0x28,0x4b,0x41,0xcf,0xe6,0x77,0xbb,0x7b,0xc6,0x80,0xf5,0xcc } }, + { 0x1965d0, 0x40, /* scoped RunState layout and nesting */ + { 0x73,0x1e,0x73,0xa0,0xb2,0x9f,0x58,0x56,0x7a,0x17,0xb8,0xb5,0x93,0xf1,0x92,0x67, + 0x1f,0x2a,0xca,0x02,0xf2,0x1c,0x5c,0xa0,0x94,0x45,0x89,0xb1,0xad,0xab,0x70,0x32 } }, + { 0x196611, 0x1c, /* scoped RunState quit flag and restore */ + { 0xe5,0x39,0xde,0xc2,0x15,0x2a,0x4f,0x02,0x2b,0x08,0xfd,0xac,0x00,0x0e,0xdd,0xdc, + 0x2d,0x65,0xc3,0xa8,0xab,0xfa,0x56,0x01,0x07,0x08,0x0a,0x23,0xbb,0x52,0x31,0x43 } }, + { 0x49cdf, 0x12, /* only the CEF TID_RENDERER branch reaches the patched call */ + { 0x02,0xca,0xd6,0x9f,0x06,0x15,0x03,0x12,0xe2,0x50,0xef,0x2f,0x79,0xac,0x8d,0xad, + 0xad,0xf7,0xc8,0x87,0xea,0x5e,0xc2,0xbb,0xe4,0x97,0x09,0x59,0x31,0xfb,0x6f,0x4e } }, + { IOS_SCH296_RENDER_DRAIN_SITE, 0x5, + { 0xdd,0xe9,0x97,0xcb,0xdb,0x86,0x26,0x8e,0x5a,0xc3,0x84,0x9e,0x27,0x82,0x61,0xcd, + 0x5f,0xb3,0x7d,0xe1,0x98,0x1d,0x86,0x25,0xa0,0x6c,0x94,0x12,0x17,0x4e,0xb4,0x80 } }, + { 0x13eec0, 0x11, /* original global browser drain remains unchanged */ + { 0x0f,0xad,0xe3,0x90,0x21,0xb1,0xb3,0xac,0x1b,0xf0,0xf7,0x17,0x05,0x49,0xcd,0xe7, + 0x96,0x91,0xf2,0xf2,0x82,0xe6,0x43,0x19,0xee,0x4b,0xb6,0x6d,0xa0,0xaf,0xf7,0x5a } }, + { 0x4ba4e, 0x7, /* Connect keeps replace_old in R12B */ + { 0xb3,0x42,0xe1,0xb5,0x09,0x94,0x4a,0xe2,0x36,0x60,0xd2,0x7c,0x0b,0xcc,0x79,0x45, + 0x2c,0x10,0xd5,0xd5,0xf7,0xf9,0x00,0x8e,0x98,0x37,0xf1,0xbd,0x4f,0x13,0x7f,0x01 } }, + { 0x491c9, 0x31, /* renderer's Channel Response -> Connect(name, replace_old) */ + { 0x3c,0x30,0x26,0x41,0xb5,0x4d,0xae,0xce,0x29,0xff,0xf1,0xaf,0x06,0xa2,0x9e,0x91, + 0xa9,0xe0,0xea,0x87,0xff,0x69,0x74,0x5a,0xb3,0x56,0xc4,0x72,0x6c,0xd9,0x0e,0x47 } }, + { 0x4bbe6, 0x3d, /* Connect removes the previous named channel only when replace_old is true */ + { 0x85,0x2e,0xe2,0xca,0x0c,0x63,0xc6,0x25,0x2f,0x3f,0xd1,0x93,0x03,0x5c,0x38,0x3f, + 0xdb,0xaf,0x6c,0xcd,0x81,0x79,0x21,0x9b,0x3e,0x95,0x53,0xe5,0x88,0xce,0x4e,0xc4 } }, + { 0x4ba10, 0xe, /* embedded IPC client appends a connection and increments its counts */ + { 0x1a,0x90,0x5d,0xa9,0xfd,0x37,0x16,0x45,0xff,0x9c,0x9a,0x1d,0x2b,0x30,0x26,0xfc, + 0x55,0x37,0x22,0xd4,0xf8,0xf3,0x1c,0xdb,0xcf,0x71,0x01,0xe1,0x47,0xd4,0xc3,0xef } }, + { 0x4bdaf, 0x3e, /* explicit disconnect locates and closes only the named connection */ + { 0x42,0xe4,0x12,0x01,0x77,0x6b,0x6d,0x0e,0x66,0x9b,0x43,0x21,0xcf,0x94,0xb2,0xf3, + 0x05,0xc4,0xd3,0x7a,0xb3,0x8f,0xd7,0xad,0x13,0x77,0x0c,0xa2,0x52,0xe1,0xc1,0x51 } }, + { 0x4c0d0, 0x8, /* original process-wide manager getter */ + { 0x5b,0x3d,0x62,0x35,0x88,0x9a,0x15,0xee,0x77,0x40,0xa4,0xf7,0x83,0xde,0x67,0xa9, + 0x56,0xb1,0x8f,0xcd,0x1b,0x02,0x2a,0x64,0x04,0xa7,0x8c,0x87,0x93,0x35,0x8a,0xab } }, + { 0x4c840, 0x5, /* Initialize entry; captures the live main command line */ + { 0xc1,0x68,0x0a,0x7b,0x16,0x88,0xcf,0x4f,0x20,0x14,0x55,0x9f,0xce,0x61,0x3b,0x47, + 0x1a,0x88,0xc0,0x77,0x20,0xd0,0xc3,0x61,0x9f,0xbf,0x9a,0x63,0xdd,0x55,0xde,0xeb } }, + { 0x4b0e3, 0x7, /* constructor singleton store */ + { 0x4f,0x88,0x77,0x67,0x08,0x9b,0x18,0xf9,0xaf,0xa6,0x56,0x5e,0xa1,0x58,0x87,0xdb, + 0x2f,0x40,0x80,0x1b,0xa4,0xa7,0xa7,0x29,0xd4,0xae,0x38,0xc5,0x4b,0x85,0x0d,0x2a } }, + { 0x4c8a6, 0x7, /* Initialize role store */ + { 0x0e,0x87,0x5d,0xc4,0x18,0xa1,0x92,0x01,0xc1,0xd2,0x6d,0x2f,0x91,0xb2,0x5c,0xd9, + 0x42,0x51,0xee,0x5a,0xbd,0xd5,0x67,0x91,0x7e,0x2a,0x3c,0xbc,0x04,0xf4,0x0d,0x0c } }, + { 0x4e084, 0x18, /* global browser loop initialization, R14 = this */ + { 0x5d,0xe0,0x4a,0x46,0x22,0x36,0x33,0x36,0x53,0xf6,0x34,0x7e,0x47,0xf7,0x5f,0x5e, + 0xd5,0x6b,0x0f,0xd9,0x86,0xa8,0x06,0x2d,0x4e,0x5d,0xa8,0x31,0x23,0x2e,0x6a,0xa3 } }, + { 0x4e0de, 0xa, /* browser-role-only handler registration */ + { 0x3a,0xde,0xcd,0xeb,0x13,0xca,0xcd,0x77,0x7b,0x9b,0x3e,0x24,0x0d,0x35,0x16,0x13, + 0x60,0xf9,0xcb,0x95,0xde,0x84,0x08,0x81,0x29,0x40,0x6c,0x28,0xda,0x4e,0xa3,0x05 } }, + { 0x4e128, 0x20, /* initial channel connection from the command line */ + { 0xb2,0xbf,0x16,0x6d,0xa0,0x49,0x36,0xe3,0xbc,0x71,0x0b,0xb6,0x25,0x4b,0x17,0xd7, + 0xbb,0xe8,0x22,0x96,0x3e,0x53,0x63,0xdc,0xcd,0x44,0xba,0x1f,0xf2,0x0f,0xfa,0x63 } }, + { 0x4b240, 0x50, /* destructor: RDI = this, RSI = 0, only RCX live after the singleton clear */ + { 0x27,0x2f,0xbd,0x98,0x8a,0x59,0x70,0x13,0xb1,0xb3,0xe0,0x31,0xe2,0xe0,0xb9,0x5c, + 0x6e,0x2c,0xce,0x47,0x22,0x08,0xd5,0x4e,0x9d,0xae,0x37,0x57,0x3f,0xd3,0xf0,0x28 } }, + { 0x4af20, 0x2a, /* manager constructor ABI and initial fields */ + { 0x7c,0xf0,0x14,0x50,0x6e,0x58,0x18,0xa4,0x05,0xbe,0x4c,0x0b,0x9c,0xc9,0x77,0x1e, + 0xc8,0x66,0xa7,0x85,0xda,0x5b,0x64,0x75,0xea,0x26,0xe2,0xfb,0xe7,0x56,0x27,0xeb } }, + { 0x4b0d4, 0xf, /* embedded client construction and final owned-object field */ + { 0x8e,0xc2,0xb6,0x7c,0x85,0xf0,0xeb,0xcb,0x9a,0x10,0x95,0x14,0xd8,0xad,0x0f,0xe7, + 0xd3,0xa4,0xf3,0xe6,0x3f,0x64,0x3f,0x62,0xd4,0x76,0x47,0x81,0xd0,0xb0,0x04,0x5d } }, + { 0x4e240, 0x1e, /* Initialize consumes its by-value command-line reference */ + { 0xe8,0xcd,0xc5,0xe3,0x65,0x96,0xd4,0x39,0x5e,0x07,0xd4,0x6e,0xe0,0x65,0x11,0x5c, + 0xea,0xb3,0xe4,0x99,0x90,0x79,0x63,0x87,0xea,0xa6,0x85,0x13,0x21,0x2c,0xc5,0x3b } }, + { 0x4e765, 0x24, /* inlined manager lookup after a CALL; RAX/RCX/flags rewritten before use */ + { 0x90,0x60,0x8d,0x6e,0x8b,0x06,0x4e,0x2c,0xeb,0x64,0x4e,0x34,0x03,0x2f,0x22,0xa9, + 0xea,0x97,0xed,0xe1,0xcc,0x31,0xc0,0x21,0x8a,0x04,0x8e,0x41,0x75,0xb3,0xd1,0xf7 } }, + { 0x1622f4, 0x6, /* helper sized deallocator */ + { 0x6f,0xc3,0x6c,0xb0,0x1f,0xd7,0x87,0xe3,0x1e,0x2e,0x8b,0x56,0x11,0xfe,0x8a,0xe5, + 0xd5,0x50,0xe7,0x6c,0x3c,0x64,0x3f,0xa4,0xf3,0x62,0xc9,0x8b,0x55,0xdf,0xcd,0x04 } }, + { IOS_SCH296_WEBKIT, 0x20, /* original OnWebKitInitialized */ + { 0x1a,0x66,0x35,0xcb,0x9f,0x7a,0x1e,0x99,0xa9,0x71,0x21,0x8d,0x52,0x26,0x71,0xc8, + 0xec,0x12,0xf9,0xf1,0xe2,0x4f,0x04,0x93,0xa5,0xb4,0x1c,0x09,0x6d,0xeb,0x45,0x40 } }, + { 0x196db3, 0xd, /* RET, then int3 padding: task AddRef */ + { 0xfa,0x14,0xaf,0x33,0x83,0x8c,0x18,0xaa,0x65,0x46,0x20,0xc1,0xc1,0xed,0x5f,0x1a, + 0x28,0xc9,0xc5,0x87,0x4b,0xb4,0xf5,0xfa,0xfa,0xe0,0x46,0xb4,0x07,0x4a,0x5c,0x15 } }, + { 0x197183, 0xd, /* RET, then int3 padding: task Release */ + { 0xfa,0x14,0xaf,0x33,0x83,0x8c,0x18,0xaa,0x65,0x46,0x20,0xc1,0xc1,0xed,0x5f,0x1a, + 0x28,0xc9,0xc5,0x87,0x4b,0xb4,0xf5,0xfa,0xfa,0xe0,0x46,0xb4,0x07,0x4a,0x5c,0x15 } }, + { 0x1964f4, 0xc, /* RET, then int3 padding: task HasOneRef */ + { 0xf7,0x77,0x9d,0x90,0x63,0x35,0x27,0x79,0x04,0xd5,0xdf,0x8f,0x37,0x6e,0xdc,0xc6, + 0x50,0xb1,0xcd,0xc0,0x5a,0x85,0x57,0x50,0x90,0xee,0x46,0x8a,0x2a,0xff,0x24,0xa8 } }, + { 0x198cf4, 0xc, /* RET, then int3 padding: task HasAtLeastOneRef */ + { 0xf7,0x77,0x9d,0x90,0x63,0x35,0x27,0x79,0x04,0xd5,0xdf,0x8f,0x37,0x6e,0xdc,0xc6, + 0x50,0xb1,0xcd,0xc0,0x5a,0x85,0x57,0x50,0x90,0xee,0x46,0x8a,0x2a,0xff,0x24,0xa8 } }, + { 0x483e0, 0x10, /* RET, then int3 padding: global-loop stub */ + { 0x52,0xbe,0x34,0x27,0x50,0x79,0xab,0xd5,0x9f,0xd4,0xed,0xc2,0xd5,0x6f,0xe1,0x81, + 0xe5,0x21,0x44,0x48,0x9f,0x0d,0xa2,0x25,0x17,0x0c,0x9d,0x43,0x88,0x73,0x82,0x4d } }, + { 0x195d60, 0x10, /* RET, then int3 padding: initial-connect stub */ + { 0x52,0xbe,0x34,0x27,0x50,0x79,0xab,0xd5,0x9f,0xd4,0xed,0xc2,0xd5,0x6f,0xe1,0x81, + 0xe5,0x21,0x44,0x48,0x9f,0x0d,0xa2,0x25,0x17,0x0c,0x9d,0x43,0x88,0x73,0x82,0x4d } }, +}; + +static const struct ios_sch_blob ios_sch296_blob[] = +{ + { IOS_SCH296_RENDER_CHANNEL_THUNK, ios_sch296_renderer_channels, sizeof(ios_sch296_renderer_channels), 0, 1, + "renderer channel .text tail not zero" }, + { IOS_SCH296_THUNK, ios_sch296_thunk, sizeof(ios_sch296_thunk), 0, 0, ".text tail not zero" }, + { IOS_SCH296_LOOP_INIT, ios_sch296_loop_thunk, sizeof(ios_sch296_loop_thunk), 0, 0, + "renderer loop .text tail not zero" }, + { IOS_SCH296_TASK_ADD, ios_sch296_task_add, sizeof(ios_sch296_task_add), 0xcc, 0, + "renderer task callback padding not int3" }, + { IOS_SCH296_TASK_RELEASE, ios_sch296_task_release, sizeof(ios_sch296_task_release), 0xcc, 0, + "renderer task callback padding not int3" }, + { IOS_SCH296_TASK_ONE, ios_sch296_task_one, sizeof(ios_sch296_task_one), 0xcc, 0, + "renderer task callback padding not int3" }, + { IOS_SCH296_TASK_ANY, ios_sch296_task_any, sizeof(ios_sch296_task_any), 0xcc, 0, + "renderer task callback padding not int3" }, + { IOS_SCH296_RENDER_DRAIN, ios_sch296_render_drain, sizeof(ios_sch296_render_drain), 0, 0, + "renderer drain .text tail not zero" }, + { IOS_SCH296_IPC_SPLIT, ios_sch296_ipc_split, sizeof(ios_sch296_ipc_split), 0, 1, + "IPC split .text tail not zero" }, + { IOS_SCH296_IPC_DIRECT, ios_sch296_ipc_direct, sizeof(ios_sch296_ipc_direct), 0, 1, + "IPC split .text tail not zero" }, + { IOS_SCH296_IPC_GLOBAL_LOOP, ios_sch296_ipc_global_loop, sizeof(ios_sch296_ipc_global_loop), 0xcc, 1, + "IPC split padding not int3" }, + { IOS_SCH296_IPC_INITIAL_CONNECT, ios_sch296_ipc_initial_connect, sizeof(ios_sch296_ipc_initial_connect), + 0xcc, 1, "IPC split padding not int3" }, +}; + +static const struct ios_sch_hook ios_sch296_hook[] = +{ + { IOS_SCH296_RENDER_DRAIN_SITE, IOS_SCH296_RENDER_DRAIN, 5, 0xe8, 0 }, + { 0x4c0d0, IOS_SCH296_IPC_SPLIT, 7, 0xe9, 1 }, + { 0x4c840, IOS_SCH296_IPC_CAPTURE, 5, 0xe9, 1 }, + { 0x4b0e3, IOS_SCH296_IPC_STORE, 7, 0xe9, 1 }, + { 0x4c8a6, IOS_SCH296_IPC_ROLE, 7, 0xe9, 1 }, + { 0x4e097, IOS_SCH296_IPC_GLOBAL_LOOP, 5, 0xe8, 1 }, + { 0x4e143, IOS_SCH296_IPC_INITIAL_CONNECT, 5, 0xe8, 1 }, + { 0x4b279, IOS_SCH296_IPC_DESTROY, 7, 0xe9, 1 }, + { 0x4e76a, IOS_SCH296_IPC_DIRECT, 7, 0xe9, 1 }, + { IOS_SCH296_RENDER_CHANNEL_KEEP, IOS_SCH296_RENDER_CHANNEL_THUNK, 7, 0xe9, 1 }, +}; + +static const struct ios_sch_zero ios_sch296_zero[] = +{ + { IOS_SCH296_TASK, 64, "renderer task .data tail not zero" }, + { IOS_SCH296_IPC_CONTEXT, 16, "IPC split context not zero" }, + { IOS_SCH296_CACHE, 8, ".data tail not zero" }, +}; + +static const struct ios_sch_build ios_sch_builds[] = +{ + { IOS_SCH_STAMP, IOS_SCH_SIZE, 0x17f34c, 0x17f400, IOS_SCH_DATA, 0x43bc, + IOS_SCH_VT_BROWSER, IOS_SCH_VT_RENDERER, IOS_SCH_VT_RPH, IOS_SCH_GET_HANDLER, IOS_SCH_GET_NULL, IOS_SCH_WEBKIT, + IOS_SCH_THUNK, IOS_SCH_LOOP_INIT, IOS_SCH_LOOP_PUMP, IOS_SCH_TASK_ADD, IOS_SCH_TASK_RELEASE, + IOS_SCH_TASK_ONE, IOS_SCH_TASK_ANY, IOS_SCH_TASK, + ios_sch_code, ARRAY_SIZE(ios_sch_code), ios_sch_blob, ARRAY_SIZE(ios_sch_blob), + ios_sch_hook, ARRAY_SIZE(ios_sch_hook), ios_sch_zero, ARRAY_SIZE(ios_sch_zero), + "bounded I/O RunState, IPC rebind and UI notification traces installed; " }, + { IOS_SCH296_STAMP, IOS_SCH296_SIZE, IOS_SCH296_TEXT_VS, IOS_SCH296_TEXT_RAW, IOS_SCH296_DATA, IOS_SCH296_DATA_VS, + IOS_SCH296_VT_BROWSER, IOS_SCH296_VT_RENDERER, IOS_SCH296_VT_RPH, IOS_SCH296_GET_HANDLER, IOS_SCH296_GET_NULL, + IOS_SCH296_WEBKIT, + IOS_SCH296_THUNK, IOS_SCH296_LOOP_INIT, IOS_SCH296_LOOP_PUMP, IOS_SCH296_TASK_ADD, IOS_SCH296_TASK_RELEASE, + IOS_SCH296_TASK_ONE, IOS_SCH296_TASK_ANY, IOS_SCH296_TASK, + ios_sch296_code, ARRAY_SIZE(ios_sch296_code), ios_sch296_blob, ARRAY_SIZE(ios_sch296_blob), + ios_sch296_hook, ARRAY_SIZE(ios_sch296_hook), ios_sch296_zero, ARRAY_SIZE(ios_sch296_zero), + "no diagnostic traces (2.4.0.296 has no room for them); " }, +}; + /* Why `base` (SocialClubHelper.exe, mapped) cannot take the patch, or NULL when - * it can; on success *vtbase is the guest base the CefApp vtables use (ptr when - * the directory was applied, else the preferred base), which the thunk pointer - * is written in. */ + * it can; on success *found is its build and *vtbase the guest base the CefApp + * vtables use (ptr when the directory was applied, else the preferred base), + * which the thunk pointer is written in. */ static const char *ios_sch_mismatch( const char *base, SIZE_T total_size, const IMAGE_NT_HEADERS *nt, - const IMAGE_SECTION_HEADER *sec, ULONG64 *vtbase ) + const IMAGE_SECTION_HEADER *sec, const struct ios_sch_build **found, + ULONG64 *vtbase ) { - const ULONG64 *vb = (const ULONG64 *)(base + IOS_SCH_VT_BROWSER); - const ULONG64 *vr = (const ULONG64 *)(base + IOS_SCH_VT_RENDERER); - const ULONG64 *vh = (const ULONG64 *)(base + IOS_SCH_VT_RPH); + const struct ios_sch_build *build = NULL; + const ULONG64 *vb, *vr, *vh; ULONG64 b = (ULONG64)(ULONG_PTR)base; int i, text = 0, data = 0; - size_t k; + size_t k, j; if (nt->FileHeader.Machine != IMAGE_FILE_MACHINE_AMD64 || nt->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) return "not a 64-bit x64 image"; - if (nt->FileHeader.TimeDateStamp != IOS_SCH_STAMP || nt->OptionalHeader.SizeOfImage != IOS_SCH_SIZE || - total_size < IOS_SCH_SIZE) return "another build (TimeDateStamp / SizeOfImage)"; + for (k = 0; k < ARRAY_SIZE(ios_sch_builds); k++) + if (nt->FileHeader.TimeDateStamp == ios_sch_builds[k].stamp && + nt->OptionalHeader.SizeOfImage == ios_sch_builds[k].size) build = &ios_sch_builds[k]; + if (!build || total_size < build->size) return "another build (TimeDateStamp / SizeOfImage)"; + *found = build; for (i = 0; i < nt->FileHeader.NumberOfSections; i++) { - if (sec[i].VirtualAddress == IOS_SCH_TEXT && sec[i].Misc.VirtualSize == 0x17f34c && - sec[i].SizeOfRawData == 0x17f400 && (sec[i].Characteristics & IMAGE_SCN_MEM_EXECUTE)) text = 1; - if (sec[i].VirtualAddress == IOS_SCH_DATA && sec[i].Misc.VirtualSize == 0x43bc && + if (sec[i].VirtualAddress == IOS_SCH_TEXT && sec[i].Misc.VirtualSize == build->text_vs && + sec[i].SizeOfRawData == build->text_raw && (sec[i].Characteristics & IMAGE_SCN_MEM_EXECUTE)) text = 1; + if (sec[i].VirtualAddress == build->data && sec[i].Misc.VirtualSize == build->data_vs && (sec[i].Characteristics & IMAGE_SCN_MEM_WRITE)) data = 1; } if (!text || !data) return "section layout differs"; - for (k = 0; k < sizeof(ios_sch_code) / sizeof(ios_sch_code[0]); k++) + for (k = 0; k < build->code_count; k++) { unsigned char digest[CC_SHA256_DIGEST_LENGTH]; - CC_SHA256( base + ios_sch_code[k].rva, ios_sch_code[k].len, digest ); - if (memcmp( digest, ios_sch_code[k].sha256, sizeof(digest) )) return "code bytes differ"; + CC_SHA256( base + build->code[k].rva, build->code[k].len, digest ); + if (memcmp( digest, build->code[k].sha256, sizeof(digest) )) return "code bytes differ"; } /* The vtables carry guest pointers in one base -- ptr (directory applied) * or the preferred base (ml949 / sub-floor). Read it from one slot and * require the other three consistent, and the base one of the two. */ - *vtbase = vb[IOS_SCH_SLOT_BROWSER] - IOS_SCH_GET_HANDLER; + vb = (const ULONG64 *)(base + build->vt_browser); + vr = (const ULONG64 *)(base + build->vt_renderer); + vh = (const ULONG64 *)(base + build->vt_rph); + *vtbase = vb[IOS_SCH_SLOT_BROWSER] - build->get_handler; if (*vtbase != b && *vtbase != IOS_SCH_IMAGE_BASE) return "CefApp vtable base is neither the view nor the preferred base"; - if (vb[IOS_SCH_SLOT_RENDERER] != *vtbase + IOS_SCH_GET_NULL || - vr[IOS_SCH_SLOT_BROWSER] != *vtbase + IOS_SCH_GET_NULL || - vr[IOS_SCH_SLOT_RENDERER] != *vtbase + IOS_SCH_GET_HANDLER) + if (vb[IOS_SCH_SLOT_RENDERER] != *vtbase + build->get_null || + vr[IOS_SCH_SLOT_BROWSER] != *vtbase + build->get_null || + vr[IOS_SCH_SLOT_RENDERER] != *vtbase + build->get_handler) return "CefApp vtables differ"; - if (vh[0] != *vtbase + IOS_SCH_WEBKIT) return "renderer OnWebKitInitialized differs"; - for (k = 0; k < sizeof(ios_sch_renderer_channels); k++) - if (base[IOS_SCH_RENDER_CHANNEL_THUNK + k]) return "renderer channel .text tail not zero"; - for (k = 0; k < sizeof(ios_sch_thunk); k++) - if (base[IOS_SCH_THUNK + k]) return ".text tail not zero"; - for (k = 0; k < sizeof(ios_sch_loop_thunk); k++) - if (base[IOS_SCH_LOOP_INIT + k]) return "renderer loop .text tail not zero"; - for (k = 0; k < 64; k++) - if (base[IOS_SCH_TASK + k]) return "renderer task .data tail not zero"; - for (k = 0; k < sizeof(ios_sch_run_trace); k++) - if (base[IOS_SCH_RUN_INSTALL + k]) return "RunState trace .text tail not zero"; - for (k = 0; k < IOS_SCH_RUN_RING_SIZE; k++) - if (base[IOS_SCH_RUN_RING + k]) return "RunState trace .data tail not zero"; - for (k = 0; k < sizeof(ios_sch_render_drain); k++) - if (base[IOS_SCH_RENDER_DRAIN + k]) return "renderer drain .text tail not zero"; - for (k = 0; k < sizeof(ios_sch_ipc_rebind); k++) - if (base[IOS_SCH_IPC_TRACE + k]) return "IPC trace .text tail not zero"; - if (*(const unsigned int *)(base + IOS_SCH_IPC_COUNT)) return "IPC trace counter not zero"; - for (k = 0; k < sizeof(ios_sch_ipc_split); k++) - if (base[IOS_SCH_IPC_SPLIT + k]) return "IPC split .text tail not zero"; - for (k = 0; k < 16; k++) - if (base[IOS_SCH_IPC_CONTEXT + k]) return "IPC split context not zero"; - for (k = 0; k < sizeof(ios_sch_ui_trace); k++) - if (base[IOS_SCH_UI_TRACE + k]) return "UI notification trace .text tail not zero"; - if (*(const unsigned int *)(base + IOS_SCH_UI_COUNT)) return "UI notification trace counter not zero"; - if (*(const ULONG64 *)(base + IOS_SCH_CACHE)) return ".data tail not zero"; + if (vh[0] != *vtbase + build->webkit) return "renderer OnWebKitInitialized differs"; + for (k = 0; k < build->blob_count; k++) + for (j = 0; j < build->blob[k].len; j++) + if ((unsigned char)base[build->blob[k].rva + j] != build->blob[k].fill) return build->blob[k].what; + for (k = 0; k < build->zero_count; k++) + for (j = 0; j < build->zero[k].len; j++) + if (base[build->zero[k].rva + j]) return build->zero[k].what; return NULL; } @@ -16319,10 +16752,12 @@ static void ios_sc_render_handler_patch( char *base, SIZE_T total_size, const IM const IMAGE_SECTION_HEADER *sec, const UNICODE_STRING *nt_name ) { static int enabled = -1; + const struct ios_sch_build *build = NULL; const char *why; ULONG64 vtbase = 0; const char *ipc_setting = getenv( "MADEIRA_SC_IPC_SPLIT" ); int ipc_split = !(ipc_setting && ipc_setting[0] == '0'); + unsigned int k; if (enabled < 0) { @@ -16331,81 +16766,28 @@ static void ios_sc_render_handler_patch( char *base, SIZE_T total_size, const IM } if (!enabled || !nt_name || !nt_name->Buffer || !ios_sc_path_is_helper( nt_name->Buffer, nt_name->Length / sizeof(WCHAR) )) return; - if ((why = ios_sch_mismatch( base, total_size, nt, sec, &vtbase ))) + if ((why = ios_sch_mismatch( base, total_size, nt, sec, &build, &vtbase ))) { - dprintf( 2, "[sc-rph] madeira-bcd: SocialClubHelper.exe at %p not patched: %s -- its pages get no " - "renderer bridge under --single-process (the launcher times out loading them)\n", base, why ); + dprintf( 2, "[sc-rph] madeira-bcd: SocialClubHelper.exe at %p (TimeDateStamp 0x%08x, SizeOfImage 0x%x) " + "not patched: %s -- its pages get no renderer bridge under --single-process (the launcher " + "times out loading them)\n", base, (unsigned int)nt->FileHeader.TimeDateStamp, + (unsigned int)nt->OptionalHeader.SizeOfImage, why ); return; } - /* The thunk bytes go into the view (ptr); the slot carries the guest - * pointer in the vtables' base (vtbase), which FEX runs through the alias. */ - memcpy( base + IOS_SCH_THUNK, ios_sch_thunk, sizeof(ios_sch_thunk) ); - memcpy( base + IOS_SCH_LOOP_INIT, ios_sch_loop_thunk, sizeof(ios_sch_loop_thunk) ); - memcpy( base + IOS_SCH_RUN_INSTALL, ios_sch_run_trace, sizeof(ios_sch_run_trace) ); - memcpy( base + IOS_SCH_RENDER_DRAIN, ios_sch_render_drain, sizeof(ios_sch_render_drain) ); - memcpy( base + IOS_SCH_IPC_TRACE, ios_sch_ipc_rebind, sizeof(ios_sch_ipc_rebind) ); - memcpy( base + IOS_SCH_UI_TRACE, ios_sch_ui_trace, sizeof(ios_sch_ui_trace) ); - { - int32_t disp = (int32_t)(IOS_SCH_UI_TRACE - IOS_SCH_UI_CTOR - 5); - base[IOS_SCH_UI_CTOR] = (char)0xe9; - memcpy( base + IOS_SCH_UI_CTOR + 1, &disp, sizeof(disp) ); - } - { - int32_t disp = (int32_t)(IOS_SCH_IPC_TRACE - IOS_SCH_IPC_CONNECT - 5); - base[IOS_SCH_IPC_CONNECT] = (char)0xe9; - memcpy( base + IOS_SCH_IPC_CONNECT + 1, &disp, sizeof(disp) ); - } - { - int32_t disp = (int32_t)(IOS_SCH_RENDER_DRAIN - IOS_SCH_RENDER_DRAIN_SITE - 5); - /* Keep CALL so the renderer dispatcher resumes after draining work. */ - base[IOS_SCH_RENDER_DRAIN_SITE] = (char)0xe8; - memcpy( base + IOS_SCH_RENDER_DRAIN_SITE + 1, &disp, sizeof(disp) ); - } - if (ipc_split) - { - /* Multiple pages share this renderer process. The helper's IPC client - * already owns an indexed set of connections; a Channel Response must - * add its channel without disconnecting the previous page's channel. - * Keep the browser transport, explicit disconnects and window-close - * notifications unchanged. The thunk also bounds the client's table; - * the following TEST overwrites its flags. */ - static const struct { unsigned int site, dest, span; unsigned char opcode; } hook[] = - { - { 0x4b290, IOS_SCH_IPC_SPLIT, 7, 0xe9 }, - { 0x4ba50, IOS_SCH_IPC_CAPTURE, 5, 0xe9 }, - { 0x4a5c3, IOS_SCH_IPC_STORE, 7, 0xe9 }, - { 0x4baab, IOS_SCH_IPC_ROLE, 7, 0xe9 }, - { 0x4d255, IOS_SCH_IPC_GLOBAL_LOOP, 5, 0xe8 }, - { 0x4d2fb, IOS_SCH_IPC_INITIAL_CONNECT, 5, 0xe8 }, - { 0x4a719, IOS_SCH_IPC_DESTROY, 7, 0xe9 }, - { 0x4da6c, IOS_SCH_IPC_DIRECT, 7, 0xe9 }, - { IOS_SCH_RENDER_CHANNEL_KEEP, IOS_SCH_RENDER_CHANNEL_THUNK, 7, 0xe9 }, - }; - unsigned int k; - memcpy( base + IOS_SCH_IPC_SPLIT, ios_sch_ipc_split, sizeof(ios_sch_ipc_split) ); - memcpy( base + IOS_SCH_RENDER_CHANNEL_THUNK, ios_sch_renderer_channels, - sizeof(ios_sch_renderer_channels) ); - for (k = 0; k < ARRAY_SIZE(hook); k++) - { - int32_t disp = (int32_t)(hook[k].dest - hook[k].site - 5); - base[hook[k].site] = (char)hook[k].opcode; - memcpy( base + hook[k].site + 1, &disp, sizeof(disp) ); - memset( base + hook[k].site + 5, 0x90, hook[k].span - 5 ); - } - } - /* Both hooks branch within the image, so no new DIR64 pointer is needed. */ - { - const unsigned int site[2] = { 0x174121, 0x17412b }; - const unsigned int dest[2] = { IOS_SCH_RUN_INSTALL, IOS_SCH_RUN_RESTORE }; - const unsigned int span[2] = { 7, 9 }; - int j; - for (j = 0; j < 2; j++) - { - int32_t disp = (int32_t)(dest[j] - site[j] - 5); - base[site[j]] = (char)0xe9; - memcpy( base + site[j] + 1, &disp, sizeof(disp) ); - memset( base + site[j] + 5, 0x90, span[j] - 5 ); - } + /* The code goes into the view (ptr); the slots carry guest pointers in + * the vtables' base (vtbase), which FEX runs through the alias. Every hook + * branches within the image, so no new DIR64 pointer is needed. */ + for (k = 0; k < build->blob_count; k++) + if (ipc_split || !build->blob[k].ipc) + memcpy( base + build->blob[k].rva, build->blob[k].code, build->blob[k].len ); + for (k = 0; k < build->hook_count; k++) + { + const struct ios_sch_hook *hook = &build->hook[k]; + int32_t disp = (int32_t)(hook->dest - hook->site - 5); + if (hook->ipc && !ipc_split) continue; + base[hook->site] = (char)hook->opcode; + memcpy( base + hook->site + 1, &disp, sizeof(disp) ); + memset( base + hook->site + 5, 0x90, hook->span - 5 ); } /* Unlike the existing vtable slots, these new pointers have no DIR64 @@ -16416,22 +16798,20 @@ static void ios_sc_render_handler_patch( char *base, SIZE_T total_size, const IM * or a loop still referenced by the helper's IPC wait thread. */ { ULONG64 callback_base = (ULONG64)(ULONG_PTR)base; - ULONG64 task[8] = { 0x30, callback_base + IOS_SCH_TASK_ADD, callback_base + IOS_SCH_TASK_RELEASE, - callback_base + IOS_SCH_TASK_ONE, callback_base + IOS_SCH_TASK_ANY, - callback_base + IOS_SCH_LOOP_PUMP, 0, 1 }; - memcpy( base + IOS_SCH_TASK, task, sizeof(task) ); - } - ((ULONG64 *)(base + IOS_SCH_VT_BROWSER))[IOS_SCH_SLOT_RENDERER] = vtbase + IOS_SCH_THUNK; - ((ULONG64 *)(base + IOS_SCH_VT_RPH))[0] = vtbase + IOS_SCH_LOOP_INIT; - dprintf( 2, "[sc-rph] madeira-bcd: SocialClubHelper.exe at %p (vtable base 0x%llx): the browser app now hands " - "out its renderer app's CefRenderProcessHandler (thunk guest 0x%llx) -- the page bridge runs under " - "--single-process; renderer MessageLoop adapter installed (CEF task, callback view base 0x%llx, " - "10 ms delay, 32-work budget); bounded I/O RunState trace installed; " - "renderer startup drains its own TLS loop; bounded IPC rebind trace installed; " - "bounded UI notification trace installed; %s " + ULONG64 task[8] = { 0x30, callback_base + build->task_add, callback_base + build->task_release, + callback_base + build->task_one, callback_base + build->task_any, + callback_base + build->loop_pump, 0, 1 }; + memcpy( base + build->task, task, sizeof(task) ); + } + ((ULONG64 *)(base + build->vt_browser))[IOS_SCH_SLOT_RENDERER] = vtbase + build->thunk; + ((ULONG64 *)(base + build->vt_rph))[0] = vtbase + build->loop_init; + dprintf( 2, "[sc-rph] madeira-bcd: SocialClubHelper.exe at %p (TimeDateStamp 0x%08x, vtable base 0x%llx): the " + "browser app now hands out its renderer app's CefRenderProcessHandler (thunk guest 0x%llx) -- the " + "page bridge runs under --single-process; renderer MessageLoop adapter installed (CEF task, callback " + "view base 0x%llx, 10 ms delay, 32-work budget); renderer startup drains its own TLS loop; %s%s " "(MADEIRA_SC_RENDER_HANDLER=0 turns it off)\n", - base, (unsigned long long)vtbase, (unsigned long long)(vtbase + IOS_SCH_THUNK), - (unsigned long long)(ULONG_PTR)base, + base, build->stamp, (unsigned long long)vtbase, (unsigned long long)(vtbase + build->thunk), + (unsigned long long)(ULONG_PTR)base, build->extras, ipc_split ? "separate renderer IPC manager installed; renderer channels retained" : "IPC split disabled" ); } diff --git a/tests/host/check-sc-helper-binary.py b/tests/host/check-sc-helper-binary.py new file mode 100644 index 0000000000..3e76d5cb61 --- /dev/null +++ b/tests/host/check-sc-helper-binary.py @@ -0,0 +1,414 @@ +#!/usr/bin/env python3 +"""[sc-rph] against the owner's real SocialClubHelper.exe, read as data only. + +Set MADEIRA_SC_HELPER_EXE to one or more helper paths (os.pathsep-separated); +without it this test prints SKIP and passes. The binary is never committed and +never executed: Python maps its sections the way Wine's loader does (raw data, +zero fill to the section's page end) and the production guard and patch from +build/ntdll-unix/virtual_ios.c run on that byte copy with a real SHA-256. + +Per supplied build (selected by its TimeDateStamp / SizeOfImage entry in +ios_sch_builds) it checks: + - every guarded window's SHA-256 (hashlib), the CefApp vtable slots, the zero + / RET + int3 destinations, and that no DIR64 relocation touches a guarded + window, a hook, a destination or a zero range while the five vtable slots + the guard reads are relocated; + - no RUNTIME_FUNCTION (.pdata) covers a written destination, so the stubs in + int3 padding and the .text tail sit outside every function; + - the production patch at the preferred base and in a relocated view logs + success and changes only the declared blobs, hooks, task and two vtable + slots, with each hook a branch to its destination; + - MADEIRA_SC_IPC_SPLIT=0 leaves every IPC blob and hook alone; + - MADEIRA_SC_RENDER_HANDLER=0, MADEIRA_SC_CEF=0, another image name and a + one-byte change in any guarded window each write nothing (a mismatch logs + its reason). +""" +from pathlib import Path +import hashlib +import os +import re +import struct +import subprocess +import sys +import tempfile + +paths = [p for p in os.environ.get('MADEIRA_SC_HELPER_EXE', '').split(os.pathsep) if p] +if not paths: + print('SKIP: set MADEIRA_SC_HELPER_EXE to a SocialClubHelper.exe to check it (never executed)') + sys.exit(0) + +root = Path(__file__).resolve().parents[2] +native = (root / 'build/ntdll-unix/virtual_ios.c').read_text() +IMAGE_BASE = 0x140000000 + + +def define(name): + return int(re.search(r'#define ' + name + r'\s+(0x[\da-fA-F]+|\d+)', native)[1], 0) + + +def value(token): + return define(token) if token.startswith('IOS') else int(token, 16) + + +def table(name): + start = native.index(' ' + name + '[] =') + return native[start:native.index('\n};', start)] + + +def array_bytes(name): + start = native.index('static const unsigned char ' + name + '[') + body = native[start:native.index('};', start) + 2] + return bytes(int(x, 16) for x in re.findall(r'0x([\da-fA-F]{2})', body[body.index('{'):])) + + +def function(signature): + start = native.index(signature) + return native[start:native.index('\n}', start) + 2] + + +def build_tables(): + builds = re.search(r'ios_sch_builds\[\] =\n\{(.*?)\n\};', native, re.S)[1] + out = [] + for index, prefix in enumerate(re.findall(r'\n \{ (IOS_SCH\w*?)STAMP,', builds)): + s = prefix[len('IOS_SCH'):-1] + d = lambda macro, P=prefix: define(P + macro) + code = [(value(m[1]), int(m[2], 16), bytes(int(x, 16) for x in re.findall(r'0x([\da-f]{2})', m[3]))) + for m in re.finditer(r'\{ (IOS_SCH\w+|0x[\da-f]+), (0x[\da-f]+),[^{]*\{([^}]*)\}', + table('ios_sch%s_code' % s))] + blobs = [(value(m[1]), array_bytes(m[2]), int(m[3], 16), int(m[4])) for m in re.finditer( + r'\{ (IOS_SCH\w+), (ios_sch\w+), sizeof\(\2\),\s*(0|0xcc), ([01]),', table('ios_sch%s_blob' % s))] + hooks = [(value(m[1]), value(m[2]), int(m[3]), int(m[4], 16), int(m[5])) for m in re.finditer( + r'\{ (IOS_SCH\w+|0x[\da-f]+), (IOS_SCH\w+), (\d), (0x[\da-f]+), ([01]) \}', table('ios_sch%s_hook' % s))] + zeros = [(value(m[1]), int(m[2])) for m in + re.finditer(r'\{ (IOS_SCH\w+), (\d+),', table('ios_sch%s_zero' % s))] + assert code and blobs and hooks and zeros, prefix + out.append(dict(index=index, prefix=prefix, stamp=d('STAMP'), size=d('SIZE'), code=code, blobs=blobs, + hooks=hooks, zeros=zeros, vt_browser=d('VT_BROWSER'), vt_renderer=d('VT_RENDERER'), + vt_rph=d('VT_RPH'), get_handler=d('GET_HANDLER'), get_null=d('GET_NULL'), + webkit=d('WEBKIT'), thunk=d('THUNK'), loop_init=d('LOOP_INIT'), task=d('TASK'))) + return out + + +class Image: + """The helper's sections at their RVAs, as Wine's loader lays them out.""" + + def __init__(self, path): + raw = Path(path).read_bytes() + e = struct.unpack_from('> 12 == 10: + out.append(page + (entry & 0xfff)) + else: + assert entry >> 12 == 0, 'only DIR64 relocations expected' + p += block + return out + + def functions(self): + va, size = self.dirs[3] + return [struct.unpack_from(' +#include +#include +#include +#include +#include +#include +typedef uint64_t ULONG64; +typedef uintptr_t ULONG_PTR; +typedef size_t SIZE_T; +typedef uint16_t WCHAR; +typedef struct { WCHAR *Buffer; uint16_t Length; } UNICODE_STRING; +typedef struct { + struct { uint16_t Machine, NumberOfSections; uint32_t TimeDateStamp; } FileHeader; + struct { uint16_t Magic; uint32_t SizeOfImage; } OptionalHeader; +} IMAGE_NT_HEADERS; +typedef struct { uint32_t VirtualAddress; union { uint32_t VirtualSize; } Misc; + uint32_t SizeOfRawData, Characteristics; } IMAGE_SECTION_HEADER; +#define IMAGE_FILE_MACHINE_AMD64 0x8664 +#define IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b +#define IMAGE_SCN_MEM_EXECUTE 0x20000000 +#define IMAGE_SCN_MEM_WRITE 0x80000000 +#define ARRAY_SIZE(x) (sizeof(x)/sizeof((x)[0])) +#define CC_SHA256_DIGEST_LENGTH 32 +static int ios_sc_cef_enabled(void) { const char *e = getenv("MADEIRA_SC_CEF"); return !(e && *e == '0'); } +/* FIPS 180-4 SHA-256, standing in for CommonCrypto's CC_SHA256. */ +static unsigned char *CC_SHA256(const void *data, uint32_t len, unsigned char *md) +{ + static const uint32_t k[64] = { + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2}; + uint32_t h[8] = {0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19}; + const unsigned char *p = data; + uint64_t bits = (uint64_t)len * 8; + size_t total = ((size_t)len + 9 + 63) & ~(size_t)63, off; + for (off = 0; off < total; off += 64) + { + unsigned char block[64]; + uint32_t w[64], a, b, c, d, e, f, g, hh; + int i; + for (i = 0; i < 64; i++) + { + size_t at = off + i; + block[i] = at < len ? p[at] : at == len ? 0x80 : at >= total - 8 ? (unsigned char)(bits >> (8 * (total - 1 - at))) : 0; + } + for (i = 0; i < 16; i++) w[i] = (uint32_t)block[4*i] << 24 | block[4*i+1] << 16 | block[4*i+2] << 8 | block[4*i+3]; +#define R(x, n) ((x) >> (n) | (x) << (32 - (n))) + for (i = 16; i < 64; i++) + w[i] = w[i-16] + (R(w[i-15], 7) ^ R(w[i-15], 18) ^ (w[i-15] >> 3)) + w[i-7] + + (R(w[i-2], 17) ^ R(w[i-2], 19) ^ (w[i-2] >> 10)); + a = h[0]; b = h[1]; c = h[2]; d = h[3]; e = h[4]; f = h[5]; g = h[6]; hh = h[7]; + for (i = 0; i < 64; i++) + { + uint32_t t1 = hh + (R(e, 6) ^ R(e, 11) ^ R(e, 25)) + ((e & f) ^ (~e & g)) + k[i] + w[i]; + uint32_t t2 = (R(a, 2) ^ R(a, 13) ^ R(a, 22)) + ((a & b) ^ (a & c) ^ (b & c)); + hh = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t1 + t2; + } +#undef R + h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e; h[5] += f; h[6] += g; h[7] += hh; + } + for (int i = 0; i < 32; i++) md[i] = (unsigned char)(h[i / 4] >> (24 - 8 * (i % 4))); + return md; +} +''' + +DRIVER = r''' +static unsigned char *load(const char *path, size_t *size) +{ + FILE *f = fopen(path, "rb"); + assert(f && !fseek(f, 0, SEEK_END)); + *size = (size_t)ftell(f); + rewind(f); + unsigned char *p = mmap(NULL, *size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + assert(p != MAP_FAILED && fread(p, 1, *size, f) == *size && !fclose(f)); + return p; +} +static void save(const char *path, const void *p, size_t size) +{ + FILE *f = fopen(path, "wb"); + assert(f && fwrite(p, 1, size, f) == size && !fclose(f)); +} +/* argv: image, DIR64 RVA list, "preferred" | "view", image name, before, after */ +int main(int argc, char **argv) +{ + size_t size, nrel; + assert(argc == 7); + unsigned char *image = load(argv[1], &size); + uint32_t *rel = (uint32_t *)load(argv[2], &nrel); + const unsigned char *pe = image + *(uint32_t *)(image + 0x3c); + IMAGE_NT_HEADERS nt = {{ *(uint16_t *)(pe + 4), *(uint16_t *)(pe + 6), *(uint32_t *)(pe + 8) }, + { *(uint16_t *)(pe + 24), *(uint32_t *)(pe + 24 + 56) }}; + const unsigned char *table = pe + 24 + *(uint16_t *)(pe + 20); + IMAGE_SECTION_HEADER sec[32]; + assert(nt.FileHeader.NumberOfSections <= 32 && nt.OptionalHeader.SizeOfImage == size); + for (int i = 0; i < nt.FileHeader.NumberOfSections; i++) + { + const unsigned char *s = table + 40 * i; + sec[i] = (IMAGE_SECTION_HEADER){ *(uint32_t *)(s + 12), {*(uint32_t *)(s + 8)}, *(uint32_t *)(s + 16), + *(uint32_t *)(s + 36) }; + } + if (!strcmp(argv[3], "view")) + { + /* The directory applied at the view: the vtables then carry view pointers. */ + uint64_t delta = (uint64_t)(uintptr_t)image - 0x140000000ull; + for (size_t i = 0; i < nrel / 4; i++) *(uint64_t *)(image + rel[i]) += delta; + } + else assert(!strcmp(argv[3], "preferred")); + WCHAR path[64]; + size_t n = strlen(argv[4]); + assert(n < 64); + for (size_t i = 0; i < n; i++) path[i] = (unsigned char)argv[4][i]; + UNICODE_STRING name = {path, (uint16_t)(n * sizeof(WCHAR))}; + save(argv[5], image, size); + fprintf(stderr, "view=0x%llx\n", (unsigned long long)(uintptr_t)image); + fflush(stderr); + ios_sc_render_handler_patch((char *)image, size, &nt, sec, &name); + save(argv[6], image, size); + return 0; +} +''' + +region = native[native.index('#define IOS_SCH_IMAGE_BASE'): + native.index('\n}', native.index('static void ios_sc_render_handler_patch(')) + 2] + + +def run(binary, folder, image, mode, env=None, name='C:\\Program Files\\Social Club\\SocialClubHelper.exe'): + (folder / 'image.bin').write_bytes(image) + result = subprocess.run([str(binary), str(folder / 'image.bin'), str(folder / 'relocs.bin'), mode, name, + str(folder / 'before.bin'), str(folder / 'after.bin')], + capture_output=True, text=True, timeout=60, env={**os.environ, **(env or {})}) + assert not result.returncode, result.stderr + view = int(re.search(r'view=0x([\da-f]+)', result.stderr)[1], 16) + return ((folder / 'before.bin').read_bytes(), (folder / 'after.bin').read_bytes(), + result.stderr, view) + + +def changed(before, after): + return [i for i in range(len(before)) if before[i] != after[i]] if before != after else [] + + +def check_binary(path, builds, binary, folder): + pe = Image(path) + assert pe.machine == 0x8664 and pe.magic == 0x20b and pe.base == IMAGE_BASE, 'x64 image at the preferred base' + matches = [b for b in builds if (b['stamp'], b['size']) == (pe.stamp, pe.size)] + assert matches, 'TimeDateStamp 0x%08x SizeOfImage 0x%x is not an analysed build' % (pe.stamp, pe.size) + build = matches[0] + label = '%s (TimeDateStamp 0x%08x)' % (Path(path).name, pe.stamp) + img = bytes(pe.img) + + # --- data checks, independent of the C guard ------------------------------- + for rva, n, digest in build['code']: + assert hashlib.sha256(img[rva:rva + n]).digest() == digest, (label, hex(rva)) + q = lambda rva: struct.unpack_from(' lo] + for rva, n, _ in build['code']: + assert not hits(rva, rva + n), (label, hex(rva), 'relocation inside a guarded window') + for rva, code, _, _ in build['blobs']: + assert not hits(rva, rva + len(code)), (label, hex(rva), 'relocation inside a destination') + for site, _, span, _, _ in build['hooks']: + assert not hits(site, site + span), (label, hex(site), 'relocation inside a hook') + for rva, n in build['zeros']: + assert not hits(rva, rva + n), (label, hex(rva), 'relocation inside a zero range') + for rva in (build['vt_browser'] + 8 * B, build['vt_browser'] + 8 * R, build['vt_renderer'] + 8 * B, + build['vt_renderer'] + 8 * R, build['vt_rph']): + assert rva in relocs, (label, hex(rva), 'the guard reads a relocated slot') + for begin, end in pe.functions(): + for rva, code, _, _ in build['blobs']: + assert end <= rva or begin >= rva + len(code), (label, hex(rva), 'destination inside a function') + print('PASS: %s: %d SHA-256 windows, vtables, %d destinations, %d zero ranges; no relocation or function ' + 'covers what the patch writes' % (label, len(build['code']), len(build['blobs']), len(build['zeros'])), + flush=True) + + # --- the production guard and patch on the mapped copy ---------------------- + (folder / 'relocs.bin').write_bytes(struct.pack('<%dI' % len(relocs), *relocs)) + rel32 = lambda b, at: struct.unpack_from('code[k].rva && sch_padding_ranges[i][1] == sch_build->code[k].len) + return 1; + return 0; +} static unsigned char sch_filler(size_t rva, size_t i) { return (unsigned char)((rva + i) * 131u + 7u); } static void sch_fill(char *image) { sch_fixture_image = image; - for (size_t k = 0; k < ARRAY_SIZE(ios_sch_code); k++) - for (size_t i = 0; i < ios_sch_code[k].len; i++) - image[ios_sch_code[k].rva + i] = (char)sch_filler(ios_sch_code[k].rva, i); + for (size_t k = 0; k < sch_build->code_count; k++) + for (size_t i = 0; i < sch_build->code[k].len; i++) + image[sch_build->code[k].rva + i] = sch_padding(k) ? (char)(i ? 0xcc : 0xc3) + : (char)sch_filler(sch_build->code[k].rva, i); } static unsigned char *CC_SHA256(const void *data, uint32_t len, unsigned char *md) { memset(md, 0, CC_SHA256_DIGEST_LENGTH); - for (size_t k = 0; k < ARRAY_SIZE(ios_sch_code); k++) + for (size_t k = 0; k < sch_build->code_count; k++) { - const unsigned char *p = (const unsigned char *)sch_fixture_image + ios_sch_code[k].rva; + const unsigned char *p = (const unsigned char *)sch_fixture_image + sch_build->code[k].rva; size_t i; - if ((const void *)p != data || len != ios_sch_code[k].len) continue; - for (i = 0; i < len && p[i] == sch_filler(ios_sch_code[k].rva, i); i++) ; - if (i == len) memcpy(md, ios_sch_code[k].sha256, CC_SHA256_DIGEST_LENGTH); + if ((const void *)p != data || len != sch_build->code[k].len) continue; + for (i = 0; i < len; i++) + if (p[i] != (sch_padding(k) ? (i ? 0xcc : 0xc3) : sch_filler(sch_build->code[k].rva, i))) break; + if (i == len) memcpy(md, sch_build->code[k].sha256, CC_SHA256_DIGEST_LENGTH); } return md; } ''' -defines = '\n'.join(x for x in native.splitlines() if x.startswith('#define IOS_SCH_')) -blob = bytes(int(x, 16) for x in re.findall(r'0x([\da-fA-F]{2})', array('ios_sch_ipc_split'))) -symbols = { - 'cef_currently_on': 0x181af8, 'renderer_manager': define('IOS_SCH_IPC_CONTEXT'), - 'command_line': define('IOS_SCH_IPC_CONTEXT') + 8, 'browser_manager': 0x1cdb90, - 'helper_new': 0x13e708, 'manager_ctor': 0x4a400, 'manager_init': 0x4ba50, - 'init_continue': 0x4ba55, 'ctor_continue': 0x4a5ca, 'role_continue': 0x4bab2, - 'helper_global_loop': 0x11bb70, 'manager_connect': 0x4ac10, - 'manager_dtor': 0x4a6e0, 'helper_delete': 0x13e744, 'dtor_continue': 0x4a720, - 'helper_log': 0x111340, 'manager_getter': 0x4b290, 'direct_continue': 0x4da73, -} -entries = { - 'sc_ipc_get_manager': 'IOS_SCH_IPC_SPLIT', 'sc_ipc_capture_config': 'IOS_SCH_IPC_CAPTURE', - 'sc_ipc_ctor_store': 'IOS_SCH_IPC_STORE', 'sc_ipc_renderer_role': 'IOS_SCH_IPC_ROLE', - 'sc_ipc_global_loop': 'IOS_SCH_IPC_GLOBAL_LOOP', - 'sc_ipc_initial_connect': 'IOS_SCH_IPC_INITIAL_CONNECT', 'sc_ipc_destroy': 'IOS_SCH_IPC_DESTROY', - 'sc_ipc_direct_getter': 'IOS_SCH_IPC_DIRECT', -} -with tempfile.TemporaryDirectory(prefix='madeira-sc-split-') as name: - folder = Path(name) - linker = folder / 'split.ld' - linker.write_text('SECTIONS { . = %d; .text : { *(.text) } }\n' % define('IOS_SCH_IPC_SPLIT') + - '\n'.join('%s = %d;' % x for x in symbols.items()) + '\n') - subprocess.run(['as', '--64', str(root / 'tests/host/sc-ipc-split.S'), - '-o', str(folder / 'split.o')], check=True) - subprocess.run(['ld', '-T', str(linker), str(folder / 'split.o'), - '-o', str(folder / 'split.elf')], check=True) - subprocess.run(['objcopy', '-O', 'binary', '--only-section=.text', - str(folder / 'split.elf'), str(folder / 'split.bin')], check=True) - assert (folder / 'split.bin').read_bytes() == blob - table = subprocess.check_output(['nm', str(folder / 'split.elf')], text=True) - for entry, macro in entries.items(): - assert int(re.search(r'([\da-f]+) T ' + entry + r'\b', table)[1], 16) == define(macro) - trace = bytes(int(x, 16) for x in re.findall(r'0x([\da-fA-F]{2})', array('ios_sch_ipc_rebind'))) - assert define('IOS_SCH_IPC_TRACE') + len(trace) <= define('IOS_SCH_IPC_SPLIT') - assert define('IOS_SCH_IPC_SPLIT') + len(blob) <= 0x181000 - assert define('IOS_SCH_IPC_COUNT') + 4 <= define('IOS_SCH_IPC_CONTEXT') - assert define('IOS_SCH_IPC_CONTEXT') + 16 <= define('IOS_SCH_CACHE') - channel_symbols = {'reply_continue': define('IOS_SCH_RENDER_CHANNEL_KEEP') + 7, - 'reply_cleanup': define('IOS_SCH_RENDER_CHANNEL_REPLY') + 0x19} - channel_linker = folder / 'channels.ld' - channel_linker.write_text('SECTIONS { . = %d; .text : { *(.text) } }\n' % define('IOS_SCH_RENDER_CHANNEL_THUNK') + - '\n'.join('%s = %d;' % x for x in channel_symbols.items()) + '\n') - subprocess.run(['as', '--64', str(root / 'tests/host/sc-renderer-channels.S'), - '-o', str(folder / 'channels.o')], check=True) - subprocess.run(['ld', '-T', str(channel_linker), str(folder / 'channels.o'), - '-o', str(folder / 'channels.elf')], check=True) - subprocess.run(['objcopy', '-O', 'binary', '--only-section=.text', - str(folder / 'channels.elf'), str(folder / 'channels.bin')], check=True) - channel_blob = bytes(int(x, 16) for x in re.findall(r'0x([\da-fA-F]{2})', array('ios_sch_renderer_channels'))) - assert (folder / 'channels.bin').read_bytes() == channel_blob - assert 0x180400 <= define('IOS_SCH_RENDER_CHANNEL_THUNK') - assert define('IOS_SCH_RENDER_CHANNEL_THUNK') + len(channel_blob) <= define('IOS_SCH_THUNK') - - code = r''' +PRELUDE = r''' #define _GNU_SOURCE #include #include @@ -146,11 +153,9 @@ def function(signature): static unsigned char *CC_SHA256(const void *data, uint32_t len, unsigned char *md); static int ios_sc_cef_enabled(void) { const char *e = getenv("MADEIRA_SC_CEF"); return !(e && *e == '0'); } #define dprintf(...) ((void)0) -''' + defines + '\n' + '\n'.join(array(x) for x in ( - 'ios_sch_thunk', 'ios_sch_loop_thunk', 'ios_sch_run_trace', 'ios_sch_render_drain', - 'ios_sch_ipc_rebind', 'ios_sch_ipc_split', 'ios_sch_ui_trace', 'ios_sch_renderer_channels')) + '\n' + guards + '\n' + SHA_STANDIN + '\n' + '\n'.join(function(x) for x in ( - 'static int ios_sc_path_is_helper(', 'static const char *ios_sch_mismatch(', - 'static void ios_sc_render_handler_patch(')) + r''' +''' + +FIXTURE = r''' struct Client { uint16_t *channel; unsigned int active, handler, channels, disconnects; @@ -180,12 +185,18 @@ def function(signature): static struct Manager browser; static struct Manager *(ABI *get_manager)(void); extern struct Manager *ABI probe_get(void *); +extern struct Manager *ABI probe_abi(void *); extern void ABI store_ctor(struct Manager *, void *); extern void ABI store_role(struct Manager *, unsigned int, void *); extern void ABI call_loop(struct Manager *, void *); extern void ABI initial_connect(struct Manager *, const uint16_t *, unsigned int, void *); extern struct Manager *ABI lookup_direct(void *); extern void ABI receive_channel(struct Manager *, const uint16_t **, unsigned int, void *); +#if FULL_PRESERVE +#define PROBE probe_get /* every volatile register, the vectors and CF survive */ +#else +#define PROBE probe_abi /* a Win64 function: its nonvolatile registers survive */ +#endif static _Thread_local int on_renderer; static unsigned int allocations, constructions, initializations, global_loops, deletes, logs, creation_logs, destroys; static int shutdown_complete; @@ -196,8 +207,8 @@ def function(signature): static uint16_t channel0[] = {'r','g','s','c','_','i','p','c','_','c','4','_','c','h','a','n','n','e','l','_','0',0}; static uint16_t channel1[] = {'r','g','s','c','_','i','p','c','_','c','4','_','c','h','a','n','n','e','l','_','1',0}; static uint16_t channel2[] = {'r','g','s','c','_','i','p','c','_','c','4','_','c','h','a','n','n','e','l','_','2',0}; -static struct Manager **browser_slot(void) { return (void *)(image + 0x1cdb90); } -static struct Manager **renderer_slot(void) { return (void *)(image + IOS_SCH_IPC_CONTEXT); } +static struct Manager **browser_slot(void) { return (void *)(image + R_BROWSER); } +static struct Manager **renderer_slot(void) { return (void *)(image + R_CONTEXT); } static int ABI currently_on(int thread) { assert(thread == 6); @@ -224,7 +235,7 @@ def function(signature): { assert(on_renderer && *renderer_slot() == m && *browser_slot() == &browser); constructions++; init_members(m); - store_ctor(m, image + 0x4a5c3); + store_ctor(m, image + R_CTOR_STORE); assert(*browser_slot() == &browser); return m; } @@ -257,11 +268,11 @@ def function(signature): atomic_store(&initializing, 1); while (!atomic_load(&proceed)) { } } - store_role(m, 0, image + 0x4baab); + store_role(m, 0, image + R_ROLE_STORE); assert(m->role == (unsigned int)on_renderer); if (on_renderer) assert(get_manager() == m); /* original Initialize uses this getter too */ - call_loop(m, image + 0x4d255); - initial_connect(m, channel0, 0, image + 0x4d2fb); + call_loop(m, image + R_LOOP_CALL); + initial_connect(m, channel0, 0, image + R_CONNECT_CALL); if (on_renderer) assert(!m->client.active); /* browser's channel must never be opened here */ else assert(m->client.active); config.ref.refs--; @@ -316,13 +327,14 @@ def function(signature): /* Same aligned frame and live values as the original destructor at its hook. */ const unsigned char prologue[] = {0x57,0x56,0x48,0x83,0xec,0x28,0x48,0x89,0xcf,0x31,0xf6, 0x48,0x8b,0x8f,0x28,0x1a,0,0,0xe9,0,0,0,0}; - memcpy(image + 0x4a6e0, prologue, sizeof(prologue)); - int32_t disp = 0x4a719 - (0x4a6e0 + sizeof(prologue)); - memcpy(image + 0x4a6e0 + sizeof(prologue) - 4, &disp, 4); + memcpy(image + R_DTOR, prologue, sizeof(prologue)); + int32_t disp = R_DTOR_STORE - (R_DTOR + sizeof(prologue)); + memcpy(image + R_DTOR + sizeof(prologue) - 4, &disp, 4); + /* After the hook only RCX (the owned object), RDI and RSI are read. */ unsigned char tail[] = {0x48,0x89,0xca,0x48,0x89,0xf9,0x48,0xb8,0,0,0,0,0,0,0,0, 0xff,0xd0,0x48,0x83,0xc4,0x28,0x5e,0x5f,0xc3}; uintptr_t fn = (uintptr_t)finish_destroy; - memcpy(tail + 8, &fn, 8); memcpy(image + 0x4a720, tail, sizeof(tail)); + memcpy(tail + 8, &fn, 8); memcpy(image + R_DTOR_STORE + 7, tail, sizeof(tail)); } static void channel_reply_fixture(const char *before) { @@ -330,9 +342,9 @@ def function(signature): * CefString wrapper (RSI), CALL Connect, and reach the original reply cleanup. * The helper's own bytes stay out of the tree, so this stand-in replaces the * filler between the hook's end and the cleanup, which the patch leaves alone. */ - const size_t at = IOS_SCH_RENDER_CHANNEL_KEEP + 7, end = IOS_SCH_RENDER_CHANNEL_REPLY + 0x19; + const size_t at = R_KEEP + 7, end = R_REPLY + 0x19; unsigned char code[] = {0x48,0x8b,0x16,0xe8,0,0,0,0}; /* mov rdx,[rsi]; call Connect */ - int32_t disp = (int32_t)(IOS_SCH_IPC_CONNECT - (at + sizeof(code))); + int32_t disp = (int32_t)(R_CONNECT - (at + sizeof(code))); assert(end >= at + sizeof(code) && !memcmp(image + at, before + at, end - at)); memcpy(code + 4, &disp, 4); memcpy(image + at, code, sizeof(code)); @@ -342,22 +354,22 @@ def function(signature): { (void)unused; on_renderer = 1; - struct Manager *m = probe_get(get_manager); + struct Manager *m = PROBE(get_manager); assert(m != &browser && m == *renderer_slot() && m->role == 1); - assert(lookup_direct(image + 0x4da6c) == m); + assert(lookup_direct(image + R_DIRECT) == m); m->client.handler = 2; /* renderer handler registration from OnWebKitInitialized */ assert(config.ref.refs == 1 && m->owned != browser.owned && m->name.heap != browser.name.heap); const uint16_t *name = channel1; - receive_channel(m, &name, 0, image + IOS_SCH_RENDER_CHANNEL_KEEP); + receive_channel(m, &name, 0, image + R_KEEP); assert(browser.client.active && browser.name.heap[20] == '0' && browser.client.handler == 1); assert(m->client.active && m->name.heap[20] == '1' && m->client.handler == 2); name = channel2; - receive_channel(m, &name, 1, image + IOS_SCH_RENDER_CHANNEL_KEEP); + receive_channel(m, &name, 1, image + R_KEEP); /* The old instruction would close channel_1 here and notify its peer. * The production patch must leave both frontend connections alive. */ assert(m->client.channels == ((1u << 1) | (1u << 2))); assert(!m->client.disconnects && browser.client.channels == 1 && browser.client.handler == 1); - receive_channel(m, &name, 1, image + IOS_SCH_RENDER_CHANNEL_KEEP); + receive_channel(m, &name, 1, image + R_KEEP); assert(m->client.channels == ((1u << 1) | (1u << 2)) && !m->client.disconnects); assert(m->client.count == 2); /* Execute both capacity branches against the exact client count offset. @@ -365,13 +377,13 @@ def function(signature): * must not call Connect or touch the connections already owned. */ unsigned int calls = connect_calls; m->client.count = 63; - receive_channel(m, &name, 1, image + IOS_SCH_RENDER_CHANNEL_KEEP); + receive_channel(m, &name, 1, image + R_KEEP); assert(connect_calls == calls + 1 && m->client.count == 63); m->client.count = 64; - receive_channel(m, &name, 1, image + IOS_SCH_RENDER_CHANNEL_KEEP); + receive_channel(m, &name, 1, image + R_KEEP); assert(connect_calls == calls + 1 && m->client.count == 64); m->client.count = UINT32_MAX; - receive_channel(m, &name, 1, image + IOS_SCH_RENDER_CHANNEL_KEEP); + receive_channel(m, &name, 1, image + R_KEEP); assert(connect_calls == calls + 1 && m->client.count == UINT32_MAX); assert(m->client.channels == ((1u << 1) | (1u << 2)) && !m->client.disconnects); m->client.count = 2; @@ -402,83 +414,112 @@ def function(signature): "pmovmskb %xmm2,%eax; cmp $65535,%eax; jne probe_fail; pmovmskb %xmm3,%eax; cmp $65535,%eax; jne probe_fail;" "pmovmskb %xmm4,%eax; cmp $65535,%eax; jne probe_fail; pmovmskb %xmm5,%eax; cmp $65535,%eax; jne probe_fail;" "mov %rdi,%rax; jmp probe_return; probe_fail: xor %eax,%eax;" -"probe_return: add $40,%rsp; pop %rdi; pop %rbx; ret\n"); +"probe_return: add $40,%rsp; pop %rdi; pop %rbx; ret\n" +/* Win64 callee contract only: RBX, RBP, RSI, RDI, R12-R15 and XMM6 survive. */ +"probe_abi: push %rbx; push %rbp; push %rsi; push %rdi; push %r12; push %r13; push %r14; push %r15;" +"sub $24,%rsp; movdqu %xmm6,(%rsp); mov %rcx,%rax;" +"mov $101,%ebx; mov $102,%ebp; mov $103,%esi; mov $104,%edi; mov $105,%r12d; mov $106,%r13d;" +"mov $107,%r14d; mov $108,%r15d; pcmpeqd %xmm6,%xmm6; sub $32,%rsp; call *%rax; add $32,%rsp;" +"cmp $101,%rbx; jne abi_fail; cmp $102,%rbp; jne abi_fail; cmp $103,%rsi; jne abi_fail; cmp $104,%rdi; jne abi_fail;" +"cmp $105,%r12; jne abi_fail; cmp $106,%r13; jne abi_fail; cmp $107,%r14; jne abi_fail; cmp $108,%r15; jne abi_fail;" +"mov %rax,%rcx; pmovmskb %xmm6,%eax; cmp $65535,%eax; jne abi_fail; mov %rcx,%rax; jmp abi_return;" +"abi_fail: xor %eax,%eax;" +"abi_return: movdqu (%rsp),%xmm6; add $24,%rsp; pop %r15; pop %r14; pop %r13; pop %r12; pop %rdi; pop %rsi;" +"pop %rbp; pop %rbx; ret\n"); int main(int argc, char **argv) { - image = mmap(NULL, IOS_SCH_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + sch_build = &ios_sch_builds[BUILD]; + assert(sch_build->stamp == B_STAMP && sch_build->size == B_SIZE); + image = mmap(NULL, B_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); assert(image != MAP_FAILED); - IMAGE_NT_HEADERS nt = {{ IMAGE_FILE_MACHINE_AMD64, 2, IOS_SCH_STAMP }, - { IMAGE_NT_OPTIONAL_HDR64_MAGIC, IOS_SCH_SIZE }}; + IMAGE_NT_HEADERS nt = {{ IMAGE_FILE_MACHINE_AMD64, 2, B_STAMP }, + { IMAGE_NT_OPTIONAL_HDR64_MAGIC, B_SIZE }}; IMAGE_SECTION_HEADER sec[2] = { - { IOS_SCH_TEXT, {0x17f34c}, 0x17f400, IMAGE_SCN_MEM_EXECUTE }, - { IOS_SCH_DATA, {0x43bc}, 0x1a00, IMAGE_SCN_MEM_WRITE }}; + { IOS_SCH_TEXT, {B_TEXT_VS}, B_TEXT_RAW, IMAGE_SCN_MEM_EXECUTE }, + { B_DATA, {B_DATA_VS}, 0x1a00, IMAGE_SCN_MEM_WRITE }}; WCHAR path[] = {'S','o','c','i','a','l','C','l','u','b','H','e','l','p','e','r','.','e','x','e'}; UNICODE_STRING name = {path, sizeof(path)}; sch_fill(image); uint64_t base = IOS_SCH_IMAGE_BASE; - *(uint64_t *)(image + IOS_SCH_VT_BROWSER + IOS_SCH_SLOT_BROWSER * 8) = base + IOS_SCH_GET_HANDLER; - *(uint64_t *)(image + IOS_SCH_VT_BROWSER + IOS_SCH_SLOT_RENDERER * 8) = base + IOS_SCH_GET_NULL; - *(uint64_t *)(image + IOS_SCH_VT_RENDERER + IOS_SCH_SLOT_RENDERER * 8) = base + IOS_SCH_GET_HANDLER; - *(uint64_t *)(image + IOS_SCH_VT_RENDERER + IOS_SCH_SLOT_BROWSER * 8) = base + IOS_SCH_GET_NULL; - *(uint64_t *)(image + IOS_SCH_VT_RPH) = base + IOS_SCH_WEBKIT; + *(uint64_t *)(image + sch_build->vt_browser + IOS_SCH_SLOT_BROWSER * 8) = base + sch_build->get_handler; + *(uint64_t *)(image + sch_build->vt_browser + IOS_SCH_SLOT_RENDERER * 8) = base + sch_build->get_null; + *(uint64_t *)(image + sch_build->vt_renderer + IOS_SCH_SLOT_RENDERER * 8) = base + sch_build->get_handler; + *(uint64_t *)(image + sch_build->vt_renderer + IOS_SCH_SLOT_BROWSER * 8) = base + sch_build->get_null; + *(uint64_t *)(image + sch_build->vt_rph) = base + sch_build->webkit; /* The image holds the filler, not the helper's code: the opt-out is checked * against the image as it was before the patch. */ - char *before = malloc(IOS_SCH_SIZE); + char *before = malloc(B_SIZE); assert(before); - memcpy(before, image, IOS_SCH_SIZE); - ios_sc_render_handler_patch(image, IOS_SCH_SIZE, &nt, sec, &name); + memcpy(before, image, B_SIZE); + ios_sc_render_handler_patch(image, B_SIZE, &nt, sec, &name); if (argc > 1 && !strcmp(argv[1], "disabled")) { - assert(!memcmp(image + 0x4b290, before + 0x4b290, 8)); - for (size_t i = 0; i < sizeof(ios_sch_ipc_split); i++) assert(!image[IOS_SCH_IPC_SPLIT+i]); - assert((unsigned char)image[IOS_SCH_IPC_CONNECT] == 0xe9); - assert((unsigned char)image[IOS_SCH_RENDER_DRAIN_SITE] == 0xe8); - assert(!memcmp(image + IOS_SCH_RENDER_CHANNEL_KEEP, before + IOS_SCH_RENDER_CHANNEL_KEEP, 7)); - for (size_t i = 0; i < sizeof(ios_sch_renderer_channels); i++) - assert(!image[IOS_SCH_RENDER_CHANNEL_THUNK+i]); + assert(!memcmp(image + R_GETTER, before + R_GETTER, 8)); + for (size_t k = 0; k < sch_build->blob_count; k++) + if (sch_build->blob[k].ipc) + assert(!memcmp(image + sch_build->blob[k].rva, before + sch_build->blob[k].rva, sch_build->blob[k].len)); + for (size_t k = 0; k < sch_build->hook_count; k++) + if (sch_build->hook[k].ipc) + assert(!memcmp(image + sch_build->hook[k].site, before + sch_build->hook[k].site, sch_build->hook[k].span)); +#if REBIND_TRACE + assert((unsigned char)image[R_CONNECT] == 0xe9); +#endif + assert((unsigned char)image[sch_build == &ios_sch_builds[0] ? IOS_SCH_RENDER_DRAIN_SITE + : IOS_SCH296_RENDER_DRAIN_SITE] == 0xe8); + assert(!memcmp(image + R_KEEP, before + R_KEEP, 7)); free(before); - assert(!munmap(image, IOS_SCH_SIZE)); + assert(!munmap(image, B_SIZE)); puts("PASS: IPC split opt-out preserves the earlier renderer, drain and diagnostic patches"); return 0; } - assert((unsigned char)image[0x4b290] == 0xe9); - assert((unsigned char)image[IOS_SCH_RENDER_CHANNEL_KEEP] == 0xe9); + assert((unsigned char)image[R_GETTER] == 0xe9); + assert((unsigned char)image[R_KEEP] == 0xe9); channel_reply_fixture(before); free(before); - if (argc > 1 && !strcmp(argv[1], "legacy")) - memcpy(image + 0x4b290, "\x48\x8b\x05\xf9\x28\x18\x00\xc3", 8); + if (argc > 1 && !strcmp(argv[1], "legacy")) { + /* The original getter: mov rax, [rip + browser singleton]; ret */ + unsigned char getter[8] = {0x48,0x8b,0x05,0,0,0,0,0xc3}; + int32_t disp = (int32_t)(R_BROWSER - (R_GETTER + 7)); + memcpy(getter + 3, &disp, 4); + memcpy(image + R_GETTER, getter, sizeof(getter)); + } if (argc > 1 && !strcmp(argv[1], "old-rebind")) - memcpy(image + IOS_SCH_RENDER_CHANNEL_KEEP, "\x44\x0f\xb6\xc3\x48\x8b\xc8", 7); - stub(0x13e708, (uintptr_t)allocate); stub(0x4a400, (uintptr_t)construct); - image[0x4a5ca] = image[0x4bab2] = (char)0xc3; - stub(0x4ba55, (uintptr_t)initialize); stub(0x11bb70, (uintptr_t)global_loop); - stub(IOS_SCH_IPC_CONNECT + 5, (uintptr_t)connect_body); - stub(0x111340, (uintptr_t)log_rebind); stub(0x13e744, (uintptr_t)delete_manager); + memcpy(image + R_KEEP, "\x44\x0f\xb6\xc3\x48\x8b\xc8", 7); + stub(R_NEW, (uintptr_t)allocate); stub(R_CTOR, (uintptr_t)construct); + image[R_CTOR_STORE + 7] = image[R_ROLE_STORE + 7] = (char)0xc3; + stub(R_INIT + 5, (uintptr_t)initialize); stub(R_GLOBAL_LOOP, (uintptr_t)global_loop); + stub(R_CONNECT + (REBIND_TRACE ? 5 : 0), (uintptr_t)connect_body); +#if CREATION_LOG || REBIND_TRACE + stub(R_LOG, (uintptr_t)log_rebind); +#else + (void)log_rebind; +#endif + stub(R_DELETE, (uintptr_t)delete_manager); const unsigned char loop_return[] = {0x48,0x83,0xc4,0x20,0x41,0x5e,0xc3}; const unsigned char connect_return[] = {0x48,0x83,0xc4,0x28,0xc3}; const unsigned char direct_return[] = {0x48,0x89,0xd8,0x48,0x83,0xc4,0x20,0x5b,0xc3}; - memcpy(image + 0x4d25a, loop_return, sizeof(loop_return)); - memcpy(image + 0x4d300, connect_return, sizeof(connect_return)); - memcpy(image + 0x4da73, direct_return, sizeof(direct_return)); + memcpy(image + R_LOOP_CALL + 5, loop_return, sizeof(loop_return)); + memcpy(image + R_CONNECT_CALL + 5, connect_return, sizeof(connect_return)); + memcpy(image + R_DIRECT + 7, direct_return, sizeof(direct_return)); const unsigned char reply_return[] = {0x48,0x83,0xc4,0x28,0x5e,0x5b,0xc3}; - memcpy(image + IOS_SCH_RENDER_CHANNEL_REPLY + 0x19, reply_return, sizeof(reply_return)); + memcpy(image + R_REPLY + 0x19, reply_return, sizeof(reply_return)); destructor_fixture(); - *(uintptr_t *)(image + 0x181af8) = (uintptr_t)currently_on; - assert(!mprotect(image + 0x1000, 0x180000, PROT_READ | PROT_EXEC)); + *(uintptr_t *)(image + R_CURRENTLY_ON) = (uintptr_t)currently_on; + assert(!mprotect(image + 0x1000, B_TEXT_END - 0x1000, PROT_READ | PROT_EXEC)); static void *ref_vtable[] = {(void *)add_ref}; config = (struct Config){NULL, vbtable, {ref_vtable, 1}}; *browser_slot() = &browser; init_members(&browser); browser.client.handler = 1; - get_manager = (void *)(image + 0x4b290); - assert(probe_get(get_manager) == &browser && !allocations); - assert(lookup_direct(image + 0x4da6c) == &browser); + get_manager = (void *)(image + R_GETTER); + assert(PROBE(get_manager) == &browser && !allocations); + assert(lookup_direct(image + R_DIRECT) == &browser); struct Config *argument = &config; config.ref.refs++; /* same ownership as the original WinMain call */ - ((int (ABI *)(struct Manager *, struct Config **))(image + 0x4ba50))(&browser, &argument); + ((int (ABI *)(struct Manager *, struct Config **))(image + R_INIT))(&browser, &argument); assert(config.ref.refs == 1 && browser.client.active && global_loops == 1); /* Browser reconnection still closes its old channel when requested; only * the renderer's Channel Response instruction changed. */ - ((void (ABI *)(struct Manager *, const uint16_t *, unsigned int))(image + IOS_SCH_IPC_CONNECT))(&browser, channel1, 1); + ((void (ABI *)(struct Manager *, const uint16_t *, unsigned int))(image + R_CONNECT))(&browser, channel1, 1); assert(browser.client.channels == (1u << 1) && browser.client.disconnects == 1); - ((void (ABI *)(struct Manager *, const uint16_t *, unsigned int))(image + IOS_SCH_IPC_CONNECT))(&browser, channel0, 1); + ((void (ABI *)(struct Manager *, const uint16_t *, unsigned int))(image + R_CONNECT))(&browser, channel0, 1); assert(browser.client.channels == 1 && browser.client.disconnects == 2); pause_init = argc == 1; pthread_t thread; @@ -491,31 +532,147 @@ def function(signature): } assert(!pthread_join(thread, NULL)); assert(get_manager() == &browser && browser.client.active && *renderer_slot()); - assert(logs == 7 && creation_logs == 1 && config.ref.refs == 1); + assert(logs == (REBIND_TRACE ? 7 : 0) && creation_logs == CREATION_LOG && config.ref.refs == 1); shutdown_complete = 1; - ((void (ABI *)(struct Manager *))(image + 0x4a6e0))(&browser); + ((void (ABI *)(struct Manager *))(image + R_DTOR))(&browser); assert(destroys == 2 && deletes == 1 && !*renderer_slot() && !*browser_slot()); assert(destroyed_channels == 3); - assert(!munmap(image, IOS_SCH_SIZE)); + assert(!munmap(image, B_SIZE)); puts("PASS: browser channel_0 and renderer channels_1/2 retain their managers, handlers and connections"); puts("PASS: repeated replies retain connections; the 64-slot bound avoids opening another transport"); +#if FULL_PRESERVE puts("PASS: Win64 scalar/vector/flags state, consumed CefRefPtr, initialization reentry and concurrent browser lookup"); +#else + puts("PASS: Win64 nonvolatile state, consumed CefRefPtr, initialization reentry and concurrent browser lookup"); +#endif puts("PASS: renderer is destroyed and freed once after CEF shutdown, before clearing the browser singleton"); } ''' + + +def link(folder, source, sections, symbols, defsym): + script = 'SECTIONS { %s }\n' % ' '.join('%s 0x%x : { *(%s) }' % (s, a, s) for s, a in sections) + script += '\n'.join('%s = %d;' % item for item in symbols.items()) + '\n' + (folder / 'link.ld').write_text(script) + subprocess.run(['as', '--64', *defsym, str(root / 'tests/host' / source), '-o', str(folder / 'a.o')], check=True) + subprocess.run(['ld', '-T', str(folder / 'link.ld'), str(folder / 'a.o'), '-o', str(folder / 'a.elf')], check=True) + out = {} + for section, _ in sections: + subprocess.run(['objcopy', '-O', 'binary', '--only-section=' + section, + str(folder / 'a.elf'), str(folder / 'a.bin')], check=True) + out[section] = (folder / 'a.bin').read_bytes() + table = subprocess.check_output(['nm', str(folder / 'a.elf')], text=True) + return out, {m[2]: int(m[1], 16) for m in re.finditer(r'(?m)^([\da-f]+) [Tt] (\w+)$', table)} + + +def check_build(index, build, folder): + P = build['prefix'] + d = lambda name: define(P + name) + symbols = { + 'cef_currently_on': build['currently_on'], 'renderer_manager': d('IPC_CONTEXT'), + 'command_line': d('IPC_CONTEXT') + 8, 'browser_manager': build['browser'], + 'helper_new': build['helper_new'], 'manager_ctor': build['ctor'], 'manager_init': build['init'], + 'init_continue': build['init'] + 5, 'ctor_continue': build['ctor_store'] + 7, + 'role_continue': build['role_store'] + 7, 'helper_global_loop': build['global_loop'], + 'manager_connect': build['connect'], 'manager_dtor': build['dtor'], + 'helper_delete': build['helper_delete'], 'dtor_continue': build['dtor_store'] + 7, + 'manager_getter': build['getter'], 'direct_continue': build['direct'] + 7, + } + if build['log']: + symbols['helper_log'] = build['log'] + sections = [(section, d(macro)) for section, macro, _ in build['split']] + out, names = link(folder, 'sc-ipc-split.S', sections, symbols, build['defsym']) + for section, _, blob in build['split']: + assert out[section] == array_bytes(blob), (build['name'], section) + entries = { + 'sc_ipc_get_manager': 'IPC_SPLIT', 'sc_ipc_capture_config': 'IPC_CAPTURE', + 'sc_ipc_ctor_store': 'IPC_STORE', 'sc_ipc_renderer_role': 'IPC_ROLE', + 'sc_ipc_global_loop': 'IPC_GLOBAL_LOOP', 'sc_ipc_initial_connect': 'IPC_INITIAL_CONNECT', + 'sc_ipc_destroy': 'IPC_DESTROY', 'sc_ipc_direct_getter': 'IPC_DIRECT', + } + for entry, macro in entries.items(): + assert names[entry] == d(macro), (build['name'], entry) + keep, reply = d('RENDER_CHANNEL_KEEP'), d('RENDER_CHANNEL_REPLY') + out, _ = link(folder, 'sc-renderer-channels.S', [('.text', d('RENDER_CHANNEL_THUNK'))], + {'reply_continue': keep + 7, 'reply_cleanup': reply + 0x19}, build['defsym']) + assert out['.text'] == array_bytes(P.lower().rstrip('_') + '_renderer_channels'), build['name'] + assert keep == reply + 5 + # The C hook table installs exactly these hooks for the split, at the sites + # the fixture drives (each site is inside a guarded digest range). + expect = { + (build['getter'], d('IPC_SPLIT'), 7, 0xe9, 1), (build['init'], d('IPC_CAPTURE'), 5, 0xe9, 1), + (build['ctor_store'], d('IPC_STORE'), 7, 0xe9, 1), (build['role_store'], d('IPC_ROLE'), 7, 0xe9, 1), + (build['loop_call'], d('IPC_GLOBAL_LOOP'), 5, 0xe8, 1), + (build['connect_call'], d('IPC_INITIAL_CONNECT'), 5, 0xe8, 1), + (build['dtor_store'], d('IPC_DESTROY'), 7, 0xe9, 1), (build['direct'], d('IPC_DIRECT'), 7, 0xe9, 1), + (keep, d('RENDER_CHANNEL_THUNK'), 7, 0xe9, 1), + } + hooks = hook_table(build['hooks']) + assert {h for h in hooks if h[4]} == expect, (build['name'], sorted(expect ^ {h for h in hooks if h[4]})) + table = code_table(build['table']) + for site, _, span, _, _ in hooks: + assert any(rva <= site and site + span <= rva + n for rva, n, _ in table), (build['name'], hex(site)) + padding = [(rva, n) for rva, n, dg in table if dg == hashlib.sha256(b'\xc3' + b'\xcc' * (n - 1)).digest()] + # Layout: tail blobs between .text's VirtualSize and its page end, padding + # stubs inside a guarded RET + int3 run past its RET, and the renderer + # manager slot in .data's zero-filled page slack ahead of the handler cache. + text_start, text_end = 0x1000 + build['text_vs'], (0x1000 + build['text_vs'] + 0xfff) & ~0xfff + for section, macro, blob in build['split'] + [('.text', 'RENDER_CHANNEL_THUNK', P.lower().rstrip('_') + '_renderer_channels')]: + at, n = d(macro), len(array_bytes(blob)) + assert (text_start <= at and at + n <= text_end or + any(rva < at and at + n <= rva + size for rva, size in padding)), (build['name'], macro) + assert build['data'] + build['data_vs'] <= d('IPC_CONTEXT') and d('IPC_CONTEXT') + 16 <= d('CACHE') + assert d('CACHE') + 8 <= (build['data'] + build['data_vs'] + 0xfff) & ~0xfff + assert build['connect'] == d('IPC_CONNECT') + if build['rebind_trace']: + trace = array_bytes('ios_sch_ipc_rebind') + assert define('IOS_SCH_IPC_TRACE') + len(trace) <= define('IOS_SCH_IPC_SPLIT') + assert define('IOS_SCH_IPC_COUNT') + 4 <= define('IOS_SCH_IPC_CONTEXT') + print('PASS: %s: assembled split/channel bytes, entry RVAs and hook table match' % build['name'], flush=True) + + text_end = (0x1000 + build['text_vs'] + 0xfff) & ~0xfff + defines = { + 'BUILD': index, 'B_STAMP': d('STAMP'), 'B_SIZE': d('SIZE'), 'B_TEXT_VS': build['text_vs'], + 'B_TEXT_RAW': build['text_raw'], 'B_DATA': build['data'], 'B_DATA_VS': build['data_vs'], + 'B_TEXT_END': text_end, 'FULL_PRESERVE': build['full_preserve'], + 'CREATION_LOG': build['creation_log'], 'REBIND_TRACE': build['rebind_trace'], + 'R_BROWSER': build['browser'], 'R_CONTEXT': d('IPC_CONTEXT'), 'R_CURRENTLY_ON': build['currently_on'], + 'R_NEW': build['helper_new'], 'R_DELETE': build['helper_delete'], 'R_CTOR': build['ctor'], + 'R_INIT': build['init'], 'R_CTOR_STORE': build['ctor_store'], 'R_ROLE_STORE': build['role_store'], + 'R_GLOBAL_LOOP': build['global_loop'], 'R_LOOP_CALL': build['loop_call'], + 'R_CONNECT_CALL': build['connect_call'], 'R_CONNECT': build['connect'], 'R_DTOR': build['dtor'], + 'R_DTOR_STORE': build['dtor_store'], 'R_GETTER': build['getter'], 'R_DIRECT': build['direct'], + 'R_LOG': build['log'], 'R_KEEP': keep, 'R_REPLY': reply, + } + padding_c = 'static const unsigned int sch_padding_ranges[][2] = {%s};\n' % ( + ','.join('{0x%x,%d}' % p for p in padding) if padding else '{0,0}') + code = PRELUDE + function('static int ios_sc_path_is_helper(') + '\n' + region + '\n' + padding_c + \ + SHA_STANDIN + FIXTURE driver = folder / 'test.c' driver.write_text(code) binary = folder / 'test' subprocess.run([os.environ.get('CC', 'cc'), '-std=c11', '-Wall', '-Wextra', '-Werror', - '-O2', '-fsanitize=address,undefined', '-g', '-pthread', str(driver), + '-O2', '-fsanitize=address,undefined', '-g', '-pthread', + *('-D%s=%d' % item for item in defines.items()), str(driver), '-o', str(binary)], check=True) + print('--- %s' % build['name'], flush=True) subprocess.run([str(binary)], check=True, timeout=30) old = subprocess.run([str(binary), 'legacy'], capture_output=True, text=True, timeout=30, preexec_fn=lambda: resource.setrlimit(resource.RLIMIT_CORE, (0, 0))) assert old.returncode and 'm != &browser' in old.stderr, old.stderr - print('PASS: restoring the legacy getter reproduces the shared manager ownership failure', flush=True) + print('PASS: %s: restoring the legacy getter reproduces the shared manager ownership failure' + % build['name'], flush=True) old_rebind = subprocess.run([str(binary), 'old-rebind'], capture_output=True, text=True, timeout=30, preexec_fn=lambda: resource.setrlimit(resource.RLIMIT_CORE, (0, 0))) assert old_rebind.returncode and 'm->client.channels ==' in old_rebind.stderr, old_rebind.stderr - print('PASS: restoring the old Channel Response instruction closes the first frontend connection', flush=True) + print('PASS: %s: restoring the old Channel Response instruction closes the first frontend connection' + % build['name'], flush=True) subprocess.run([str(binary), 'disabled'], check=True, env={**os.environ, 'MADEIRA_SC_IPC_SPLIT': '0'}) + + +with tempfile.TemporaryDirectory(prefix='madeira-sc-split-') as name: + builds = re.search(r'ios_sch_builds\[\] =\n\{(.*?)\n\};', native, re.S)[1] + assert re.findall(r'\n \{ (IOS_SCH\w*?)STAMP,', builds) == [b['prefix'] for b in BUILDS], \ + 'every analysed build has a fixture, in table order' + for index, build in enumerate(BUILDS): + check_build(index, build, Path(name)) diff --git a/tests/host/check-sc-render-handler.py b/tests/host/check-sc-render-handler.py index a2ac0fc3df..348d6d5da2 100644 --- a/tests/host/check-sc-render-handler.py +++ b/tests/host/check-sc-render-handler.py @@ -3,32 +3,38 @@ Under --single-process Chromium asks the browser CefApp for the renderer's CefRenderProcessHandler, and Social Club's browser app returns NULL, so the page -bridge never ran (GTA log 2026-10-04 12:56: the launcher's pages loaded, nobody -opened the per-page IPC channels, SC_INIT_ERR_WEBSITE_FAILED_LOAD after 75 s). -build/ntdll-unix/virtual_ios.c installs a small thunk in the helper's own image -at map time and points the browser app's GetRenderProcessHandler slot at it. - -This checks the patch structurally, without a Wine run and without reproducing -the helper's own code (the owner's SocialClubHelper.exe is never committed): +bridge never ran (launcher log 2026-10-04 12:56: the launcher's pages loaded, +nobody opened the per-page IPC channels, SC_INIT_ERR_WEBSITE_FAILED_LOAD after +75 s). build/ntdll-unix/virtual_ios.c installs a small thunk in the helper's own +image at map time and points the browser app's GetRenderProcessHandler slot at it. + +This checks the patch structurally for every analysed helper build +(ios_sch_builds), without a Wine run and without reproducing the helper's own +code (the owner's SocialClubHelper.exe is never committed): - the call site sits after relocation and before the section protections and the eager JIT-pool copy, so the copy of .text carries the thunk; - - the thunk is a self-consistent position-independent x64 blob: its five - rip-relative rel32 displacements land exactly on the two cache slots and the + - each build's thunk is tests/host/sc-render-handler.S assembled against that + build's RVAs, and a self-consistent position-independent x64 blob: its + rip-relative rel32 displacements land exactly on the cache slot and the three helper routines it names (operator new, the renderer-app constructor - and the handler getter), computed from the documented RVAs; its two short - branches stay inside the blob; it ends in ret; - - ios_sch_mismatch guards every byte it relies on: it requires the analysed - TimeDateStamp and SizeOfImage, the .text/.data layout, the five code-byte - windows, the four CefApp vtable slots in the view/preferred base, and zero - code/cache tails. The additional renderer-loop adapter and WebKit callback - are executed by check-sc-render-loop.py; + and the handler getter), each the start of a guarded digest window; its + short branches stay inside the blob; it ends in ret; + - ios_sch_mismatch guards every byte it relies on: it selects the build by its + TimeDateStamp and SizeOfImage, then requires that build's .text/.data + layout, code-byte digests, the four CefApp vtable slots in the view or + preferred base, and zero (or int3) destinations. The additional + renderer-loop adapter and WebKit callback are executed by + check-sc-render-loop.py; + - every hook overwrites only bytes inside a guarded digest window; - the guard and the writes are gated by MADEIRA_SC_RENDER_HANDLER and MADEIRA_SC_CEF and by the image being SocialClubHelper.exe. -Needs only python3. +Needs python3 and binutils (as, ld, objcopy). """ from pathlib import Path import re import struct +import subprocess +import tempfile root = Path(__file__).resolve().parents[2] native = (root / 'build/ntdll-unix/virtual_ios.c').read_text() @@ -45,6 +51,21 @@ def define(name): return int(t, 16) if t.startswith('0x') else int(t) +def array_bytes(name): + start = native.index('static const unsigned char ' + name + '[') + body = native[start:native.index('};', start) + 2] + return bytes(int(x, 16) for x in re.findall(r'0x([\da-fA-F]{2})', body[body.index('{'):])) + + +def table(name): + start = native.index(' ' + name + '[] =') + return native[start:native.index('\n};', start)] + + +def value(token): + return define(token) if token.startswith('IOS') else int(token, 16) + + # --- call site ---------------------------------------------------------------- mapper = function(native, 'static NTSTATUS map_image_into_view(') call = mapper.index('ios_sc_render_handler_patch( ptr, total_size, nt, sec, nt_name );') @@ -55,93 +76,130 @@ def define(name): assert call < mapper.index('set_vprot( view, ptr + sec[i].VirtualAddress, size, vprot )'), \ 'before the section protections' -# --- constants ---------------------------------------------------------------- -THUNK = define('IOS_SCH_THUNK') -CACHE = define('IOS_SCH_CACHE') -NEW = 0x13e708 -CTOR = 0x69a0 -GET = define('IOS_SCH_GET_HANDLER') -VT_BROWSER = define('IOS_SCH_VT_BROWSER') -VT_RENDERER = define('IOS_SCH_VT_RENDERER') -GET_NULL = define('IOS_SCH_GET_NULL') SLOT_B = define('IOS_SCH_SLOT_BROWSER') SLOT_R = define('IOS_SCH_SLOT_RENDERER') - -# --- the thunk blob ----------------------------------------------------------- -blob = native[native.index('static const unsigned char ios_sch_thunk[112] ='):] -blob = blob[:blob.index('};') + 2] -thunk = bytes(int(b, 16) for b in re.findall(r'0x([0-9a-fA-F]{2})', blob)) -assert len(thunk) == 112, len(thunk) -assert 0xc3 in thunk, 'the thunk returns' - -# Every `e8 ` (call) and `48 8b 05 / 48 89 05 ` (cache load/store): -# resolve the target from the instruction address relative to the blob at THUNK. -targets = {} -i = 0 -while i < len(thunk) - 4: - if thunk[i] == 0xe8: # call rel32 - disp = struct.unpack_from('` (call) and `48 8b 05 / 48 89 05 ` (cache + # load/store): resolve the target from the instruction address. + targets = {} + i = 0 + while i < len(thunk) - 4: + if thunk[i] == 0xe8: # call rel32 + disp = struct.unpack_from('size', 'IMAGE_FILE_MACHINE_AMD64', 'VirtualAddress == IOS_SCH_TEXT', + 'VirtualAddress == build->data', 'build->code[k].sha256', 'IOS_SCH_SLOT_BROWSER', + 'IOS_SCH_SLOT_RENDERER', 'build->get_handler', 'build->get_null', 'build->webkit', + 'build->blob[k].fill', 'build->zero[k].len'): assert need in guard, need - -code_tbl = native[native.index('static const struct { unsigned int rva'):] -code_tbl = code_tbl[:code_tbl.index('};') + 2] -rvas = [int(x, 16) for x in re.findall(r'\{ (?:IOS_SCH_GET_HANDLER|IOS_SCH_GET_NULL|0x[0-9a-f]+),', code_tbl)] \ - if False else None -# the five guarded windows name new, the ctor, the getter, the null-getter and the C++ slot-4 call -assert 'IOS_SCH_GET_HANDLER' in code_tbl and 'IOS_SCH_GET_NULL' in code_tbl -assert code_tbl.count('{ ') >= 5, 'five code-byte windows' +# the vtable base is read from a slot (the helper is mapped high but runs at its +# preferred base through the sub-floor when its directory is not applied), and +# accepted only as the view or the preferred base. +assert '*vtbase = vb[IOS_SCH_SLOT_BROWSER] - build->get_handler;' in guard, 'derives the vtable base from a slot' +assert "!= IOS_SCH_IMAGE_BASE" in guard and '*vtbase != b' in guard, 'base is the view or the preferred base' patch = function(native, 'static void ios_sc_render_handler_patch(') assert 'MADEIRA_SC_RENDER_HANDLER' in patch and 'ios_sc_cef_enabled()' in patch +assert 'MADEIRA_SC_IPC_SPLIT' in patch assert 'ios_sc_path_is_helper(' in patch, 'only SocialClubHelper.exe' -assert patch.index('if ((why = ios_sch_mismatch(') < patch.index('memcpy( base + IOS_SCH_THUNK'), \ +assert patch.index('if ((why = ios_sch_mismatch(') < patch.index('memcpy( base + build->blob[k].rva'), \ 'the guard runs before any write' -assert 'memcpy( base + IOS_SCH_THUNK, ios_sch_thunk, sizeof(ios_sch_thunk) );' in patch, 'writes the thunk' -assert '((ULONG64 *)(base + IOS_SCH_VT_BROWSER))[IOS_SCH_SLOT_RENDERER] = vtbase + IOS_SCH_THUNK;' \ +after = patch[patch.index('if ((why = ios_sch_mismatch('):] +assert after.index('return;') < after.index('memcpy('), 'a mismatch returns before writing' +assert '((ULONG64 *)(base + build->vt_browser))[IOS_SCH_SLOT_RENDERER] = vtbase + build->thunk;' \ in patch, 'points the browser app slot 4 at the thunk, in the vtables\' own base' -# the vtable base is read from a slot (the helper is mapped high but runs at its -# preferred base through the sub-floor when its directory is not applied), and -# accepted only as the view or the preferred base. -assert '*vtbase = vb[IOS_SCH_SLOT_BROWSER] - IOS_SCH_GET_HANDLER;' in guard, 'derives the vtable base from a slot' -assert "!= IOS_SCH_IMAGE_BASE" in guard and '*vtbase != b' in guard, 'base is the view or the preferred base' -# the two writes land in zero tails past SizeOfRawData (.text raw 0x17f400, .data raw end 0x1cc000+0x1a00) -assert THUNK >= 0x1000 + 0x17f400 - 0x1000, 'thunk past .text raw data' -assert 0x180800 <= THUNK < 0x181000 and 0x1d0000 <= CACHE < 0x1d1000, (hex(THUNK), hex(CACHE)) # The CefApp change is still only browser slot 4. The separate render-process # handler vtable's WebKit callback now initializes and pumps the renderer loop. -assert 'VT_BROWSER))[IOS_SCH_SLOT_RENDERER]' in patch -assert 'VT_RENDERER' not in patch.split('memcpy')[1] and 'SLOT_BROWSER]' not in patch.split('memcpy')[1], \ +assert 'vt_renderer' not in patch and 'SLOT_BROWSER]' not in patch, \ 'CefApp browser slot 3 and the renderer app vtable are unchanged' -assert '((ULONG64 *)(base + IOS_SCH_VT_RPH))[0] = vtbase + IOS_SCH_LOOP_INIT;' in patch +assert '((ULONG64 *)(base + build->vt_rph))[0] = vtbase + build->loop_init;' in patch print('PASS: guarded CefApp slot 4 change and render-process-handler WebKit callback are wired') print('PASS: [sc-rph] renderer-process-handler patch is wired, guarded and self-consistent') diff --git a/tests/host/check-sc-render-loop.py b/tests/host/check-sc-render-loop.py index 7e7e902df0..4ac9b8361e 100644 --- a/tests/host/check-sc-render-loop.py +++ b/tests/host/check-sc-render-loop.py @@ -4,10 +4,13 @@ Also compile the actual image guard/patch, test both vtable pointer bases and reject mismatched images without writes. Exercise the preferred-base case with DIR64 rebasing of the existing vtable slots, without relocations for the new -task callbacks: those must already name the mapped image. No supplied binary. +task callbacks: those must already name the mapped image. Every analysed +helper build (ios_sch_builds) runs: the first build with its .text tail +layout, 2.4.0.296 with its task callbacks in int3 padding. No supplied binary. Requires x86_64 Linux, GNU binutils, Python and a C compiler. """ from pathlib import Path +import hashlib import os import re import resource @@ -29,91 +32,88 @@ def define(name): return int(value, 0) -def array(name): +def array_bytes(name): start = native.index('static const unsigned char ' + name + '[') - return native[start:native.index('};', start) + 2] + body = native[start:native.index('};', start) + 2] + return bytes(int(x, 16) for x in re.findall(r'0x([\da-fA-F]{2})', body[body.index('{'):])) -blob = bytes(int(x, 16) for x in re.findall(r'0x([\da-fA-F]{2})', array('ios_sch_loop_thunk'))) -start = native.index('static const struct { unsigned int rva;') -code_table = native[start:native.index('};', start) + 2] -# The helper's code stays out of the tree: ios_sch_code holds each range's SHA-256. -# The fixture image holds a filler in each range instead, and this stand-in for -# CommonCrypto answers a range's own digest exactly while its filler is intact. +def code_table(name): + """(rva, len, digest) of each guarded range of one build's table.""" + start = native.index(' ' + name + '[] =') + body = native[start:native.index('\n};', start)] + out = [] + for m in re.finditer(r'\{ (IOS_SCH\w+|0x[\da-f]+), (0x[\da-f]+),[^{]*\{([^}]*)\}', body): + rva = define(m[1]) if m[1].startswith('IOS') else int(m[1], 16) + out.append((rva, int(m[2], 16), bytes(int(x, 16) for x in re.findall(r'0x([\da-f]{2})', m[3])))) + return out + + +def padding_ranges(table): + """Ranges whose digest is a RET followed by int3 padding (no helper code).""" + return [(rva, n) for rva, n, d in table if d == hashlib.sha256(b'\xc3' + b'\xcc' * (n - 1)).digest()] + + +# The patch region: every #define, blob, digest table, build table, guard and patch. +region = native[native.index('#define IOS_SCH_IMAGE_BASE'): + native.index('\n}', native.index('static void ios_sc_render_handler_patch(')) + 2] + +# Per build: what the fixture needs beyond ios_sch_builds (helper routines the +# adapter calls and the regions the zero tails must stay in). +BUILDS = [ + dict(name='first build', prefix='IOS_SCH_', table='ios_sch_code', defsym=[], + helper_new=0x13e708, run_until_idle=0x1728b0, global_drain=0x11be60, global_loop=0x1cea00, + text_vs=0x17f34c, text_raw=0x17f400, data=define('IOS_SCH_DATA'), data_vs=0x43bc, data_raw=0x1a00, + loop=['ios_sch_loop_thunk'], diagnostics=True), + dict(name='2.4.0.296', prefix='IOS_SCH296_', table='ios_sch296_code', defsym=['--defsym', 'SCH296=1'], + helper_new=0x1622b8, run_until_idle=0x1965d0, global_drain=0x13eec0, global_loop=0x1f6a10, + text_vs=define('IOS_SCH296_TEXT_VS'), text_raw=define('IOS_SCH296_TEXT_RAW'), + data=define('IOS_SCH296_DATA'), data_vs=define('IOS_SCH296_DATA_VS'), data_raw=0x1a00, + loop=['ios_sch296_loop_thunk', 'ios_sch296_task_add', 'ios_sch296_task_release', + 'ios_sch296_task_one', 'ios_sch296_task_any'], diagnostics=False), +] + +# The helper's code stays out of the tree: each build's table holds each range's +# SHA-256. The fixture image holds a filler in each range instead, and this +# stand-in for CommonCrypto answers a range's own digest exactly while its filler +# is intact. RET + int3 padding ranges hold those bytes themselves (their +# digests are checked against hashlib above, they are not the helper's code). SHA_STANDIN = r''' +static const struct ios_sch_build *sch_build; static char *sch_fixture_image; +static int sch_padding(size_t k) +{ + for (size_t i = 0; i < ARRAY_SIZE(sch_padding_ranges); i++) + if (sch_padding_ranges[i][0] == sch_build->code[k].rva && sch_padding_ranges[i][1] == sch_build->code[k].len) + return 1; + return 0; +} static unsigned char sch_filler(size_t rva, size_t i) { return (unsigned char)((rva + i) * 131u + 7u); } static void sch_fill(char *image) { sch_fixture_image = image; - for (size_t k = 0; k < ARRAY_SIZE(ios_sch_code); k++) - for (size_t i = 0; i < ios_sch_code[k].len; i++) - image[ios_sch_code[k].rva + i] = (char)sch_filler(ios_sch_code[k].rva, i); + for (size_t k = 0; k < sch_build->code_count; k++) + for (size_t i = 0; i < sch_build->code[k].len; i++) + image[sch_build->code[k].rva + i] = sch_padding(k) ? (char)(i ? 0xcc : 0xc3) + : (char)sch_filler(sch_build->code[k].rva, i); } static unsigned char *CC_SHA256(const void *data, uint32_t len, unsigned char *md) { memset(md, 0, CC_SHA256_DIGEST_LENGTH); - for (size_t k = 0; k < ARRAY_SIZE(ios_sch_code); k++) + for (size_t k = 0; k < sch_build->code_count; k++) { - const unsigned char *p = (const unsigned char *)sch_fixture_image + ios_sch_code[k].rva; + const unsigned char *p = (const unsigned char *)sch_fixture_image + sch_build->code[k].rva; size_t i; - if ((const void *)p != data || len != ios_sch_code[k].len) continue; - for (i = 0; i < len && p[i] == sch_filler(ios_sch_code[k].rva, i); i++) ; - if (i == len) memcpy(md, ios_sch_code[k].sha256, CC_SHA256_DIGEST_LENGTH); + if ((const void *)p != data || len != sch_build->code[k].len) continue; + for (i = 0; i < len; i++) + if (p[i] != (sch_padding(k) ? (i ? 0xcc : 0xc3) : sch_filler(sch_build->code[k].rva, i))) break; + if (i == len) memcpy(md, sch_build->code[k].sha256, CC_SHA256_DIGEST_LENGTH); } return md; } ''' -defines = '\n'.join(line for line in native.splitlines() if line.startswith('#define IOS_SCH_')) -symbols = { - 'current_loop': define('IOS_SCH_CURRENT_LOOP'), 'helper_new': 0x13e708, - 'loop_ctor': define('IOS_SCH_LOOP_CTOR'), 'original_webkit': define('IOS_SCH_WEBKIT'), - 'loop_work': define('IOS_SCH_LOOP_WORK'), 'loop_delayed': define('IOS_SCH_LOOP_DELAYED'), - 'loop_idle': define('IOS_SCH_LOOP_IDLE'), 'task': define('IOS_SCH_TASK'), - 'loop_cache': define('IOS_SCH_TASK') + 0x30, 'cef_post_delayed': define('IOS_SCH_POST_TASK'), -} - -with tempfile.TemporaryDirectory(prefix='madeira-sc-loop-') as name: - folder = Path(name) - linker = folder / 'loop.ld' - linker.write_text('SECTIONS { . = %d; .text : { *(.text) } }\n' % define('IOS_SCH_LOOP_INIT') + - '\n'.join('%s = %d;' % item for item in symbols.items()) + '\n') - subprocess.run(['as', '--64', str(root / 'tests/host/sc-render-loop.S'), - '-o', str(folder / 'loop.o')], check=True) - subprocess.run(['ld', '-T', str(linker), str(folder / 'loop.o'), - '-o', str(folder / 'loop.elf')], check=True) - subprocess.run(['objcopy', '-O', 'binary', '--only-section=.text', - str(folder / 'loop.elf'), str(folder / 'loop.bin')], check=True) - assert (folder / 'loop.bin').read_bytes() == blob, 'assembly differs from production bytes' - table = subprocess.check_output(['nm', str(folder / 'loop.elf')], text=True) - for symbol, macro in ( - ('sc_loop_init', 'IOS_SCH_LOOP_INIT'), ('sc_loop_pump', 'IOS_SCH_LOOP_PUMP'), - ('sc_loop_add_ref', 'IOS_SCH_TASK_ADD'), ('sc_loop_release', 'IOS_SCH_TASK_RELEASE'), - ('sc_loop_one_ref', 'IOS_SCH_TASK_ONE'), ('sc_loop_any_ref', 'IOS_SCH_TASK_ANY'), - ): - assert int(re.search(r'([\da-f]+) T ' + symbol + r'\b', table)[1], 16) == define(macro) - assert define('IOS_SCH_THUNK') + 112 <= define('IOS_SCH_LOOP_INIT') - assert 0x180400 <= define('IOS_SCH_LOOP_INIT') < define('IOS_SCH_LOOP_INIT') + len(blob) <= 0x181000 - assert 0x1d03bc <= define('IOS_SCH_TASK') < define('IOS_SCH_TASK') + 64 <= define('IOS_SCH_CACHE') - print('PASS: assembly matches production bytes, entry RVAs and disjoint zero-tail ranges', flush=True) - - drain_linker = folder / 'drain.ld' - drain_linker.write_text('SECTIONS { . = %d; .text : { *(.text) } }\n' % define('IOS_SCH_RENDER_DRAIN') + - 'current_loop = %d; run_until_idle = 0x1728b0;\n' % define('IOS_SCH_CURRENT_LOOP')) - subprocess.run(['as', '--64', str(root / 'tests/host/sc-render-drain.S'), - '-o', str(folder / 'drain.o')], check=True) - subprocess.run(['ld', '-T', str(drain_linker), str(folder / 'drain.o'), - '-o', str(folder / 'drain.elf')], check=True) - subprocess.run(['objcopy', '-O', 'binary', '--only-section=.text', - str(folder / 'drain.elf'), str(folder / 'drain.bin')], check=True) - drain_blob = bytes(int(x, 16) for x in re.findall(r'0x([\da-fA-F]{2})', array('ios_sch_render_drain'))) - assert (folder / 'drain.bin').read_bytes() == drain_blob - trace_blob = bytes(int(x, 16) for x in re.findall(r'0x([\da-fA-F]{2})', array('ios_sch_run_trace'))) - assert define('IOS_SCH_RUN_INSTALL') + len(trace_blob) <= define('IOS_SCH_RENDER_DRAIN') - assert define('IOS_SCH_RENDER_DRAIN') + len(drain_blob) <= 0x181000 - - code = r''' +PRELUDE = r''' #define _GNU_SOURCE #include #include @@ -145,33 +145,146 @@ def array(name): static int ios_sc_cef_enabled(void) { const char *e = getenv("MADEIRA_SC_CEF"); return !(e && e[0] == '0'); } /* Test output contains no guest payload or addresses. */ #define dprintf(...) ((void)0) -''' + defines + '\n' + array('ios_sch_thunk') + '\n' + array('ios_sch_loop_thunk') + '\n' + array('ios_sch_run_trace') + '\n' + array('ios_sch_render_drain') + '\n' + array('ios_sch_ipc_rebind') + '\n' + array('ios_sch_ipc_split') + '\n' + array('ios_sch_ui_trace') + '\n' + array('ios_sch_renderer_channels') + '\n' + code_table + '\n' + SHA_STANDIN + '\n' + function('static int ios_sc_path_is_helper(') + '\n' + function('static const char *ios_sch_mismatch(') + '\n' + function('static void ios_sc_render_handler_patch(') + r''' +''' + + +def link(folder, source, sections, symbols, defsym): + """Assemble `source`, link each (section, address) and return section bytes and symbols.""" + script = 'SECTIONS { %s }\n' % ' '.join('%s 0x%x : { *(%s) }' % (s, a, s) for s, a in sections) + script += '\n'.join('%s = %d;' % item for item in symbols.items()) + '\n' + (folder / 'link.ld').write_text(script) + subprocess.run(['as', '--64', *defsym, str(root / 'tests/host' / source), '-o', str(folder / 'a.o')], check=True) + subprocess.run(['ld', '-T', str(folder / 'link.ld'), str(folder / 'a.o'), '-o', str(folder / 'a.elf')], check=True) + out = {} + for section, _ in sections: + subprocess.run(['objcopy', '-O', 'binary', '--only-section=' + section, + str(folder / 'a.elf'), str(folder / 'a.bin')], check=True) + out[section] = (folder / 'a.bin').read_bytes() + table = subprocess.check_output(['nm', str(folder / 'a.elf')], text=True) + names = {m[2]: int(m[1], 16) for m in re.finditer(r'(?m)^([\da-f]+) [Tt] (\w+)$', table)} + return out, names + + +def check_build(index, build, folder): + P = build['prefix'] + d = lambda name: define(P + name) + table = code_table(build['table']) + padding = padding_ranges(table) + text_end = (0x1000 + build['text_vs'] + 0xfff) & ~0xfff + data_end = (build['data'] + build['data_vs'] + 0xfff) & ~0xfff + + # --- the adapter and drain assemble to the production bytes at their RVAs --- + loop_symbols = { + 'current_loop': d('CURRENT_LOOP'), 'helper_new': build['helper_new'], + 'loop_ctor': d('LOOP_CTOR'), 'original_webkit': d('WEBKIT'), + 'loop_work': d('LOOP_WORK'), 'loop_delayed': d('LOOP_DELAYED'), + 'loop_idle': d('LOOP_IDLE'), 'task': d('TASK'), + 'loop_cache': d('TASK') + 0x30, 'cef_post_delayed': d('POST_TASK'), + } + if build['diagnostics']: + sections = [('.text', d('LOOP_INIT'))] + else: + sections = [('.text', d('LOOP_INIT')), ('.text.add_ref', d('TASK_ADD')), ('.text.release', d('TASK_RELEASE')), + ('.text.one_ref', d('TASK_ONE')), ('.text.any_ref', d('TASK_ANY'))] + out, names = link(folder, 'sc-render-loop.S', sections, loop_symbols, build['defsym']) + for (section, _), blob in zip(sections, build['loop']): + assert out[section] == array_bytes(blob), (build['name'], section, 'assembly differs from production bytes') + for symbol, macro in ( + ('sc_loop_init', 'LOOP_INIT'), ('sc_loop_pump', 'LOOP_PUMP'), + ('sc_loop_add_ref', 'TASK_ADD'), ('sc_loop_release', 'TASK_RELEASE'), + ('sc_loop_one_ref', 'TASK_ONE'), ('sc_loop_any_ref', 'TASK_ANY'), + ): + assert names[symbol] == d(macro), (build['name'], symbol) + out, _ = link(folder, 'sc-render-drain.S', [('.text', d('RENDER_DRAIN'))], + {'current_loop': d('CURRENT_LOOP'), 'run_until_idle': build['run_until_idle']}, build['defsym']) + assert out['.text'] == array_bytes(P.lower().rstrip('_') + '_render_drain') + + # --- every blob sits in its section's zero tail or in guarded RET+int3 padding --- + blobs = re.search(r'ios_sch%s_blob\[\] =\n\{(.*?)\n\};' % ('' if build['diagnostics'] else '296'), native, re.S)[1] + placed = [] + entries = list(re.finditer(r'\{ (IOS_SCH\w+), (ios_sch\w+), sizeof\(\2\),\s*(0x[\da-f]+|0),\s*([01]),', blobs)) + assert len(entries) == blobs.count('{ IOS_SCH'), (build['name'], 'unparsed blob entries') + for m in entries: + rva, length, fill = define(m[1]), len(array_bytes(m[2])), int(m[3], 0) + placed.append((rva, rva + length)) + if fill == 0: + assert 0x1000 + build['text_vs'] <= rva and rva + length <= text_end, (build['name'], m[2]) + else: + assert fill == 0xcc and any(p + 1 <= rva and rva + length <= p + n for p, n in padding), (build['name'], m[2]) + data_ranges = [(d('TASK'), 64), (d('IPC_CONTEXT'), 16), (d('CACHE'), 8)] + if build['diagnostics']: + data_ranges += [(d('RUN_RING'), d('RUN_RING_SIZE')), (d('IPC_COUNT'), 4), (d('UI_COUNT'), 4)] + for rva, n in data_ranges: + assert build['data'] + build['data_vs'] <= rva and rva + n <= data_end, (build['name'], hex(rva)) + placed.append((rva, rva + n)) + placed.sort() + assert all(a[1] <= b[0] for a, b in zip(placed, placed[1:])), (build['name'], 'overlapping blobs') + print('PASS: %s: assembly matches production bytes, entry RVAs and disjoint tail/padding ranges' + % build['name'], flush=True) + + padding_c = 'static const unsigned int sch_padding_ranges[][2] = {%s};\n' % ( + ','.join('{0x%x,%d}' % p for p in padding) if padding else '{0,0}') + defines = { + 'BUILD': index, 'B_STAMP': d('STAMP'), 'B_SIZE': d('SIZE'), 'B_TEXT_VS': build['text_vs'], + 'B_TEXT_RAW': build['text_raw'], 'B_DATA': build['data'], 'B_DATA_VS': build['data_vs'], + 'B_DATA_RAW': build['data_raw'], 'B_TEXT_END': text_end, + 'B_HELPER_NEW': build['helper_new'], 'B_RUN_UNTIL_IDLE': build['run_until_idle'], + 'B_GLOBAL_DRAIN': build['global_drain'], 'B_GLOBAL_LOOP': build['global_loop'], + } + for name in ('VT_BROWSER', 'VT_RENDERER', 'VT_RPH', 'GET_HANDLER', 'GET_NULL', 'WEBKIT', 'THUNK', 'CACHE', + 'LOOP_INIT', 'LOOP_PUMP', 'TASK', 'TASK_ADD', 'TASK_RELEASE', 'TASK_ONE', 'TASK_ANY', + 'CURRENT_LOOP', 'LOOP_CTOR', 'LOOP_WORK', 'LOOP_DELAYED', 'LOOP_IDLE', 'POST_TASK', + 'RENDER_DRAIN', 'RENDER_DRAIN_SITE'): + defines['B_' + name] = d(name) + flags = ['-D%s=%d' % item for item in defines.items()] + + code = PRELUDE + function('static int ios_sc_path_is_helper(') + '\n' + region + '\n' + padding_c + \ + SHA_STANDIN + r''' static IMAGE_NT_HEADERS nt; static IMAGE_SECTION_HEADER sections[2]; static WCHAR helper_name[] = {'S','o','c','i','a','l','C','l','u','b','H','e','l','p','e','r','.','e','x','e'}; static UNICODE_STRING name = { helper_name, sizeof(helper_name) }; static void reset_image(char *image, uint64_t base) { - memset(image, 0, IOS_SCH_SIZE); - nt = (IMAGE_NT_HEADERS){{ IMAGE_FILE_MACHINE_AMD64, 2, IOS_SCH_STAMP }, - { IMAGE_NT_OPTIONAL_HDR64_MAGIC, IOS_SCH_SIZE }}; - sections[0] = (IMAGE_SECTION_HEADER){ IOS_SCH_TEXT, {0x17f34c}, 0x17f400, IMAGE_SCN_MEM_EXECUTE }; - sections[1] = (IMAGE_SECTION_HEADER){ IOS_SCH_DATA, {0x43bc}, 0x1a00, IMAGE_SCN_MEM_WRITE }; + memset(image, 0, B_SIZE); + nt = (IMAGE_NT_HEADERS){{ IMAGE_FILE_MACHINE_AMD64, 2, B_STAMP }, + { IMAGE_NT_OPTIONAL_HDR64_MAGIC, B_SIZE }}; + sections[0] = (IMAGE_SECTION_HEADER){ IOS_SCH_TEXT, {B_TEXT_VS}, B_TEXT_RAW, IMAGE_SCN_MEM_EXECUTE }; + sections[1] = (IMAGE_SECTION_HEADER){ B_DATA, {B_DATA_VS}, B_DATA_RAW, IMAGE_SCN_MEM_WRITE }; sch_fill(image); - uint64_t *vb = (void *)(image + IOS_SCH_VT_BROWSER), *vr = (void *)(image + IOS_SCH_VT_RENDERER); - vb[IOS_SCH_SLOT_BROWSER] = vr[IOS_SCH_SLOT_RENDERER] = base + IOS_SCH_GET_HANDLER; - vb[IOS_SCH_SLOT_RENDERER] = vr[IOS_SCH_SLOT_BROWSER] = base + IOS_SCH_GET_NULL; - *(uint64_t *)(image + IOS_SCH_VT_RPH) = base + IOS_SCH_WEBKIT; + uint64_t *vb = (void *)(image + B_VT_BROWSER), *vr = (void *)(image + B_VT_RENDERER); + vb[IOS_SCH_SLOT_BROWSER] = vr[IOS_SCH_SLOT_RENDERER] = base + B_GET_HANDLER; + vb[IOS_SCH_SLOT_RENDERER] = vr[IOS_SCH_SLOT_BROWSER] = base + B_GET_NULL; + *(uint64_t *)(image + B_VT_RPH) = base + B_WEBKIT; } static void reject_without_writes(char *image) { - char *before = malloc(IOS_SCH_SIZE); + char *before = malloc(B_SIZE); assert(before); - memcpy(before, image, IOS_SCH_SIZE); - ios_sc_render_handler_patch(image, IOS_SCH_SIZE, &nt, sections, &name); - assert(!memcmp(before, image, IOS_SCH_SIZE)); + memcpy(before, image, B_SIZE); + ios_sc_render_handler_patch(image, B_SIZE, &nt, sections, &name); + assert(!memcmp(before, image, B_SIZE)); free(before); } +/* A byte the patch may change: a blob, a hook span, a .data range or one of + * the two vtable slots. Anything else differing is an undeclared write. */ +static int declared(size_t rva, int ipc) +{ + const struct ios_sch_build *b = sch_build; + for (size_t k = 0; k < b->blob_count; k++) + if ((ipc || !b->blob[k].ipc) && rva >= b->blob[k].rva && rva < b->blob[k].rva + b->blob[k].len) return 1; + for (size_t k = 0; k < b->hook_count; k++) + if ((ipc || !b->hook[k].ipc) && rva >= b->hook[k].site && rva < b->hook[k].site + b->hook[k].span) return 1; + if (rva >= b->task && rva < b->task + 64) return 1; + if (rva >= b->vt_browser + 8 * IOS_SCH_SLOT_RENDERER && rva < b->vt_browser + 8 * IOS_SCH_SLOT_RENDERER + 8) return 1; + if (rva >= b->vt_rph && rva < b->vt_rph + 8) return 1; + return 0; +} +static void check_declared(const char *before, const char *image, int ipc) +{ + for (size_t rva = 0; rva < B_SIZE; rva++) + if (before[rva] != image[rva]) assert(declared(rva, ipc)); +} struct Task; struct RunState { uint32_t level, quit; void *dispatcher; }; struct Loop { unsigned char pad[0x128]; struct RunState *state; unsigned char tail[0x30]; }; @@ -361,152 +474,165 @@ def array(name): int main(int argc, char **argv) { (void)argv; - char *image = mmap(NULL, IOS_SCH_SIZE, PROT_READ | PROT_WRITE, + sch_build = &ios_sch_builds[BUILD]; + assert(sch_build->stamp == B_STAMP && sch_build->size == B_SIZE); + for (size_t k = 0; k < ARRAY_SIZE(ios_sch_builds); k++) + assert(k == BUILD || ios_sch_builds[k].stamp != B_STAMP); + char *image = mmap(NULL, B_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); assert(image != MAP_FAILED); uint64_t view = (uintptr_t)image, got = 0; + const struct ios_sch_build *found = NULL; reset_image(image, view); if (argc > 1 && !strcmp(argv[1], "disabled")) { reject_without_writes(image); - assert(!munmap(image, IOS_SCH_SIZE)); + assert(!munmap(image, B_SIZE)); puts("PASS: disabled renderer/CEF adapter leaves the image unchanged"); return 0; } - assert(!ios_sch_mismatch(image, IOS_SCH_SIZE, &nt, sections, &got) && got == view); + assert(!ios_sch_mismatch(image, B_SIZE, &nt, sections, &found, &got) && got == view && found == sch_build); nt.FileHeader.TimeDateStamp ^= 1; reject_without_writes(image); reset_image(image, view); nt.OptionalHeader.SizeOfImage++; reject_without_writes(image); reset_image(image, view); sections[0].Misc.VirtualSize++; reject_without_writes(image); + reset_image(image, view); sections[0].SizeOfRawData += 0x200; reject_without_writes(image); reset_image(image, view); sections[1].Characteristics = 0; reject_without_writes(image); - for (size_t i = 0; i < sizeof(ios_sch_code) / sizeof(ios_sch_code[0]); i++) { - reset_image(image, view); image[ios_sch_code[i].rva] ^= 1; reject_without_writes(image); + reset_image(image, view); sections[1].Misc.VirtualSize++; reject_without_writes(image); + for (size_t i = 0; i < sch_build->code_count; i++) { + reset_image(image, view); image[sch_build->code[i].rva] ^= 1; reject_without_writes(image); + reset_image(image, view); image[sch_build->code[i].rva + sch_build->code[i].len - 1] ^= 1; + reject_without_writes(image); + } + /* Every blob destination (zero tail or int3 padding) and .data range, + * first and last byte. */ + for (size_t i = 0; i < sch_build->blob_count; i++) { + reset_image(image, view); image[sch_build->blob[i].rva] ^= 1; reject_without_writes(image); + reset_image(image, view); image[sch_build->blob[i].rva + sch_build->blob[i].len - 1] ^= 1; + reject_without_writes(image); } - const size_t dirty[] = { IOS_SCH_THUNK, IOS_SCH_LOOP_INIT, IOS_SCH_TASK, - IOS_SCH_RENDER_CHANNEL_THUNK, - IOS_SCH_RENDER_CHANNEL_THUNK + sizeof(ios_sch_renderer_channels) - 1, - IOS_SCH_TASK + 63, IOS_SCH_CACHE, IOS_SCH_VT_RPH, IOS_SCH_RUN_INSTALL, - IOS_SCH_RUN_INSTALL + sizeof(ios_sch_run_trace) - 1, - IOS_SCH_RUN_RING, IOS_SCH_RUN_RING + IOS_SCH_RUN_RING_SIZE - 1, - IOS_SCH_RENDER_DRAIN, IOS_SCH_RENDER_DRAIN + sizeof(ios_sch_render_drain) - 1, - IOS_SCH_IPC_TRACE, IOS_SCH_IPC_TRACE + sizeof(ios_sch_ipc_rebind) - 1, - IOS_SCH_IPC_COUNT, IOS_SCH_IPC_COUNT + 3, - IOS_SCH_IPC_SPLIT, IOS_SCH_IPC_SPLIT + sizeof(ios_sch_ipc_split) - 1, - IOS_SCH_IPC_CONTEXT, IOS_SCH_IPC_CONTEXT + 15, - IOS_SCH_UI_TRACE, IOS_SCH_UI_TRACE + sizeof(ios_sch_ui_trace) - 1, - IOS_SCH_UI_COUNT, IOS_SCH_UI_COUNT + 3 }; - for (size_t i = 0; i < sizeof(dirty) / sizeof(dirty[0]); i++) { - reset_image(image, view); image[dirty[i]] ^= 1; reject_without_writes(image); + for (size_t i = 0; i < sch_build->zero_count; i++) { + reset_image(image, view); image[sch_build->zero[i].rva] ^= 1; reject_without_writes(image); + reset_image(image, view); image[sch_build->zero[i].rva + sch_build->zero[i].len - 1] ^= 1; + reject_without_writes(image); } + reset_image(image, view); image[B_THUNK] ^= 1; reject_without_writes(image); + reset_image(image, view); image[B_TASK + 63] ^= 1; reject_without_writes(image); + reset_image(image, view); image[B_CACHE] ^= 1; reject_without_writes(image); + reset_image(image, view); image[B_VT_RPH] ^= 1; reject_without_writes(image); reset_image(image, view); - *(uint64_t *)(image + IOS_SCH_VT_BROWSER + IOS_SCH_SLOT_BROWSER * 8) = 0x13370000; + *(uint64_t *)(image + B_VT_BROWSER + IOS_SCH_SLOT_BROWSER * 8) = 0x13370000; reject_without_writes(image); reset_image(image, view); - ios_sc_render_handler_patch(image, IOS_SCH_SIZE, &nt, sections, &name); - int32_t drain_disp = 0; - assert((unsigned char)image[IOS_SCH_RENDER_DRAIN_SITE] == 0xe8); - memcpy(&drain_disp, image + IOS_SCH_RENDER_DRAIN_SITE + 1, sizeof(drain_disp)); - assert(IOS_SCH_RENDER_DRAIN_SITE + 5 + drain_disp == IOS_SCH_RENDER_DRAIN); - assert(!memcmp(image + IOS_SCH_RENDER_DRAIN, ios_sch_render_drain, sizeof(ios_sch_render_drain))); - int32_t ipc_disp = 0; - assert((unsigned char)image[IOS_SCH_IPC_CONNECT] == 0xe9); - memcpy(&ipc_disp, image + IOS_SCH_IPC_CONNECT + 1, sizeof(ipc_disp)); - assert(IOS_SCH_IPC_CONNECT + 5 + ipc_disp == IOS_SCH_IPC_TRACE); - assert(!memcmp(image + IOS_SCH_IPC_TRACE, ios_sch_ipc_rebind, sizeof(ios_sch_ipc_rebind))); - assert(!memcmp(image + IOS_SCH_IPC_SPLIT, ios_sch_ipc_split, sizeof(ios_sch_ipc_split))); - const size_t split_sites[] = {0x4b290, 0x4ba50, 0x4a5c3, 0x4baab, 0x4d255, 0x4d2fb, 0x4a719, 0x4da6c}; - const size_t split_entries[] = {IOS_SCH_IPC_SPLIT, IOS_SCH_IPC_CAPTURE, IOS_SCH_IPC_STORE, - IOS_SCH_IPC_ROLE, IOS_SCH_IPC_GLOBAL_LOOP, IOS_SCH_IPC_INITIAL_CONNECT, IOS_SCH_IPC_DESTROY, - IOS_SCH_IPC_DIRECT}; - for (size_t i = 0; i < ARRAY_SIZE(split_sites); i++) { - int32_t displacement = 0; - assert((unsigned char)image[split_sites[i]] == ((i == 4 || i == 5) ? 0xe8 : 0xe9)); - memcpy(&displacement, image + split_sites[i] + 1, sizeof(displacement)); - assert(split_sites[i] + 5 + displacement == split_entries[i]); - } - const size_t trace_sites[] = {0x174121, 0x17412b}; - const size_t trace_entries[] = {IOS_SCH_RUN_INSTALL, IOS_SCH_RUN_RESTORE}; - const size_t trace_spans[] = {7, 9}; - assert(!memcmp(image + IOS_SCH_RUN_INSTALL, ios_sch_run_trace, sizeof(ios_sch_run_trace))); - for (size_t i = 0; i < 2; i++) { + *(uint64_t *)(image + B_VT_RENDERER + IOS_SCH_SLOT_RENDERER * 8) += 0x10; + reject_without_writes(image); + reset_image(image, view); + char *before = malloc(B_SIZE); + assert(before); + memcpy(before, image, B_SIZE); + ios_sc_render_handler_patch(image, B_SIZE, &nt, sections, &name); + check_declared(before, image, 1); + for (size_t i = 0; i < sch_build->blob_count; i++) + assert(!memcmp(image + sch_build->blob[i].rva, sch_build->blob[i].code, sch_build->blob[i].len)); + for (size_t i = 0; i < sch_build->hook_count; i++) { + const struct ios_sch_hook *h = &sch_build->hook[i]; int32_t displacement = 0; - assert((unsigned char)image[trace_sites[i]] == 0xe9); - memcpy(&displacement, image + trace_sites[i] + 1, sizeof(displacement)); - assert(trace_sites[i] + 5 + displacement == trace_entries[i]); - for (size_t j = 5; j < trace_spans[i]; j++) - assert((unsigned char)image[trace_sites[i] + j] == 0x90); + assert((unsigned char)image[h->site] == h->opcode && (h->opcode == 0xe8 || h->opcode == 0xe9)); + memcpy(&displacement, image + h->site + 1, sizeof(displacement)); + assert(h->site + 5 + displacement == h->dest); + for (size_t j = 5; j < h->span; j++) assert((unsigned char)image[h->site + j] == 0x90); + /* the destination is the entry of one of this build's blobs */ + int entry = 0; + for (size_t k = 0; k < sch_build->blob_count; k++) + if (h->dest >= sch_build->blob[k].rva && h->dest < sch_build->blob[k].rva + sch_build->blob[k].len) entry = 1; + assert(entry); } - task = (void *)(image + IOS_SCH_TASK); - assert(*(uint64_t *)(image + IOS_SCH_VT_RPH) == view + IOS_SCH_LOOP_INIT); - assert((uintptr_t)task->execute == view + IOS_SCH_LOOP_PUMP); + int32_t drain_disp = 0; + assert((unsigned char)image[B_RENDER_DRAIN_SITE] == 0xe8); + memcpy(&drain_disp, image + B_RENDER_DRAIN_SITE + 1, sizeof(drain_disp)); + assert(B_RENDER_DRAIN_SITE + 5 + drain_disp == B_RENDER_DRAIN); + task = (void *)(image + B_TASK); + assert(*(uint64_t *)(image + B_VT_RPH) == view + B_LOOP_INIT); + assert(*(uint64_t *)(image + B_VT_BROWSER + IOS_SCH_SLOT_RENDERER * 8) == view + B_THUNK); + assert((uintptr_t)task->execute == view + B_LOOP_PUMP); + reset_image(image, view); + setenv("MADEIRA_SC_IPC_SPLIT", "0", 1); + memcpy(before, image, B_SIZE); + ios_sc_render_handler_patch(image, B_SIZE, &nt, sections, &name); + unsetenv("MADEIRA_SC_IPC_SPLIT"); + check_declared(before, image, 0); + for (size_t i = 0; i < sch_build->blob_count; i++) + if (sch_build->blob[i].ipc) + assert(!memcmp(image + sch_build->blob[i].rva, before + sch_build->blob[i].rva, sch_build->blob[i].len)); + assert(*(uint64_t *)(image + B_VT_RPH) == view + B_LOOP_INIT); + free(before); reset_image(image, IOS_SCH_IMAGE_BASE); - assert(!ios_sch_mismatch(image, IOS_SCH_SIZE, &nt, sections, &got) && got == IOS_SCH_IMAGE_BASE); - ios_sc_render_handler_patch(image, IOS_SCH_SIZE, &nt, sections, &name); - assert(*(uint64_t *)(image + IOS_SCH_VT_RPH) == IOS_SCH_IMAGE_BASE + IOS_SCH_LOOP_INIT); - task = (void *)(image + IOS_SCH_TASK); - assert((uintptr_t)task->add_ref == view + IOS_SCH_TASK_ADD); - assert((uintptr_t)task->release == view + IOS_SCH_TASK_RELEASE); - assert((uintptr_t)task->one_ref == view + IOS_SCH_TASK_ONE); - assert((uintptr_t)task->any_ref == view + IOS_SCH_TASK_ANY); - assert((uintptr_t)task->execute == view + IOS_SCH_LOOP_PUMP); + assert(!ios_sch_mismatch(image, B_SIZE, &nt, sections, &found, &got) && got == IOS_SCH_IMAGE_BASE); + ios_sc_render_handler_patch(image, B_SIZE, &nt, sections, &name); + assert(*(uint64_t *)(image + B_VT_RPH) == IOS_SCH_IMAGE_BASE + B_LOOP_INIT); + task = (void *)(image + B_TASK); + assert((uintptr_t)task->add_ref == view + B_TASK_ADD); + assert((uintptr_t)task->release == view + B_TASK_RELEASE); + assert((uintptr_t)task->one_ref == view + B_TASK_ONE); + assert((uintptr_t)task->any_ref == view + B_TASK_ANY); + assert((uintptr_t)task->execute == view + B_LOOP_PUMP); /* The pool's existing DIR64 entries rebase the vtable slots only. The * new data-tail task has no relocation entries, as in the helper image. */ - *(uint64_t *)(image + IOS_SCH_VT_BROWSER + IOS_SCH_SLOT_RENDERER * 8) += view - IOS_SCH_IMAGE_BASE; - *(uint64_t *)(image + IOS_SCH_VT_RPH) += view - IOS_SCH_IMAGE_BASE; - assert(!memcmp(image + IOS_SCH_LOOP_INIT, ios_sch_loop_thunk, sizeof(ios_sch_loop_thunk))); - puts("PASS: production patch accepts view/preferred bases and rejects changed code/layout/tails without writes"); - - stub(image, IOS_SCH_CURRENT_LOOP, (uintptr_t)get_loop); - stub(image, 0x13e708, (uintptr_t)helper_new); - stub(image, IOS_SCH_LOOP_CTOR, (uintptr_t)construct_loop); - stub(image, IOS_SCH_WEBKIT, (uintptr_t)original_webkit); - stub(image, IOS_SCH_LOOP_WORK, (uintptr_t)do_work); - stub(image, IOS_SCH_LOOP_DELAYED, (uintptr_t)do_delayed); - stub(image, IOS_SCH_LOOP_IDLE, (uintptr_t)do_idle); - stub(image, 0x1728b0, (uintptr_t)run_until_idle); + *(uint64_t *)(image + B_VT_BROWSER + IOS_SCH_SLOT_RENDERER * 8) += view - IOS_SCH_IMAGE_BASE; + *(uint64_t *)(image + B_VT_RPH) += view - IOS_SCH_IMAGE_BASE; + puts("PASS: production patch accepts view/preferred bases, writes only its declared ranges and rejects changed code/layout/tails without writes"); + + stub(image, B_CURRENT_LOOP, (uintptr_t)get_loop); + stub(image, B_HELPER_NEW, (uintptr_t)helper_new); + stub(image, B_LOOP_CTOR, (uintptr_t)construct_loop); + stub(image, B_WEBKIT, (uintptr_t)original_webkit); + stub(image, B_LOOP_WORK, (uintptr_t)do_work); + stub(image, B_LOOP_DELAYED, (uintptr_t)do_delayed); + stub(image, B_LOOP_IDLE, (uintptr_t)do_idle); + stub(image, B_RUN_UNTIL_IDLE, (uintptr_t)run_until_idle); /* The browser's global drain, as a stand-in (the helper's own bytes stay out * of the tree, the image holds the filler): RunUntilIdle on the loop the * process-wide slot holds. mov rcx, [rip + slot]; jmp RunUntilIdle */ unsigned char global_drain[12] = {0x48,0x8b,0x0d,0,0,0,0,0xe9,0,0,0,0}; - int32_t slot_disp = 0x1cea00 - (0x11be60 + 7), run_disp = 0x1728b0 - (0x11be60 + 12); + int32_t slot_disp = B_GLOBAL_LOOP - (B_GLOBAL_DRAIN + 7), run_disp = B_RUN_UNTIL_IDLE - (B_GLOBAL_DRAIN + 12); memcpy(global_drain + 3, &slot_disp, sizeof(slot_disp)); memcpy(global_drain + 8, &run_disp, sizeof(run_disp)); - memcpy(image + 0x11be60, global_drain, sizeof(global_drain)); + memcpy(image + B_GLOBAL_DRAIN, global_drain, sizeof(global_drain)); /* Self-written call-frame fixture around the real patched CALL. */ const unsigned char enter[] = {0x48,0x83,0xec,0x28}; const unsigned char leave[] = {0x48,0x83,0xc4,0x28,0xc3}; - memcpy(image + IOS_SCH_RENDER_DRAIN_SITE - sizeof(enter), enter, sizeof(enter)); - memcpy(image + IOS_SCH_RENDER_DRAIN_SITE + 5, leave, sizeof(leave)); - drain_entry = (void *)(image + IOS_SCH_RENDER_DRAIN_SITE - sizeof(enter)); + memcpy(image + B_RENDER_DRAIN_SITE - sizeof(enter), enter, sizeof(enter)); + memcpy(image + B_RENDER_DRAIN_SITE + 5, leave, sizeof(leave)); + drain_entry = (void *)(image + B_RENDER_DRAIN_SITE - sizeof(enter)); if (argc > 1 && !strcmp(argv[1], "legacy-drain")) { /* Control: the original global drain must fail the TLS ownership * check while the browser's state is active on the other thread. */ - int32_t old_disp = 0x11be60 - IOS_SCH_RENDER_DRAIN_SITE - 5; - memcpy(image + IOS_SCH_RENDER_DRAIN_SITE + 1, &old_disp, sizeof(old_disp)); + int32_t old_disp = B_GLOBAL_DRAIN - B_RENDER_DRAIN_SITE - 5; + memcpy(image + B_RENDER_DRAIN_SITE + 1, &old_disp, sizeof(old_disp)); } - *(uintptr_t *)(image + IOS_SCH_POST_TASK) = (uintptr_t)post_delayed; - assert(!mprotect(image + 0x1000, 0x180000, PROT_READ | PROT_EXEC)); + *(uintptr_t *)(image + B_POST_TASK) = (uintptr_t)post_delayed; + assert(!mprotect(image + 0x1000, B_TEXT_END - 0x1000, PROT_READ | PROT_EXEC)); struct Loop browser = {0}; struct RunState active_browser = {7, 0, (void *)0x87654321}; browser.state = &active_browser; browser_loop = &browser; browser_parent = &active_browser; - *(struct Loop **)(image + 0x1cea00) = &browser; + *(struct Loop **)(image + B_GLOBAL_LOOP) = &browser; current = &browser; pthread_t thread; - void *initialize = (void *)(uintptr_t)*(uint64_t *)(image + IOS_SCH_VT_RPH); - assert(initialize == image + IOS_SCH_LOOP_INIT); + void *initialize = (void *)(uintptr_t)*(uint64_t *)(image + B_VT_RPH); + assert(initialize == image + B_LOOP_INIT); assert(!pthread_create(&thread, NULL, run_renderer, initialize)); assert(!pthread_join(thread, NULL)); assert(current == &browser); /* Renderer initialization never borrows browser TLS. */ assert(browser.state == browser_parent && active_browser.level == 7 && !active_browser.quit && active_browser.dispatcher == (void *)0x87654321); allocation_fail = 0; - int before = posts; - ((void (ABI *)(void *))(image + IOS_SCH_LOOP_INIT))(expected_handler); - assert(current == &browser && posts == before && constructions == 1); + int before_posts = posts; + ((void (ABI *)(void *))(image + B_LOOP_INIT))(expected_handler); + assert(current == &browser && posts == before_posts && constructions == 1); assert(browser.state == browser_parent && drain_calls == 4); - ((void (ABI *)(void))(image + 0x11be60))(); /* Browser's original global drain still works. */ + ((void (ABI *)(void))(image + B_GLOBAL_DRAIN))(); /* Browser's original global drain still works. */ assert(browser.state == browser_parent && drain_calls == 5); - assert(!munmap(image, IOS_SCH_SIZE)); + assert(!munmap(image, B_SIZE)); puts("PASS: preferred vtables + unrelocated task callbacks execute; TLS, IPC work/timers, bounds, refs and shutdown"); puts("PASS: renderer startup drains its TLS work, nested states restore, active browser state stays intact"); return 0; @@ -517,12 +643,21 @@ def array(name): driver.write_text(code) binary = folder / 'test' subprocess.run([os.environ.get('CC', 'cc'), '-std=c11', '-Wall', '-Wextra', '-Werror', - '-O2', '-fsanitize=address,undefined', '-g', '-pthread', str(driver), + '-O2', '-fsanitize=address,undefined', '-g', '-pthread', *flags, str(driver), '-o', str(binary)], check=True) + print('--- %s' % build['name'], flush=True) subprocess.run([str(binary)], check=True) legacy = subprocess.run([str(binary), 'legacy-drain'], capture_output=True, text=True, preexec_fn=lambda: resource.setrlimit(resource.RLIMIT_CORE, (0, 0))) assert legacy.returncode != 0 and 'loop == current' in legacy.stderr, legacy.stderr - print('PASS: original renderer CALL fails the cross-thread TLS ownership control', flush=True) + print('PASS: %s: original renderer CALL fails the cross-thread TLS ownership control' % build['name'], flush=True) for variable in ('MADEIRA_SC_RENDER_HANDLER', 'MADEIRA_SC_CEF'): subprocess.run([str(binary), 'disabled'], check=True, env={**os.environ, variable: '0'}) + + +with tempfile.TemporaryDirectory(prefix='madeira-sc-loop-') as name: + builds = re.search(r'ios_sch_builds\[\] =\n\{(.*?)\n\};', native, re.S)[1] + assert re.findall(r'\n \{ (IOS_SCH\w*?)STAMP,', builds) == [b['prefix'] for b in BUILDS], \ + 'every analysed build has a fixture, in table order' + for index, build in enumerate(BUILDS): + check_build(index, build, Path(name)) diff --git a/tests/host/check-sc-runstate.py b/tests/host/check-sc-runstate.py index 0a10ac04f0..8781bbfe5a 100644 --- a/tests/host/check-sc-runstate.py +++ b/tests/host/check-sc-runstate.py @@ -27,6 +27,13 @@ def define(name): assert define('IOS_SCH_TASK_ANY') + 11 <= define('IOS_SCH_RUN_INSTALL') assert define('IOS_SCH_RUN_INSTALL') + len(blob) <= 0x181000 assert int(re.search(r'#define IOS_SC_RUN_RING_RVA (0x[\da-f]+)', header.read_text())[1], 0) == define('IOS_SCH_RUN_RING') +# 2.4.0.296 has no room for the trace: its build entry installs no RunState +# hook and its patch never names the ring, which the diagnostic gate rejects. +later = native[native.index('static const struct ios_sch_blob ios_sch296_blob[]'): + native.index('static const struct ios_sch_build ios_sch_builds[]')] +assert 'run_trace' not in later and 'RUN_' not in later +assert not re.search(r'#define IOS_SCH296_RUN', native) +assert define('IOS_SCH296_STAMP') == 0x6ae5e38b and define('IOS_SCH296_SIZE') == 0x26d000 with tempfile.TemporaryDirectory(prefix='madeira-sc-runstate-') as name: folder = Path(name) @@ -220,6 +227,12 @@ def define(name): *(uint32_t *)(image + 0x88) ^= 1; assert(!ios_sc_diag_helper((uintptr_t)image, (uintptr_t)image + 0x173be4)); *(uint32_t *)(image + 0x88) ^= 1; + /* 2.4.0.296 (ios_sch_builds[1]) has no RunState trace or ring: never read one. */ + *(uint32_t *)(image + 0x88) = 0x6ae5e38b; *(uint32_t *)(image + 0xd0) = 0x26d000; + assert(!ios_sc_diag_helper((uintptr_t)image, (uintptr_t)image + 0x173be4)); + *(uint32_t *)(image + 0xd0) = 0x244000; + assert(!ios_sc_diag_helper((uintptr_t)image, (uintptr_t)image + 0x173be4)); + *(uint32_t *)(image + 0x88) = 0x6a86d563; memcpy(pool + IOS_SC_RUN_RING_RVA, ring, sizeof(*ring)); uint64_t stack[256] = {0}; stack[12] = (uintptr_t)image + 0x17412b; ios_sc_runstate_dump((uintptr_t)image, (uintptr_t)image + 0x173be4, (uintptr_t)&p, (uintptr_t)stack); diff --git a/tests/host/check-sc-ui-close.py b/tests/host/check-sc-ui-close.py index 9286fc242d..55d3829f1a 100644 --- a/tests/host/check-sc-ui-close.py +++ b/tests/host/check-sc-ui-close.py @@ -15,6 +15,11 @@ native = (root / 'build/ntdll-unix/virtual_ios.c').read_text() +def sch_table(name): + start = native.index(' ' + name + '[] =') + return native[start:native.index('\n};', start)] + + def define(name): return int(re.search(r'#define ' + name + r'\s+(0x[\da-fA-F]+|\d+)', native)[1], 0) @@ -30,6 +35,16 @@ def define(name): assert define('IOS_SCH_IPC_CONTEXT') + 16 <= define('IOS_SCH_UI_COUNT') assert define('IOS_SCH_UI_COUNT') + 4 <= define('IOS_SCH_CACHE') + +# The trace is the first build's alone (ios_sch_builds[0]): its tables install +# the observer and its constructor hook; 2.4.0.296's tables carry neither, so that +# build's message constructor runs unmodified. +first_blob, first_hook = sch_table('ios_sch_blob'), sch_table('ios_sch_hook') +later_blob, later_hook = sch_table('ios_sch296_blob'), sch_table('ios_sch296_hook') +assert '{ IOS_SCH_UI_TRACE, ios_sch_ui_trace, sizeof(ios_sch_ui_trace), 0, 0,' in first_blob +assert '{ IOS_SCH_UI_CTOR, IOS_SCH_UI_TRACE, 5, 0xe9, 0 },' in first_hook +assert 'ui_trace' not in later_blob and 'UI_' not in later_hook and not re.search(r'#define IOS_SCH296_UI', native) + with tempfile.TemporaryDirectory(prefix='madeira-ui-close-') as tmp: folder = Path(tmp) linker = folder / 'trace.ld' diff --git a/tests/host/sc-ipc-split.S b/tests/host/sc-ipc-split.S index d47fe2c823..9fa1d91276 100644 --- a/tests/host/sc-ipc-split.S +++ b/tests/host/sc-ipc-split.S @@ -4,6 +4,13 @@ * allocator, constructor and initializer; no live manager is shallow-copied. * The main command-line reference stays alive until after cef_shutdown. The * additional by-value reference passed to Initialize is explicitly retained. + * + * Assembled twice. Without symbols: the first helper build, everything in one + * .text tail blob. With --defsym SCH296=1: Social Club 2.4.0.296, whose .text + * tail has 792 bytes, so the getter and destructor hooks are plain Win64 + * functions (their sites have no live volatile state, see virtual_ios.c), the + * creation log line is dropped, and the three small stubs go into their own + * sections, which the test links at verified int3 padding between functions. */ .intel_syntax noprefix .text @@ -11,6 +18,46 @@ .global sc_ipc_renderer_role, sc_ipc_global_loop, sc_ipc_initial_connect .global sc_ipc_destroy .global sc_ipc_direct_getter +.ifdef SCH296 +/* Every caller CALLs the getter (the one inlined load is routed through a CALL + * below), so only the Win64 nonvolatile registers need to survive. */ +sc_ipc_get_manager: + sub rsp, 0x28 + mov ecx, 6 /* TID_RENDERER */ + call qword ptr [rip+cef_currently_on] + test eax, eax + je browser + mov rax, qword ptr [rip+renderer_manager] + test rax, rax + jne get_done /* also supports Initialize's own getters */ + cmp qword ptr [rip+command_line], 0 + je browser /* CEF has not been initialized yet */ + mov ecx, 0x1a30 + call helper_new + test rax, rax + je browser + mov qword ptr [rip+renderer_manager], rax + mov rcx, rax + call manager_ctor + mov rdx, qword ptr [rip+command_line] + mov qword ptr [rsp+0x20], rdx /* CefRefPtr passed by value to Initialize */ + mov rax, qword ptr [rdx+8] + lea rcx, [rdx+8] + movsxd rax, dword ptr [rax+4] + add rcx, rax + mov rax, qword ptr [rcx] + call qword ptr [rax] /* CefBaseRefCounted::AddRef */ + lea rdx, [rsp+0x20] + mov rcx, qword ptr [rip+renderer_manager] + call manager_init + mov rax, qword ptr [rip+renderer_manager] + jmp get_done +browser: + mov rax, qword ptr [rip+browser_manager] +get_done: + add rsp, 0x28 + ret +.else sc_ipc_get_manager: pushfq push rcx @@ -80,9 +127,14 @@ get_done: pop rcx popfq ret +.endif sc_ipc_capture_config: +.ifdef SCH296 + mov qword ptr [rsp+0x10], rbx /* displaced Initialize instruction */ +.else mov qword ptr [rsp+0x20], rbx /* displaced Initialize instruction */ +.endif push rax mov rax, qword ptr [rdx] mov qword ptr [rip+command_line], rax @@ -108,12 +160,19 @@ role_done: popfq jmp role_continue +.ifdef SCH296 +.pushsection .text.global_loop, "ax" +.endif sc_ipc_global_loop: cmp r14, qword ptr [rip+renderer_manager] je global_loop_done /* browser already owns the global I/O loop */ jmp helper_global_loop global_loop_done: ret +.ifdef SCH296 +.popsection +.pushsection .text.initial_connect, "ax" +.endif sc_ipc_initial_connect: cmp rcx, qword ptr [rip+renderer_manager] @@ -121,11 +180,39 @@ sc_ipc_initial_connect: jmp manager_connect initial_connect_done: ret +.ifdef SCH296 +.popsection +.endif /* Called from the destructor after the original handle-close code, with * RDI=this, RSI=0 and RCX=this->owned object. Destroy the renderer before * clearing the browser singleton, after the main path has called cef_shutdown. * The recursive renderer destructor clears only the renderer cache. */ +.ifdef SCH296 +/* 2.4.0.296: CloseHandle was the last call, and the code after the hook + * reloads or tests everything it uses except RCX. The frame is aligned. */ +sc_ipc_destroy: + cmp rdi, qword ptr [rip+renderer_manager] + je destroy_renderer + cmp qword ptr [rip+renderer_manager], 0 + je destroy_store + push rcx + sub rsp, 0x28 + mov rcx, qword ptr [rip+renderer_manager] + mov qword ptr [rsp+0x20], rcx + call manager_dtor + mov rcx, qword ptr [rsp+0x20] + mov edx, 0x1a30 + call helper_delete + add rsp, 0x28 + pop rcx +destroy_store: + mov qword ptr [rip+browser_manager], rsi + jmp dtor_continue +destroy_renderer: + mov qword ptr [rip+renderer_manager], 0 + jmp dtor_continue +.else sc_ipc_destroy: pushfq cmp rdi, qword ptr [rip+renderer_manager] @@ -174,8 +261,19 @@ destroy_renderer: mov qword ptr [rip+renderer_manager], 0 popfq jmp dtor_continue +.endif /* One helper function inlines the singleton load instead of calling its * getter. Route that lookup by the same CEF thread, preserving its live RAX. */ +.ifdef SCH296 +/* 2.4.0.296: the load follows a CALL and the code after it overwrites RAX, + * RCX and the flags before reading them: CALL the getter like its callers. */ +.pushsection .text.direct, "ax" +sc_ipc_direct_getter: + call manager_getter + mov rbx, rax + jmp direct_continue +.popsection +.else sc_ipc_direct_getter: pushfq push rax @@ -188,3 +286,4 @@ sc_ipc_direct_getter: jmp direct_continue created_message: .asciz "[sc-ipc] renderer manager=%p browser=%p init=%u" +.endif diff --git a/tests/host/sc-render-handler.S b/tests/host/sc-render-handler.S new file mode 100644 index 0000000000..84123a94b6 --- /dev/null +++ b/tests/host/sc-render-handler.S @@ -0,0 +1,43 @@ +/* [sc-rph] browser CefApp::GetRenderProcessHandler replacement embedded in + * virtual_ios.c (ios_sch_thunk, ios_sch296_thunk). check-sc-render-handler.py + * links the symbols to each build's guarded helper RVAs and compares the bytes + * with the production blob. RDX is the returned CefRefPtr; the renderer app + * is built once with the helper's own operator new and constructor, and one + * reference to it stays in the .data tail cache. */ +.intel_syntax noprefix +.text +.global sc_render_handler +sc_render_handler: + push rbx + sub rsp, 0x30 + mov rbx, rdx + mov rax, qword ptr [rip+cache] + test rax, rax + jnz have + mov ecx, 0x60 + call helper_new + test rax, rax + jz none + mov rcx, rax + mov edx, 1 /* most derived */ + call renderer_app_ctor + mov qword ptr [rsp+0x20], rax + mov rcx, qword ptr [rax+8] /* AddRef through the virtual base */ + movsxd rcx, dword ptr [rcx+4] + lea rcx, [rax+rcx+8] + mov rdx, qword ptr [rcx] + call qword ptr [rdx] + mov rax, qword ptr [rsp+0x20] + mov qword ptr [rip+cache], rax +have: + mov rcx, rax + mov rdx, rbx + call get_handler /* App::GetRenderProcessHandler: this+0x28 */ +out: + mov rax, rbx + add rsp, 0x30 + pop rbx + ret +none: + mov qword ptr [rbx], 0 + jmp out diff --git a/tests/host/sc-render-loop.S b/tests/host/sc-render-loop.S index a647edda6d..9ca3ae3b0e 100644 --- a/tests/host/sc-render-loop.S +++ b/tests/host/sc-render-loop.S @@ -1,6 +1,9 @@ /* Win64 renderer-loop adapter embedded in virtual_ios.c. * check-sc-render-loop.py links the symbols to the guarded helper RVAs and - * checks these bytes against the production blob before executing it. */ + * checks these bytes against the production blob before executing it. + * With --defsym SCH296=1 (Social Club 2.4.0.296) the four task callbacks are + * in sections of their own, linked at verified int3 padding between helper + * functions; the adapter itself is unchanged. */ .intel_syntax noprefix .text .global sc_loop_init, sc_loop_pump, sc_loop_add_ref, sc_loop_release, sc_loop_one_ref, sc_loop_any_ref @@ -87,26 +90,35 @@ pump_done: pop rsi pop rbx ret +.ifdef SCH296 +.macro sc_callback name +.section .text.\name, "ax" +.endm +.else /* The 15-byte gap spelled out as production has it (an 11-byte NOP, then a * 4-byte one): GNU as releases order a two-NOP pad differently. */ .byte 0x66,0x66,0x2e,0x0f,0x1f,0x84,0x00,0x00,0x00,0x00,0x00 .byte 0x0f,0x1f,0x40,0x00 +.macro sc_callback name .balign 16, 0x90 +.endm +.endif +sc_callback add_ref sc_loop_add_ref: lock inc dword ptr [rcx+0x38] ret -.balign 16, 0x90 +sc_callback release sc_loop_release: lock dec dword ptr [rcx+0x38] xor eax, eax /* the image retains an owner reference */ ret -.balign 16, 0x90 +sc_callback one_ref sc_loop_one_ref: cmp dword ptr [rcx+0x38], 1 sete al movzx eax, al ret -.balign 16, 0x90 +sc_callback any_ref sc_loop_any_ref: cmp dword ptr [rcx+0x38], 0 setne al