Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
660 changes: 520 additions & 140 deletions build/ntdll-unix/virtual_ios.c

Large diffs are not rendered by default.

414 changes: 414 additions & 0 deletions tests/host/check-sc-helper-binary.py

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions tests/host/check-sc-ipc-rebind.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,28 @@
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)


start = native.index('static const unsigned char ios_sch_ipc_rebind[')
array = native[start:native.index('};', start) + 2]
blob = bytes(int(h, 16) for h in re.findall(r'0x([\da-fA-F]{2})', array))
# The rebind trace is the first build's alone (ios_sch_builds[0]). 2.4.0.296's
# tables leave its Connect routine unhooked; that build keeps only the channel
# retention hook in its Channel Response (check-sc-ipc-split.py executes it).
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_IPC_TRACE, ios_sch_ipc_rebind, sizeof(ios_sch_ipc_rebind), 0, 0,' in first_blob
assert '{ IOS_SCH_IPC_CONNECT, IOS_SCH_IPC_TRACE, 5, 0xe9, 0 },' in first_hook
assert 'rebind' not in later_blob and 'IPC_CONNECT' not in later_hook and 'IPC_TRACE' not in later_hook
assert '{ IOS_SCH296_RENDER_CHANNEL_KEEP, IOS_SCH296_RENDER_CHANNEL_THUNK, 7, 0xe9, 1 },' in later_hook
assert not re.search(r'#define IOS_SCH296_IPC_(TRACE|COUNT)', native)

with tempfile.TemporaryDirectory(prefix='madeira-sc-ipc-') as name:
folder = Path(name)
Expand Down
447 changes: 302 additions & 145 deletions tests/host/check-sc-ipc-split.py

Large diffs are not rendered by default.

236 changes: 147 additions & 89 deletions tests/host/check-sc-render-handler.py

Large diffs are not rendered by default.

489 changes: 312 additions & 177 deletions tests/host/check-sc-render-loop.py

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions tests/host/check-sc-runstate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand Down
15 changes: 15 additions & 0 deletions tests/host/check-sc-ui-close.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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'
Expand Down
99 changes: 99 additions & 0 deletions tests/host/sc-ipc-split.S
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,60 @@
* 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
.global sc_ipc_get_manager, sc_ipc_capture_config, sc_ipc_ctor_store
.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
Expand Down Expand Up @@ -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
Expand All @@ -108,24 +160,59 @@ 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]
je initial_connect_done /* wait for the renderer's Channel Response */
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]
Expand Down Expand Up @@ -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
Expand All @@ -188,3 +286,4 @@ sc_ipc_direct_getter:
jmp direct_continue
created_message:
.asciz "[sc-ipc] renderer manager=%p browser=%p init=%u"
.endif
43 changes: 43 additions & 0 deletions tests/host/sc-render-handler.S
Original file line number Diff line number Diff line change
@@ -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
20 changes: 16 additions & 4 deletions tests/host/sc-render-loop.S
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading