From 72093a1b584725ccdb358dd8b050f9d5f8682795 Mon Sep 17 00:00:00 2001 From: lucannez64 <58551020+lucannez64@users.noreply.github.com> Date: Fri, 16 Oct 2026 09:08:54 +0200 Subject: [PATCH] Make the CFHost cancel probe wait for delivery and reject completed lookups Wait up to 2 s for the cancellation callback instead of one 50 ms run-loop pass, count only callbacks without stored addresses so a completed lookup cannot pass as a cancellation, print the failing stage, and have the runner require the exact success line since Darling can exit zero after a trap. Co-Authored-By: Claude Opus 5.5 --- docs/TESTING.md | 6 ++++-- tools/riot-cfhost-cancel-check.c | 26 ++++++++++++++++++++------ tools/run-riot-native-check.py | 3 +++ 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/docs/TESTING.md b/docs/TESTING.md index 64488ed..fff7da9 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -406,7 +406,9 @@ python3 build/runtime/tools/run-riot-native-check.py riot-cfhost-cancel-check Run against the installed CFNetwork override. Expect `CFHOST_CANCEL_CHECK cancel callback lifetime repeated pass=1`. The unpatched -library traps in `CFRetain(NULL)`; the runner checks the success marker because +library traps in `CFRetain(NULL)`; the runner requires the exact success line because Darling can return status zero even when the guest traps. The probe uses localhost and exercises 100 cancellations with both retained and early-released caller -references, without launching or inspecting Riot. +references, without launching or inspecting Riot. A failure prints +`CFHOST_CANCEL_CHECK fail stage=...`; `unexpected-callback` means a completed +lookup arrived instead of the cancellation. diff --git a/tools/riot-cfhost-cancel-check.c b/tools/riot-cfhost-cancel-check.c index f74d8fc..b1ff012 100644 --- a/tools/riot-cfhost-cancel-check.c +++ b/tools/riot-cfhost-cancel-check.c @@ -2,11 +2,22 @@ #include #include -static unsigned callbacks; +static unsigned callbacks, unexpected; static void completed(CFHostRef host, CFHostInfoType type, const CFStreamError *error, void *info) { - if (host && type == kCFHostAddresses && info == &callbacks) + /* A cancellation stores no addresses; a completed lookup always does. */ + Boolean resolved = false; + if (host) CFHostGetAddressing(host, &resolved); + if (host && type == kCFHostAddresses && info == &callbacks && !resolved) ++callbacks; + else + ++unexpected; +} + +static int fail(const char *stage, unsigned i) { + printf("CFHOST_CANCEL_CHECK fail stage=%s iteration=%u callbacks=%u unexpected=%u\n", + stage, i, callbacks, unexpected); + return 1; } int main(void) { @@ -14,14 +25,17 @@ int main(void) { for (unsigned i = 0; i < 100; ++i) { CFHostRef host = CFHostCreateWithName(NULL, CFSTR("localhost")); CFStreamError error = {0}; - if (!host || !CFHostSetClient(host, completed, &context)) return 1; + if (!host || !CFHostSetClient(host, completed, &context)) return fail("client", i); CFHostScheduleWithRunLoop(host, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); - if (!CFHostStartInfoResolution(host, kCFHostAddresses, &error)) return 2; + if (!CFHostStartInfoResolution(host, kCFHostAddresses, &error)) return fail("start", i); CFHostCancelInfoResolution(host, kCFHostAddresses); /* The cancel source must keep its host alive until delivery. */ if (i % 2) CFRelease(host); - CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.05, false); - if (callbacks != i + 1) return 3; + CFAbsoluteTime deadline = CFAbsoluteTimeGetCurrent() + 2.0; + while (callbacks != i + 1 && !unexpected && CFAbsoluteTimeGetCurrent() < deadline) + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.05, true); + if (unexpected) return fail("unexpected-callback", i); + if (callbacks != i + 1) return fail("cancel-callback", i); if (!(i % 2)) CFRelease(host); } puts("CFHOST_CANCEL_CHECK cancel callback lifetime repeated pass=1"); diff --git a/tools/run-riot-native-check.py b/tools/run-riot-native-check.py index df6fddb..2eafd83 100644 --- a/tools/run-riot-native-check.py +++ b/tools/run-riot-native-check.py @@ -88,6 +88,9 @@ with tempfile.TemporaryFile() as output: if (args.check == "riot-running-application-check" and line != "RUNNING_APP found=1 activated=1 missing=1"): result = 1 + if (args.check == "riot-cfhost-cancel-check" and + line != "CFHOST_CANCEL_CHECK cancel callback lifetime repeated pass=1"): + result = 1 if (args.check == "riot-mach-exception-state-check" and line != "MACH_EXCEPTION_STATE state=1 identity=1 handled=2 server=1"): result = 1