L’application fonctionnait sous Android 9 et meurt sur le même téléphone après la mise à jour vers Android 10. Votre profil est intact, les certificats sont les mêmes, et le plantage survient avant tout trafic d’authentification. Logcat affiche d’abord un pointeur nul, et quelques lignes au-dessus se trouve Cannot run program ...: error=13, Permission denied. Ce message concerne l’exécution d’un fichier, pas la lecture de votre configuration.

Ce que l’erreur 13 refuse vraiment

Android 10, niveau d’API 29, a empêché les applications d’exécuter des fichiers qu’elles écrivaient dans leur propre répertoire de données. La règle vise le stockage d’abord inscriptible puis exécutable, ce qui est exactement ce que crée un lanceur qui copie depuis les assets. Les anciens builds du cœur ics-openvpn décompressent un exécutable openvpn depuis l’APK au premier lancement et le démarrent avec ProcessBuilder. La décompression réussit toujours, c’est l’exécution qui est refusée. Une application qui cible l’API 29 hérite de la règle même lorsqu’elle tourne sur un appareil sorti avec une version plus ancienne, et c’est pourquoi une mise à jour peut casser un build que personne n’a touché.

Rien dans le refus n’est propre à un nom de dossier. Un fichier que l’application a créé sous son répertoire de données est traité comme non exécutable quel que soit le chemin, donc getFilesDir(), getCacheDir() et un sous-répertoire personnalisé échouent tous de la même façon.

Séparez le plantage de l’auxiliaire d’un problème de profil

Lisez le journal autour du démarrage du processus au lieu de faire défiler depuis le haut. La signature à chercher est la ligne error=13, et la trace de pile au-dessus nomme généralement la méthode du lanceur qui a appelé ProcessBuilder :

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

Si l’auxiliaire n’a jamais démarré, vous ne verrez aucune sortie de journal OpenVPN : ni négociation de chiffrement, ni routes poussées, ni configuration DNS. Si ces lignes apparaissent et que l’échec arrive plus tard, le binaire a tourné et le problème est dans le profil, le serveur ou la couche de routage. Cette seule distinction épargne beaucoup de temps qui partirait autrement à modifier des fichiers .ovpn qui n’étaient jamais en cause. Elle vous dit aussi laquelle des deux corrections ci-dessous vous est réellement nécessaire.

Vérifiez aussi le niveau d’API sur l’appareil que vous testez, et non celui de votre configuration de build. Une image d’émulateur en API 28 ne reproduit rien, car la restriction ne mord qu’à partir de 29. Vérifiez en même temps le SDK cible dans votre fichier de build : c’est son relèvement qui fait entrer la règle.

adb shell getprop ro.build.version.sdk

Pourquoi copier le binaire ailleurs n’aide pas

Le contournement évident, écrire l’auxiliaire dans un autre répertoire inscriptible, échoue pour la même raison que l’original. Android autorise toujours l’exécution des bibliothèques partagées installées dans le répertoire des bibliothèques natives de l’application, parce que l’installeur les empaquette au lieu de les écrire à l’exécution. Ce répertoire est le seul endroit vers lequel le lanceur devrait pointer. Tout ce que vous décompressez vous-même à l’exécution, quel que soit son nom, reste dans la catégorie refusée.

Le stockage externe est pire : il est monté en noexec, et un auxiliaire qui y est placé ne peut être démarré par aucune application. Effacer les données de l’application ne rétablit pas non plus l’ancien comportement, car le lancement suivant recrée simplement la même copie non exécutable.

Pointez le lanceur vers une bibliothèque empaquetée

Renommez l’auxiliaire en nom de bibliothèque partagée et livrez-le sous jniLibs pour chaque ABI que vous prenez en charge, puis faites renvoyer au lanceur le chemin de la bibliothèque installée à partir de l’API 28 :

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

La bibliothèque doit être décompressée à l’installation pour que ce chemin contienne un vrai fichier, donc définissez android:extractNativeLibs="true" sur l’élément <application>. Si un fusionneur de manifeste apporte une autre valeur, ajoutez tools:replace="android:extractNativeLibs" à côté. La référence de l’élément application documente ce que contrôle l’attribut.

Attention à une variable locale dupliquée quand vous corrigez le lanceur à la main. Les anciennes sources déclarent nativeAPI plus loin dans la même méthode, et une seconde déclaration est une erreur de compilation et non d’exécution, donc le build échoue vite et l’erreur se repère facilement. Gardez la liste des ABI du bloc Gradle synchronisée avec les dossiers que vous ajoutez.

Faites correspondre l’artefact de version au build de test

Un APK de débogage peut monter le tunnel alors qu’un bundle de version plante encore, car un bundle empaquette les bibliothèques natives sous une autre forme et peut les servir compressées depuis l’archive. Si seuls les builds issus du bundle échouent, ajoutez ceci dans gradle.properties et reconstruisez :

android.bundle.enableUncompressedNativeLibs=false

Confirmez ensuite que le paquet installé annonce l’ABI que vous attendez, et répétez la vérification sur un appareil 32 bits et un 64 bits si vous livrez les deux. Le gestionnaire de paquets nomme l’ABI qu’il a choisie, donc un appareil retombé en 32 bits apparaît ici plutôt que dans un rapport de plantage :

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

Une bibliothèque manquante pour une ABI reproduit l’erreur de permission d’origine sur cet appareil seul, et c’est pourquoi un seul téléphone de test qui fonctionne n’est pas une preuve. Livrez le drapeau de manifeste, le nom de la bibliothèque et le réglage du bundle ensemble ; changer l’un et oublier les autres vous ramène au même plantage. La correction durable est de passer à une version du cœur maintenue au lieu de traîner le correctif pour toujours.

Articles similaires