Diagnóstico de fallos

referencia técnica · Actualizado el 25 de septiembre de 2026

Es un índice de síntomas. Se entra por el mensaje literal que se tiene delante, no por el concepto, porque cuando algo falla lo único que se tiene es una cadena de texto y ninguna pista de a qué capa pertenece. Cada entrada tiene la misma forma —síntoma literal → qué significa → causa raíz → qué comprobar → cómo se arregla— y una convención de honestidad: (reproducido) marca lo provocado en la máquina de referencia, con la cadena copiada de la salida real; (fuente) marca lo copiado del código que lo emite —AOSP, jadx o apktool— con el fichero identificado pero sin reproducir aquí; y ⚠️ marca lo que no está verificado, diciendo por qué.

Dos avisos generales. Los mensajes de error mienten sobre la causa con frecuencia: el caso de manual está en «§4.2 · La trampa mayor: zipalign después de firmar (reproducido)», donde un zipalign ejecutado un paso tarde produce Signature stripped?, que suena a ataque y es un error de orden. Y el error real casi nunca está en la salida de la orden que falló: en instalación está en logcat; en apktool, en el log de aapt2 y no en la excepción; en jadx, en el .java generado y no en la consola.

2. Instalación: los códigos INSTALL_*

2.1 De dónde salen y cómo se leen

Están enumerados en frameworks/base/core/java/android/content/pm/PackageManager.java de AOSP. Todos los valores son negativos, todos los símbolos están marcados @hide —no son API pública— y adb install los devuelve como texto, sin el número. Dos familias que llevan a sitios distintos: INSTALL_PARSE_FAILED_* (−100 a −109, más −124 y −125) significa que el paquete no se pudo leer —el fallo está en el fichero y lo produce el analizador—; INSTALL_FAILED_* (−1 a −29 y −110 en adelante) significa que el paquete se leyó bien y el sistema lo rechaza por su relación con lo instalado, con el dispositivo o con la política. Nueve códigos salen como número y no como texto, porque PackageManager.installStatusToString() no tiene un case para ellos: −26, −27, −119, −120, −124, −125, −126, −127 y −129. El que más se ve es -124, el de resources.arsc comprimido («§2.5 · Lo que responde adb install, reproducido»). Y una regla operativa: el motivo concreto está en logcat, no en la salida de adb install — el instalador devuelve un código seco, y el mensaje que dice qué entrada, qué certificado o qué split falla lo escribe PackageManager en el log del sistema.

Reproducidos: los mensajes de la «sección 2.5 · Lo que responde adb install, reproducido» son la salida literal de adb install en un emulador Android 14 arm64-v8a, con APK de prueba construidos con aapt2 y firmados con apksigner, el 25 de septiembre de 2026. INSTALL_FAILED_NO_MATCHING_ABIS se reprodujo además el 13 de agosto con splits reales («Formatos y extensiones», secciones «3.1» y «6.2»). Los valores, las descripciones y las condiciones internas proceden del código de AOSP en la etiqueta android-17.0.0_r1, leído directamente (?format=TEXT, decodificado) el 25 de septiembre de 2026.

2.2 Fallos de análisis del paquete

Código Valor Qué significa Dónde se lanza y con qué condición
..._NOT_APK −100 La ruta no es un fichero o no acaba en .apk ApkLiteParseUtils: directorio sin paquetes (No packages found in split), o ApkAssets.loadFromFd lanza IOException
..._BAD_MANIFEST −101 No se pudo recuperar el manifiesto ApkLiteParseUtils: splits que discrepan del primero (Inconsistent package…, Inconsistent version…), nombre de split repetido, o Missing base APK in <dir>
..._UNEXPECTED_EXCEPTION −102 Excepción inesperada parseApkLiteInner(): catch general de XmlPullParserException | IOException | RuntimeException. Cajón de sastre: no dice nada por sí mismo, el motivo está en logcat
..._NO_CERTIFICATES −103 Sin certificados utilizables «§2.3 · Los cinco que hay que conocer al detalle»
..._INCONSISTENT_CERTIFICATES −104 Certificados incoherentes «§2.3 · Los cinco que hay que conocer al detalle»
..._CERTIFICATE_ENCODING · ..._BAD_SHARED_USER_ID −105 · −107 CertificateEncodingException en algún fichero · sharedUserId inválido ApkSignatureVerifier · ParsingPackageUtils (nombre inválido, o sharedUserId en una biblioteca SDK o estática; comprobado en android-17.0.0_r1)
..._MANIFEST_EMPTY −109 Ni <application> ni <instrumentation> El instalador ya no lo lanza. Solo lo emite PackageParser, el analizador antiguo, marcado @Deprecated. ParsingPackageUtils trata el mismo caso como error diferido (MISSING_APP_TAG) y lo devuelve como ..._MANIFEST_MALFORMED con targetSdk 30 o superior; con 29 o menos, el APK instala («§2.5 · Lo que responde adb install, reproducido»)
..._BAD_PACKAGE_NAME −106 Nombre de paquete inválido o ausente en el manifiesto «§2.3 · Los cinco que hay que conocer al detalle»
..._MANIFEST_MALFORMED −108 Problema estructural en el manifiesto «§2.3 · Los cinco que hay que conocer al detalle»
..._RESOURCES_ARSC_COMPRESSED −124 resources.arsc comprimido o desalineado «§2.3 · Los cinco que hay que conocer al detalle»

2.3 Los cinco que hay que conocer al detalle

INSTALL_PARSE_FAILED_NO_CERTIFICATES. El paquete no tiene firma utilizable, o no la que el rango de API exige. Lo produce ApkSignatureVerifier.java en varios sitios con mensajes distintos: cuando el esquema mínimo requerido es más nuevo que cualquiera de los presentes —"No signature found in package of version … or newer for package …"—, cuando se captura SignatureNotFoundException y no se puede caer a un esquema anterior —"No APK Signature Scheme v4/v3/v2 signature in package …"— y, en el camino de v1, "Package … has no certificates at entry AndroidManifest.xml". En la práctica, tres orígenes: se olvidó firmar —apktool y APKEditor producen APK sin firma por diseño, y bundletool build-apks sin --ks también si no hay ~/.android/debug.keystore—, se firmó solo con v1 teniendo un targetSdk que exige v2 («§3.3 · Target SDK version 36 requires a minimum of signature scheme v2 (reproducido)»), o se pasó zipalign después de firmar («§4.2 · La trampa mayor: zipalign después de firmar (reproducido)»). Comprobar: apksigner verify --verbose --min-sdk-version <el minSdk real> app.apk, y el inventario físico de META-INF/. Arreglo: firmar en el orden correcto —alinear y después apksigner sign con v2 y v3 («Pipeline de modificación → §7.4 · La receta completa, ejecutada»).

INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES. Dentro del paquete —o del conjunto de splits— no todo está firmado por lo mismo. Dos sitios: ApkSignatureVerifier.java en la verificación completa de v1, cuando una entrada del ZIP tiene firmas distintas de las del manifiesto ("Package … has mismatched certificates at entry …"); y ParsingPackageUtils.getSigningDetails() / FrameworkParsingPackageUtils, cuando !Signature.areExactMatch(...) entre un split y el base ("<baseCodePath> has mismatched certificates"). Pero al instalar splits refirmados por separado o mezclados de dos descargas no se suele llegar aquí: con adb install-multiple o cualquier instalador que use una sesión de PackageInstaller, la sesión compara antes cada split con el primero y falla con INSTALL_FAILED_INVALID_APK: … signatures are inconsistent (PackageInstallerSession.assertApkConsistentLocked). Lo mismo si discrepan el paquete o el versionCode: … version code N inconsistent with M. Comprobar: un bucle de apksigner verify --print-certs sobre todos los splits tiene que devolver un solo certificate SHA-256 digest («Pipeline de análisis → §5.4 · Firma y splits»). Arreglo: refirmar todos con la misma clave, cada uno alineado antes.

INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME. Lo produce FrameworkParsingPackageUtils.validateName(), cuyas reglas están implementadas ahí literalmente y no en ninguna especificación. Recorre carácter a carácter con dos banderas, front y hasSep: [a-zA-Z] siempre vale y pone front = false; [0-9] y _ valen solo si front == false, o sea que un segmento no puede empezar por dígito ni por subrayado; . pone hasSep = true y front = true; y cualquier otro carácter da "bad character '<c>'". Con requireFilename añade "Invalid filename" y "the length of the name is greater than 223"; y al final, con requireSeparator y sin ningún punto, "must have at least one '.' separator". El paquete se valida con requireSeparator=true, requireFilename=true; los nombres de split con false, false; los tipos con false, true. Aparece casi siempre tras un --rename-manifest-package con un guion, un segmento que empieza por dígito (com.ejemplo.2app) o ningún punto. aapt2 no deja construirlo: error: attribute 'package' in <manifest> tag is not a valid Android package name: 'com.ejemplo.2app'., así que el error solo llega al instalador desde herramientas que reescriben el manifiesto por su cuenta.

INSTALL_PARSE_FAILED_MANIFEST_MALFORMED. Varios sitios de ApkLiteParseUtils.java, con mensajes muy informativos si se llegan a ver en logcat: "No start tag found", "No <manifest> tag", "Invalid manifest split types: ", un <uses-split> sin android:name, o un <uses-sdk-library> / <uses-static-library> mal formado o duplicado —aapt2, de nuevo, no deja enlazar un <uses-split> sin nombre—. Con targetSdk 30 o superior también lo da un manifiesto sin <application> ni <instrumentation>, que es el caso que el código antiguo llamaba MANIFEST_EMPTY. Los atributos de split que quedan en el manifiesto de un APK fusionado no dan este error («Fusión de splits → §5 · Saneado del manifiesto»): un split= sobrante hace que el instalador tome el APK por un split y responda INSTALL_FAILED_INVALID_APK: Full install must include a base package, y un isSplitRequired="true" sobrante da INSTALL_FAILED_MISSING_SPLIT («§2.4 · Fallos de instalación»). Sobre un APK fusionado el aapt2 dump xmltree … | grep -E "split=|splitTypes|isSplitRequired|isFeatureSplit|configForSplit" no debe devolver nada. Si el manifiesto ni siquiera se puede leer, el error está un nivel más abajo y aapt2 lo dice con precisión: «§8.3 · AXML malformado (reproducido)».

INSTALL_PARSE_FAILED_RESOURCES_ARSC_COMPRESSED — con la trampa reproducida. Significa que el resources.arsc de alguno de los APK va comprimido o no está alineado a 4 bytes, así que el sistema no lo puede mapear con mmap. Lo produce ParsingPackageUtils.parseBaseApk(): tras un análisis correcto del manifiesto comprueba assets.containsAllocatedTable() —la tabla tuvo que reservarse en RAM en vez de mapearse— y emite un error diferido con el identificador de cambio de comportamiento RESOURCES_ARSC_COMPRESSED = 132742131, anotado @EnabledAfter(targetSdkVersion = Q): solo se activa para targetSdk 30 y superior; con 29 o menos, el mismo APK instala. El texto en AOSP es "Targeting R+ (version 30 and above) requires the resources.arsc of installed APKs to be stored uncompressed and aligned on a 4-byte boundary", y es lo que se ve en el emulador con targetSdk 36, con el código como número: Failure [-124: Failed parse during installPackageLI: Targeting R+ …]. Con targetSdk 29, el mismo APK responde Success.

Y aquí está la trampa: las herramientas locales no lo detectan. Sobre un APK construido a propósito con resources.arsc comprimido, aapt2 dump badging responde con normalidad (package: name='org.fossify.math' versionCode='10' …) y zipalign -c -v 4 lo marca 3322768 resources.arsc (OK - compressed) saliendo con 0. La comprobación hay que hacerla a mano —zipfile.ZipFile(apk).getinfo('resources.arsc').compress_type == 0— y el arreglo es reempaquetar forzando stored en esa entrada.

2.4 Fallos de instalación

Código Valor Qué significa
INSTALL_FAILED_INVALID_APK −2 El archivo del paquete es inválido
INSTALL_FAILED_INSUFFICIENT_STORAGE −4 No hay espacio suficiente en el dispositivo
INSTALL_FAILED_UPDATE_INCOMPATIBLE −7 Ya hay un paquete con ese nombre y firma distinta, y sus datos no se borraron
INSTALL_FAILED_DEXOPT −11 Heredado. En android-17.0.0_r1 un dexopt que falla no aborta la instalación: DexOptHelper lo deja escrito, «Don't fail application installs if the dexopt step fails»
INSTALL_FAILED_OLDER_SDK −12 El minSdkVersion es mayor que la versión del dispositivo
INSTALL_FAILED_TEST_ONLY −15 El manifiesto lleva android:testOnly="true" y no se pasó -t
INSTALL_FAILED_CPU_ABI_INCOMPATIBLE −16 Trae código nativo y ninguna ABI es compatible
INSTALL_FAILED_VERSION_DOWNGRADE −25 El versionCode es menor que el instalado
INSTALL_FAILED_MISSING_SPLIT −28 Requiere al menos un split y no se aportó
INSTALL_FAILED_DEPRECATED_SDK_VERSION −29 El targetSdkVersion está por debajo del mínimo instalable. -t lo levanta en una aplicación testOnly, y --bypass-low-target-sdk-block en cualquiera
INSTALL_FAILED_INTERNAL_ERROR −110 Fallo del sistema
INSTALL_FAILED_NO_MATCHING_ABIS −113 El código nativo no coincide con ninguna ABI del sistema
INSTALL_FAILED_ABORTED −115 Sin Javadoc en AOSP: solo /** @hide */

⚠️ INSTALL_FAILED_MULTI_ARCH_NOT_SUPPORTED no existe. Circula en foros, pero no aparece en PackageManager.java de la etiqueta android-17.0.0_r1. La constante real más cercana es INSTALL_FAILED_MULTI_ARCH_NOT_MATCH_ALL_NATIVE_ABIS = -131, para aplicaciones con android:multiArch="true" cuyo código nativo no cubre todas las ABIs del sistema.

INSTALL_FAILED_UPDATE_INCOMPATIBLE. InstallPackageHelper.preparePackage(), en el camino de reemplazo, cuando las credenciales de firma nuevas no tienen CertCapabilities.INSTALLED_DATA respecto a las instaladas y no es un rollback válido a un ancestro del linaje: "New package has a different signature: " + pkgName. Pero el mensaje que se ve casi siempre es otro: antes, el mismo preparePackage() hace una comprobación temprana con PackageManagerServiceUtils.verifySignatures(), que salta primero con "Existing package " + packageName + " signatures do not match newer version; ignoring!". Hay otras dos rutas al mismo código —fallo de checkUpgradeKeySetLocked, y un cambio de SDK en una biblioteca sin subir su versionMajor— y una cuarta que AOSP reconoce como reutilización indebida: en scanInstallPackages() se devuelve el mismo código cuando la actualización cambia PROPERTY_NO_APP_DATA_STORAGE, con un TODO en el fuente admitiendo que no corresponde. Es decir: no siempre es de firma. En la práctica aparece siempre que se instala un APK refirmado encima del original, lo cual es inevitable («Pipeline de modificación → §7.3 · La firma original no se puede conservar, y no es culpa de las herramientas»), y el único arreglo es adb uninstall con pérdida de los datos.

INSTALL_FAILED_VERSION_DOWNGRADE. La comprobación real es PackageManagerServiceUtils.checkDowngrade(), más fina de lo que sugiere el nombre: falla si el versionCode nuevo es menor, o si son iguales y el baseRevisionCode nuevo es menor, o si el splitRevisionCode de cualquier split coincidente es menor — salvo que isDowngradePermitted() lo autorice, lo que ocurre con adb install -d sobre una compilación depurable. Ojo con haber tocado el versionCode al reempaquetar sin querer: apktool lo repuebla desde apktool.yml.

INSTALL_FAILED_MISSING_SPLIT. PackageInstallerSession.validateApkInstallLocked(), en dos sitios y los dos con el mensaje "Missing split for " + mPackageName: en instalación completa, cuando baseApk.isSplitRequired() y stagedSplits.size() <= 1 o !stagedSplitTypes.containsAll(requiredSplitTypes); y heredando, cuando se han quitado todos los splits o los tipos aportados no cubren los requeridos. Hay que mirar los dos mecanismos que conviven —el booleano isSplitRequired y el par requiredSplitTypes/splitTypes—, porque el «muestrario» tiene aplicaciones con cada uno («Fusión de splits → §5.2 · Los dos mecanismos conviven»). Arreglo: adb install-multiple con el conjunto completo, o fusionar y sanear el manifiesto.

INSTALL_FAILED_NO_MATCHING_ABIS y INSTALL_FAILED_CPU_ABI_INCOMPATIBLE. Casi siempre al fusionar splits cogiendo de menos. Es el único de los tres ejes que produce un fallo duro —densidad e idioma degradan en silencio («Fusión de splits → §3.1 · El problema»)—, y se comprueba comparando las ABIs bajo lib/ con adb shell getprop ro.product.cpu.abilist.

2.5 Lo que responde adb install, reproducido

Ejecutado el 25 de septiembre de 2026 en un emulador Android 14 arm64-v8a (sdk_phone64_arm64, compilación userdebug), con APK de prueba construidos con aapt2 2.20-15087165 y firmados con apksigner 0.9. adb install escribe adb: failed to install <apk>: Failure [<código>: <mensaje>], e install-multiple, adb: failed to finalize session y debajo Failure […]; la tabla da lo que va entre corchetes. vmdl….tmp es el directorio temporal de la sesión, distinto en cada intento.

Caso Lo que devuelve
Sin firmar INSTALL_PARSE_FAILED_NO_CERTIFICATES: Failed to collect certificates from /data/app/vmdl….tmp/base.apk: Attempt to get length of null array
Solo v1, con targetSdk 36 INSTALL_PARSE_FAILED_NO_CERTIFICATES: Scanning Failed.: No signature found in package of version 2 or newer for package com.ejemplo.prueba
resources.arsc comprimido, targetSdk 36 -124: Failed parse during installPackageLI: Targeting R+ (version 30 and above) requires the resources.arsc of installed APKs to be stored uncompressed and aligned on a 4-byte boundary
El mismo, con targetSdk 29 Success
Sin <application>, targetSdk 30 INSTALL_PARSE_FAILED_MANIFEST_MALFORMED: Failed parse during installPackageLI: /data/app/vmdl….tmp/base.apk (at Binary XML file line #1): <manifest> does not contain an <application> or <instrumentation>
El mismo, con targetSdk 29 Success
minSdkVersion 35 en Android 14 INSTALL_FAILED_OLDER_SDK: Requires newer sdk version #35 (current version is #34)
targetSdkVersion 21 INSTALL_FAILED_DEPRECATED_SDK_VERSION: App package must target at least SDK version 23, but found 21. Con --bypass-low-target-sdk-block, Success
android:testOnly="true" sin -t INSTALL_FAILED_TEST_ONLY: Failed to install test-only apk. Did you forget to add -t?. Con -t, Success, también con targetSdk 21
La misma aplicación firmada con otra clave INSTALL_FAILED_UPDATE_INCOMPATIBLE: Existing package com.ejemplo.prueba signatures do not match newer version; ignoring!
Un versionCode menor que el instalado INSTALL_FAILED_VERSION_DOWNGRADE: Downgrade detected: Update version code 2 is older than current 3. Con -r -d, en esta compilación userdebug, Success
split="config.es" en un APK completo INSTALL_FAILED_INVALID_APK: Full install must include a base package
android:isSplitRequired="true" en <manifest>, sin splits INSTALL_FAILED_MISSING_SPLIT: Missing split for com.ejemplo.prueba. En <application> no hace nada: se instala
install-multiple con el split firmado por otra clave INSTALL_FAILED_INVALID_APK: /data/app/vmdl….tmp/split-es-vc2-b.apk signatures are inconsistent
install-multiple con un split de otra versión INSTALL_FAILED_INVALID_APK: /data/app/vmdl….tmp/split-es-vc3-a.apk version code 3 inconsistent with 2
install-multiple con el mismo split dos veces INSTALL_FAILED_INVALID_APK: Split config.es was defined multiple times
install-multiple con un split y sin el base INSTALL_FAILED_INVALID_APK: Full install must include a base package
Dos entradas AndroidManifest.xml en el ZIP INSTALL_PARSE_FAILED_NOT_APK: Failed to parse /data/app/vmdl….tmp/base.apk: Failed to load asset path /data/app/vmdl….tmp/base.apk
install --incremental con el .idsig de otro APK INSTALL_PARSE_FAILED_NOT_APK: Failed to parse …: Failed to load asset path …
install --incremental con el .idsig truncado Falla en el ordenador, antes de llegar al dispositivo: Failed to read data: End of file.

Cuatro cosas que no se deducen del código leído en frío:

  • El APK sin firmar no dice que le falte la firma. Attempt to get length of null array es el texto de una excepción interna, no un mensaje pensado para nadie.
  • Las dos entradas duplicadas no llegan a la firma. Las rechaza libziparchive al abrir el fichero, y lo que queda en logcat es Zip: Found duplicate entry AndroidManifest.xml y Failed to open APK '…': Duplicate entries in archive.
  • Con el .idsig de otro APK, tampoco. En una instalación incremental el sistema de ficheros comprueba cada bloque contra el árbol de Merkle del .idsig, y el primer bloque ya falla: Zip: failed to read at offset 0 y Failed to open APK '…': I/O error. La verificación v4 que describe el código, y que acabaría en NO_CERTIFICATES, no llega a ejecutarse. Sin --incremental, el .idsig se ignora y el APK se instala por v2 o v3.
  • adb install sin -r también reemplaza. Instalar dos veces el mismo APK responde Success las dos veces.

3. Firma

3.1 DOES NOT VERIFY — el prefijo común

apksigner verify responde siempre con esa línea, sale con código 1, y el motivo va en las líneas ERROR: siguientes. Los cinco que se ven de verdad:

Línea ERROR: Causa
Missing META-INF/MANIFEST.MF No hay firma ninguna, o se destruyó el APK Signing Block y no había v1 · «§3.2 · Bloque desaparecido frente a contenido modificado (reproducido)»
JAR signature META-INF/X.SF indicates the APK is signed using APK Signature Scheme v2 but no such signature was found. Signature stripped? Había v1 y v2/v3, y el bloque desapareció · «§4.2 · La trampa mayor: zipalign después de firmar (reproducido)»
APK integrity check failed. CHUNKED_SHA256 digest mismatch. El contenido cambió después de firmar · «§3.2 · Bloque desaparecido frente a contenido modificado (reproducido)»
Target SDK version 36 requires a minimum of signature scheme v2; the APK is not signed with this or a later signature scheme Se firmó solo con v1 · «§3.3 · Target SDK version 36 requires a minimum of signature scheme v2 (reproducido)»
Verified using vN scheme: false sin DOES NOT VERIFY No es un error: es la trampa del rango de SDK · «§3.4 · Verified using v1 scheme: false con el MANIFEST.MF dentro (reproducido)»

3.2 Bloque desaparecido frente a contenido modificado (reproducido)

Es la distinción que más tiempo ahorra, y las dos familias se confunden constantemente.

ERROR: Missing META-INF/MANIFEST.MF significa que el verificador no encontró ningún bloque de firma y cayó al camino de v1, donde lo primero que busca es MANIFEST.MF. Dos causas: el APK nunca se firmó —apktool y APKEditor producen APK sin firma por diseño, y bundletool build-apks sin --ks también si no hay ~/.android/debug.keystore; reproducido sobre un APK del muestrario sin META-INF/ y sobre el universal.apk de bundletool --mode=universal— o se ejecutó zipalign después de firmar un APK que solo tenía v2/v3 («§4.2 · La trampa mayor: zipalign después de firmar (reproducido)»). Si unzip -l app.apk | grep -E 'META-INF/(MANIFEST\.MF|[^/]*\.(SF|RSA|DSA|EC))$' no devuelve nada y el APK mide menos que antes, es el segundo caso: hay que firmar el de antes de alinear.

CHUNKED_SHA256 digest mismatch significa lo contrario: el bloque sigue ahí y es legible, y lo que no cuadra es el digest. Cambiando un solo byte dentro de resources.arsc de un APK ya firmado, sin mover ningún offset —el fichero mide exactamente lo mismo antes y después—:

DOES NOT VERIFY
ERROR: APK Signature Scheme v3 signer #1: APK integrity check failed. CHUNKED_SHA256 digest mismatch. Expected: <655a9adc…cafe8d6c>, actual: <bf95b77b…39f57b21>

La regla: digest mismatch es contenido modificado —«alguien tocó el fichero»—; Signature stripped? y Missing META-INF/MANIFEST.MF son bloque desaparecido —casi siempre, «se ejecutó una herramienta en el orden equivocado»—. El arreglo, en los dos casos, es volver a firmar («§3.5 · Fallos al firmar (reproducidos)»).

3.3 Target SDK version 36 requires a minimum of signature scheme v2 (reproducido)

Firmando una copia del muestrario con --v1-signing-enabled true y las demás a false:

DOES NOT VERIFY
ERROR: Target SDK version 36 requires a minimum of signature scheme v2; the APK is not signed with this or a later signature scheme
WARNING: META-INF/com/android/build/gradle/app-metadata.properties not protected by signature. …
WARNING: META-INF/version-control-info.textproto not protected by signature. …

Firmar solo con v1 hoy es no firmar: la firma es criptográficamente correcta y el APK no se instala, porque Android exige un esquema mínimo según el targetSdkVersion. Cuatro de las trece herramientas evaluadas firman solo con v1, y sus APK parecen correctos hasta el último paso. Los WARNING son el otro dato: la firma JAR no cubre las entradas de META-INF/, así que cualquiera puede modificarlas sin invalidarla — la debilidad que motivó v2. Arreglo: --v2-signing-enabled true --v3-signing-enabled true; v1 solo si minSdkVersion < 24.

3.4 Verified using v1 scheme: false con el MANIFEST.MF dentro (reproducido)

No hay fallo. apksigner verify no responde «¿está presente este esquema?» sino «¿se usó este esquema para verificar en el rango de API que me has dado?». El mismo APK firmado con v1+v2+v3, con dos rangos distintos, da Verified using v1 scheme (JAR signing): false con --min-sdk-version 24 y true con 21 — y META-INF/PROC.SF, META-INF/PROC.RSA y META-INF/MANIFEST.MF están dentro en los dos casos. Para saber qué esquemas hay, se mira el ZIP y el APK Signing Block; para saber si instalará, se ejecuta verify con el minSdkVersion real de la aplicación.

3.5 Fallos al firmar (reproducidos)

Síntoma Causa Arreglo
Failed to load signer "signer #1" + java.io.IOException: keystore password was incorrect Contraseña del keystore incorrecta Revisar --ks-pass; preferir env: o file: sobre pass:
Failed to load signer "signer #1": x.p12 entry "ALIAS" does not contain a key El alias no existe keytool -list -v -keystore x.p12
com.android.apksig.apk.ApkFormatException: Malformed APK: not a ZIP archive No es un ZIP «§8.1 · El fichero no es lo que dice ser (reproducido)»
com.android.apksig.apk.ApkFormatException: Missing AndroidManifest.xml Es un ZIP pero no un APK Se cogió el contenedor en vez del APK de dentro

Y la imposibilidad de fondo: la firma original nunca se puede conservar tras modificar el contenido. No es una limitación de las herramientas, es la propiedad que hace útil a la firma («Pipeline de modificación → §7.3 · La firma original no se puede conservar, y no es culpa de las herramientas»).

4. Alineación

4.1 (BAD - n) de zipalign -c (reproducido)

Sobre split_config.arm64_v8a.apk del SAI de Netflix del muestrario:

    4096 lib/arm64-v8a/libavif_android.so (BAD - 4096)
  851968 lib/arm64-v8a/libbugsnag-ndk.so (OK)
 2039808 lib/arm64-v8a/libbugsnag-plugin-android-anr.so (BAD - 8192)
 7974912 lib/arm64-v8a/libtensorflowlite_jni_gms_client.so (BAD - 12288)
Verification FAILED

El n no es un código de error: es el resto de dividir el offset de los datos entre la alineación exigida. libavif_android.so empieza en el byte 4.096, y 4096 mod 16384 = 4096: o sea, cuántos bytes sobran. Causa raíz: el APK está alineado a 4 KiB y se le exige 16 KiB, y el mismo fichero con zipalign -c 4 sale con 0 —las mismas entradas, dos veredictos opuestos, porque son dos preguntas distintas—. Esos seis splits de Netflix son el único incumplimiento de 16 KB en los 717 APK del muestrario. Arreglo: zipalign -P 16 -f 4 entrada.apk salida.apk, antes de firmar.

Los tres veredictos por entrada: (OK) es stored y alineada; (OK - compressed) es comprimida y la alineación no le aplica; (BAD - n) es stored desalineada por n bytes.

4.2 La trampa mayor: zipalign después de firmar (reproducido)

El fallo de manual del ecosistema, y el que produce el diagnóstico más engañoso.

$BT/apksigner verify --verbose firmado.apk | head -4      # v1 + v2 + v3, todo correcto
$BT/zipalign -P 16 -f 4 firmado.apk roto.apk; echo "zipalign exit=$?"
$BT/apksigner verify --verbose roto.apk; echo "verify exit=$?"
zipalign exit=0
DOES NOT VERIFY
ERROR: JAR signer PROC.RSA: JAR signature META-INF/PROC.SF indicates the APK is signed using APK Signature Scheme v2 but no such signature was found. Signature stripped?
ERROR: JAR signer PROC.RSA: JAR signature META-INF/PROC.SF indicates the APK is signed using APK Signature Scheme v3 but no such signature was found. Signature stripped?
verify exit=1

zipalign no invalida la firma: la borra. Reescribe el ZIP entrada por entrada a un fichero nuevo, con Central Directory y EOCD nuevos, y el hueco donde vivía el APK Signing Block no se reproduce. El .SF de v1 sí sobrevive —es una entrada normal del ZIP— y sigue declarando X-Android-APK-Signed: 2, 3; el verificador ve esa declaración, no encuentra los bloques, y acusa de un stripping que no hubo. Y si el APK no tenía v1, el mensaje es aún más desconcertante —mismo experimento firmando solo con v2+v3: DOES NOT VERIFY / ERROR: Missing META-INF/MANIFEST.MF—: un fichero que nunca tuvo MANIFEST.MF, acusado de que le falta.

La señal delatora: zipalign devolvió 0. Para él la operación fue un éxito. Y el APK resultante mide algunos kilobytes menos que el firmado: la diferencia es el bloque desaparecido más el relleno recalculado. Arreglo: recuperar el APK de antes de firmar, alinearlo y firmar después.

Orden Resultado
construir → alinear → firmar Correcto
construir → firmar → alinear Firma destruida
construir → firmar → tocar un fichero digest mismatch («§3.2 · Bloque desaparecido frente a contenido modificado (reproducido)»)
alinear → firmar → zipalign -c Correcto: -c solo lee

4.3 zipalign -c pasa y el APK sigue siendo incompatible con 16 KB

zipalign -c responde a «¿están alineadas las entradas que debían estarlo?», no a «¿cumple este APK la regla de 16 KB?». Un APK con extractNativeLibs="true" comprime sus .so, no tiene ninguna entrada a la que la regla aplique, y pasa siempre. Y hay un segundo requisito que zipalign no mira: los segmentos LOAD del propio ELF tienen que estar alineados a 2**14. Los dos controles son necesarios y ninguno basta («Alineación → §3.1 · Qué es», y «Código nativo y ELF»).

Cuando el que falla es el segundo, el síntoma en un dispositivo de 16 KB depende de la versión, y solo en una habla de alineación (reproducido):

  • Android 15 de salida: la aplicación instala y System.loadLibrary falla con dlopen failed: empty/missing DT_HASH/DT_GNU_HASH in "…" (new hash type from the future?); con un p_align de 8 KiB, peor: carga y muere en la primera llamada con Fatal signal 4 (SIGILL).
  • Android 16: carga en el modo de compatibilidad, con un aviso al usuario y pageSizeCompat=4 en dumpsys package. Solo si el manifiesto lo desactiva, o si el p_align no es exactamente 4 KiB, falla con "…" program alignment (4096) cannot be smaller than system page size (16384).

La matriz completa está en la «sección 6.4 de Código nativo y ELF · Qué pasa en el dispositivo».

5. apktool

⚠️ Nada de esta sección está reproducido: no se ha provocado ninguno de estos fallos para verlo. Las cadenas literales proceden del código fuente del proyecto, leído directamente en el commit 79b6338 del 10 de agosto de 2026 —rama main, que es la 3.x— y en los tags v2.7.0 y v2.9.3 para las variantes antiguas.

5.1 El aviso de versión, que es la mitad del problema

Las cadenas cambiaron entre 2.x y 3.x, y las que circulan por los foros son las antiguas: si se busca un mensaje y no aparece, casi siempre es esto.

2.x 3.x (main)
Paquete brut.androlib.err · clase CantFindFrameworkResException Paquete brut.androlib.exceptions · clase FrameworkNotFoundException
Can't find framework resources for package of id: 1 Could not find framework resources for package ID 1. + salto de línea + You must install proper framework files, see project website for more info.
Could not decode arsc file (sin punto) Could not decode arsc file. (con punto)
Invalid config flags detected. Dropping resources: Invalid resource config detected. Dropping resources: %s %s

⚠️ El texto de ayuda de --keep-broken-res en el Main.java de 3.x sigue citando la redacción 2.x del mensaje al que remite: está desactualizado en el propio proyecto.

5.2 Las excepciones de brut.androlib (3.x)

Clase Mensaje literal
AndrolibException Clase base, sin mensaje fijo. Extiende brut.common.BrutException
FrameworkNotFoundException Could not find framework resources for package ID <id>. + You must install proper framework files, see project website for more info.
InFileNotFoundException · OutDirExistsException · RawXmlEncounteredException · NinePatchNotFoundException Input file (<ruta>) was not found or was not readable. · Destination directory (<ruta>) already exists. Use -f option if you want to overwrite it. · Could not decode XML. · Could not find nine patch chunk.

InFileNotFoundException, OutDirExistsException y FrameworkNotFoundException se imprimen sin traza de pila y con salida 1; el resto, con traza. Y falta una más, UndefinedResObjectException, cuyo mensaje es estructurado —entry: pkgId=0x%02x, typeId=0x%02x, entryId=0x%04x, config=%s, overlayable: pkgId=0x%02x, name=%s— y dice exactamente qué identificador no resolvió.

Could not find framework resources for package ID 1. apktool no tiene el APK de framework para resolver los identificadores del espacio 0x01....... Trae empotrado el framework estándar, así que con aplicaciones normales de Play no pasa: pasa con las de sistema y de fabricante —Samsung, MIUI, HTC—. Arreglo: apktool if framework-res.apk, o apktool if -t miui framework-miui-res.apk y luego apktool d -t miui app.apk.

Could not decode arsc file. El lector de resources.arsc de apktool se atascó, y las issues cerradas identifican el patrón: el tamaño declarado de un chunk no corresponde al contenido. iBotPeaches en la #3036: «what appears to be happening is the reported size of a chunk is wrong … the chunks we are reading are exceeding the size that was reported of that chunk»; en la #2989: «The header chunk is lying about its reported size.» Segunda familia: las tablas sparse (#3199, #3298), donde el criterio correcto no es el orden de los chunks sino el propio flag de sparse, con el umbral de AOSP de al menos 1.000 recursos.

⚠️ --force-manual-size no existe y nunca ha existido en apktool: se buscó en los 2.376 commits de todas las ramas del repositorio, desde 2010, y en la documentación oficial, con cero apariciones. El control de sparse es la clave sparseResources de apktool.yml, no un flag; si una guía lo menciona, viene de un fork.

Qué comprobar: si aapt2 dump resources sí lee el fichero, el problema es de apktool y no de la tabla. Arreglos por coste: apktool d -r, --keep-broken-res, o APKEditor, que usa otro lector.

Invalid resource config detected. Dropping resources: … es un aviso, no un error: apktool ha encontrado una configuración que no entiende y está descartando esos recursos; la ejecución continúa y el resultado está incompleto. --keep-broken-res los conserva, y hay que arreglarlos a mano antes de reconstruir.

5.3 Fallos de reconstrucción por aapt2

Aquí está el detalle que hace perder más tiempo: apktool no aporta ningún mensaje propio. El envoltorio de aapt2 captura la BrutException y la reenvuelve en un AndrolibException(ex) sin mensaje, solo con la causa, así que lo que se ve es lo que produce brut.util.OS.exec: Execution failed (exit code = 1): [.../aapt2, link, -o, ..., --manifest, ...]. Los errores reales de aapt2 —los error: resource … is private, los Multiple resources— llegan por un camino distinto: un reenviador de flujos que los registra en el log a nivel warn. Por eso aparecen en el log y nunca en el texto de la excepción, y por eso buscar «Multiple resources» en el código de apktool no devuelve nada. Hay que subir el nivel de log y leer las líneas de aapt2.

Error de aapt2 Causa Arreglo
resource … is private El APK original referencia un recurso del framework marcado como privado. Es legal en un binario construido y aapt2 lo rechaza al enlazar, porque su trabajo es impedirlo apktool d -r, o APKEditor
Multiple resources / símbolos duplicados Identificadores que apktool no supo resolver y sustituyó por @null. Issue #2836: «This works for the first resource, but as you see becomes duplicate symbols» Actualizar apktool, o APKEditor

Dos datos más: apktool siempre pasa --legacy a aapt2 —comentario del fuente: «Treats error that used to be valid in aapt1 as warnings in aapt2»— y aapt1 ya no existe, Main.java responde Legacy aapt is no longer supported. desde 2025.

5.4 Lo que apktool no hace, y hay que saber

  • No firma. La FAQ oficial: «Apktool builds unsigned APKs. This means some directories and files like META-INF are missing.»
  • No alinea. Verificado por inspección del código fuente de la rama main: no hay ninguna referencia a zipalign, a alineación de página ni a 16.384. Registra qué entradas van stored en doNotCompress y las escribe sin comprimir, pero nunca las alinea: zipalign es estrictamente externo.
  • --copy-original solo sirve para v1 (FAQ), y --match-original impide reconstruir: la ayuda dice literalmente Keep files closest to original as possible (prevents rebuild).
  • Hay APK que no soporta: los construidos con herramientas modificadas para OEM concretos «are not built with regular AOSP tools and are not compatible with Apktool».

6. jadx

⚠️ Nada de esta sección está reproducido: no se ha provocado ninguno de estos fallos para verlo. Las cadenas literales proceden del código fuente del proyecto, leído directamente en el commit e738a26 del 5 de agosto de 2026, con fichero y línea identificados.

6.1 Corrección previa: dos cadenas que circulan y no existen

Cadena que circula Realidad
// decompilation failed No existe en jadx. No aparece en ninguna parte del árbol. Lo más parecido es "Class decompilation failed", mensaje de una JadxRuntimeException del modo --single-class, no un comentario del código generado
// JADX WARNING: inconsistent code La forma real es /* JADX WARN: <mensaje> */. Y no se puede buscar en el fuente: se compone en ejecución como "JADX " + nivel.name() + ": " en JadxCommentsAttr, así que grepear el repositorio no devuelve nada y lleva a concluir que es falsa

⚠️ La «sección 3.5 de Decompiladores a Java · Los modos de fallo, y qué significa cada uno» cita las dos formas incorrectas. Los modos de fallo que describe son correctos; los literales, no.

6.2 Los marcadores reales, con su fichero

Literal Dónde se emite Qué significa
/* JADX ERROR: <msg> … */ (con dos espacios tras /*) codegen/utils/CodeGenUtils.java:48 Error duro en ese nodo. Si hay causa, la traza de pila Java completa va incrustada dentro del comentario
/* JADX WARN: … */, /* JADX INFO: … */, /* JADX DEBUG: … */ JadxCommentsAttr + CodeGenUtils.addComments Aviso del nivel correspondiente. Por defecto --comments-level es info: WARN e INFO se emiten, DEBUG no
Code decompiled incorrectly, please refer to instructions dump. + To view partially-correct add '--show-bad-code' argument codegen/MethodGen.java:114-127 El peligroso. Ver «§6.3 · Code decompiled incorrectly — el que hay que tomarse en serio». La falta de la palabra «code» en el segundo está en el fuente, no es errata de este documento
throw new UnsupportedOperationException("Method not decompiled: …") MethodGen.java:365-373 El método no se reconstruyó; se emite un lanzamiento para que el fichero siga siendo parseable
Method dump skipped, instruction units count: <n> + To view this dump add '--comments-level debug' option MethodGen.java:381 y :385 El volcado se omitió por tamaño; el umbral es más de 200 unidades de instrucción
// Can't load method instructions: <msg> / // Can't load method instructions. · // Blocks not ready for simple mode, using fallback MethodGen.java:402, :410 · :308 No se pudieron decodificar las instrucciones · cayó al modo de respaldo
Load error · Code generation failed RootNode.java:192 · ClassNode.java:409 Fallo al cargar la entrada · ver «§6.4 · Un fichero .java cuyo contenido entero es una traza de pila»

Mensajes representativos tras JADX WARN:, verbatim del fuente: Multi-variable type inference failed, Type inference incomplete: some casts might be missing, Unreachable blocks removed: , Failed to restore switch over string. Please report as a decompilation issue.

6.3 Code decompiled incorrectly — el que hay que tomarse en serio

No es un comentario de una línea: es un bloque de tres.

/*
    Code decompiled incorrectly, please refer to instructions dump.
    To view partially-correct add '--show-bad-code' argument
*/

Significa que jadx llegó a producir Java y luego detectó que su propia reconstrucción no es coherente con el grafo de partida; por defecto no lo enseña y sustituye el cuerpo por el volcado.

El mecanismo, que aclara la confusión habitual: ClassGen.addMethodCode tiene tres niveles. Aviso (addWarn) pone la bandera INCONSISTENT_CODE, añade el comentario JADX WARN y suma al contador — un solo aviso ya basta para que el método caiga al modo de respaldo. Error (addError) añade el atributo JADX_ERROR y fuerza el respaldo igualmente. El fallo de clase entera es «§6.4 · Un fichero .java cuyo contenido entero es una traza de pila». Y --show-bad-code no arregla nada: solo cambia la bandera para que se imprima el Java parcialmente reconstruido —que puede ser incorrecto— en lugar del volcado. Es la opción correcta para auditar y la peligrosa para confiar.

Y el dato que corrige un error extendido: jadx no emite smali en el fichero .java. Lo que pone en el modo de respaldo es su volcado de instrucciones interno, en su propia representación; el smali de verdad existe solo como superficie de API —JavaClass.getSmali()—, alimenta la pestaña «Smali» de jadx-gui y nunca aparece en la salida de la CLI.

6.4 Un fichero .java cuyo contenido entero es una traza de pila

La clase entera falló. ClassNode.generateClassCode() captura StackOverflowError | Exception, registra "Code generation failed" y devuelve como contenido del fichero la traza de pila en crudo. Un pipeline que cuente .java producidos concluirá que la clase se decompiló.

6.5 Salida vacía, cuelgues y OutOfMemoryError

Causa raíz, con explicación del mantenedor. La issue #2676, cerrada: «errors during instructions decoding handled per single instruction, but there are several big methods (~100K instruction units) with errors at almost all instructions, so all these errors are collected to be added into generated code later (JADX_ERROR attribute). This is very slow and eats a lot of memory.» Es decir: un DEX con anti-análisis produce un error por instrucción, y acumularlos mata el proceso. La #2744 es análoga para código muy ofuscado. Arreglos: JAVA_OPTS="-Xmx8g", -j 4, excluir paquetes, jadx-gui con su caché en disco, o -m fallback.

6.6 Los códigos de salida, que sí sirven para automatizar

Código Condición
0 Éxito
1 JadxArgsValidateException (Incorrect arguments: {}) o cualquier Throwable (Process error:)
2 No se cargó ninguna clase y se pasó --no-res: Load failed! No classes for decompile!
3 Decompiló, pero con errores. Imprime el informe y luego finished with errors, count: {}

El 3 es el que importa: hay salida, y parte de ella son trazas de pila y volcados de instrucciones. Un 0, en cambio, no garantiza cobertura completa: los avisos no cuentan como error y ya bastan para que un método caiga al modo de respaldo. El informe previo lo emite ErrorsCounter.printReport() con los formatos "{} errors occurred in following nodes:" y "{} warnings in {} nodes".

7. Fusión de splits

Síntoma Causa raíz Qué comprobar
NoClassDefFoundError en ejecución Falta un DEX, la renumeración dejó un hueco, o el DEX de un feature module quedó delante de los del base y sombreó una clase unzip -l final.apk | grep -oE "classes[0-9]*\.dex" | sort -V — consecutiva y sin huecos
Los textos salen en inglés; los iconos, borrosos La tabla de recursos no se fusionó. Los ficheros de res/ se copiaron y ninguna entrada apunta a ellos aapt2 dump badging final.apk | grep -E "^(locales|densities|native-code)". Si locales es solo '--_--', no se fusionó
Un módulo de funcionalidad no está · dos Package con el mismo id= Se descartaron los splits con isFeatureSplit=true o su paquete propio · colisión real de package ID aapt2 dump resources final.apk | grep "^Package" debe listar dos paquetes con id distintos. La colisión es un aborto, no un aviso
INSTALL_FAILED_MISSING_SPLIT sobre el fusionado Quedaron atributos de split en el manifiesto «§2.3 · Los cinco que hay que conocer al detalle» y «Fusión de splits → §5 · Saneado del manifiesto»
No instala: INSTALL_FAILED_INVALID_APK: Failed to extract native libraries, res=-2 extractNativeLibs="false" con las .so comprimidas o sin alinear a la página zipalign -c -P 16 4, y comprobar compress_type de las entradas lib/
Instala y muere en System.loadLibrary con UnsatisfiedLinkError No se fusionó el split de ABI: el APK no trae lib/<abi>/ para el dispositivo unzip -l final.apk | grep ' lib/' — tiene que estar la ABI del dispositivo
Arranca y se cierra sin traza La aplicación consulta sus propios splits en ejecución «§9.1 · La aplicación instala, arranca y se cierra». No tiene arreglo dentro del alcance de esta documentación

El dato que acota el problema: cruzando todas las rutas de todos los splits de cuatro contenedores del muestrario, los conflictos reales son como mucho seis ficheros —manifiesto, tabla, los tres de firma y classes.dex con feature modules—; nada de res/, lib/ ni assets/ colisiona con contenido distinto («Fusión de splits → §2.2 · Qué colisiona de verdad: medido»).

8. Formatos

8.1 El fichero no es lo que dice ser (reproducido)

Qué pasa aapt2 dice apksigner dice
No es un ZIP error: failed opening zip: Invalid file. ApkFormatException: Malformed APK: not a ZIP archive
Es un ZIP sin AndroidManifest.xml error: could not identify format of APK. ApkFormatException: Missing AndroidManifest.xml

La segunda fila aparece constantemente al pasar el contenedor —un .xapk, un .apkm— a una herramienta que espera un APK; la detección estructural del paso 0 de «Pipeline de análisis» lo resuelve antes de llegar aquí.

8.2 resources.arsc que no parsea (reproducido)

Provocado inflando el campo size del chunk raíz, y truncando el fichero:

size_grande.apk: error: corrupt resources.arsc: chunk's data extends past the end of the document (type=02 header_size=12 size=4572644).
truncado.apk:   error: corrupt resources.arsc: chunk's data extends past the end of the document (type=02 header_size=12 size=3572644).

Un chunk declara un tamaño mayor que lo que queda de fichero: la misma familia de fallo que las issues de apktool de «§5.2 · Las excepciones de brut.androlib (3.x)», y el mensaje da los tres campos de la cabecera, que es lo que hace falta para localizarlo con xxd. Lo que aapt2 tolera y sorprende: con packageCount = 99 en un fichero de un paquete, aapt2 dump resources lo lee entero sin protestar —su lector no confía en ese contador, recorre hasta que se acaban los datos—. Un lector propio que sí confíe en él leerá fuera de rango.

El caso legítimo que rompe lectores estrictos: cinco splits del SAI de Netflix traen un resources.arsc de 40 bytes exactos:

00000000: 0200 0c00 2800 0000 0000 0000 0100 1c00  ....(...........
00000010: 1c00 0000 0000 0000 0000 0000 0001 0000  ................
00000020: 1c00 0000 0000 0000                      ........

RES_TABLE_TYPE = 0x0002, headerSize = 12, size = 40, packageCount = 0, seguido de un string pool vacío de 28 bytes. Es una tabla válida: aapt2 dump resources responde solo Binary APK y sale con 0. Pero rompe cualquier lector que asuma al menos un paquete. Y en el mismo contenedor, split_voip.apk no tiene resources.arsc en absoluto.

8.3 AXML malformado (reproducido)

Los tres mensajes salen del mismo código de AOSP (ResourceType.cpp) que usa el dispositivo, así que reproducirlos con aapt2 predice lo que dirá el instalador:

Provocado Salida literal
size del chunk raíz mayor que el fichero W ResourceType: Bad XML block: header size 8 or total size 25572 is larger than data size 21476
Fichero truncado a la mitad W ResourceType: Bad XML block: header size 8 or total size 21476 is larger than data size 10738
Tipo del chunk raíz cambiado a 0x00FF W ResourceType: Bad XML block: expected root block type 3, got 255

Los tres van seguidos de <fichero>: error: failed to parse binary AndroidManifest.xml: failed to initialize ResXMLTree., que en el dispositivo es INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION (−102): ApkAssets.openXml() lanza FileNotFoundException con "Corrupt XML binary file" y el catch general de parseApkLiteInner() la convierte en ese código.

8.4 DEX con checksum inválido (reproducido)

Corrompiendo los bytes 8 a 11 —el Adler-32— de un classes.dex del muestrario y volviendo a ejecutar dexdump -f:

E dexdump : dexdump.cc:2037 Failure to verify dex file 'checksum_malo.dex': Bad checksum (73027441, expected efbeadde)

Cómo se lee. El primer número es el checksum calculado sobre el contenido; el segundo, precedido de expected, es el que estaba escrito en la cabecera — la redacción es contraintuitiva, porque expected es lo que el fichero afirma, no lo que debería ser.

El detalle que enseña la estructura. Corrompiendo en cambio la signature SHA-1 —bytes 12 a 31— el mensaje es también de checksum, Bad checksum (709a6bcb, expected 73027441), porque el Adler-32 cubre todo lo que va detrás del byte 11, incluida la signature: tocar la firma rompe primero el checksum. Y con la magic rota, Expected valid zip or dex file. Aparece al editar un DEX a mano con un editor hexadecimal sin recalcular la cabecera — el ensamblador smali la recalcula solo; un sed sobre el binario, no.

9. Síntomas de protección activa que se confunden con errores de herramienta

Los cuatro comparten una propiedad: el pipeline no falla en ningún paso y el problema aparece en ejecución. Aquí se detectan y se informan; neutralizarlos está fuera del alcance de esta documentación, y el mecanismo de cada uno está en «Anti-análisis y hardening» y «Packers».

9.1 La aplicación instala, arranca y se cierra

No es un fallo del reempaquetado: el APK verifica, alinea e instala. Suele ser una comprobación de integridad en ejecución: la aplicación compara la huella SHA-256 de su propio certificado —vía PackageManager.getPackageInfo(..., GET_SIGNING_CERTIFICATES)— contra una constante, o el digest de una entrada del ZIP, o consulta una atestación remota. Refirmar cambia la huella por construcción, así que la comprobación falla siempre, y que cambió se ve con apksigner verify --print-certs sobre el original y sobre el modificado. Otra variante: un APK fusionado en el que la aplicación consulta sus propios splits (ApplicationInfo.splitSourceDirs, splitNames, SplitInstallManager) y los encuentra vacíos —la issue #211 de APKEditor, abierta en septiembre de 2025, recoge una veintena de aplicaciones fusionadas desde Play que caen con segfault en libpairipcore.so en Android 14 y 15, aunque ese caso concreto es antimanipulación de PairIP y no la consulta de splits (ver «Fusión de splits → §10.5 · Aplicaciones que comprueban su propia estructura de splits»).

9.2 Funciona en emulador y no en dispositivo, o al revés

Detección de entorno: anti-emulador —ro.product.model, ro.kernel.qemu, /dev/qemu_pipe, el número de serie, los sensores— o, al revés, anti-debug/anti-root que solo se dispara en el dispositivo real. Los indicadores sobreviven a la ofuscación porque son cadenas que llegan al sistema, así que dexdump -d app.apk | grep -oE "ro\.kernel\.qemu|/dev/qemu_pipe|goldfish|TracerPid" basta, sobre el APK para que entren todos los classes*.dex.

9.3 Decompila, pero el código no tiene sentido

Cuatro causas que hay que separar antes de sacar conclusiones:

Lo que se ve Causa Cómo se distingue
Métodos con Code decompiled incorrectly o JADX ERROR Límite del decompilador El marcador está ahí y se ve. Es «§6 · jadx»
La aplicación habla por red y no hay ni una URL en el DEX Cifrado de strings de un ofuscador comercial dexdump -d | grep -oE 'https?://…' no devuelve casi nada
Todo el flujo es un switch gigante dentro de un while Aplanamiento de control de flujo La estructura es uniforme en muchos métodos a la vez
classes.dex minúsculo, un .so grande y un assets/ opaco Packer: lo decompilado es el stub «§6.4 de Pipeline de análisis · Comprobación 3 — la forma del DEX»

La regla. Antes de concluir «este código es raro» hay que descartar que el problema sea el decompilador con el recuento de «§6.6 · Los códigos de salida, que sí sirven para automatizar»: un método marcado como incoherente no es evidencia de nada.

9.4 Funciona la primera vez y luego no

Un packer que descifra el payload y lo cachea, o una atestación remota que ya ha reportado el dispositivo. Lo relevante es lo negativo: no es un fallo de reempaquetado, porque un APK mal reempaquetado falla de forma determinista.

10. Árbol de diagnóstico

   ¿QUÉ HA PASADO?
        │
        ├─▶ NO INSTALA ───────────────────────────────────────────────────
        │     ¿el mensaje empieza por INSTALL_PARSE_FAILED_?
        │       sí → el fichero no se pudo leer                      §2.2
        │            NO_CERTIFICATES / INCONSISTENT_CERT ─▶ firma     §3
        │            MANIFEST_MALFORMED ─▶ estructura del manifiesto  §2.3
        │            RESOURCES_ARSC_COMPRESSED ─▶ nada local lo ve    §2.3
        │       no → el sistema lo rechaza                            §2.4
        │            UPDATE_INCOMPATIBLE ─▶ refirmado: desinstalar
        │            MISSING_SPLIT ─▶ install-multiple, o sanear manifiesto
        │            INVALID_APK "Full install must include a base package"
        │                                  ─▶ split= sobrante         §2.3
        │            NO_MATCHING_ABIS ─▶ falta el split de ABI
        │            INVALID_APK "Failed to extract native libraries"
        │                                  ─▶ .so comprimidas         §7
        │     ¿ni siquiera llega a un código INSTALL_*? ─▶ apksigner  §3.1
        │
        ├─▶ INSTALA Y PETA ───────────────────────────────────────────────
        │     ¿arranca y se cierra sin traza?     ─▶ integridad en ejec. §9.1
        │     ¿muere en System.loadLibrary?       ─▶ ABI sin fusionar    §7
        │     ¿NoClassDefFoundError?              ─▶ DEX mal renumerado   §7
        │     ¿falla solo la pantalla parcheada?  ─▶ verificación de ART:
        │                                            banco de registros
        │                                            (02-modificación §5.2)
        │     ¿faltan textos o iconos?            ─▶ tabla sin fusionar   §7
        │     ¿va en emulador y no en real?       ─▶ detección de entorno §9.2
        │
        ├─▶ NO DECOMPILA / NO DESEMPAQUETA ───────────────────────────────
        │     apktool "Could not find framework resources" ─▶ apktool if §5.2
        │             "Could not decode arsc file"         ─▶ -r, o APKEditor
        │             "Execution failed (exit code = 1)"    ─▶ log de aapt2 §5.3
        │     jadx    exit 2, "No classes for decompile"    ─▶ ¿es un APK? §8.1
        │             OutOfMemoryError o cuelgue            ─▶ -Xmx, -j    §6.5
        │     aapt2   "could not identify format of APK" ─▶ es el contenedor §8.1
        │             "corrupt resources.arsc" §8.2 · "ResXMLTree" §8.3
        │
        └─▶ DECOMPILA MAL ────────────────────────────────────────────────
              PRIMERO: contar los marcadores                    §6.2, §6.6
              ¿jadx salió con 3? → hay huecos, y sabe cuántos
              ¿"Code decompiled incorrectly"? → ese Java puede mentir §6.3
              ¿el .java entero es una traza de pila? → clase fallida  §6.4
              descartado el decompilador:
              ¿ni una URL en el DEX?          ─▶ strings cifrados     §9.3
              ¿switch gigante en todos lados? ─▶ flujo aplanado       §9.3
              ¿DEX minúsculo + .so grande?    ─▶ packer: es el stub   §9.3
              ¿nombres a.b.c?                 ─▶ R8: buscar mapping.txt

Fuentes

Todas las URL se consultaron el 12 de agosto de 2026, salvo donde se indica otra fecha. Los ficheros de AOSP de las fuentes 1 y 2 son de la etiqueta android-17.0.0_r1, se consultaron el 25 de septiembre de 2026 y cuelgan de https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/, descargados con ?format=TEXT y decodificados.

  1. AOSP — core/java/android/content/pm/PackageManager.java · https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/core/java/android/content/pm/PackageManager.java · íntegras las tablas de «§2.2» y «§2.4» con sus valores y descripciones, la marca @hide de todos, y la comprobación de que INSTALL_FAILED_MULTI_ARCH_NOT_SUPPORTED no existe.
  2. AOSP — los cinco ficheros que producen los errores de «§2.3» y «§2.4», bajo la misma raíz: core/java/android/content/pm/parsing/ApkLiteParseUtils.java (sitios y mensajes de NOT_APK, BAD_MANIFEST, UNEXPECTED_EXCEPTION y MANIFEST_MALFORMED); core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java (las reglas de validateName() y el límite de 223 caracteres); core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java junto a core/java/android/content/pm/parsing/result/ParseInput.java (el error diferido RESOURCES_ARSC_COMPRESSED, su ChangeId 132742131 con @EnabledAfter(targetSdkVersion = Q) y el texto del error); core/java/android/util/apk/ApkSignatureVerifier.java (NO_CERTIFICATES e INCONSISTENT_CERTIFICATES); y services/core/java/com/android/server/pm/ con PackageInstallerSession.java, InstallPackageHelper.java y PackageManagerServiceUtils.java (MISSING_SPLIT, UPDATE_INCOMPATIBLE y VERSION_DOWNGRADE, incluido el TODO del propio AOSP sobre la reutilización del segundo).
  3. jadx — código fuente, commit e738a26 del 5 de agosto de 2026, repositorio clonado y leído directamente: codegen/MethodGen.java, codegen/utils/CodeGenUtils.java, dex/attributes/nodes/JadxCommentsAttr.java, dex/nodes/ClassNode.java, utils/ErrorsCounter.java y jadx-cli/…/JadxCLI.java · https://github.com/skylot/jadx/blob/master/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java · todos los literales de «§6.2 · Los marcadores reales, con su fichero», el mecanismo de tres niveles de «§6.3 · Code decompiled incorrectly — el que hay que tomarse en serio», el comportamiento de clase fallida de «§6.4 · Un fichero .java cuyo contenido entero es una traza de pila» y los códigos de salida de «§6.6 · Los códigos de salida, que sí sirven para automatizar».
  4. jadx — issues cerradas #2676 y #2744 · https://github.com/skylot/jadx/issues/2676 · las explicaciones de causa raíz de skylot sobre el consumo de memoria por error-por-instrucción y el desbordamiento de pila en la inferencia de tipos («§6.5 · Salida vacía, cuelgues y OutOfMemoryError»), citadas literalmente.
  5. Apktool — código fuente, commit 79b6338 del 10 de agosto de 2026 (rama main, 3.x), repositorio clonado · https://github.com/iBotPeaches/Apktool/tree/main/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions y brut.j.util/src/main/java/brut/util/OS.java · la tabla de «§5.2 · Las excepciones de brut.androlib (3.x)», el mensaje Execution failed (exit code = …) de «§5.3 · Fallos de reconstrucción por aapt2», el uso de --legacy, Legacy aapt is no longer supported. y la comprobación por inspección de que apktool no alinea («§5.4 · Lo que apktool no hace, y hay que saber»).
  6. Apktool — tags v2.7.0 y v2.9.3 · https://raw.githubusercontent.com/iBotPeaches/Apktool/v2.7.0/brut.apktool/apktool-lib/src/main/java/brut/androlib/err/CantFindFrameworkResException.java · las cadenas 2.x de «§5.1 · El aviso de versión, que es la mitad del problema».
  7. Apktool — issues cerradas #3036, #2989, #2824, #2836, #3199 y #3298 · https://github.com/iBotPeaches/Apktool/issues/3036 · las explicaciones de causa raíz de iBotPeaches sobre tamaños de chunk mentirosos, entradas duplicadas y tablas sparse («§5.2», «§5.3»). La ausencia de --force-manual-size se verificó sobre los 2.376 commits de todas las ramas y sobre https://apktool.org/docs/cli-parameters/.
  8. Apktool — FAQ oficial · https://apktool.org/docs/faq/ · las citas literales de «§5.4 · Lo que apktool no hace, y hay que saber».
  9. Ejecuciones locales sobre el muestrario — 12 de agosto de 2026 con build-tools 37.0.0 (apksigner 0.9, zipalign, aapt2 2.20-15087165, dexdump), OpenJDK 21.0.10 y Python 3.12.10 sobre macOS Darwin 25.5.0. De aquí salen todos los mensajes marcados (reproducido): «§2.3», «§3.2» a «§3.5», «§4.1», «§4.2» y «§8.1» a «§8.4 · DEX con checksum inválido (reproducido)». Provocados sobre copias de org.fossify.calculator_1.4.0.apk y sobre com.netflix.mediaclient.sai.zip en el scratchpad. En ningún momento se ha escrito en el muestrario.
  10. AOSP — etiqueta android-16.0.0_r1, bajo https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-16.0.0_r1/ · Consultado el 25 de septiembre de 2026. De PackageManagerServiceUtils.java (verifySignatures()) e InstallPackageHelper.java (preparePackage()) sale el mensaje signatures do not match newer version; ignoring! («sección 2.4 · Fallos de instalación»); de PackageInstallerSession.java (validateApkInstallLocked()) y ApkLiteParseUtils.java, que los atributos de split sobrantes dan Full install must include a base package o Missing split for y no MANIFEST_MALFORMED (secciones «2.3» y «10»); y de core/jni/android_content_res_ApkAssets.cpp junto a parseApkLiteInner(), que un AXML corrupto da UNEXPECTED_EXCEPTION («sección 8.3 · AXML malformado (reproducido)»).
  11. Ejecución local — 25 de septiembre de 2026, unzip -l sobre org.fossify.calculator_1.4.0.apk: lleva entradas en META-INF/ (85 .version de AndroidX, entre otras) y ninguna de firma v1, así que grep META-INF no sirve para detectar la firma («sección 3.2 · Bloque desaparecido frente a contenido modificado (reproducido)»).