From 538626351be5291599908f136d6b06bd5d37961d Mon Sep 17 00:00:00 2001 From: GenericJam Date: Tue, 13 Oct 2026 16:48:24 -0600 Subject: [PATCH 1/2] Relabel otp/ before the Android run-as ERTS probe (MOB-478) After an APK reinstall on a rooted emulator the app runs under new SELinux MCS categories, but the otp/ tree mix mob.deploy --native pushed as root kept the previous install's. The run-as ERTS probe couldn't read it and the deploy failed with "OTP runtime missing on device" before its only relabel, which ran last. Relabel before the probe too; the final relabel after every write stays. --- AGENTS.md | 13 ++- CHANGELOG.md | 10 ++ lib/mob_dev/deployer.ex | 6 ++ .../mob_dev/deployer_android_relabel_test.exs | 92 +++++++++++++++++++ 4 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 test/mob_dev/deployer_android_relabel_test.exs diff --git a/AGENTS.md b/AGENTS.md index 990a9d6..dadf43f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -454,12 +454,15 @@ Real fix: `sim_runtime_dir/0` should detect `ios/build.zig` and use schemes set, the launcher must be `singleTask`/`singleInstance` (enforced, never rewritten; mob_new's template is `singleTop`). See `decisions/2026-10-03-url-schemes.md`. -- **Android deploys relabel `otp/` once, last.** Anything written as root - (`adb root` push, `ln -s`) keeps root's SELinux categories until +- **Android deploys relabel `otp/` last (and also first).** Anything written + as root (`adb root` push, `ln -s`) keeps root's SELinux categories until `relabel_otp_android/1` in `deploy_android/3` runs. Add new device writes - before that call, never after: the dist (hot) path doesn't restart, so there - is no later relabel, and the app fails on its next launch (P11: exqlite's - NIF symlink, `dlopen ... not found`). + before the final call, never after: the dist (hot) path doesn't restart, so + there is no later relabel, and the app fails on its next launch (P11: + exqlite's NIF symlink, `dlopen ... not found`). The first call, before the + `run-as` ERTS probe, exists because after an APK reinstall the root-pushed + tree can carry the previous install's categories, which `run-as` can't read + (MOB-478). - **Physical iOS BEAM overrides must be exact and self-verifying.** The app prefers `Documents/otp/` over its complete signed bundle. Replace that directory rather than incrementally merging it, require `.beam` before diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d3bd13..537195f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,16 @@ collision and fails the build. See `decisions/2026-10-13-style-packages-bundle-fonts.md`. +### Fixed + +- **`mix mob.deploy --native --android` works again after the app is + reinstalled on a rooted emulator (MOB-478).** The reinstalled app runs + under new SELinux MCS categories, but the OTP tree the deploy pushed as + root kept the previous install's, so the `run-as` check for ERTS couldn't + read it and the deploy failed with "OTP runtime missing on device". The + deploy now relabels `otp/` before that check too, not only after the last + write. Without root the relabel is a no-op, as before. + ## [0.7.24] - 2026-10-13 ### Fixed diff --git a/lib/mob_dev/deployer.ex b/lib/mob_dev/deployer.ex index a8d5b14..aeda737 100644 --- a/lib/mob_dev/deployer.ex +++ b/lib/mob_dev/deployer.ex @@ -269,6 +269,12 @@ defmodule MobDev.Deployer do {:skipped, "#{pkg} not installed on #{device.name || serial} (ABI mismatch or app not built for this platform)"} else + # Also before the run-as probe below. After an APK reinstall the app + # runs under new MCS categories, but the otp/ tree `--native` pushed as + # root can carry the previous install's; run-as can't read it, the probe + # reports ERTS missing, and the deploy stopped before the relabel below. + relabel_otp_android(serial) + case ensure_erts_on_device(serial, pkg) do :ok -> case push_beams_android(serial, beam_dirs) do diff --git a/test/mob_dev/deployer_android_relabel_test.exs b/test/mob_dev/deployer_android_relabel_test.exs new file mode 100644 index 0000000..44a9143 --- /dev/null +++ b/test/mob_dev/deployer_android_relabel_test.exs @@ -0,0 +1,92 @@ +defmodule MobDev.DeployerAndroidRelabelTest do + @moduledoc """ + An Android deploy relabels `otp/` before the `run-as` ERTS probe, not only + after the last write (MOB-478). After an APK reinstall on a rooted emulator, + the OTP tree `--native` pushed as root carried the previous install's MCS + categories; `run-as` couldn't read it, the probe reported ERTS missing, and + the deploy aborted before its only relabel. + + Runs `Deployer.deploy_all/1` against a fake `adb` on PATH that behaves like + that device: the probe sees nothing until a `chcon` has run. + + async: false — PATH is process-global. + """ + use ExUnit.Case, async: false + + alias MobDev.{Deployer, Device} + + @moduletag :tmp_dir + + # The probe's failure output is what the emulator printed on 2026-10-13 + # (Scene3d_arm, android-35 google_apis userdebug): the unreadable erts-* + # dir doesn't match the glob, so ls is handed the glob itself. + @fake_adb """ + #!/bin/sh + set -f + printf '%s\\n' "$*" >> "$FAKE_ADB_DIR/calls" + case "$*" in + *"pm list packages "*) for a; do last=$a; done; echo "package:$last" ;; + *" root") echo "adbd is already running as root" ;; + *"id -u") echo 0 ;; + *chcon*) : > "$FAKE_ADB_DIR/relabeled" ;; + *"sh -c 'ls "*) + [ -e "$FAKE_ADB_DIR/relabeled" ] && exit 0 + cmd=$* + globs=${cmd#*\\'ls } + for g in ${globs%\\'}; do echo "ls: $g: No such file or directory"; done + exit 1 ;; + esac + exit 0 + """ + + setup %{tmp_dir: tmp} do + bin = Path.join(tmp, "bin") + File.mkdir_p!(bin) + adb = Path.join(bin, "adb") + File.write!(adb, @fake_adb) + File.chmod!(adb, 0o755) + + path = System.get_env("PATH") + System.put_env("PATH", bin <> ":" <> path) + System.put_env("FAKE_ADB_DIR", tmp) + + on_exit(fn -> + System.put_env("PATH", path) + System.delete_env("FAKE_ADB_DIR") + end) + + :ok + end + + test "a reinstalled app's stale otp/ labels are fixed before the ERTS probe, and again last", + %{tmp_dir: tmp} do + emulator = %Device{ + name: "emu", + serial: "emulator-5620", + platform: :android, + type: :emulator + } + + ExUnit.CaptureIO.capture_io(fn -> + assert {[_], [], []} = + Deployer.deploy_all( + devices: [emulator], + platforms: [:android], + restart: false, + force_fs: true + ) + end) + + calls = tmp |> Path.join("calls") |> File.read!() |> String.split("\n", trim: true) + probe = Enum.find_index(calls, &(&1 =~ "sh -c 'ls ")) + assert is_integer(probe), "the run-as ERTS probe never ran" + + assert [first_relabel, last_relabel] = + for({call, i} <- Enum.with_index(calls), call =~ "chcon", do: i) + + assert first_relabel < probe + # Nothing written after the final relabel: a root write after it would + # keep root's categories (the exqlite NIF symlink, P11). + assert last_relabel == length(calls) - 1 + end +end From faa2b6917b691a2056e4e97f533b1ea250a23e62 Mon Sep 17 00:00:00 2001 From: GenericJam Date: Tue, 13 Oct 2026 16:54:07 -0600 Subject: [PATCH 2/2] CHANGELOG: describe MOB-478's stale labels accurately --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 537195f..da283c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,10 +22,10 @@ - **`mix mob.deploy --native --android` works again after the app is reinstalled on a rooted emulator (MOB-478).** The reinstalled app runs under new SELinux MCS categories, but the OTP tree the deploy pushed as - root kept the previous install's, so the `run-as` check for ERTS couldn't - read it and the deploy failed with "OTP runtime missing on device". The - deploy now relabels `otp/` before that check too, not only after the last - write. Without root the relabel is a no-op, as before. + root came out with the previous install's, so the `run-as` check for ERTS + couldn't read it and the deploy failed with "OTP runtime missing on + device". The deploy now relabels `otp/` before that check too, not only + after the last write. Without root the relabel is a no-op, as before. ## [0.7.24] - 2026-10-13