NEURLFilterManager: inconsistent per-domain blocking (some sites never block, one flaps) - confirmed PIR database is correct

Hello,

Some context: DropBet is an app that helps people with gambling addiction stay off gambling sites and apps, using NEURLFilterManager/PIR to block a large list of known gambling domains system-wide. For a lot of our users this filter is a core part of their recovery > reliability really matters here.

Since fixing an unrelated HPKE encryption bug (separate thread, now closed), the filter works end-to-end for the first time, but blocking is inconsistent per domain in a way I can't explain from my side:

  • winamax.fr and the Winamax app: blocked reliably, every single time > clean "connection failed" style block, confirmed to load fine with protection off.
  • unibet.fr and betclic.fr: never blocked, consistently > confirmed even after a full device reboot (rules out stale caching), and confirmed identical across both Safari and Chrome on iOS tested side-by-side (so not browser-specific).
  • olybet.fr: flaps > the page reloads every 2-3 seconds with protection on, also identical across both browsers. Confirmed this only happens with the filter enabled (tested toggling protection off/on against the same page).
  • unibet.fr auto-redirects into the native Unibet app via a Universal Link before the page loads, unlike betclic.fr which stays in the browser > different code path, possibly relevant.

To rule out any issue on our end, I wrote a standalone test that queries our deployed PIR database shards directly - bypassing OHTTP, the app, and the device entirely - for all of the domains above plus their www. variants. All of them correctly return the blocked value, no exceptions, including unibet.fr and betclic.fr which never block on the actual device. So the database, the PIR protocol implementation, and the server are confirmed correct for every domain I've tested.

This points at something in how NEURLFilterManager/ciphermld applies a correctly-received PIR verdict on-device - possibly related to query batching, an evaluation-key edge case, or Universal Link handling bypassing the filter before a web request is even made. Is there anything you can share about how the client applies results once decrypted, or known issues around any of this?

Written by RomainDropB in 855454021

To rule out any issue on our end, I wrote a standalone test that queries our deployed PIR database shards directly

Nice.

I think it also makes sense to rule out the web browsers themselves, and you can do that by writing a tiny app that calls the verdict(for:) API. If you do that, do you see the same issues?

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hello Quinn @DTS Engineer,
Thanks for the pointer,

I ran the direct NEURLFilter.verdict(for:) call > this confirms the issue is not browser-related at all, and points to something specific in how the verdict is computed.
Results (from the SDK header, NEURLFilterVerdictAllow = 2, Deny = 3, Unknown = 1):

winamax.fr -> Deny (3)    — correct
unibet.fr  -> Allow (2)   — wrong
betclic.fr -> Allow (2)   — wrong
olybet.fr  -> Allow (2)   — wrong (this is the one that flaps in-browser)
fdj.fr     -> Deny (3)    — correct

This is the key part: it's returning a confident Allow, not Unknown (which would mean the validation itself failed). And I'd already independently proven, by querying our deployed PIR database shards directly (bypassing OHTTP, the app, and the device entirely), that every one of these domains - including unibet.fr and betclic.fr - correctly returns the blocked value in our data. Our local bloom prefilter file is also confirmed byte-correct, with all of these domains properly hashed in.

So: the local prefilter data is right, the PIR database is right, yet the on-device verdict computation still returns the wrong answer for these specific domains. That narrows it down to something inside the verdict pipeline itself - bloom-check-to-PIR-query handoff, or PIR response interpretation - rather than any input data I can inspect or fix on my end. Is there anything you can share about how NEURLFilter resolves a verdict internally, or does this look like something worth filing as a bug?