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