Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
elixir 1.20.0-otp-29
erlang 29.0
erlang 29.1.2
zig 0.17.0-dev.269+ebff43698
6 changes: 0 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,6 @@ Before committing changes, run **all** in this order:
mix test # full suite must pass (call out any pre-existing flake explicitly)
mix format # apply Elixir formatting
mix credo --strict # **whole tree, not just changed files** — includes ExSlop (catches AI-generated patterns: blanket rescue, narrator docs, etc). Pre-existing issues are tracked separately, but new ones (including in tests) must be fixed
mix erlfmt --check priv/android/crypto.erl # Erlang formatting
mix mob.security_scan --strict # surface new CVEs / drift before they ship
```

Expand All @@ -244,11 +243,6 @@ mix reach.check --dead-code # 71 findings (some macro DSL false positives)
mix reach # interactive HTML architecture report
```

Auto-fix:
```bash
mix erlfmt --write priv/android/crypto.erl
```

`mix mob.security_scan` covers Hex deps, Android Gradle deps, iOS
Swift Package deps, the **bundled OpenSSL/OTP/Elixir/SQLite versions**
(via fingerprint of `~/.mob/cache/otp-*-{hash}/` against
Expand Down
92 changes: 76 additions & 16 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,42 @@

### Security

- **Device runtime rebuilt on OTP 29.1.2 and OpenSSL 3.5.9 (`@otp_hash`
`5c9c69fc` → `d199a49a`; MOB-493, MOB-494).** Every app shipped OTP 29.0
(erts 17.0, ssl 11.7, public_key 1.21), which lacks 47 OTP security fixes.
Among them: CVE-2026-89422 and CVE-2026-55953 (critical; TLS clients that
skip or bypass server authentication), CVE-2026-42789 (public_key accepts a
non-CA as an intermediate issuer), CVE-2026-94200 (TLS client ticket-store
poisoning) and CVE-2026-75538 (inet TCP driver overflow from an
unauthenticated peer). The crypto NIF was statically linked against
OpenSSL 3.4.0, which had 55 CVEs fixed in later 3.4.x releases, and the 3.4
branch reached end of life on 2026-10-22. The new runtime is erts 17.1.1,
ssl 11.7.8, public_key 1.21.8 and inets 9.8.1, with OpenSSL 3.5.9 (LTS
until 2030). Elixir 1.20.1 and the bundled exqlite BEAMs are unchanged.
`bundled_versions.exs` records the new bundle, including its OTP tag.
- **Apps run the runtime's ssl, not the developer's (MOB-493).** `mix
mob.deploy` (Android and the iOS simulator) and `mix mob.release --android`
copied the *host's* OTP `ssl` app into the app's own BEAM directory. That
directory comes first on the code path, so it shadowed the runtime's
`lib/ssl-*`: every Android release and dev install ran whatever ssl the
building machine had. With a 29.0 host that is 11.7, which has
CVE-2026-89422 and CVE-2026-55953, whatever runtime the app shipped. The
copy dates from runtimes without ssl; every current runtime ships it.
Deploys now also delete those files from a device that an older mob_dev
left them on. Verified on a Moto G: before, `code:which(ssl)` was
`files/otp/<app>/ssl.beam` (11.7); after, `lib/ssl-11.7.8`. On iOS, the
dev build (simulator and phone) no longer compiles no-op `crypto` and
`ssl` shims for LiveView apps. The crypto shim (`supports/1 -> []`,
`verify/5 -> true`) shadowed the real crypto every iOS app links, so
`ssl:connect/4` raised; the phone-only ssl shim returned
`ssl_not_supported`. `mix mob.release --ios` reused that directory and
now deletes both shims from it. The iOS build and release no longer copy
the host's runtime_tools, asn1 and public_key over the runtime's. The
Android crypto-shim fallbacks in the deployer and in `mix mob.release
--android` (`priv/android/crypto.erl`), dead since every runtime ships
crypto, are gone. The fallback EPMD patch now checks the `run_daemon(g)`
call site rather than any `#ifndef NO_DAEMON`, which upstream `epmd.c`
already has.
- **Dev server: bandit 1.12.5 → 1.13.1 (MOB-482).** Fixes EEF-CVE-2026-105659
(a client trickling HTTP/2 `WINDOW_UPDATE` frames could pin stream processes
indefinitely) and EEF-CVE-2026-105660 (an HTTP/1 absolute-form request
Expand Down Expand Up @@ -41,6 +77,29 @@
a failed boot on an app generated before mob_new added them shows a blank
screen. Detected, not patched: `MobBridge.kt` is app-owned.

### Upgrading

- **Needs mob ≥ 0.9.25.** The new runtime's ERTS directory is `erts-17.1.1`.
Older mob hard-codes `erts-17.0` in the Android launcher and in the
iOS-simulator build, so an app on mob ≤ 0.9.24 crashes at boot with this
runtime. Physical-iOS and release builds already read the version from the
tarball. Update both: `mix deps.update mob mob_dev`, then
`mix mob.deploy --native`.
- Host `.tool-versions` moves to erlang 29.1.2 (mob_dev, mob, mob_new and
generated apps).
- **MOB-495.** `mix mob.security_scan --strict` and `mix mob.release
--security-gate` now fail on a `5c9c69fc` runtime (critical ssl advisories
CVE-2026-89422 and CVE-2026-55953, OpenSSL 3.4 end of life). The
`d199a49a` runtime in this release passes them; delete the old
`~/.mob/cache/otp-*-5c9c69fc` dirs once no app on this machine still uses
that runtime.
- **SQLite CVEs.** A project on exqlite 0.36.x (SQLite 3.51.3) now gets
CVE-2026-11822, CVE-2026-11824 (fixed in 3.53.2) and CVE-2025-70873 (fixed
in 3.52.0), which fail `--strict` and `--security-gate`. Matching is by
version only, so CVE-2025-70873 is reported although it is in the zipfile
extension, which exqlite doesn't compile. exqlite 0.37.0 bundles SQLite
3.53.2 and clears all three.

### Changed

- **`mix mob.security_scan` reports CVEs in the bundled OTP and OpenSSL
Expand All @@ -61,7 +120,23 @@
passes, instead of only "security scan clean".
- `mix mob.doctor`'s JDK rows no longer name AGP 8.2.0, which new apps no
longer use.

- **The release scripts rebuild the runtime from a clean OTP tag.** The
Android builds and both iOS builds now install vendored xcomp confs from
`scripts/release/xcomp/`, as x86_64 already did:
- arm64 and arm32 Android: `llvm-ar`/`llvm-ranlib` (upstream's stubs link
an empty `libei.a` on macOS), 16 KB page alignment, `-Os` with
gc-sections, and no debugger/observer/et/cdv. arm32 also passes
`--disable-year2038`, because 32-bit bionic's `time_t` can't widen.
- iOS: `DED_LD` goes through the cc driver, because OTP 29.1's hardening
flags break raw `ld`.

The Android scripts also build the static `asn1rt_nif.a`, which `boot`
only emits with `--enable-static-nifs`. The iOS `{spawn, LinkedInDriver}`
erts fix, which was hand-applied to earlier builds, is now
`scripts/release/patches/0003-ios-spawn-linked-in-driver.patch`, applied by
both iOS xcompile scripts. The device script's check for patch 0002 (EPMD
`NO_DAEMON`) looked for `#ifndef NO_DAEMON`, which upstream `epmd.c` already
contains, so it skipped the patch; it now looks for the patch's own comment.
- **`mix mob.security_scan` reports CVEs in the project's SQLite.** SQLite is
compiled per app from `deps/exqlite/c_src/sqlite3.c`, and the
`bundled_runtime` layer only printed its version beside a link to
Expand All @@ -73,21 +148,6 @@
no OTP tarballs cached. See
`decisions/2026-10-22-sqlite-cves-from-sqlite-org.md`.

### Upgrading

- **MOB-495.** With the current `5c9c69fc` runtime cached, `mix
mob.security_scan --strict` and `mix mob.release --security-gate` now fail
(critical ssl advisories CVE-2026-89422 and CVE-2026-55953, OpenSSL 3.4 end
of life). They pass again once mob_dev ships a runtime on patched OTP and
OpenSSL; until then `--skip bundled_runtime` skips the layer.

- **SQLite CVEs.** A project on exqlite 0.36.x (SQLite 3.51.3) now gets
CVE-2026-11822, CVE-2026-11824 (fixed in 3.53.2) and CVE-2025-70873 (fixed
in 3.52.0), which fail `--strict` and `--security-gate`. Matching is by
version only, so CVE-2025-70873 is reported although it is in the zipfile
extension, which exqlite doesn't compile. exqlite 0.37.0 bundles SQLite
3.53.2 and clears all three.

## [0.7.29] - 2026-10-15

### Fixed
Expand Down
47 changes: 21 additions & 26 deletions build_release.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,31 +55,16 @@ Built from a full cross-compiled OTP release for `arm-unknown-linux-androideabi`
(armeabi-v7a). Same structure as arm64. Required for 32-bit-only devices (e.g.
Motorola E 2020).

`asn1rt_nif.a` must be compiled separately — it is not emitted by the OTP build
system for arm32. Build it with:
`asn1rt_nif.a`: OTP only emits the static asn1 NIF when configured with
`--enable-static-nifs`, which the Android builds don't use, so the
Android scripts build it with OTP's own target after `boot`:

```bash
NDK=~/Library/Android/sdk/ndk/27.2.12479018/toolchains/llvm/prebuilt/darwin-x86_64/bin
OTP_SRC=~/code/otp

$NDK/armv7a-linux-androideabi21-clang \
-march=armv7-a -mfloat-abi=softfp -mthumb \
-fvisibility=hidden -fno-common -fno-strict-aliasing \
-fstack-protector-strong -O2 \
-I "$OTP_SRC/erts/arm-unknown-linux-androideabi" \
-I "$OTP_SRC/erts/include/arm-unknown-linux-androideabi" \
-I "$OTP_SRC/erts/emulator/beam" \
-I "$OTP_SRC/erts/include" \
-DHAVE_CONFIG_H \
-DSTATIC_ERLANG_NIF_LIBNAME=asn1rt_nif \
-c "$OTP_SRC/lib/asn1/c_src/asn1_erl_nif.c" \
-o /tmp/asn1rt_nif_arm32.o

$NDK/llvm-ar rc /tmp/asn1rt_nif_arm32.a /tmp/asn1rt_nif_arm32.o
$NDK/llvm-ranlib /tmp/asn1rt_nif_arm32.a
ERL_TOP=~/code/otp make -C ~/code/otp/lib/asn1/c_src TARGET=arm-unknown-linux-androideabi static_lib
```

Then include it in the tarball at `erts-<vsn>/lib/asn1rt_nif.a` (see Step 2b).
(`xcompile_android_arm32.sh` does this; arm64 and x86_64 likewise.) The
tarball script picks it up from `lib/asn1/priv/lib/<triple>/asn1rt_nif.a`.

### iOS device (`otp-ios-device-<hash>.tar.gz`)

Expand Down Expand Up @@ -127,8 +112,17 @@ Built from a cross-compiled OTP for `aarch64-apple-iossimulator`. Needs:

## Prerequisites

- A cross-compiled OTP build. The OTP source tree at `~/code/otp` (commit `7721ab74`)
has iOS simulator and Android targets already compiled.
- The OTP source at `~/code/otp`, checked out at an **upstream release tag**
(`otp-d199a49a` was built from `OTP-29.1.2`):
`git clone --depth 1 --branch OTP-29.1.2 https://github.com/erlang/otp.git ~/code/otp`.
Take the newest patch release of the current major; the security fixes only
land there (29.0.x stopped getting them once 29.1 shipped).
- OpenSSL at `~/code/openssl` — see [`scripts/release/openssl/README.md`](scripts/release/openssl/README.md)
for the pinned tag. Build all targets with the `openssl/*.sh` scripts first.
- The mob patches in [`scripts/release/patches/`](scripts/release/patches/) are applied by the
iOS xcompile scripts (0001/0002 device-only fork guards, 0003 the
`{spawn, LinkedInDriver}` fix for both iOS targets). Each one must still apply
to the new tag; if a patch fails, port it before building.
- `gh` CLI authenticated to the `GenericJam` GitHub account.

---
Expand All @@ -137,10 +131,11 @@ Built from a cross-compiled OTP for `aarch64-apple-iossimulator`. Needs:

```bash
cd ~/code/otp
git rev-parse --short HEAD # e.g. 7721ab74
git rev-parse --short=8 HEAD # e.g. d199a49a for OTP-29.1.2
```

Use this hash everywhere below as `<hash>`.
Use this hash everywhere below as `<hash>`, and record the tag in the
`bundled_versions.exs` entry (`otp_version`) so the source is recoverable.

---

Expand Down Expand Up @@ -226,7 +221,7 @@ ERTS_LIB32="$STAGE32/erts-16.3/lib"
cp "$OTP_SRC/erts/emulator/zstd/obj/arm-unknown-linux-androideabi/opt/libzstd.a" "$ERTS_LIB32/"
cp "$OTP_SRC/erts/emulator/pcre/obj/arm-unknown-linux-androideabi/opt/libepcre.a" "$ERTS_LIB32/"
cp "$OTP_SRC/erts/emulator/ryu/obj/arm-unknown-linux-androideabi/opt/libryu.a" "$ERTS_LIB32/"
cp /tmp/asn1rt_nif_arm32.a "$ERTS_LIB32/asn1rt_nif.a"
cp "$OTP_SRC/lib/asn1/priv/lib/arm-unknown-linux-androideabi/asn1rt_nif.a" "$ERTS_LIB32/asn1rt_nif.a"

ERTS_INC32="$STAGE32/erts-16.3/include"
mkdir -p "$ERTS_INC32"
Expand Down
3 changes: 2 additions & 1 deletion decisions/2026-05-25-real-crypto-ssl-on-device.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Ship real OpenSSL crypto + ssl on device, not md5/no-op shims

- Date: 2026-05-25
- Status: accepted
- Status: accepted; the Android stub fallback and the iOS LiveView crypto shim
were removed by 2026-10-22-otp-29.1.2-runtime-no-host-otp-apps.md

## Context
Early device OTP runtimes were built `--without-ssl`, so the release scripts
Expand Down
77 changes: 77 additions & 0 deletions decisions/2026-10-22-otp-29.1.2-runtime-no-host-otp-apps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Runtime rebuilt on OTP 29.1.2 / OpenSSL 3.5.9; no host OTP apps on the device

- Date: 2026-10-22
- Status: accepted
- Issues: MOB-493, MOB-494

## Context

The `otp-5c9c69fc` runtime was OTP 29.0 with OpenSSL 3.4.0. It lacked 47 OTP
security fixes (two of them critical TLS client bugs), and OpenSSL 3.4.0
lacked 55 CVEs, with the 3.4 branch going end-of-life on 2026-10-22. The
29.0.x line no longer gets patches, so the fix is the newest 29.x (29.1.2)
with OpenSSL on the 3.5 LTS branch.

While verifying the new runtime on a phone, `code:which(ssl)` turned out to
point at `files/otp/<app>/ssl.beam` instead of the runtime's
`lib/ssl-11.7.8`. Three places copied the **host's** ssl app into the app's
flat BEAM directory: `Deployer.collect_beam_dirs/0`,
`ReleaseAndroid.collect_beam_dirs/0` and the iOS-sim native build. That
directory comes first on `-pa`. The copy was added when the runtimes had no
ssl; every runtime since the crypto-enabled tarballs ships one. So every
Android release and dev install ran the building machine's ssl, and a runtime
upgrade alone fixed nothing in TLS. Review found two iOS siblings:

- The iOS dev build (simulator and phone) compiled no-op shims into the beams
dir for LiveView apps: an `ssl` (phone only) that returned
`ssl_not_supported`, and a `crypto` with `supports/1 -> []` and
`verify/5 -> true`. That crypto shadowed the real one every iOS app links,
so `ssl:connect/4` raised in `ssl_config`. `mix mob.release --ios` reuses
that dir. The Android crypto-shim fallbacks (deployer and `mix mob.release
--android`) were already dead, since every runtime ships crypto.
- That build and the iOS release script also copied the host's
runtime_tools, asn1 and public_key into `lib/<host-vsn>`. The code server
takes the highest version, so a newer host could replace the runtime's
patched public_key.

## Decision

1. Publish a **new** hash, `otp-d199a49a` (the commit of the OTP-29.1.2 tag),
rather than clobbering `5c9c69fc`, as in
2026-06-17-otp-hash-elixir-1.20.1.md. erts moves to 17.1.1.
2. Rebuild from a pristine upstream tag, with every build input in this repo:
- vendored xcomp confs for all five targets;
- patches 0001–0003. 0003, the iOS `{spawn, LinkedInDriver}` fix, had
only ever been applied by hand. 0002's marker was wrong: upstream
already contains `#ifndef NO_DAEMON`, so the patch was skipped.
- the static `asn1rt_nif.a` via OTP's `static_lib` target;
- `--disable-year2038` on arm32;
- the iOS xmerl deps regeneration.

The `5c9c69fc` tarballs could not have been reproduced from this repo
alone.
3. Never put host OTP apps, or shims of OTP apps, on the device. Only the
app, its deps and EEx (Elixir stdlib) are pushed or copied. Everything
else comes from the runtime, which ships ssl, public_key, asn1,
runtime_tools and crypto on every target.
- Deploys and builds delete the host-ssl and shim files an older mob_dev
left behind, because a deploy otherwise only adds files.
- `patch_epmd_source/1` now checks for "already patched" at the
`run_daemon(g)` call site, for the same reason patch 0002's marker had
to change.
4. Require `mob ~> 0.9.25`. Older mob hard-codes `erts-17.0` in the Android
launcher, and in the iOS-sim fallback used when `-DERTS_VSN` isn't
passed, so it can't boot erts-17.1.1. mob 0.9.25 reads the directory at
runtime. As a Hex requirement it keeps an app's mob and mob_dev in step
without a doctor check.

## Consequences

- Moving to the new runtime means updating both deps and doing a `--native`
deploy. Apps pinned to an older mob_dev keep `5c9c69fc`.
- The host's OTP version no longer affects the device's ssl, public_key or
asn1. That also removes a source of host/device version skew, e.g. ssl from
a 28.x host running on a 29 runtime.
- OpenSSL 3.5.x point releases and OTP 29.x patch releases are now a rebuild
from this repo plus a manifest bump. MOB-495's scan reports when one is
needed.
40 changes: 10 additions & 30 deletions guides/publishing_to_google_play.md
Original file line number Diff line number Diff line change
Expand Up @@ -632,36 +632,16 @@ step 5 => {'EXIT',{{badmatch,{error,{crypto,{"no such file or directory","crypto
The BEAM started, but application boot failed when starting `:ecto_sqlite3` → `:ecto`
→ `:crypto`.

**Root cause**: The Mob pre-built Android OTP release does not include the `:crypto`
OTP application. Cross-compiling OpenSSL for Android was not part of the initial OTP
build. Many common deps (ecto, phoenix_pubsub, plug_crypto, phoenix) declare
`:crypto` in their `{applications, [...]}` list in their `.app` files. When
`Application.ensure_all_started` walks the dep tree, the OTP application controller
tries to load `crypto.app` and fails — even if `crypto.beam` were present, the
controller requires the `.app` spec to register the application before starting it.

**Fix**: `mix mob.release --android` (via `MobDev.ReleaseAndroid`) now handles this
automatically in two steps during staging:

1. **Patch all `.app` files** (`patch_crypto_deps!/1`): Walks every `*.app` file in
the staging tree and removes `:crypto` from each `{applications, [...]}` list.
This prevents `ensure_all_started` from even trying to start `:crypto` as a dep.

2. **Inject a crypto stub** (`add_crypto_stub!/2`): Compiles
`mob_dev/priv/android/crypto.erl` — a minimal module that implements only
`crypto:strong_rand_bytes/1` via `:rand` (the only function Ecto calls at runtime,
for UUID generation). Also writes a `crypto.app` spec with no `{mod, ...}` entry,
so the app controller can load and "start" `:crypto` without invoking any NIF
initialization.

This is transparent as of the version where these functions were added. If you are on
an older `mob_dev` that doesn't have `patch_crypto_deps!/1`, upgrade `mob_dev` or
check `release_android.ex` for the current staging pipeline.

**Note on cryptographic strength**: The stub uses `:rand` seeded at BEAM start —
not a cryptographically secure RNG. This is acceptable for a local-only mobile app
that has no TLS or encryption use cases. If your app does require real crypto, you
will need to cross-compile OpenSSL and include the real `:crypto` NIF.
**Root cause**: The release's OTP tree has no `:crypto` application. Many common deps
(ecto, phoenix_pubsub, plug_crypto, phoenix) list `:crypto` in their `.app` files, so
`Application.ensure_all_started` fails without it. Early Mob Android runtimes were
built without OpenSSL and `mix mob.release --android` stubbed `:crypto` for them.
Every current runtime ships the real `crypto` application, and the native build links
its OpenSSL NIF (`crypto.a` + `libcrypto.a`), so the stub is gone (MOB-493).

**Fix**: Upgrade `mob_dev`, delete the cached runtime (`~/.mob/cache/otp-android-*`)
so it is downloaded again, and rerun `mix mob.release --android`. Check that the
staged tree contains `lib/crypto-*/ebin/crypto.app`.

### App crashes immediately on launch (before any UI appears)

Expand Down
2 changes: 1 addition & 1 deletion guides/python_embedding.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ doesn't lock Mob into wheel ecosystem maintenance.

**App crashes silently on launch with no Python output.** First check
for OTP version mismatch — your local Erlang must match the device
runtime's ERTS (currently OTP 29 / erts-17.0). `mise` reads
runtime's ERTS (currently OTP 29.1.2 / erts-17.1.1). `mise` reads
`.tool-versions`; if `mise current` disagrees with `mise exec -- erl`,
your shell hasn't picked up the project's pinned version.

Expand Down
2 changes: 1 addition & 1 deletion lib/mix/tasks/mob.doctor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ defmodule Mix.Tasks.Mob.Doctor do
{:fail, "OTP", "#{otp} — OTP #{@min_otp} or later required", elixir_upgrade_hint()}

n < @warn_otp ->
{:warn, "OTP", "#{otp} — OTP #{@warn_otp}+ recommended (device runtime is OTP 28)",
{:warn, "OTP", "#{otp} — OTP #{@warn_otp}+ recommended (device runtime is OTP 29)",
elixir_upgrade_hint()}

true ->
Expand Down
Loading
Loading