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