diff --git a/.github/workflows/source-check.yml b/.github/workflows/source-check.yml index b1b6e19..1e2b41c 100644 --- a/.github/workflows/source-check.yml +++ b/.github/workflows/source-check.yml @@ -37,6 +37,7 @@ jobs: - run: python -m unittest discover -s tests -p 'test_exec_guest.py' - run: python -m unittest discover -s tests -p 'test_close_league_client_in_game.py' - run: python -m unittest discover -s tests -p 'test_recover_stale_client.py' + - run: python -m unittest discover -s tests -p 'test_ifmedia.py' - run: python -m unittest discover -s tests -p 'test_clean_stale_helpers.py' - run: python -m unittest discover -s tests -p 'test_hide_league_client_in_game.py' - run: python -m unittest discover -s tests -p 'test_client_in_game_tricks.py' diff --git a/patches/xnu.patch b/patches/xnu.patch index 8d5c15f..0d6b480 100644 --- a/patches/xnu.patch +++ b/patches/xnu.patch @@ -6200,3 +6200,97 @@ diff --git a/darling/src/libsystem_kernel/emulation/src/linux_premigration/vchro ctxt->current_root = prefix_path; ctxt->current_root_len = prefix_path_len; } + +diff --git a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/ioctl/socket.c b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/ioctl/socket.c +index f890ab2..8969577 100644 +--- a/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/ioctl/socket.c ++++ b/darling/src/libsystem_kernel/emulation/src/xnu_syscall/bsd/helper/ioctl/socket.c +@@ -2,12 +2,29 @@ + + #include + #include +-#include + #include ++#include + #include + #include + #include + ++#include ++ ++extern int strcmp(const char* a, const char* b); ++extern int strncmp(const char* a, const char* b, size_t n); ++extern char* strncpy(char* destination, const char* source, size_t length); ++ ++#define LINUX_SIOCGIFFLAGS 0x8913 ++#define LINUX_IFNAMSIZ 16 ++ ++struct linux_ifreq { ++ char lifr_name[LINUX_IFNAMSIZ]; ++ union { ++ short lifr_flags; ++ char lifr_pad[24]; ++ }; ++}; ++ + int handle_socket(int fd, unsigned int cmd, void* arg, int* retval) + { + switch (cmd) +@@ -19,6 +36,57 @@ int handle_socket(int fd, unsigned int cmd, void* arg, int* retval) + case SIOCAIFADDR_IN6: // set IPv6 address + *retval = -ENOTSUP; + return IOCTL_HANDLED; ++ case SIOCGIFMEDIA: ++#ifdef SIOCGIFXMEDIA ++ case SIOCGIFXMEDIA: ++#endif ++ { ++ struct ifmediareq* ifmr = (struct ifmediareq*) arg; ++ struct linux_ifreq ifr; ++ int status; ++ ++ if (ifmr == NULL || ifmr->ifm_count < 0) ++ { ++ *retval = -EINVAL; ++ return IOCTL_HANDLED; ++ } ++ ++ ifr.lifr_name[LINUX_IFNAMSIZ - 1] = '\0'; ++ strncpy(ifr.lifr_name, ifmr->ifm_name, LINUX_IFNAMSIZ - 1); ++ status = __real_ioctl(fd, LINUX_SIOCGIFFLAGS, &ifr); ++ if (status < 0) ++ { ++ *retval = status; ++ return IOCTL_HANDLED; ++ } ++ ++ if (ifr.lifr_flags & IFF_LOOPBACK) ++ { ++ *retval = -ENOTSUP; ++ return IOCTL_HANDLED; ++ } ++ ++ if (strncmp(ifmr->ifm_name, "wl", 2) == 0) ++ ifmr->ifm_current = IFM_IEEE80211 | IFM_AUTO; ++ else ++ ifmr->ifm_current = IFM_ETHER | IFM_AUTO; ++ ifmr->ifm_mask = 0; ++ ifmr->ifm_status = ((ifr.lifr_flags & IFF_UP) && (ifr.lifr_flags & IFF_RUNNING)) ++ ? (IFM_AVALID | IFM_ACTIVE) : IFM_AVALID; ++ ifmr->ifm_active = ifmr->ifm_current; ++ if (ifmr->ifm_count > 0) ++ { ++ if (ifmr->ifm_ulist == NULL) ++ { ++ *retval = -EFAULT; ++ return IOCTL_HANDLED; ++ } ++ ifmr->ifm_ulist[0] = ifmr->ifm_current; ++ } ++ ifmr->ifm_count = 1; ++ *retval = 0; ++ return IOCTL_HANDLED; ++ } + } + return IOCTL_PASS; + } diff --git a/tests/test_ifmedia.py b/tests/test_ifmedia.py new file mode 100644 index 0000000..2c4392c --- /dev/null +++ b/tests/test_ifmedia.py @@ -0,0 +1,94 @@ +"""Compile the added interface-media handler with inert ioctl fixtures.""" +from pathlib import Path +import shutil +import unittest + +import test_runtime_compatibility as sources + + +@unittest.skipUnless(shutil.which('cc'), 'C compiler required') +class InterfaceMediaTests(unittest.TestCase): + run_c = sources.CompatibilityChecks.run_c + + def test_media_queries_preserve_buffers_and_exclude_loopback(self): + patch = (Path(__file__).resolve().parents[1] / 'patches/xnu.patch').read_text() + section = patch.split('diff --git a/darling/src/libsystem_kernel/emulation/src/' + 'xnu_syscall/bsd/helper/ioctl/socket.c ', 1)[1] + added = '\n'.join(line[1:] for line in section.splitlines() + if line.startswith('+') and not line.startswith('+++')) + handler = added.split('\t\tcase SIOCGIFMEDIA:', 1)[1] + self.run_c(r''' +#include +#include +#include +#define SIOCGIFMEDIA 1 +#define SIOCGIFXMEDIA 2 +#define IOCTL_HANDLED 1 +#define IOCTL_PASS 0 +#define IFF_UP 1 +#define IFF_LOOPBACK 8 +#define IFF_RUNNING 64 +#define IFM_ETHER 0x20 +#define IFM_IEEE80211 0x80 +#define IFM_AUTO 0 +#define IFM_AVALID 1 +#define IFM_ACTIVE 2 +#define LINUX_SIOCGIFFLAGS 0x8913 +#define LINUX_IFNAMSIZ 16 +struct linux_ifreq { char lifr_name[16]; short lifr_flags; char pad[22]; }; +struct ifmediareq { + char ifm_name[16]; + int ifm_current, ifm_mask, ifm_status, ifm_active, ifm_count; + int *ifm_ulist; +}; +static short flags; +static int ioctl_result; +static int __real_ioctl(int fd, int command, struct linux_ifreq *request) { + assert(fd == 3 && command == LINUX_SIOCGIFFLAGS); + request->lifr_flags = flags; return ioctl_result; +} +static int handle_socket(int fd, unsigned cmd, void *arg, int *retval) { + switch (cmd) { case SIOCGIFMEDIA: +''' + handler + r''' + } + return IOCTL_PASS; +} +int main(void) { + struct ifmediareq request = {.ifm_name = "eth0"}; + int result = 99, list[] = {-1, -2}; + flags = IFF_UP | IFF_RUNNING; + assert(handle_socket(3, SIOCGIFMEDIA, &request, &result) == IOCTL_HANDLED); + assert(result == 0 && request.ifm_count == 1 && request.ifm_ulist == NULL); + assert(request.ifm_current == IFM_ETHER && request.ifm_active == IFM_ETHER); + assert(request.ifm_status == (IFM_AVALID | IFM_ACTIVE)); + request.ifm_count = 2; request.ifm_ulist = list; + assert(handle_socket(3, SIOCGIFXMEDIA, &request, &result) == IOCTL_HANDLED); + assert(result == 0 && request.ifm_count == 1 && request.ifm_ulist == list); + assert(list[0] == IFM_ETHER && list[1] == -2); + strcpy(request.ifm_name, "wlan0"); request.ifm_count = 1; + handle_socket(3, SIOCGIFMEDIA, &request, &result); + assert(result == 0 && list[0] == IFM_IEEE80211); + flags = 0; + handle_socket(3, SIOCGIFMEDIA, &request, &result); + assert(result == 0 && request.ifm_status == IFM_AVALID); + flags = IFF_UP | IFF_RUNNING | IFF_LOOPBACK; + list[0] = -1; + handle_socket(3, SIOCGIFMEDIA, &request, &result); + assert(result == -ENOTSUP && list[0] == -1 && request.ifm_ulist == list); + flags = IFF_UP | IFF_RUNNING; request.ifm_count = -1; + handle_socket(3, SIOCGIFMEDIA, &request, &result); + assert(result == -EINVAL); + request.ifm_count = 1; request.ifm_ulist = NULL; + handle_socket(3, SIOCGIFMEDIA, &request, &result); + assert(result == -EFAULT); + handle_socket(3, SIOCGIFMEDIA, NULL, &result); + assert(result == -EINVAL); + request.ifm_count = 0; ioctl_result = -ENODEV; + handle_socket(3, SIOCGIFMEDIA, &request, &result); + assert(result == -ENODEV); +} +''') + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/check-riot-ifmedia.py b/tools/check-riot-ifmedia.py new file mode 100644 index 0000000..7ae77dd --- /dev/null +++ b/tools/check-riot-ifmedia.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Build and run the standalone SIOCGIFMEDIA probe inside Darling. + +No Riot or League process is started. The probe only enumerates local +interfaces and issues SIOCGIFMEDIA, which is the call Vivox uses before +vx_req_connector_create. +""" +import argparse +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys + +ROOT = Path(__file__).resolve().parents[1] +CONFIG = ROOT / 'build/config.json' +SOURCE = ROOT / 'tools/riot-ifmedia-check.c' +OUTPUT = ROOT / 'build/riot-ifmedia-check' + + +def find_sdk(): + candidates = [ + ROOT / 'build/darling-src/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk', + ROOT / 'build/darling-src/build/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk', + ] + for candidate in candidates: + if (candidate / 'usr/include/sys/socket.h').is_file(): + return candidate + raise SystemExit('MacOSX.sdk not found; run ./scripts/build.sh first') + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--guest-home', default=None, + help='guest home, e.g. /Users/example (default: build/config.json)') + parser.add_argument('--library-path', default=None, + help='extra guest DYLD_LIBRARY_PATH entry, e.g. a rebuilt kernel overlay') + parser.add_argument('--timeout', type=float, default=60) + args = parser.parse_args() + + config = json.loads(CONFIG.read_text()) if CONFIG.exists() else {} + guest_home = args.guest_home or config.get('guest_home') + if not guest_home: + guest_home = '/Users/' + os.environ.get('USER', '') + sdk = find_sdk() + + clang = shutil.which('clang') + if not clang: + raise SystemExit('clang not found') + OUTPUT.parent.mkdir(exist_ok=True) + subprocess.run([ + clang, '-target', 'x86_64-apple-macosx11.0', '-D__DARWIN_UNIX03=0', + '-fuse-ld=lld', '-nostdlib', '-isysroot', str(sdk), + str(SOURCE), '-lSystem', '-o', str(OUTPUT), + ], check=True) + + libraries = guest_home.rstrip('/') + '/RuntimeLibraries' + if args.library_path: + libraries = args.library_path.rstrip('/') + ':' + libraries + guest_probe = '/Volumes/SystemRoot' + str(OUTPUT) + env = {key: os.environ[key] for key in ('PATH', 'HOME', 'USER', 'LOGNAME', 'DISPLAY', + 'XAUTHORITY', 'XDG_RUNTIME_DIR', 'DBUS_SESSION_BUS_ADDRESS', 'LANG') if key in os.environ} + result = subprocess.run([ + 'darling', 'shell', 'env', 'DYLD_LIBRARY_PATH=' + libraries, + guest_probe, + ], env=env, timeout=args.timeout) + return result.returncode + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/riot-ifmedia-check.c b/tools/riot-ifmedia-check.c new file mode 100644 index 0000000..7e3167e --- /dev/null +++ b/tools/riot-ifmedia-check.c @@ -0,0 +1,58 @@ +// Vivox's network reachability provider calls SIOCGIFMEDIA on each interface +// and drops every interface whose ioctl fails. Darling used to pass the Darwin +// request through to Linux, which returns ENOTTY. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int check(const char *name, int required) { + int sock = socket(AF_INET, SOCK_DGRAM, 0); + struct ifmediareq ifmr; + memset(&ifmr, 0, sizeof(ifmr)); + strncpy(ifmr.ifm_name, name, sizeof(ifmr.ifm_name) - 1); + errno = 0; + int rc = ioctl(sock, SIOCGIFMEDIA, &ifmr); + close(sock); + int active = (ifmr.ifm_status & (IFM_AVALID | IFM_ACTIVE)) == (IFM_AVALID | IFM_ACTIVE); + int known_type = IFM_TYPE(ifmr.ifm_current) == IFM_ETHER || + IFM_TYPE(ifmr.ifm_current) == IFM_IEEE80211; + printf("IFMEDIA interface=%s rc=%d errno=%d current=0x%x status=0x%x active=%d type=%d\n", + name, rc, errno, ifmr.ifm_current, ifmr.ifm_status, active, known_type); + if (rc != 0) return required; + return required && (!active || !known_type); +} + +int main(int argc, char **argv) { + int failures = 0; + if (argc > 1) { + for (int i = 1; i < argc; i++) failures += check(argv[i], 1); + } else { + struct ifaddrs *interfaces = NULL; + char name[IFNAMSIZ] = {0}; + if (getifaddrs(&interfaces) != 0) { + printf("IFMEDIA getifaddrs failed errno=%d\n", errno); + return 1; + } + for (struct ifaddrs *entry = interfaces; entry; entry = entry->ifa_next) { + if (!entry->ifa_addr || entry->ifa_addr->sa_family != AF_INET) continue; + if (entry->ifa_flags & IFF_LOOPBACK) continue; + strncpy(name, entry->ifa_name, sizeof(name) - 1); + break; + } + freeifaddrs(interfaces); + if (!name[0]) { + printf("IFMEDIA no non-loopback IPv4 interface\n"); + return 1; + } + failures += check(name, 1); + } + printf("IFMEDIA_CHECK failures=%d\n", failures); + return failures ? 1 : 0; +}