Binarul auxiliar OpenVPN eșuează pe Android 10: permisiune refuzată
Android 10 refuză să execute un auxiliar pe care aplicația l-a scris în propriul spațiu de stocare. Citește logcat, livrează auxiliarul ca bibliotecă nativă și repară buildurile bundle.
Aplicația funcționa pe Android 9 și moare pe același telefon după actualizarea la Android 10. Profilul tău este neatins, certificatele sunt aceleași, iar prăbușirea vine înainte de orice trafic de autentificare. Logcat arată mai întâi un indicator nul, iar cu câteva linii mai sus se află Cannot run program ...: error=13, Permission denied. Acel mesaj este despre executarea unui fișier, nu despre citirea configurației tale.
Ce refuză de fapt eroarea 13
Android 10, nivelul API 29, a oprit aplicațiile să execute fișiere pe care le scriau în propriul director de date. Regula vizează stocarea care este mai întâi scriibilă și apoi executabilă, iar exact asta creează un launcher care copiază din assets. Buildurile mai vechi ale nucleului ics-openvpn dezarhivează la prima rulare un executabil openvpn din APK și îl pornesc cu ProcessBuilder. Dezarhivarea reușește în continuare; ceea ce este refuzat este execuția. O aplicație care țintește API 29 moștenește regula chiar și când rulează pe un dispozitiv livrat cu o versiune mai veche, de aceea o actualizare poate strica un build pe care nimeni nu l-a atins.
Nimic din refuz nu este specific unui singur nume de folder. Un fișier creat de aplicație sub directorul ei de date este tratat ca neexecutabil indiferent de cale, așa că getFilesDir(), getCacheDir() și un subdirector propriu eșuează toate la fel.
Separă prăbușirea auxiliarului de o problemă de profil
Citește jurnalul din jurul pornirii procesului în loc să derulezi de la început. Semnătura pe care o cauți este linia error=13, iar urma de stivă de deasupra numește de obicei metoda launcherului care a apelat ProcessBuilder:
adb logcat -d | grep -n -i -E "cannot run program|permission denied|startOpenVPN"Dacă auxiliarul nu a pornit niciodată, nu vei vedea nicio ieșire de jurnal OpenVPN: nici negociere de cifrare, nici rute împinse, nici configurare DNS. Dacă acele linii apar totuși și eșecul vine mai târziu, binarul a rulat, iar problema este în profil, în server sau în stratul de rutare. Acea singură distincție economisește mult timp care altfel s-ar fi dus pe editarea fișierelor .ovpn care nu au fost niciodată vinovate. Îți spune și care dintre cele două remedii de mai jos îți trebuie cu adevărat.
Verifică și nivelul API pe dispozitivul pe care îl testezi, nu pe cel din configurația de build. O imagine de emulator pe API 28 nu reproduce nimic, pentru că restricția mușcă doar de la 29 în sus. Verifică în același timp SDK-ul țintă din fișierul de build: ridicarea lui este cea care aduce regula.
adb shell getprop ro.build.version.sdkDe ce copierea binarului altundeva nu ajută
Soluția evidentă, scrierea auxiliarului într-un alt director scriibil, eșuează din același motiv din care a eșuat originalul. Android permite în continuare execuția bibliotecilor partajate instalate în directorul de biblioteci native al aplicației, pentru că instalatorul le împachetează, iar ele nu sunt scrise la rulare. Acel director este singurul loc către care ar trebui să indice launcherul. Orice dezarhivezi tu însuți la rulare, oricum i-ai spune, rămâne în categoria refuzată.
Stocarea externă este mai rea: este montată cu noexec, iar un auxiliar plasat acolo nu poate fi pornit de nicio aplicație. Ștergerea datelor aplicației nu readuce nici comportamentul vechi, pentru că următoarea pornire recreează pur și simplu aceeași copie neexecutabilă.
Îndreaptă launcherul către o bibliotecă împachetată
Redenumește auxiliarul într-un nume de bibliotecă partajată și livrează-l sub jniLibs pentru fiecare ABI pe care îl susții, apoi fă launcherul să returneze calea bibliotecii instalate de la API 28 înainte:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return new File(context.getApplicationInfo().nativeLibraryDir,
"libovpnexec.so").getPath();
}Biblioteca trebuie dezarhivată la instalare pentru ca acea cale să conțină un fișier real, așa că setează android:extractNativeLibs="true" pe elementul <application>. Dacă un merger de manifest aduce altă valoare, adaugă tools:replace="android:extractNativeLibs" lângă el. referința pentru elementul application documentează ce controlează atributul.
Ai grijă la o variabilă locală duplicată când patchuiești launcherul de mână. Sursele mai vechi declară nativeAPI mai târziu în aceeași metodă, iar o a doua declarație este o eroare de compilare, nu de rulare, așa că buildul eșuează repede și greșeala se vede ușor. Ține lista de ABI din blocul Gradle în pas cu folderele pe care le adaugi.
Fă artefactul de lansare să corespundă buildului de test
Un APK de depanare poate ridica tunelul în timp ce un bundle de lansare încă se prăbușește, pentru că un bundle împachetează bibliotecile native într-o altă formă și le poate servi comprimate din arhivă. Dacă eșuează doar buildurile derivate din bundle, adaugă asta în gradle.properties și reconstruiește:
android.bundle.enableUncompressedNativeLibs=falseApoi confirmă că pachetul instalat raportează ABI-ul pe care îl aștepți, și repetă verificarea pe un dispozitiv pe 32 de biți și pe unul pe 64 de biți dacă le livrezi pe amândouă. Managerul de pachete numește ABI-ul ales, așa că un dispozitiv care a căzut înapoi la 32 de biți apare aici, nu într-un raport de prăbușire:
adb shell dumpsys package com.example.openvpn | grep -i primaryCpuAbiO bibliotecă lipsă pentru un ABI reproduce eroarea originală de permisiune doar pe acel dispozitiv, de aceea un singur telefon de test care funcționează nu este dovadă. Livrează steagul din manifest, numele bibliotecii și setarea bundle împreună; schimbi unul și le uiți pe celelalte, te întorci la aceeași prăbușire. Remediul durabil este trecerea la o versiune de nucleu întreținută, în loc să cari patchul pentru totdeauna.