Die App lief unter Android 9 und stirbt auf demselben Telefon nach dem Update auf Android 10. Dein Profil ist unangetastet, die Zertifikate sind dieselben, und der Absturz kommt vor jeglichem Authentifizierungsverkehr. Logcat zeigt zuerst einen Nullzeiger, und ein paar Zeilen darüber steht Cannot run program ...: error=13, Permission denied. Diese Meldung betrifft das Ausführen einer Datei, nicht das Lesen deiner Konfiguration.

Was Fehler 13 wirklich verweigert

Android 10, API-Level 29, hat Apps daran gehindert, Dateien auszuführen, die sie in ihr eigenes Datenverzeichnis geschrieben haben. Die Regel zielt auf Speicher, der erst beschreibbar und dann ausführbar ist, und genau das erzeugt ein Launcher, der aus den Assets herauskopiert. Ältere Builds des ics-openvpn-Kerns entpacken beim ersten Start eine ausführbare Datei openvpn aus dem APK und starten sie mit ProcessBuilder. Das Entpacken gelingt weiterhin; verweigert wird die Ausführung. Eine App, die auf API 29 zielt, erbt die Regel auch dann, wenn sie auf einem Gerät läuft, das mit einer älteren Version ausgeliefert wurde, weshalb ein Update einen Build brechen kann, den niemand angefasst hat.

Nichts an der Verweigerung ist auf einen bestimmten Ordnernamen beschränkt. Eine Datei, die die App unter ihrem Datenverzeichnis angelegt hat, gilt unabhängig vom Pfad als nicht ausführbar, also scheitern getFilesDir(), getCacheDir() und ein eigenes Unterverzeichnis alle auf dieselbe Weise.

Trenne den Helfer-Absturz von einem Profilproblem

Lies das Log rund um den Prozessstart, statt von oben zu scrollen. Die Signatur, nach der du suchst, ist die Zeile error=13, und der Stacktrace darüber nennt meist die Launcher-Methode, die ProcessBuilder aufgerufen hat:

adb logcat -d | grep -n -i -E "cannot run program|permission denied|startOpenVPN"

Wenn der Helfer nie gestartet ist, siehst du überhaupt keine OpenVPN-Logausgabe: keine Cipher-Aushandlung, keine gepushten Routen, keine DNS-Konfiguration. Wenn diese Zeilen doch erscheinen und der Fehler später kommt, lief die Binärdatei, und das Problem liegt im Profil, im Server oder in der Routingschicht. Diese eine Unterscheidung spart viel Zeit, die sonst für das Bearbeiten von .ovpn-Dateien draufginge, die nie schuld waren. Sie sagt dir außerdem, welche der beiden Korrekturen unten du wirklich brauchst.

Prüfe außerdem das API-Level auf dem Gerät, das du testest, nicht das in deiner Build-Konfiguration. Ein Emulator-Image auf API 28 reproduziert nichts, weil die Einschränkung erst ab 29 greift. Prüfe gleichzeitig das Ziel-SDK in deiner Build-Datei: dessen Anhebung ist es, die die Regel hereinholt.

adb shell getprop ro.build.version.sdk

Warum das Kopieren der Binärdatei woandershin nicht hilft

Der naheliegende Workaround, den Helfer in ein anderes beschreibbares Verzeichnis zu schreiben, scheitert aus demselben Grund wie das Original. Android erlaubt weiterhin die Ausführung von Shared Libraries, die in das native Bibliotheksverzeichnis der App installiert wurden, weil diese vom Installer gepackt und nicht zur Laufzeit geschrieben werden. Dieses Verzeichnis ist der eine Ort, auf den der Launcher zeigen sollte. Alles, was du zur Laufzeit selbst entpackst, wie auch immer du es nennst, bleibt in der verweigerten Kategorie.

Externer Speicher ist schlimmer: er ist mit noexec eingebunden, und ein dort abgelegter Helfer kann von keiner App gestartet werden. Auch das Löschen der App-Daten stellt das alte Verhalten nicht wieder her, denn der nächste Start legt einfach dieselbe nicht ausführbare Kopie neu an.

Richte den Launcher auf eine mitgelieferte Bibliothek

Benenne den Helfer in einen Shared-Library-Namen um und liefere ihn unter jniLibs für jede ABI aus, die du unterstützt, und lass den Launcher dann ab API 28 den Pfad der installierten Bibliothek zurückgeben:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    return new File(context.getApplicationInfo().nativeLibraryDir,
                    "libovpnexec.so").getPath();
}

Die Bibliothek muss bei der Installation entpackt werden, damit dieser Pfad eine echte Datei enthält, also setze android:extractNativeLibs="true" auf dem Element <application>. Zieht ein Manifest-Merger einen anderen Wert ein, füge tools:replace="android:extractNativeLibs" daneben hinzu. Die Referenz zum application-Element dokumentiert, was das Attribut steuert.

Achte auf eine doppelte lokale Variable, wenn du den Launcher von Hand patcht. Ältere Quellen deklarieren nativeAPI später in derselben Methode, und eine zweite Deklaration ist ein Compile-Fehler und kein Laufzeitfehler, sodass der Build schnell scheitert und der Fehler leicht zu sehen ist. Halte die ABI-Liste im Gradle-Block mit den Ordnern synchron, die du hinzufügst.

Lass das Release-Artefakt zum Test-Build passen

Eine Debug-APK kann den Tunnel aufbauen, während ein Release-Bundle weiterhin abstürzt, weil ein Bundle native Bibliotheken in anderer Form packt und sie komprimiert aus dem Archiv ausliefern kann. Wenn nur Bundle-abgeleitete Builds scheitern, füge dies in gradle.properties hinzu und baue neu:

android.bundle.enableUncompressedNativeLibs=false

Bestätige dann, dass das installierte Paket die ABI meldet, die du erwartest, und wiederhole die Prüfung auf einem 32-Bit- und einem 64-Bit-Gerät, wenn du beide auslieferst. Der Paketmanager nennt die ABI, die er gewählt hat, also taucht ein Gerät, das auf 32 Bit zurückgefallen ist, hier auf statt in einem Absturzbericht:

adb shell dumpsys package com.example.openvpn | grep -i primaryCpuAbi

Eine fehlende Bibliothek für eine ABI reproduziert den ursprünglichen Berechtigungsfehler allein auf diesem Gerät, weshalb ein einziges erfolgreiches Testtelefon kein Beweis ist. Liefere Manifest-Flag, Bibliotheksnamen und Bundle-Einstellung zusammen aus; änderst du eines und vergisst die anderen, landest du wieder beim selben Absturz. Die dauerhafte Lösung ist der Wechsel auf eine gepflegte Kernversion, statt den Patch ewig mitzuschleppen.

Ähnliche Artikel