From b53e4388e2c1b7d150a2a7cf72bc1472b412ce2c Mon Sep 17 00:00:00 2001 From: Oliver Ames Date: Wed, 28 Oct 2026 21:30:43 -0400 Subject: [PATCH] Trace local wireless hooks without enabling sessions --- dm/host/hle_offline.c | 77 ++++++++++++------- dm/host/tests/Makefile | 9 +++ dm/host/tests/t_ldn_trace.c | 73 ++++++++++++++++++ dm/ios/tools/build_ldn_trace_probe.py | 103 ++++++++++++++++++++++++++ docs/development.md | 2 + docs/env_switches.tsv | 1 + docs/ldn-tracing.md | 19 +++++ 7 files changed, 259 insertions(+), 25 deletions(-) create mode 100644 dm/host/tests/t_ldn_trace.c create mode 100644 dm/ios/tools/build_ldn_trace_probe.py create mode 100644 docs/ldn-tracing.md diff --git a/dm/host/hle_offline.c b/dm/host/hle_offline.c index 01615aa78d..d3e036592b 100644 --- a/dm/host/hle_offline.c +++ b/dm/host/hle_offline.c @@ -2,6 +2,33 @@ // Only entry points that go to a service are hooked; library functions that only read and write the game's own buffers (prepo Add / Array / Struct, err ErrorResultVariant, friends getters, socket byte-order conversion) keep running in the SDK. // Return convention as in hle_friends.c: Result returns RES_FAIL ("saved / submitted" kinds return RES_OK so the game does not retry); bool returns false; events get a bound SystemEvent that never fires. #include "hle.h" +#include +#include +#include + +// Diagnostic only: the first 64 actual LDN hook entries, including on iOS. +// Do not inspect opaque network/security/user buffers or log peer identities. +static _Atomic int g_ldn_trace_on = -1; +static _Atomic unsigned g_ldn_trace_count; +static void ldn_trace(const char *symbol, const char *format, ...) { + int saved_errno = errno, enabled = atomic_load(&g_ldn_trace_on); + if (enabled < 0) { + const char *e = getenv("DM_LDN_TRACE"); int value = e && !strcmp(e, "1"), unknown = -1; + atomic_compare_exchange_strong(&g_ldn_trace_on, &unknown, value); enabled = atomic_load(&g_ldn_trace_on); + } + if (enabled) { + unsigned n = atomic_load(&g_ldn_trace_count); + while (n < 64 && !atomic_compare_exchange_weak(&g_ldn_trace_count, &n, n + 1)) {} + if (n < 64) { + flockfile(stderr); + fprintf(stderr, "[dm] ldn trace %u/64 %s", n + 1, symbol); + va_list ap; va_start(ap, format); vfprintf(stderr, format, ap); va_end(ap); + fputc('\n', stderr); funlockfile(stderr); + } + } + errno = saved_errno; +} +#define LDN_TRACE(cname, ...) ldn_trace(hle_force_##cname.name, __VA_ARGS__) static _Alignas(16) uint8_t g_ev_ldn[0x100]; @@ -15,35 +42,35 @@ HLE_FORCE("_ZN2nn5prepo27AddInternetConnectionStatusEPNS0_10PlayReportEPKcS4_S4_ HLE_FORCE("_ZN2nn5prepo28RequestImmediateTransmissionEv", "telemetry immediate upload; nothing is sent, reports success", Result, h_prepo_Tx, void) { return RES_OK; } // ---------- ldn: local wireless. Initialization fails, state is always "none" ---------- -HLE_FORCE("_ZN2nn3ldn10InitializeEv", "local wireless (cut): initialization fails", Result, h_ldn_Init, void) { return RES_FAIL; } -HLE_FORCE("_ZN2nn3ldn8FinalizeEv", "local wireless (cut)", void, h_ldn_Fini, void) {} -HLE_FORCE("_ZN2nn3ldn8GetStateEv", "local wireless (cut): state 0 (none)", int, h_ldn_State, void) { return 0; } -HLE_FORCE("_ZN2nn3ldn22AttachStateChangeEventEPNS_2os15SystemEventTypeE", "local wireless state event: bound, never fires", void, h_ldn_Attach, void *ev) { hle_bind_system_event(ev, 0); (void)g_ev_ldn; } -HLE_FORCE("_ZN2nn3ldn11OpenStationEv", "local wireless (cut)", Result, h_ldn_OpenSta, void) { return RES_FAIL; } -HLE_FORCE("_ZN2nn3ldn12CloseStationEv", "local wireless (cut)", Result, h_ldn_CloseSta, void) { return RES_FAIL; } -HLE_FORCE("_ZN2nn3ldn15OpenAccessPointEv", "local wireless (cut)", Result, h_ldn_OpenAp, void) { return RES_FAIL; } -HLE_FORCE("_ZN2nn3ldn16CloseAccessPointEv", "local wireless (cut)", Result, h_ldn_CloseAp, void) { return RES_FAIL; } -HLE_FORCE("_ZN2nn3ldn10DisconnectEv", "local wireless (cut)", Result, h_ldn_Disc, void) { return RES_FAIL; } -HLE_FORCE("_ZN2nn3ldn14DestroyNetworkEv", "local wireless (cut)", Result, h_ldn_Destroy, void) { return RES_FAIL; } -HLE_FORCE("_ZN2nn3ldn4ScanEPNS0_11NetworkInfoEPiiRKNS0_10ScanFilterEi", "local wireless scan (cut): 0 found", Result, h_ldn_Scan, void *out, int *n, int max, const void *f, int ch) { (void)out; (void)max; (void)f; (void)ch; if (n) *n = 0; return RES_FAIL; } -HLE_FORCE("_ZN2nn3ldn11ScanPrivateEPNS0_11NetworkInfoEPiiRKNS0_10ScanFilterEi", "local wireless scan (cut): 0 found", Result, h_ldn_ScanP, void *out, int *n, int max, const void *f, int ch) { (void)out; (void)max; (void)f; (void)ch; if (n) *n = 0; return RES_FAIL; } +HLE_FORCE("_ZN2nn3ldn10InitializeEv", "local wireless (cut): initialization fails", Result, h_ldn_Init, void) { LDN_TRACE(h_ldn_Init, " offline result=%#x", RES_FAIL); return RES_FAIL; } +HLE_FORCE("_ZN2nn3ldn8FinalizeEv", "local wireless (cut)", void, h_ldn_Fini, void) { LDN_TRACE(h_ldn_Fini, " offline return=void");} +HLE_FORCE("_ZN2nn3ldn8GetStateEv", "local wireless (cut): state 0 (none)", int, h_ldn_State, void) { LDN_TRACE(h_ldn_State, " offline value=0"); return 0; } +HLE_FORCE("_ZN2nn3ldn22AttachStateChangeEventEPNS_2os15SystemEventTypeE", "local wireless state event: bound, never fires", void, h_ldn_Attach, void *ev) { LDN_TRACE(h_ldn_Attach, " offline return=void"); hle_bind_system_event(ev, 0); (void)g_ev_ldn; } +HLE_FORCE("_ZN2nn3ldn11OpenStationEv", "local wireless (cut)", Result, h_ldn_OpenSta, void) { LDN_TRACE(h_ldn_OpenSta, " offline result=%#x", RES_FAIL); return RES_FAIL; } +HLE_FORCE("_ZN2nn3ldn12CloseStationEv", "local wireless (cut)", Result, h_ldn_CloseSta, void) { LDN_TRACE(h_ldn_CloseSta, " offline result=%#x", RES_FAIL); return RES_FAIL; } +HLE_FORCE("_ZN2nn3ldn15OpenAccessPointEv", "local wireless (cut)", Result, h_ldn_OpenAp, void) { LDN_TRACE(h_ldn_OpenAp, " offline result=%#x", RES_FAIL); return RES_FAIL; } +HLE_FORCE("_ZN2nn3ldn16CloseAccessPointEv", "local wireless (cut)", Result, h_ldn_CloseAp, void) { LDN_TRACE(h_ldn_CloseAp, " offline result=%#x", RES_FAIL); return RES_FAIL; } +HLE_FORCE("_ZN2nn3ldn10DisconnectEv", "local wireless (cut)", Result, h_ldn_Disc, void) { LDN_TRACE(h_ldn_Disc, " offline result=%#x", RES_FAIL); return RES_FAIL; } +HLE_FORCE("_ZN2nn3ldn14DestroyNetworkEv", "local wireless (cut)", Result, h_ldn_Destroy, void) { LDN_TRACE(h_ldn_Destroy, " offline result=%#x", RES_FAIL); return RES_FAIL; } +HLE_FORCE("_ZN2nn3ldn4ScanEPNS0_11NetworkInfoEPiiRKNS0_10ScanFilterEi", "local wireless scan (cut): 0 found", Result, h_ldn_Scan, void *out, int *n, int max, const void *f, int ch) { LDN_TRACE(h_ldn_Scan, " offline result=%#x capacity=%d output_count=0", RES_FAIL, max); (void)out; (void)max; (void)f; (void)ch; if (n) *n = 0; return RES_FAIL; } +HLE_FORCE("_ZN2nn3ldn11ScanPrivateEPNS0_11NetworkInfoEPiiRKNS0_10ScanFilterEi", "local wireless scan (cut): 0 found", Result, h_ldn_ScanP, void *out, int *n, int max, const void *f, int ch) { LDN_TRACE(h_ldn_ScanP, " offline result=%#x capacity=%d output_count=0", RES_FAIL, max); (void)out; (void)max; (void)f; (void)ch; if (n) *n = 0; return RES_FAIL; } HLE_FORCE("_ZN2nn3ldn13CreateNetworkERKNS0_13NetworkConfigERKNS0_14SecurityConfigERKNS0_10UserConfigE", "local wireless (cut)", Result, h_ldn_Create, - const void *a, const void *b, const void *c) { (void)a; (void)b; (void)c; return RES_FAIL; } + const void *a, const void *b, const void *c) { LDN_TRACE(h_ldn_Create, " offline result=%#x", RES_FAIL); (void)a; (void)b; (void)c; return RES_FAIL; } HLE_FORCE("_ZN2nn3ldn20CreateNetworkPrivateERKNS0_13NetworkConfigERKNS0_14SecurityConfigERKNS0_17SecurityParameterERKNS0_10UserConfigEiPKNS0_12AddressEntryE", "local wireless (cut)", - Result, h_ldn_CreateP, const void *a, const void *b, const void *c, const void *d, int n, const void *e) { (void)a; (void)b; (void)c; (void)d; (void)n; (void)e; return RES_FAIL; } + Result, h_ldn_CreateP, const void *a, const void *b, const void *c, const void *d, int n, const void *e) { LDN_TRACE(h_ldn_CreateP, " offline result=%#x address_count=%d", RES_FAIL, n); (void)a; (void)b; (void)c; (void)d; (void)n; (void)e; return RES_FAIL; } HLE_FORCE("_ZN2nn3ldn7ConnectERKNS0_11NetworkInfoERKNS0_14SecurityConfigERKNS0_10UserConfigEiNS0_13ConnectOptionE", "local wireless (cut)", Result, h_ldn_Connect, - const void *a, const void *b, const void *c, int v, int o) { (void)a; (void)b; (void)c; (void)v; (void)o; return RES_FAIL; } + const void *a, const void *b, const void *c, int v, int o) { LDN_TRACE(h_ldn_Connect, " offline result=%#x", RES_FAIL); (void)a; (void)b; (void)c; (void)v; (void)o; return RES_FAIL; } HLE_FORCE("_ZN2nn3ldn14ConnectPrivateERKNS0_13NetworkConfigERKNS0_14SecurityConfigERKNS0_17SecurityParameterERKNS0_10UserConfigEiNS0_13ConnectOptionE", "local wireless (cut)", - Result, h_ldn_ConnectP, const void *a, const void *b, const void *c, const void *d, int v, int o) { (void)a; (void)b; (void)c; (void)d; (void)v; (void)o; return RES_FAIL; } -HLE_FORCE("_ZN2nn3ldn14GetNetworkInfoEPNS0_11NetworkInfoE", "local wireless (cut)", Result, h_ldn_NetInfo, void *o) { (void)o; return RES_FAIL; } -HLE_FORCE("_ZN2nn3ldn14GetIpv4AddressEPNS0_11Ipv4AddressEPNS0_10SubnetMaskE", "local wireless (cut)", Result, h_ldn_Ip, uint32_t *a, uint32_t *m) { if (a) *a = 0; if (m) *m = 0; return RES_FAIL; } -HLE_FORCE("_ZN2nn3ldn20GetSecurityParameterEPNS0_17SecurityParameterE", "local wireless (cut)", Result, h_ldn_SecPrm, void *o) { (void)o; return RES_FAIL; } -HLE_FORCE("_ZN2nn3ldn16SetAdvertiseDataEPKvm", "local wireless (cut)", Result, h_ldn_Adv, const void *d, uint64_t n) { (void)d; (void)n; return RES_FAIL; } -HLE_FORCE("_ZN2nn3ldn19GetDisconnectReasonEv", "local wireless (cut): 0", int, h_ldn_DiscReason, void) { return 0; } -HLE_FORCE("_ZN2nn3ldn20AddAcceptFilterEntryENS0_10MacAddressE", "local wireless (cut)", Result, h_ldn_Filter, uint64_t mac) { (void)mac; return RES_FAIL; } -HLE_FORCE("_ZN2nn3ldn22SetStationAcceptPolicyENS0_12AcceptPolicyE", "local wireless (cut)", Result, h_ldn_Policy, int p) { (void)p; return RES_FAIL; } -HLE_FORCE("_ZN2nn3ldn6RejectENS0_11Ipv4AddressE", "local wireless (cut)", Result, h_ldn_Reject, uint32_t a) { (void)a; return RES_FAIL; } -HLE_FORCE("_ZN2nn3ldn16GetNetworkConfigEPNS0_13NetworkConfigE", "local wireless (cut)", Result, h_ldn_NetCfg, void *o) { (void)o; return RES_FAIL; } + Result, h_ldn_ConnectP, const void *a, const void *b, const void *c, const void *d, int v, int o) { LDN_TRACE(h_ldn_ConnectP, " offline result=%#x", RES_FAIL); (void)a; (void)b; (void)c; (void)d; (void)v; (void)o; return RES_FAIL; } +HLE_FORCE("_ZN2nn3ldn14GetNetworkInfoEPNS0_11NetworkInfoE", "local wireless (cut)", Result, h_ldn_NetInfo, void *o) { LDN_TRACE(h_ldn_NetInfo, " offline result=%#x", RES_FAIL); (void)o; return RES_FAIL; } +HLE_FORCE("_ZN2nn3ldn14GetIpv4AddressEPNS0_11Ipv4AddressEPNS0_10SubnetMaskE", "local wireless (cut)", Result, h_ldn_Ip, uint32_t *a, uint32_t *m) { LDN_TRACE(h_ldn_Ip, " offline result=%#x", RES_FAIL); if (a) *a = 0; if (m) *m = 0; return RES_FAIL; } +HLE_FORCE("_ZN2nn3ldn20GetSecurityParameterEPNS0_17SecurityParameterE", "local wireless (cut)", Result, h_ldn_SecPrm, void *o) { LDN_TRACE(h_ldn_SecPrm, " offline result=%#x", RES_FAIL); (void)o; return RES_FAIL; } +HLE_FORCE("_ZN2nn3ldn16SetAdvertiseDataEPKvm", "local wireless (cut)", Result, h_ldn_Adv, const void *d, uint64_t n) { LDN_TRACE(h_ldn_Adv, " offline result=%#x byte_count=%llu", RES_FAIL, (unsigned long long)n); (void)d; (void)n; return RES_FAIL; } +HLE_FORCE("_ZN2nn3ldn19GetDisconnectReasonEv", "local wireless (cut): 0", int, h_ldn_DiscReason, void) { LDN_TRACE(h_ldn_DiscReason, " offline value=0"); return 0; } +HLE_FORCE("_ZN2nn3ldn20AddAcceptFilterEntryENS0_10MacAddressE", "local wireless (cut)", Result, h_ldn_Filter, uint64_t mac) { LDN_TRACE(h_ldn_Filter, " offline result=%#x", RES_FAIL); (void)mac; return RES_FAIL; } +HLE_FORCE("_ZN2nn3ldn22SetStationAcceptPolicyENS0_12AcceptPolicyE", "local wireless (cut)", Result, h_ldn_Policy, int p) { LDN_TRACE(h_ldn_Policy, " offline result=%#x", RES_FAIL); (void)p; return RES_FAIL; } +HLE_FORCE("_ZN2nn3ldn6RejectENS0_11Ipv4AddressE", "local wireless (cut)", Result, h_ldn_Reject, uint32_t a) { LDN_TRACE(h_ldn_Reject, " offline result=%#x", RES_FAIL); (void)a; return RES_FAIL; } +HLE_FORCE("_ZN2nn3ldn16GetNetworkConfigEPNS0_13NetworkConfigE", "local wireless (cut)", Result, h_ldn_NetCfg, void *o) { LDN_TRACE(h_ldn_NetCfg, " offline result=%#x", RES_FAIL); (void)o; return RES_FAIL; } // ---------- socket: offline by default (every call -1/ENETDOWN); with DM_NET=1 the calls are the host's own sockets (hle_net.c) ---------- enum { SK_ENETDOWN = 50, SK_EAI_FAIL = 4 }; diff --git a/dm/host/tests/Makefile b/dm/host/tests/Makefile index 3d468ff797..136bf71e1a 100644 --- a/dm/host/tests/Makefile +++ b/dm/host/tests/Makefile @@ -258,3 +258,12 @@ tsan-threads-run: $(addprefix $(B)/,$(TSAN_T)) $(B)/t_altstack DM_QOS_MAP=1 $(B)/t_notify_sched DM_QOS_MAP=1 $(B)/t_notify_sched --all-peers + +# iOS-compatible LDN entry diagnostics use the same hook registrations as the guest. +$(B)/t_ldn_trace: t_ldn_trace.c ../hle_offline.c ../hle_net.c ../hle.h ../dm.h $(GAME_H) | $(B) + $(CLANG) -O1 -g -Wall -Werror -Wno-unused-function -I.. $(GINC) -o $@ t_ldn_trace.c ../hle_net.c +.PHONY: ldn-trace +ldn-trace: $(B)/t_ldn_trace + $(B)/t_ldn_trace + +run: ldn-trace diff --git a/dm/host/tests/t_ldn_trace.c b/dm/host/tests/t_ldn_trace.c new file mode 100644 index 0000000000..d4db366987 --- /dev/null +++ b/dm/host/tests/t_ldn_trace.c @@ -0,0 +1,73 @@ +// Actual swforce hooks, opaque buffers kept untouched, bounded iOS-compatible trace. +#include "../hle_offline.c" +#include +#include +#include +static int bound, autoclear, checks, failures; +static void *event_pointer; +void hle_bind_system_event(void *ev, int clear) { bound++; event_pointer = ev; autoclear = clear; } +void dm_guest_sleep_ns(int64_t ns) { (void)ns; } +extern const struct mach_header_64 _mh_execute_header; +#define CHECK(c, message) do { checks++; if (!(c)) { failures++; printf("FAIL: %s\n", message); } } while (0) +static void *hook(const char *name) { + unsigned long bytes = 0; void *fn = NULL; unsigned count = 0; + const hle_force_ent *entries = (const hle_force_ent *)getsectiondata(&_mh_execute_header, "__DATA", "swforce", &bytes); + for (size_t i = 0; entries && i < bytes / sizeof *entries; i++) if (!strcmp(entries[i].name, name)) { fn = entries[i].fn; count++; } + CHECK(count == 1 && fn, name); return fn; +} +#define HOOK(cname, type) ((type)hook(hle_force_##cname.name)) +int main(void) { + FILE *capture = tmpfile(); int saved = dup(STDERR_FILENO); + CHECK(capture && saved >= 0, "capture available"); if (!capture || saved < 0) return 1; + fflush(stderr); dup2(fileno(capture), STDERR_FILENO); + Result (*initialize)(void) = HOOK(h_ldn_Init, Result (*)(void)); + int (*state)(void) = HOOK(h_ldn_State, int (*)(void)); + unsetenv("DM_LDN_TRACE"); g_ldn_trace_on = -1; g_ldn_trace_count = 0; + errno = EDOM; CHECK(initialize() == RES_FAIL && errno == EDOM, "default init/error unchanged"); + fflush(stderr); CHECK(ftell(capture) == 0, "default trace off"); + setenv("DM_LDN_TRACE", "1", 1); g_ldn_trace_on = -1; + errno = ERANGE; CHECK(initialize() == RES_FAIL && errno == ERANGE, "traced init/error unchanged"); + CHECK(state() == 0, "state remains none"); + uint8_t output[64], unchanged[64], event[0x100]; memset(output, 0xa5, sizeof output); memcpy(unchanged, output, sizeof output); + HOOK(h_ldn_Attach, void (*)(void *))(event); + CHECK(bound == 1 && event_pointer == event && autoclear == 0, "event bound once with original autoclear"); + int n = 999; + CHECK(HOOK(h_ldn_Scan, Result (*)(void *, int *, int, const void *, int))(output, &n, 5, (void *)1, 0) == RES_FAIL && n == 0 && !memcmp(output, unchanged, sizeof output), "scan count zero, opaque output/filter untouched"); + n = 999; + CHECK(HOOK(h_ldn_ScanP, Result (*)(void *, int *, int, const void *, int))(output, &n, 7, (void *)1, 0) == RES_FAIL && n == 0 && !memcmp(output, unchanged, sizeof output), "private scan unchanged"); + CHECK(HOOK(h_ldn_Adv, Result (*)(const void *, uint64_t))((void *)1, 1234) == RES_FAIL, "advertise count logged without reading data"); + CHECK(HOOK(h_ldn_CreateP, Result (*)(const void *, const void *, const void *, const void *, int, const void *))((void *)1,(void *)1,(void *)1,(void *)1,3,(void *)1) == RES_FAIL, "private create does not inspect security or address entries"); + CHECK(HOOK(h_ldn_Create, Result (*)(const void *, const void *, const void *))((void *)1,(void *)1,(void *)1) == RES_FAIL, "create stays unavailable"); + CHECK(HOOK(h_ldn_Connect, Result (*)(const void *, const void *, const void *, int, int))((void *)1,(void *)1,(void *)1,2,0) == RES_FAIL, "connect stays unavailable"); + CHECK(HOOK(h_ldn_ConnectP, Result (*)(const void *, const void *, const void *, const void *, int, int))((void *)1,(void *)1,(void *)1,(void *)1,2,0) == RES_FAIL, "private connect stays unavailable"); + CHECK(HOOK(h_ldn_NetInfo, Result (*)(void *))(output) == RES_FAIL && !memcmp(output, unchanged, sizeof output), "network info remains untouched"); + CHECK(HOOK(h_ldn_NetCfg, Result (*)(void *))(output) == RES_FAIL && !memcmp(output, unchanged, sizeof output), "network config remains untouched"); + CHECK(HOOK(h_ldn_SecPrm, Result (*)(void *))(output) == RES_FAIL && !memcmp(output, unchanged, sizeof output), "security output remains untouched"); + uint32_t ip = 1, mask = 2; + CHECK(HOOK(h_ldn_Ip, Result (*)(uint32_t *, uint32_t *))(&ip,&mask) == RES_FAIL && !ip && !mask, "IP outputs retain existing zero/failure behavior"); + CHECK(HOOK(h_ldn_OpenSta, Result (*)(void))() == RES_FAIL, "open station unavailable"); + CHECK(HOOK(h_ldn_CloseSta, Result (*)(void))() == RES_FAIL, "close station unavailable"); + CHECK(HOOK(h_ldn_OpenAp, Result (*)(void))() == RES_FAIL, "open AP unavailable"); + CHECK(HOOK(h_ldn_CloseAp, Result (*)(void))() == RES_FAIL, "close AP unavailable"); + CHECK(HOOK(h_ldn_Disc, Result (*)(void))() == RES_FAIL, "disconnect unavailable"); + CHECK(HOOK(h_ldn_Destroy, Result (*)(void))() == RES_FAIL, "destroy unavailable"); + CHECK(HOOK(h_ldn_DiscReason, int (*)(void))() == 0, "disconnect reason unchanged"); + CHECK(HOOK(h_ldn_Filter, Result (*)(uint64_t))(0xabcdef) == RES_FAIL, "filter unavailable"); + CHECK(HOOK(h_ldn_Policy, Result (*)(int))(1) == RES_FAIL, "accept policy unavailable"); + CHECK(HOOK(h_ldn_Reject, Result (*)(uint32_t))(0x12345678) == RES_FAIL, "reject unavailable"); + HOOK(h_ldn_Fini, void (*)(void))(); + CHECK(g_ldn_trace_count == 25, "each distinct actual hook traced once"); + int states_unchanged = 1; for (int i = 0; i < 500; i++) states_unchanged &= state() == 0; + CHECK(states_unchanged, "trace cap never changes returned state"); + CHECK(g_ldn_trace_count == 64, "trace count saturates at 64"); + fflush(stderr); dup2(saved, STDERR_FILENO); close(saved); rewind(capture); + char text[32768]; size_t bytes = fread(text, 1, sizeof text - 1, capture); text[bytes] = 0; fclose(capture); + unsigned lines = 0; for (char *p = text; *p; p++) if (*p == '\n') lines++; + CHECK(lines == 64, "only 64 lines emitted despite repeated polling"); + CHECK(strstr(text, "ldn trace 1/64 _ZN2nn3ldn10InitializeEv offline result=") && strstr(text, "ldn trace 2/64 _ZN2nn3ldn8GetStateEv"), "full symbols and ordered sequence identify real calls"); + CHECK(strstr(text, "capacity=5 output_count=0") && strstr(text,"byte_count=1234") && strstr(text,"address_count=3"), "safe scalar counts included"); + CHECK(!strstr(text,"abcdef") && !strstr(text,"12345678") && !strstr(text,"0x1 "), "no identity or opaque pointer logged"); + CHECK(bound == 1, "tracing did not bind or signal any extra event"); + printf("ldn trace: %d checks, %d failed\n", checks, failures); + return failures != 0; +} diff --git a/dm/ios/tools/build_ldn_trace_probe.py b/dm/ios/tools/build_ldn_trace_probe.py new file mode 100644 index 0000000000..02e4b2df02 --- /dev/null +++ b/dm/ios/tools/build_ldn_trace_probe.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Build an unsigned native iOS probe of actual registered diagnostic hooks. + +GAME selects a known source profile. No game files, keys or signing are used. +Install and run only through the user's authorized device workflow. +""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import subprocess +import sys + +REPO = Path(__file__).resolve().parents[3] +NAME = 'LdnTraceVerification' +KIND = 'ldn' +PRODUCTION = ['dm/host/hle_offline.c', 'dm/host/hle_net.c'] +EXTRA = ['dm/host/hle_net.c'] +HOST = r'''#import +extern int dm_trace_test_main(void); +@interface TraceScene : NSObject +@property(strong,nonatomic) UIWindow *window; +@end +@implementation TraceScene +- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)options { + (void)session; (void)options; + self.window = [[UIWindow alloc] initWithWindowScene:(UIWindowScene *)scene]; + self.window.rootViewController = [UIViewController new]; [self.window makeKeyAndVisible]; + dispatch_async(dispatch_get_main_queue(), ^{ + int result = dm_trace_test_main(); + NSURL *url = [NSBundle.mainBundle URLForResource:@"SourceProvenance" withExtension:@"json"]; + NSDictionary *source = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfURL:url] options:0 error:NULL]; + NSDictionary *report = @{@"status":@(result), @"registeredProductionHooksUsed":@YES, + @"eventBindingStubbed":@YES, @"gameRuntimeUsed":@NO, @"physicalNetworkOrCameraTested":@NO, + @"systemVersion":UIDevice.currentDevice.systemVersion, @"sourceProvenance":source}; + NSURL *documents = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask][0]; + [[NSJSONSerialization dataWithJSONObject:report options:NSJSONWritingPrettyPrinted error:NULL] + writeToURL:[documents URLByAppendingPathComponent:@"results.json"] atomically:YES]; + fflush(stdout); fflush(stderr); + }); +} +@end +@interface TraceApp : UIResponder +@end +@implementation TraceApp +@end +int main(int argc, char **argv) { @autoreleasepool { + NSURL *documents = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask][0]; + freopen([documents URLByAppendingPathComponent:@"verification.log"].fileSystemRepresentation, "w", stdout); + freopen([documents URLByAppendingPathComponent:@"errors.log"].fileSystemRepresentation, "w", stderr); + setvbuf(stdout, NULL, _IOLBF, 0); setvbuf(stderr, NULL, _IOLBF, 0); + return UIApplicationMain(argc, argv, nil, NSStringFromClass(TraceApp.class)); +} } +''' + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', required=True, type=Path, help='new scratch directory') + args = parser.parse_args() + game = os.environ.get('GAME') or os.environ.get('DM_GAME') + if not game: + parser.error('GAME or DM_GAME must select a source profile') + output = args.output.resolve() + output.mkdir(parents=True) + include = output / 'Include' + include.mkdir() + subprocess.run([sys.executable, '-B', str(REPO / 'dm/games/game.py'), 'header', game, + str(include / 'game_profile.h')], check=True) + (output / 'Host.m').write_text(HOST) + test = f'dm/host/tests/t_{KIND}_trace.c' + provenance = {'commit': subprocess.check_output(['git', '-C', str(REPO), 'rev-parse', 'HEAD'], text=True).strip(), + 'sourceSHA256': {path: hashlib.sha256((REPO / path).read_bytes()).hexdigest() + for path in [test, *PRODUCTION]}, 'gameProfile': game} + provenance_file = output / 'SourceProvenance.json' + provenance_file.write_text(json.dumps(provenance, indent=2) + '\n') + project = {'name': NAME, 'options': {'deploymentTarget': {'iOS': '26.0'}}, 'targets': {NAME: { + 'type': 'application', 'platform': 'iOS', + 'sources': [{'path': str(output / 'Host.m')}, + {'path': str(REPO / test), 'compilerFlags': '-Dmain=dm_trace_test_main'}, + *[{'path': str(REPO / path)} for path in EXTRA], + {'path': str(provenance_file), 'buildPhase': 'resources'}], + 'dependencies': [{'sdk': 'UIKit.framework'}, {'sdk': 'Foundation.framework'}], + 'settings': {'base': {'PRODUCT_BUNDLE_IDENTIFIER': f'dev.anygame.{KIND}-trace-verification', + 'ENABLE_DEBUG_DYLIB': 'NO', 'CODE_SIGN_STYLE': 'Automatic', + 'TARGETED_DEVICE_FAMILY': '1', 'HEADER_SEARCH_PATHS': [str(REPO / 'dm/host'), str(include)]}}, + 'info': {'path': 'Info.plist', 'properties': { + 'CFBundleDisplayName': NAME, 'UILaunchScreen': {}, 'UIFileSharingEnabled': True, + 'UIApplicationSceneManifest': {'UIApplicationSupportsMultipleScenes': False, 'UISceneConfigurations': { + 'UIWindowSceneSessionRoleApplication': [{'UISceneConfigurationName': 'Default', 'UISceneDelegateClassName': 'TraceScene'}]}}}}}}} + spec = output / 'project.yml' + spec.write_text(json.dumps(project, indent=2) + '\n') + subprocess.run(['xcodegen', 'generate', '--spec', str(spec)], check=True) + subprocess.run(['xcodebuild', '-project', str(output / f'{NAME}.xcodeproj'), '-scheme', NAME, + '-configuration', 'Debug', '-destination', 'generic/platform=iOS', + '-derivedDataPath', str(output / 'DerivedData'), '-jobs', '3', + 'CODE_SIGNING_ALLOWED=NO', 'build'], check=True) + print(output / f'DerivedData/Build/Products/Debug-iphoneos/{NAME}.app') + + +if __name__ == '__main__': + main() diff --git a/docs/development.md b/docs/development.md index 5227635d1d..84792f734a 100644 --- a/docs/development.md +++ b/docs/development.md @@ -572,6 +572,8 @@ The game's own work and its frame rate stay as they are; the host avoids work of Each game's notes are in its profile folder, `dm/games//README.md`: what the profile needs and why, what was found on the game, Mac routes and open items. The [games table](../dm/games/README.md#games) lists them. +Diagnostic experiments: [LDN boundary diagnostics](ldn-tracing.md) records existing guest boundaries without changing network or rendering behavior. + ## Tools (`dm/tools`) | Tool | What it does | diff --git a/docs/env_switches.tsv b/docs/env_switches.tsv index 862d058d90..583da0a17f 100644 --- a/docs/env_switches.tsv +++ b/docs/env_switches.tsv @@ -58,6 +58,7 @@ DM_KBD_PLAYER switch `own` makes the Mac keyboard player 1 on its own, with cont DM_KEYS switch Scripted input: `>:[+key][~hold][*count/interval],…` presses buttons, sticks and `Shake` at those times DM_LANG player Game language as a language code (default `en-US`; the iOS app sets the one the IPA was packed with) DM_LAUNCHER switch `0` skips the iOS settings screen or the Mac settings window and starts the game at once, with the saved choices (on the Mac also `--no-launcher`) +DM_LDN_TRACE diagnostic `=1` logs the first 64 actual `nn::ldn` hook entries with ordered sequence numbers, full symbols and scalar counts; works on iOS, never reads opaque network/security/user buffers, and preserves unavailable local-wireless behavior DM_LINK_VTICK switch `0` keeps the system's display-link rate on 144 or 165 Hz Mac screens; by default the link counts only the refreshes nearest a steady 1/60 s clock DM_LOADHASH diagnostic `=1` logs an address-independent SHA-256 per module after loading DM_LONGFRAME diagnostic `0` turns off the long-frame log (on by default), a line for a frame over 20 ms or with GPU time over 16.7 ms that is clearly above recent frames diff --git a/docs/ldn-tracing.md b/docs/ldn-tracing.md new file mode 100644 index 0000000000..dce50b5827 --- /dev/null +++ b/docs/ldn-tracing.md @@ -0,0 +1,19 @@ +# Trace the existing LDN boundary + +`DM_LDN_TRACE=1`, set before launch, records the first 64 actual `nn::ldn` hook entries on Mac and iOS. Each line contains a sequence number, the registered mangled symbol and its existing offline outcome. Scan capacity, private-create address count and advertise-data length are the only guest argument values recorded. The trace does not read opaque structures, credentials, advertisement bytes, peer addresses or user data. + +Initialization still fails, state stays none, scanning still reports zero results, and the state-change event stays bound without being signaled. Network/security output buffers retain their existing behavior. The diagnostic preserves errno around logging and stops reserving sequence numbers after 64 entries. Under concurrent calls, sequence numbers identify reservation order; stderr line order is not a cross-thread execution guarantee. + +## Use the trace + +Record the exact `main` and `sdk` build IDs, host revision and source hashes, then launch an isolated game with the flag enabled and enter its local-wireless menu through the authorized test route. Retain the `ldn trace` lines alongside the normal log. A line numbered `64/64` means the budget is exhausted. No new calls are enabled by tracing, so a game that stops at the failed `Initialize` will not produce a discovery or join sequence. + +Use the first reached hook and its caller to locate that exact SDK wrapper in the extracted memory image. Confirm argument and output sizes, Result values, state enum transitions, event binding/signaling rules, node membership and security/advertisement layout from the caller and implementation before implementing a service. Existing `DM_IMPORTS` shows linkage, not call order. `DM_HLECOUNT` works only on Mac; it is deliberately a no-op on iOS. + +The socket implementation on this branch supplies transport under `DM_NET=1`, but does not implement LDN discovery, connection state or Pia sessions. Leave the game's shipped Pia code in place while establishing its LDN contract. Do not return success or fill unknown `NetworkInfo`/security objects to move past the current failure. A later functional service needs confirmed layouts and two peers to verify discovery, join, membership updates, disconnect and event signaling. + +## Verification + +`make -C dm/host/tests GAME= ldn-trace` invokes all 25 actual registered hooks without game files or a network peer. It checks default-off logging, full symbol/sequence records, the 64-line cap under repeated polling, unchanged failure/state/output/event-binding behavior, no reads through deliberately invalid opaque pointers, scalar-count logging, identity exclusion and errno preservation. Event binding is a test stub; no real kernel event or wireless session is exercised. + +`GAME= python3 dm/ios/tools/build_ldn_trace_probe.py --output ` builds the same test in an unsigned native iOS app. It writes `Documents/results.json`, `verification.log` and `errors.log`; the report includes exact source hashes and identifies the stubbed event binding. The probe runs no guest game, sends no discovery packets and cannot establish working multiplayer.