From bdbe76fd48617c781cb3e6154a76de96b64b4f54 Mon Sep 17 00:00:00 2001 From: lucannez64 <58551020+lucannez64@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:24:19 +0200 Subject: [PATCH 1/4] Handle SIOCGIFMEDIA in the ioctl shim Vivox's network reachability provider calls SIOCGIFMEDIA for each interface and drops every interface whose ioctl fails. Darling passed the Darwin request through to Linux, which returns ENOTTY, so the SDK saw no usable interface and vx_req_connector_create failed with 1004. Return a synthetic active Ethernet/IEEE802.11 media record built from the host SIOCGIFFLAGS result, and add a standalone probe that fails on the old behavior. --- patches/xnu.patch | 80 ++++++++++++++++++++++++++++++++++++++ tools/riot-ifmedia-check.c | 58 +++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 tools/riot-ifmedia-check.c diff --git a/patches/xnu.patch b/patches/xnu.patch index a02f9cd..5498085 100644 --- a/patches/xnu.patch +++ b/patches/xnu.patch @@ -6200,3 +6200,83 @@ 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,43 @@ 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) ++ { ++ *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 (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; ++ ifmr->ifm_count = 0; ++ ifmr->ifm_ulist = NULL; ++ *retval = 0; ++ return IOCTL_HANDLED; ++ } + } + return IOCTL_PASS; + } 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; +} From 68ae23206d3a1e32bdbcf7fdeff76e4e64deea18 Mon Sep 17 00:00:00 2001 From: lucannez64 <58551020+lucannez64@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:34:14 +0200 Subject: [PATCH 2/4] Add a standalone SIOCGIFMEDIA check runner Resolve the configured macOS SDK and guest home so the probe can be built and run without placeholder paths. No Riot or League process is started. --- tools/check-riot-ifmedia.py | 72 +++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tools/check-riot-ifmedia.py diff --git a/tools/check-riot-ifmedia.py b/tools/check-riot-ifmedia.py new file mode 100644 index 0000000..c358e5c --- /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/hirew (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()) From 68dcdcb093fd86e5dc78100332001de6833354ea Mon Sep 17 00:00:00 2001 From: lucannez64 <58551020+lucannez64@users.noreply.github.com> Date: Sun, 25 Oct 2026 19:12:36 +0100 Subject: [PATCH 3/4] fix(probe): use a portable guest-home example Signed-off-by: lucannez64 <58551020+lucannez64@users.noreply.github.com> --- tools/check-riot-ifmedia.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/check-riot-ifmedia.py b/tools/check-riot-ifmedia.py index c358e5c..7ae77dd 100644 --- a/tools/check-riot-ifmedia.py +++ b/tools/check-riot-ifmedia.py @@ -33,7 +33,7 @@ def find_sdk(): def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--guest-home', default=None, - help='guest home, e.g. /Users/hirew (default: build/config.json)') + 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) From a65613b11e775c0143a71cc7809c068a8f2a183f Mon Sep 17 00:00:00 2001 From: lucannez64 <58551020+lucannez64@users.noreply.github.com> Date: Sun, 25 Oct 2026 19:30:16 +0100 Subject: [PATCH 4/4] fix(ifmedia): preserve media-list requests and reject loopback Signed-off-by: lucannez64 <58551020+lucannez64@users.noreply.github.com> --- .github/workflows/source-check.yml | 1 + patches/xnu.patch | 22 +++++-- tests/test_ifmedia.py | 94 ++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 tests/test_ifmedia.py 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 7d1fd81..0d6b480 100644 --- a/patches/xnu.patch +++ b/patches/xnu.patch @@ -6236,7 +6236,7 @@ index f890ab2..8969577 100644 int handle_socket(int fd, unsigned int cmd, void* arg, int* retval) { switch (cmd) -@@ -19,6 +36,43 @@ int handle_socket(int fd, unsigned int cmd, void* arg, int* retval) +@@ -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; @@ -6249,7 +6249,7 @@ index f890ab2..8969577 100644 + struct linux_ifreq ifr; + int status; + -+ if (ifmr == NULL) ++ if (ifmr == NULL || ifmr->ifm_count < 0) + { + *retval = -EINVAL; + return IOCTL_HANDLED; @@ -6264,6 +6264,12 @@ index f890ab2..8969577 100644 + 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 @@ -6272,8 +6278,16 @@ index f890ab2..8969577 100644 + 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; -+ ifmr->ifm_count = 0; -+ ifmr->ifm_ulist = NULL; ++ 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; + } 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()