ReviewOS

also looking at this

craft-native/craft

iOS Phase 4: the secure tier — async replies, Keychain, permissions; ratchet 86 → 74

#86
Merged glennmichael123 wants to merge feat/ios-phase4-secure into main
11 files +4297 -4
10 days ago

Follow-on to #85. Twelve more actions cross into Zig, including the first asynchronous reply, and the fixture now round-trips a secret through the real Keychain.

The async foundation (ios_async.zig)

The plan flagged async replies as the highest-risk item after Phase 1: a completion handler fires after the dispatch frame is gone (the request id is no longer anywhere) and on whatever queue the framework chose (evaluateJavaScript is main-thread-only). Losing the id silently drops correlation to action-name matching — caller B's answer handed to caller A whenever two calls of one action are in flight.

Three pieces, each checkable:

  • a slot pool capturing {request id, action} at dispatch time, with generation counters so a stale or double-firing completion cannot resolve a reused slot;
  • completion blocks that are BLOCK_IS_GLOBAL — Block_copy on a global block returns the same pointer, so no heap copy, no descriptor lifetime, no copy/dispose. Each slot has its own comptime-generated invoke with the index baked in: how a non-capturing block knows who it is;
  • delivery hopped to the main queue with dispatch_async_f — the function-pointer variant, so the hop itself needs no block.

The pool exhausts loudly (acquire → null obliges a Busy reply), and an action longer than slot storage is refused rather than truncated — a truncated name would make the reply name a different action than the page called.

Five modules, 12 actions

moduleactions
miscsetFlashlight
shortcutssetShortcuts, clearShortcuts
securestoresecureSet, secureGet, secureRemove, secureClear
biometricsetBiometricPersistence, checkBiometricPersistence, clearBiometricPersistence
permissionscheckPermission, requestPermission (async, on ios_async)

Deliberately not served: getInitialURL, registerDeepLinkHandler, takeScreenshot, openSettings stay with the Swift shim. Their Swift arms are config-gated (enableDeepLinks, enableScreenCapture) and no config channel into Zig exists yet — declaring them .unavailable would make Zig dispatch-and-refuse, stealing them from the shim that serves them correctly today. Falling through is the honest routing.

The adversarial review pass caught another lawful-payload refusal before it shipped: floats rendered with {d} into a [64]u8 buffer, when {d} on f64 needs up to 347 bytes — {"x":1e300} failed as NativeCallFailed. Sized by std.fmt.float.bufferSize now, pinned by a test.

The Keychain on a simulator, reverse-engineered

The fixture's tenth assertion round-trips a secret containing a quote and a backslash through real SecItemAdd/SecItemCopyMatching, byte-identical. Getting it to run surfaced a mechanism worth recording:

  • unsigned app → SecItemAdd fails with errSecMissingEntitlement (-34018)
  • signing application-identifier entitlements in → the launch itself is denied (POSIX 163 from RunningBoard; bisected — the same signature without them launches)
  • the simulator's actual mechanism, which Xcode uses: identity entitlements live in a __TEXT,__entitlements Mach-O section, not the signature

The harness now embeds the identity via -sectcreate and signs ad-hoc — matching what a real simulator build carries.

Verification

All ten fixture assertions on an iPhone 17 Pro simulator (iOS 26.5):

ok: page -> native … ok: error route closed with escaping intact
ok: Tier-0 action served by Zig with a live UIKit value
ok: secureSet/secureGet round-tripped a secret through the Keychain
PASS

Plus zig build test / test:ios / desktop / build-ios / build-ios-simulator / build-android, zig fmt --check, verify:cimports, pickier. The conformance ratchet is at 74 and only goes down.

11 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.