Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 55 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,78 @@ let package = Package(
name: "Oliphaunt",
platforms: [
.iOS(.v17),
.macOS(.v14)
.macOS(.v14),
],
products: [
.library(name: "Oliphaunt", targets: ["Oliphaunt"])
.library(name: "OliphauntBrokerProtocol", targets: ["OliphauntBrokerProtocol"]),
.library(name: "OliphauntBrokerXPC", targets: ["OliphauntBrokerXPC"]),
.library(name: "OliphauntIOSBroker", targets: ["OliphauntIOSBroker"]),
.library(name: "OliphauntBrokerExtension", targets: ["OliphauntBrokerExtension"]),
.library(name: "Oliphaunt", targets: ["Oliphaunt"]),
],
targets: [
.target(
name: "COliphaunt",
path: "src/sdks/swift/Sources/COliphaunt",
publicHeadersPath: "include"
),
.target(
name: "OliphauntBrokerProtocol",
path: "src/sdks/swift/Sources/OliphauntBrokerProtocol"
),
.target(
name: "OliphauntBrokerXPC",
dependencies: ["OliphauntBrokerProtocol"],
path: "src/sdks/swift/Sources/OliphauntBrokerXPC"
),
.target(
name: "Oliphaunt",
dependencies: ["COliphaunt"],
path: "src/sdks/swift/Sources/Oliphaunt"
),
.target(
name: "OliphauntIOSBroker",
dependencies: ["Oliphaunt", "OliphauntBrokerProtocol", "OliphauntBrokerXPC"],
path: "src/sdks/swift/Sources/OliphauntIOSBroker"
),
.target(
name: "OliphauntBrokerExtension",
dependencies: ["COliphaunt", "Oliphaunt", "OliphauntBrokerProtocol"],
path: "src/sdks/swift/Sources/OliphauntBrokerExtension"
),
.testTarget(
name: "OliphauntTests",
dependencies: ["Oliphaunt"],
path: "src/sdks/swift/Tests/OliphauntTests"
)
),
.testTarget(
name: "OliphauntBrokerProtocolTests",
dependencies: ["OliphauntBrokerProtocol"],
path: "src/sdks/swift/Tests/OliphauntBrokerProtocolTests"
),
.testTarget(
name: "OliphauntBrokerXPCTests",
dependencies: ["OliphauntBrokerProtocol", "OliphauntBrokerXPC"],
path: "src/sdks/swift/Tests/OliphauntBrokerXPCTests"
),
.testTarget(
name: "OliphauntBrokerExtensionTests",
dependencies: [
"Oliphaunt",
"OliphauntBrokerExtension",
"OliphauntBrokerProtocol",
],
path: "src/sdks/swift/Tests/OliphauntBrokerExtensionTests"
),
.testTarget(
name: "OliphauntIOSBrokerTests",
dependencies: [
"Oliphaunt",
"OliphauntBrokerProtocol",
"OliphauntBrokerXPC",
"OliphauntIOSBroker",
],
path: "src/sdks/swift/Tests/OliphauntIOSBrokerTests"
),
]
)
359 changes: 359 additions & 0 deletions docs/architecture/android-native-broker-spike.md

Large diffs are not rendered by default.

1,060 changes: 1,060 additions & 0 deletions docs/architecture/ios-native-broker-spike.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions docs/internal/OLIPHAUNT_PATCH_STACK.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ src/runtimes/liboliphaunt/native/tools/check-patch-stack.mjs --write
| 18 | `0018-liboliphaunt-contain-embedded-proc-signals.patch` | liboliphaunt <liboliphaunt@example.invalid> | liboliphaunt: contain embedded process signals |
| 19 | `0019-liboliphaunt-link-windows-embedded-modules-to-host.patch` | liboliphaunt <liboliphaunt@example.invalid> | liboliphaunt: link Windows embedded modules to host |
| 20 | `0020-liboliphaunt-enforce-embedded-signal-boundary.patch` | liboliphaunt <liboliphaunt@example.invalid> | liboliphaunt: enforce embedded signal boundary |
| 21 | `0021-liboliphaunt-authenticate-embedded-role.patch` | liboliphaunt <liboliphaunt@example.invalid> | liboliphaunt: authenticate embedded role |

## Changed Upstream Files

Expand All @@ -48,6 +49,7 @@ src/runtimes/liboliphaunt/native/tools/check-patch-stack.mjs --write
- `src/backend/access/transam/xlogarchive.c` (`0007-liboliphaunt-disable-shell-commands-on-apple-mobile.patch`)
- `src/backend/archive/shell_archive.c` (`0007-liboliphaunt-disable-shell-commands-on-apple-mobile.patch`)
- `src/backend/commands/event_trigger.c` (`0012-liboliphaunt-enable-event-triggers-in-embedded-backend.patch`)
- `src/backend/commands/variable.c` (`0021-liboliphaunt-authenticate-embedded-role.patch`)
- `src/backend/libpq/be-secure.c` (`0001-liboliphaunt-add-backend-host-io.patch`)
- `src/backend/libpq/pqcomm.c` (`0001-liboliphaunt-add-backend-host-io.patch`)
- `src/backend/meson.build` (`0019-liboliphaunt-link-windows-embedded-modules-to-host.patch`)
Expand All @@ -59,13 +61,15 @@ src/runtimes/liboliphaunt/native/tools/check-patch-stack.mjs --write
- `src/backend/storage/ipc/procsignal.c` (`0018-liboliphaunt-contain-embedded-proc-signals.patch`)
- `src/backend/tcop/postgres.c` (`0002-liboliphaunt-add-embedded-entrypoint.patch`, `0003-liboliphaunt-return-from-embedded-frontend-terminate.patch`, `0004-liboliphaunt-run-embedded-exit-cleanup.patch`, `0005-liboliphaunt-restore-host-cwd.patch`, `0009-liboliphaunt-guard-embedded-proc-exit.patch`, `0010-liboliphaunt-use-host-runtime-paths.patch`, `0014-liboliphaunt-use-portable-embedded-socketpair.patch`, `0018-liboliphaunt-contain-embedded-proc-signals.patch`)
- `src/backend/utils/fmgr/dfmgr.c` (`0006-liboliphaunt-add-static-extension-loader.patch`, `0008-liboliphaunt-clean-embedded-symbols.patch`)
- `src/backend/utils/init/postinit.c` (`0021-liboliphaunt-authenticate-embedded-role.patch`)
- `src/bin/initdb/initdb.c` (`0016-liboliphaunt-skip-icu-collation-version-without-icu-data.patch`)
- `src/include/libpq/libpq-be.h` (`0001-liboliphaunt-add-backend-host-io.patch`)
- `src/include/port.h` (`0011-liboliphaunt-add-android-embedded-shared-memory.patch`, `0020-liboliphaunt-enforce-embedded-signal-boundary.patch`)
- `src/include/storage/dsm_impl.h` (`0011-liboliphaunt-add-android-embedded-shared-memory.patch`)
- `src/include/storage/ipc.h` (`0004-liboliphaunt-run-embedded-exit-cleanup.patch`, `0009-liboliphaunt-guard-embedded-proc-exit.patch`)
- `src/include/tcop/backend_startup.h` (`0013-liboliphaunt-fix-embedded-backend-main-return-contract.patch`)
- `src/include/tcop/tcopprot.h` (`0003-liboliphaunt-return-from-embedded-frontend-terminate.patch`, `0008-liboliphaunt-clean-embedded-symbols.patch`)
- `src/include/utils/guc_hooks.h` (`0021-liboliphaunt-authenticate-embedded-role.patch`)
- `src/include/utils/hsearch.h` (`0017-liboliphaunt-namespace-dynahash-host-collisions.patch`)
- `src/port/chklocale.c` (`0011-liboliphaunt-add-android-embedded-shared-memory.patch`)
- `src/port/pqsignal.c` (`0020-liboliphaunt-enforce-embedded-signal-boundary.patch`)
Expand All @@ -79,6 +83,7 @@ src/runtimes/liboliphaunt/native/tools/check-patch-stack.mjs --write
| `src/backend/access/transam/xlogarchive.c` | Apple mobile embedded builds compile out optional archive shell commands. |
| `src/backend/archive/shell_archive.c` | Apple mobile embedded builds compile out optional archive shell commands. |
| `src/backend/commands/event_trigger.c` | Embedded FE/BE protocol sessions can run event triggers without changing standalone recovery behavior. |
| `src/backend/commands/variable.c` | Oliphaunt session-authorization assignments monotonically latch an observed authenticated-role demotion without catalog access during transaction cleanup. |
| `src/backend/libpq/be-secure.c` | Backend secure read/write path delegates to a host I/O vtable only when OLIPHAUNT_EMBEDDED is set. |
| `src/backend/libpq/pqcomm.c` | Standalone embedded sessions avoid waiting on a non-existent postmaster death latch. |
| `src/backend/meson.build` | Embedded MSVC extension modules link to the oliphaunt host import library instead of the standalone postgres executable. |
Expand All @@ -90,13 +95,15 @@ src/runtimes/liboliphaunt/native/tools/check-patch-stack.mjs --write
| `src/backend/storage/ipc/procsignal.c` | The one-backend embedded runtime dispatches ProcSignal flags without sending process-directed host signals. |
| `src/backend/tcop/postgres.c` | Embedded backend entrypoint, protocol lifecycle, cwd restoration, host runtime paths, and host-owned SIGUSR1 disposition. |
| `src/backend/utils/fmgr/dfmgr.c` | Static extension lookup reuses PostgreSQL dynamic function manager semantics. |
| `src/backend/utils/init/postinit.c` | Oliphaunt host-I/O sessions initialize the immutable authenticated identity from the configured role while ordinary standalone startup remains unchanged. |
| `src/bin/initdb/initdb.c` | Base runtimes skip ICU-backed collation setup until optional ICU data is present. |
| `src/include/libpq/libpq-be.h` | Host I/O vtable is attached to PostgreSQL Port state under OLIPHAUNT_EMBEDDED. |
| `src/include/port.h` | Embedded mobile builds avoid POSIX shared memory declarations and route embedded backend signal calls through the host-safe provider boundary. |
| `src/include/storage/dsm_impl.h` | Embedded mobile builds keep DSM on mmap instead of POSIX or SysV shared memory. |
| `src/include/storage/ipc.h` | Embedded cleanup and proc_exit guard declarations. |
| `src/include/tcop/backend_startup.h` | Embedded BackendMain may return after its returning PostgresMain call without retaining an invalid pg_noreturn declaration. |
| `src/include/tcop/tcopprot.h` | Embedded entrypoint and returning PostgresMain declarations. |
| `src/include/utils/guc_hooks.h` | Declares the Oliphaunt-only per-session authenticated-role latch reset used by InitPostgres. |
| `src/include/utils/hsearch.h` | Apple builds namespace PostgreSQL dynahash symbols that otherwise bind to unrelated libSystem exports. |
| `src/port/chklocale.c` | Android embedded builds avoid unsupported locale-environment mutation. |
| `src/port/pqsignal.c` | Embedded backend signal registration and emission preserve the host-owned SIGUSR1 disposition while delegating other signals. |
Expand Down Expand Up @@ -141,6 +148,7 @@ src/runtimes/liboliphaunt/native/tools/check-patch-stack.mjs --write
| Embedded ProcSignal delivery cannot escape into the host process | `0018-liboliphaunt-contain-embedded-proc-signals.patch` | `oliphaunt_send_proc_signal`, `pid != MyProcPid`, `procsignal_sigusr1_handler(SIGUSR1)`, `host owns SIGUSR1` | The one-backend embedded runtime dispatches ProcSignal flags synchronously, rejects foreign PIDs, and leaves the host SIGUSR1 disposition untouched; normal PostgreSQL server builds retain upstream signal delivery. |
| Windows embedded extension modules link to the host DLL provider | `0019-liboliphaunt-link-windows-embedded-modules-to-host.patch` | `oliphaunt_embedded_module_provider`, `requires an embedded MSVC Windows build`, `pg_mod_link_args += oliphaunt_embedded_module_provider`, `oliphaunt_embedded_module_provider == ''` | Embedded MSVC extension modules resolve PostgreSQL backend symbols from the oliphaunt host import library; ordinary PostgreSQL modules retain the upstream postgres executable link contract. |
| Embedded backend and extension signal calls preserve host SIGUSR1 ownership | `0020-liboliphaunt-enforce-embedded-signal-boundary.patch` | `oliphaunt_embedded_kill`, `oliphaunt_embedded_raise`, `!defined(FRONTEND)`, `if (signo == SIGUSR1)` | Embedded backend and extension calls cannot replace or emit host-owned SIGUSR1; other signals delegate to the platform implementation, while frontend tools and normal PostgreSQL builds retain upstream behavior. |
| Embedded sessions authenticate as the configured database role | `0021-liboliphaunt-authenticate-embedded-role.patch` | `MyProcPort->oliphaunt_io != NULL`, `InitializeSessionUserId(username, useroid, false)`, `!role_form->rolcanlogin`, `is not permitted to log in`, `assign_session_authorization`, `ResetOliphauntAuthenticatedRoleLatch`, `oliphaunt_authenticated_role_is_superuser = false`, `InitializeSessionUserIdStandalone` | Only the Oliphaunt host-I/O backend resolves its immutable authenticated identity from the configured LOGIN role and latches an observed demotion for that session so stale RESET, rollback, and DISCARD state fail closed without catalog work in GUC cleanup; ordinary standalone PostgreSQL keeps bootstrap-superuser recovery semantics. |

## Guardrails

Expand Down
3 changes: 3 additions & 0 deletions spikes/android-native-broker/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.gradle/
app/build/
reports/
136 changes: 136 additions & 0 deletions spikes/android-native-broker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Android native broker spike

This DEBUG-only spike mirrors the iOS broker experiment on Android with a
private `:broker` service process, an AIDL/Binder control plane, and a reliable
Unix socket-pair file-descriptor data plane.

The `full` fixture exercises:

- distinct host and broker PIDs plus a random worker epoch;
- PostgreSQL protocol bytes over the socket data plane;
- cancellation from a Binder thread outside the occupied database executor;
- executor-deadlock and native-output-gated `pg_sleep` fail-stop paths;
- generation-scoped Binder death, `outcomeUnknown`, no SQL replay, and recovery
to a fresh PID and epoch;
- controlled zero-read 8 MiB and 32 MiB streams that directly observe a
blocked synchronous socket write; and
- persistent data and an ambiguous committed counter across injected worker
deaths.

It is an experiment, not a production broker. Fault hooks are DEBUG-only, and
an emulator result is not physical-device evidence.

## Retained evidence

The final ten-run behavior series is:

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

`target/android-native-broker-spike/runs/pr-doc-sync-v1/` is the post-document
single-run confirmation whose source manifest includes this final README. The
ten-run series used identical executable inputs and differs only in the README
evidence-path text.

All ten API 34 arm64 emulator runs passed 11 checks. They produced 30 injected
worker deaths, 30 generation-scoped Binder-death observations, 30
`outcomeUnknown` terminals, and 30 fresh PID/epoch recoveries followed by
healthy SQL. All 10 host PIDs and all 40 worker PIDs and epochs were unique in
the retained series.

Each native fault recorded 4,202,496 bytes from the native PostgreSQL stream
for an ordered output-then-`pg_sleep(60)` query before arming a two-second
fail-stop watchdog. This is strong source-backed sequencing evidence, not a
callback from inside `pg_sleep`.

The ambiguous counter was one after recovery and derived `replayCount` was zero
in every run. That proves no replay for the instrumented mutation, not a
generic exactly-once protocol.

For both stream sizes, a host-controlled gate prevented response reads while
two diagnostics samples observed the same blocking `responseBytes` write with
`POLLOUT=false` and unchanged completion counters for at least 300 ms. The
conservative pre-read accepted-wire upper bound was 493,920 bytes for every
8 MiB and 32 MiB trial; after releasing the gate, the same generation drained
the full response. This proves synchronous socket backpressure for this
workload. It does not establish a process-memory bound, effective `SO_SNDBUF`,
or throughput SLA.

Two earlier failed attempts are retained as negative evidence and are not part
of the passing series:

- `final-witness-001-20260811T113240Z` showed that a small first-statement
`CommandComplete` remained buffered until after the sleeping statement, so it
was not a usable pre-hang witness.
- `final-output-witness-006-20260811T115345Z` showed that the first transient
non-writable socket write could advance before the client read. The final
probe therefore keeps an explicit read gate closed and resets its candidate
until one write remains unchanged for at least 300 ms.

See [the architecture and evidence report](../../docs/architecture/android-native-broker-spike.md)
for exact ranges, artifact hashes, and the proven/unproven split.

## Prerequisites

- Android SDK with the API 34 `Pixel_9_API_34_Google_API` arm64 AVD;
- JDK 17, NDK `27.0.12077973`, and CMake `3.22.1`;
- a current Android arm64 `liboliphaunt.so`; and
- prepared mobile runtime resources containing a PostgreSQL 18 template
`PGDATA`.

The defaults are:

```text
target/android-native-broker-spike/native/out/liboliphaunt.so
target/android-native-broker-spike/runtime-resources
```

Override them with `OLIPHAUNT_ANDROID_BROKER_LIBOLIPHAUNT_SO` and
`OLIPHAUNT_ANDROID_BROKER_RUNTIME_RESOURCES_DIR`.

## Run

```sh
bash spikes/android-native-broker/run-emulator.sh
```

The runner:

1. requires the canonical Android native `--check-current` gate;
2. records exact source, APK, and native-library hashes;
3. builds the Debug APK;
4. starts or reuses only the API 34 arm64 AVD;
5. installs and clears the app once;
6. launches the full fixture and rejects stale reports by run nonce; and
7. validates the JSON contract, exact worker crash PIDs, process transitions,
Binder deaths, native-output witness, replay counter, persistence, and
socket-stall arithmetic.

Evidence is written beneath:

```text
target/android-native-broker-spike/runs/<run-nonce>/
```

## Claim boundaries

- The service is a separate private app process, not an Android
`isolatedProcess`; it shares the app UID and private storage.
- Binder death is the explicit process-death signal. Reliable-socket EOF drives
the same interruption path but does not identify why the peer disappeared.
- Recovery requires a different PID and epoch plus healthy SQL.
- Once request bytes may have reached the worker, loss is `outcomeUnknown` and
the host does not retry SQL.
- The native output witness proves ordered PostgreSQL execution immediately
before the sleeping plan child, but not a direct stack observation from
inside `pg_sleep`.
- `pm clear` runs before each complete matrix, never between a fault and its
recovery check.
- PSS/RSS and drain rates are observations, not acceptance limits.
- The retained result covers one API 34 arm64 emulator image. It does not prove
physical-device behavior, other Android/OEM versions, broad reliability,
lifecycle/Doze/LMK behavior, power-loss durability, concurrency,
security isolation, a production watchdog policy, or Release/Play readiness.
46 changes: 46 additions & 0 deletions spikes/android-native-broker/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}

android {
namespace = "dev.oliphaunt.androidbrokerspike"
compileSdk = 36

defaultConfig {
applicationId = "dev.oliphaunt.androidbrokerspike"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "0.1"

testInstrumentationRunner = "android.test.InstrumentationTestRunner"
}

buildFeatures {
aidl = true
buildConfig = true
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

packaging {
jniLibs {
useLegacyPackaging = true
}
}
}

kotlin {
jvmToolchain(17)
}

dependencies {
implementation(project(":oliphaunt"))
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.coroutines.core)
testImplementation(kotlin("test"))
}
21 changes: 21 additions & 0 deletions spikes/android-native-broker/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="false"
android:label="Oliphaunt Android Broker Spike"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<service
android:name=".BrokerService"
android:exported="false"
android:process=":broker" />
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package dev.oliphaunt.androidbrokerspike;

import android.os.Bundle;
import android.os.ParcelFileDescriptor;

/** Minimal experimental control plane. Bulk protocol bytes use dataChannel. */
interface IOliphauntBroker {
Bundle hello(in Bundle request, in ParcelFileDescriptor dataChannel);
Bundle control(in Bundle request);
}
Loading
Loading