OpenVPN-hulpbinair mislukt op Android 10: toestemming geweigerd
Android 10 weigert een hulpbestand uit te voeren dat de app in zijn eigen opslag heeft geschreven. Lees logcat, lever de helper als native bibliotheek en repareer bundle-builds.
De app werkte op Android 9 en sterft op dezelfde telefoon na de update naar Android 10. Je profiel is onaangeroerd, de certificaten zijn hetzelfde, en de crash komt vóór al het verkeer voor verificatie. Logcat toont eerst een null-pointer, en een paar regels daarboven staat Cannot run program ...: error=13, Permission denied. Dat bericht gaat over het uitvoeren van een bestand, niet over het lezen van je configuratie.
Wat fout 13 werkelijk weigert
Android 10, API-niveau 29, heeft apps verhinderd bestanden uit te voeren die ze in hun eigen datamap schreven. De regel richt zich op opslag die eerst beschrijfbaar en daarna uitvoerbaar is, en precies dat maakt een launcher die uit assets kopieert. Oudere builds van de ics-openvpn-kern pakken bij de eerste start een uitvoerbaar bestand openvpn uit de APK en starten het met ProcessBuilder. Het uitpakken lukt nog steeds; de uitvoering is wat wordt geweigerd. Een app die op API 29 richt, erft de regel ook wanneer hij draait op een toestel dat met een oudere versie kwam, waardoor een update een build kan breken die niemand heeft aangeraakt.
Niets aan de weigering is specifiek voor één mapnaam. Een bestand dat de app onder zijn datamap heeft gemaakt, wordt ongeacht het pad als niet-uitvoerbaar behandeld, dus getFilesDir(), getCacheDir() en een eigen submap falen allemaal op dezelfde manier.
Scheid de crash van de helper van een profielprobleem
Lees de log rond de processtart in plaats van van bovenaf te scrollen. De signatuur die je zoekt is de regel error=13, en de stacktrace erboven noemt meestal de launchermethode die ProcessBuilder aanriep:
adb logcat -d | grep -n -i -E "cannot run program|permission denied|startOpenVPN"Als de helper nooit is gestart, zie je helemaal geen OpenVPN-loguitvoer: geen cipher-onderhandeling, geen gepushte routes, geen DNS-configuratie. Als die regels wel verschijnen en de fout komt later, liep het binaire bestand en zit het probleem in het profiel, de server of de routeringslaag. Dat ene onderscheid bespaart veel tijd die anders naar het bewerken van .ovpn-bestanden zou gaan die nooit schuldig waren. Het vertelt je ook welke van de twee oplossingen hieronder je echt nodig hebt.
Controleer ook het API-niveau op het toestel dat je test, niet dat in je buildconfiguratie. Een emulator-image op API 28 reproduceert niets, omdat de beperking pas vanaf 29 bijt. Controleer tegelijk de doelsdk in je buildbestand: het verhogen daarvan is wat de regel binnenhaalt.
adb shell getprop ro.build.version.sdkWaarom het kopiëren van het binaire bestand elders niet helpt
De voor de hand liggende omweg, de helper naar een andere beschrijfbare map schrijven, faalt om dezelfde reden als het origineel. Android staat nog steeds toe dat gedeelde bibliotheken worden uitgevoerd die in de native bibliotheekmap van de app zijn geïnstalleerd, omdat de installer ze verpakt in plaats van dat ze tijdens runtime worden geschreven. Die map is de enige plek waar de launcher op zou moeten wijzen. Alles wat je zelf tijdens runtime uitpakt, hoe je het ook noemt, blijft in de geweigerde categorie.
Externe opslag is erger: die is met noexec aangekoppeld, en een helper die daar staat kan door geen enkele app worden gestart. Appgegevens wissen herstelt het oude gedrag ook niet, omdat de volgende start gewoon dezelfde niet-uitvoerbare kopie opnieuw maakt.
Wijs de launcher op een verpakte bibliotheek
Hernoem de helper naar een naam van een gedeelde bibliotheek en lever hem onder jniLibs voor elke ABI die je ondersteunt, en laat de launcher daarna vanaf API 28 het pad van de geïnstalleerde bibliotheek teruggeven:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return new File(context.getApplicationInfo().nativeLibraryDir,
"libovpnexec.so").getPath();
}De bibliotheek moet bij installatie worden uitgepakt wil dat pad een echt bestand bevatten, dus stel android:extractNativeLibs="true" in op het element <application>. Als een manifest-merger een andere waarde binnenhaalt, voeg dan tools:replace="android:extractNativeLibs" ernaast toe. De referentie voor het application-element documenteert wat het attribuut bestuurt.
Let op een dubbele lokale variabele wanneer je de launcher met de hand patcht. Oudere bronnen declareren nativeAPI later in dezelfde methode, en een tweede declaratie is een compileerfout en geen runtimefout, dus de build faalt snel en de fout is makkelijk te zien. Houd de ABI-lijst in het Gradle-blok in de pas met de mappen die je toevoegt.
Laat het release-artefact bij de testbuild passen
Een debug-APK kan de tunnel opzetten terwijl een release-bundle nog steeds crasht, omdat een bundle native bibliotheken in een andere vorm verpakt en ze gecomprimeerd uit het archief kan aanbieden. Als alleen van bundle afgeleide builds falen, voeg dit dan toe aan gradle.properties en bouw opnieuw:
android.bundle.enableUncompressedNativeLibs=falseBevestig daarna dat het geïnstalleerde pakket de ABI meldt die je verwacht, en herhaal de controle op een 32-bits en een 64-bits toestel als je beide levert. De pakketbeheerder noemt de ABI die hij koos, dus een toestel dat terugviel naar 32 bits verschijnt hier in plaats van in een crashrapport:
adb shell dumpsys package com.example.openvpn | grep -i primaryCpuAbiEen ontbrekende bibliotheek voor één ABI reproduceert de oorspronkelijke toestemmingsfout alleen op dat toestel, en daarom is één geslaagde testtelefoon geen bewijs. Lever de manifestvlag, de bibliotheeknaam en de bundle-instelling samen; één wijzigen en de andere vergeten zet je terug bij dezelfde crash. De blijvende oplossing is overstappen op een onderhouden kernrelease in plaats van de patch voor altijd mee te dragen.