Skip to content

feat: add native broker feasibility spikes for iOS and Android - #126

Draft
f0rr0 wants to merge 5 commits into
mainfrom
f0rr0/ios-native-broker-spike
Draft

feat: add native broker feasibility spikes for iOS and Android#126
f0rr0 wants to merge 5 commits into
mainfrom
f0rr0/ios-native-broker-spike

Conversation

@f0rr0

@f0rr0 f0rr0 commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Summary

Adds native-broker feasibility spikes for iOS and Android. Both move
liboliphaunt/PostgreSQL into a separate app-owned worker process, keep SQL bytes
off the control plane, classify post-dispatch loss as outcomeUnknown without
SQL replay, and admit recovery only after a fresh process epoch and healthy
query.

This remains a draft feasibility PR. The evidence supports VIABLE WITH
LIMITATIONS
, not production readiness or a cross-device reliability SLA.

iOS 26

  • Adds OliphauntBrokerProtocol, OliphauntBrokerXPC,
    OliphauntIOSBroker, and OliphauntBrokerExtension Swift products and the
    rendered release-package graph.
  • Uses AppExtensionProcess and XPC for control plus a framed AF_UNIX FD data
    plane with explicit ownership, bounded input/raw collection, streaming,
    cancellation, checkpoint/background controls, diagnostics, and crash
    recovery.
  • Keeps the native framework, runtime resources, template PGDATA, and selected
    extensions in the worker bundle; the host does not link liboliphaunt.
  • Adds PostgreSQL patch 0021 for fail-closed configured-LOGIN authentication
    and hard stream-queue enforcement by splitting oversized backend output.

Android

  • Adds an unexported bound service in android:process=":broker", a minimal
    AIDL/Bundle control plane, and
    ParcelFileDescriptor.createReliableSocketPair() data plane.
  • Uses a single database executor, independent Binder-thread cancellation, an
    independent fail-stop scheduler, Binder death recipients, UUID worker
    epochs, and lazy fresh-generation reconnect.
  • Adds Android backend-thread cancellation: the Binder/JNI caller signals the
    embedded PostgreSQL pthread with SIGINT, preserving PostgreSQL's normal
    statement-cancel handler and SQLSTATE 57014 behavior.
  • Adds DEBUG experiments for executor deadlock, native output followed by
    pg_sleep, post-commit/pre-completion ambiguity, persistent reopen, and
    controlled 8/32 MiB socket backpressure.

Shared contract

  • The host and database worker use different PIDs and failure domains.
  • Control and diagnostics use XPC on iOS and Binder/AIDL on Android.
  • PostgreSQL protocol bytes use owned socket FDs, not control messages.
  • Every frame/request is generation-scoped; a PID alone is not identity.
  • Once request bytes may have reached a worker, worker/transport loss is
    outcomeUnknown. The host never replays SQL automatically.
  • A replacement is accepted only after a different process epoch, validated
    Ready/handshake, and healthy SQL.
  • Committed database state remains in one canonical persistent root and is
    reopened after worker loss.

iOS retained qualification

The canonical August 10 reports exactly qualified their then-current source.
Later cleanup removed unsupported private diagnostic SPI and mechanically
formatted the staged source, so these are retained feasibility evidence rather
than exact-current-commit qualification.

Environment Retained result Recovery result
iPhone 17 Pro simulator, iOS 26.4 Four lanes; 55/55 checks: 33 semantic, 7 handshake-negative, 10 extended-fault, 5 hang Seven injected crash recoveries; immediate deliberate-hang recreation did not obtain a healthy fresh worker
iPhone15,2 physical device, iOS 26.5 Two signed Debug semantic launches ×33 plus two signed Release lifecycle launches ×30; 4 launches / 126 checks Four Debug crash recoveries; both background/resume runs returned with a fresh PID and epoch after the old worker was absent
Same physical device, signed-Debug hang lane 1 launch / 5 evidence checks Evidence validation passed, but recoveryProven=false: attempts 1→2, Ready 1→1, no recovered PID/epoch

Canonical physical total including the negative hang lane is 5 launches / 131
checks
. PASS for that hang lane means its negative-evidence contract passed;
it is not recovery evidence.

iOS deliberate-hang findings

The first immediate-recreate test did not obtain a fresh healthy worker:

  • iOS 26.4 Simulator invalidated and reacquired the same PID, then the new
    Hello timed out.
  • The iOS 26.5 device made one replacement attempt but produced no second Ready
    generation; retained evidence could not distinguish stale-PID reuse from
    teardown still being in flight.

A stronger physical DEBUG matrix armed and acknowledged the fault, proved
same-epoch control responsiveness, then deadlocked an ordinary request:

Public delay before recovery demand Result
0 ms stale active-channel state; no fresh worker
50 ms (52 ms observed) stale active-channel state; no fresh worker
100 ms (106 ms observed) fresh PID, epoch, Ready, and healthy SQL
250 ms (265 ms observed) fresh PID, epoch, Ready, and healthy SQL

Unsupported private terminate/unique-instance controls also recovered at zero
delay, but all private ExtensionFoundation SPI was removed. Initial private
terminate prototypes crashed because the experiment used the wrong Swift
calling convention; that SIGSEGV was an experiment bug, not an iOS result.

Apple exposes cancel/invalidate but no public force-fresh flag, PID/exit waiter,
or termination-completion barrier. XPC cancellation is asynchronous and
non-preemptive, and AppExtensionProcess may bind an existing process. The
public delay result supports asynchronous teardown/process reuse as the
immediate mechanism; it does not establish a documented 100 ms threshold.
hangRestartable=false remains the advertised capability.

iOS repeated fail-stop controls

On one physical iPhone 14 Pro / iOS 26.5 beta stack:

  • Actor-block control: 60/60. An independent global-queue _exit(70) fired
    while the worker actor remained in a non-returning semaphore wait. Every
    trial returned outcomeUnknown at 1.003–1.068 s, then obtained a fresh PID,
    epoch, validated Ready generation, and healthy SQL.
  • Native SQL control: 60/60. An independent work item called abort()
    during SELECT pg_sleep(60), .... Every trial returned outcomeUnknown,
    then obtained a fresh PID/epoch/Ready generation and healthy SQL.
  • Three retained crash reports directly show SIGABRT on the fault-injector
    global queue while other threads are simultaneously inside
    oliphaunt_exec_protocol_stream and PostgreSQL pg_sleep. Crash-log
    coalescing makes this direct corroboration for sampled trials, not all 60.

Under an IID assumption, 60/60 gives a one-sided 95% lower success bound of
95.13%. IID is weak for serial trials on one device/build, so this is
repeatability evidence for the DEBUG mechanism, not a shipping SLA or a
production progress-sensitive watchdog policy.

iOS memory and performance

  • Physical Release control RTT medians: 2.264 ms and 2.424 ms, 20
    samples each.
  • Throttled 8 MiB and 32 MiB streams: 8,399,927 bytes in about 19.95 s and
    33,599,543 bytes in about 79.9 s, around 420 KB/s. This proves bounded
    backpressure under the imposed reader rate, not maximum throughput.
  • Slow-reader physical peaks were about 19.8 MB and 19.4 MB; minimum available
    memory was about 16.9 MB and 17.3 MB. Both runs preserved the required 8 MiB
    margin and the native 8 MiB queue ceiling.
  • Recursive device protection evidence: 1,759/1,759 Class-C entries, zero
    missing/mismatched/unavailable/unreadable/symlink entries.

CoreSimulator does not prove extension-private containment, file-protection
metadata, or available-memory headroom. Physical Class-C evidence covers
after-first-unlock, not locked-before-first-unlock behavior.

Android retained qualification

The final API 34 arm64 emulator behavior series is:

target/android-native-broker-spike/runs/pr-final-01-20260811T121603Z/
through
target/android-native-broker-spike/runs/pr-final-10-20260811T121854Z/

pr-doc-sync-v1 is an additional PASS whose source manifest includes the final
README. The ten repeated runs used the same executable inputs; their only
source-file difference is the README evidence-path text. The final committed
spike files match the retained file manifests, but the APK was built from a
dirty, hash-manifested worktree rather than a clean checkout of commit
4db5de94.

All ten full matrices passed the same 11 checks. They used ten unique host
PIDs and produced 40 unique worker PIDs and 40 unique UUID epochs.

Fault lane Samples Terminal Binder death Fresh PID + epoch + healthy SQL Request → healthy replacement
Executor deadlock + independent SIGABRT 10/10 outcomeUnknown 10/10 10/10 2,594–5,240 ms; median 2,762 ms
Native output then pg_sleep + independent SIGABRT 10/10 outcomeUnknown 10/10 10/10 2,698–7,359 ms; median 2,739 ms
Commit before completed 10/10 outcomeUnknown 10/10 10/10 597–823 ms; median 626.5 ms

All 30 faulted worker PIDs have exact retained Fatal signal 6 records.
Binder-death events are scoped to the exact PID and epoch; first loss signal
varied safely between EOF, binderDied, and onServiceDisconnected.

Android native-path witness and no replay

The native fault executes one ordered query whose first Append child emits 513
8 KiB rows and whose second child runs pg_sleep(60). The worker:

  1. observes more than 4 MiB in the native PostgreSQL callback;
  2. completes the threshold-crossing synchronous socket write;
  3. publishes a generation/request-scoped witness; and
  4. arms a two-second independent SIGABRT watchdog.

All ten native lanes witnessed exactly 4,202,496 backend bytes, then
observed Binder death and fresh recovery. This strongly proves ordered native
PostgreSQL execution immediately before the sleeping plan child. It is not a
callback from inside pg_sleep; that narrower claim remains unproven. The
report field nativeDispatchObserved is currently an alias for witness
presence and is not used as independent evidence.

Every ambiguous post-commit loss had ambiguousExecutionCount=1 and derived
replayCount=0. The committed marker survived all three broker deaths in every
run. This proves no replay for the instrumented counter and persistent reopen
within each matrix; it is not a generic exactly-once protocol or reboot/power
loss qualification.

Android controlled slow-reader evidence

The host holds an explicit zero-read gate while Binder diagnostics sample a
blocking FD with POLLOUT=false, the same synchronous response write in
progress for at least 300 ms, and unchanged completion counters. It then
releases the gate and drains the complete response from the same generation.

Stream Samples Response / chunks Full-drain time Sampled blocked write Accepted-wire upper bound
8 MiB 10/10 8,399,927 B / 1,026 432–453 ms; median 437.5 ms 300–312 ms 493,920 B every run
32 MiB 10/10 33,599,543 B / 4,102 639–766 ms; median 648 ms 300–314 ms 493,920 B every run

The cross-size bound delta was zero while response size quadrupled. This is
direct workload-specific evidence of synchronous socket backpressure, not an
effective SO_SNDBUF measurement or a general process-memory bound.

Memory rose substantially because the experimental host accumulates and
reports the final response:

  • 8 MiB PSS span: 30,992,384–31,045,632 B; RSS span:
    33,157,120–33,964,032 B.
  • 32 MiB PSS span: 97,970,176–101,406,720 B; RSS span:
    98,533,376–101,945,344 B.

Gross emulator drain rates include the deliberate gate and diagnostics and are
not maximum SQL/transport throughput or an SLA.

Android negative evidence that changed the harness

  • final-witness-001: PostgreSQL buffered a small first-statement
    CommandComplete until after the sleeping statement, so it could not serve
    as a pre-hang witness.
  • final-output-witness-006: the first transient non-writable socket write
    advanced while the client still had not read. The final probe therefore
    keeps a read gate closed and resets its candidate until the same sampled
    write remains unchanged for at least 300 ms.

Explicit limitations

By design / experimental scope

  • Both spikes expose one canonical root/session and do not implement a
    production multi-client/root server.
  • Android :broker is a separate failure domain but shares the app UID; it is
    not an adversarial-code security boundary.
  • Fault injection and fail-stop hooks are DEBUG-only.
  • No request is replayed after ambiguous loss; callers must reconcile unknown
    outcomes at the application layer.
  • iOS continues to advertise hangRestartable=false until a bounded public
    policy is qualified.

Observed platform/tool boundaries

  • iOS public immediate recreation can reacquire stale process/active-channel
    state while teardown is asynchronous.
  • CoreDevice memory-warning injection returned ENOENT; no synthetic result is
    claimed.
  • CoreSimulator lacks usable file-protection/headroom evidence and uses a
    simulator-global Application Support root.
  • Android evidence is one API 34 arm64 emulator image only; no physical Android
    device was connected.
  • Android requested socket-buffer size is not treated as an effective kernel
    value. The backpressure result is based on readiness and write-progress
    evidence instead.

Still unqualified

  • Production progress-sensitive watchdog/retry policy and false-positive
    behavior for legitimately slow SQL.
  • Physical Android behavior, other API levels/ABIs/OEM images, and cross-device
    dependability.
  • Same-host long soak, multi-client/concurrent requests, lifecycle/Doze/app
    standby/background behavior, LMK/jetsam, and leak/resource accumulation.
  • Reboot, force-stop, update, reinstall, eviction, power loss, disk-full,
    torn-write, and filesystem-corruption durability.
  • General data integrity/exactly-once, CPU, energy, maximum throughput, memory
    limits, and latency SLA.
  • Distribution builds, TestFlight/App Store, Play distribution, and review.

Verification

iOS retained/current checks

  • Swift static/package checks and 149/149 tests in nested and root manifests.
  • iPhoneOS Debug and Release compile/link for host and extension.
  • strict Swift formatting/parsing, seven broker-script Bash/ShellCheck gates,
    Ruby generator syntax, SwiftPM renderer 5/5, docs 41 routes.
  • PostgreSQL patch-stack/currentness, iOS embedded compile probe, and strict
    native host smoke covering role/authentication, stream/cancel, persistence,
    backup/restore, and native smoke twice.

Android current checks

  • :android-native-broker-spike:compileDebugKotlin PASS.
  • :android-native-broker-spike:testDebugUnitTest PASS: 7/7 tests.
  • Canonical Android arm64 native --check-current PASS for NDK
    27.0.12077973.
  • bash -n, ShellCheck, both embedded Python validators, and focused
    git diff --check PASS.
  • docs product check PASS: 41 routes.
  • Final emulator behavior series 10/10, plus post-document confirmation
    PASS.
  • Commit hooks passed before 4db5de94.

The broad repository/release gate has not been rerun from a clean checkout of
the final commit. The shared working tree still contains unrelated unstaged
release-tool and portability changes; none is in this commit.

Before this leaves draft

  • Run canonical checks from a clean checkout and obtain exact-SHA GitHub
    Qualified evidence.
  • Rerun the full iOS simulator/physical matrix if exact-current-source iOS
    evidence is required.
  • Run the strict Android matrix repeatedly on physical arm64 hardware and
    expand API/OEM coverage.
  • Decide and qualify bounded production hang/fail-stop policies, or keep
    recovery conservatively unadvertised.
  • Add long-lived integrity, lifecycle, memory-pressure, and resource-leak
    qualification.
  • Hide/separate remaining Release fault vocabulary before treating either
    broker API as production-stable.
  • Run distribution/TestFlight/App Store/Play qualification when in scope.

@f0rr0

f0rr0 commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

Physical fail-stop experiment complete

Final result on the exact signed Debug artifact and one iPhone 14 Pro running iOS 26.5 beta:

  • actor-block control: 60/60 fresh-worker recoveries;
  • in-flight native SQL (pg_sleep(60)) control: 60/60 fresh-worker recoveries;
  • every native trial observed a distinct worker PID and epoch, a new validated Ready generation, healthy SQL, old-worker absence, and clean process teardown;
  • three sampled initial-worker IPS reports directly show EXC_CRASH / SIGABRT from the independent global-queue fault injector while other threads are inside oliphaunt_exec_protocol_stream and PostgreSQL pg_sleep;
  • two later RunningBoard 0xdead10cc reports belong to recovered workers and are explicitly excluded from watchdog causality.

This demonstrates repeatability of the DEBUG fail-stop/reconnect mechanism on the tested stack. It does not qualify automatic detection of a naturally hung query, a production timeout policy, Release/App Review behavior, or a cross-device reliability SLA. hangRestartable=false remains intentional.

Current PR head: dcdaeaac6eaffb2bb20136719f6ce2e3ffcb708b.

@f0rr0 f0rr0 changed the title feat(swift): add iOS native broker spike feat: add native broker feasibility spikes for iOS and Android Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant