Diagnóstico de fallos
1. Cómo usar este catálogo
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 installen un emulador Android 14arm64-v8a, conAPKde prueba construidos conaapt2y firmados conapksigner, el 25 de septiembre de 2026.INSTALL_FAILED_NO_MATCHING_ABISse 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 etiquetaandroid-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
APKsin firmar no dice que le falte la firma.Attempt to get length of null arrayes el texto de una excepción interna, no un mensaje pensado para nadie. - Las dos entradas duplicadas no llegan a la firma. Las rechaza
libziparchiveal abrir el fichero, y lo que queda enlogcatesZip: Found duplicate entry AndroidManifest.xmlyFailed to open APK '…': Duplicate entries in archive. - Con el
.idsigde otroAPK, 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 0yFailed to open APK '…': I/O error. La verificación v4 que describe el código, y que acabaría enNO_CERTIFICATES, no llega a ejecutarse. Sin--incremental, el.idsigse ignora y elAPKse instala por v2 o v3. adb installsin-rtambién reemplaza. Instalar dos veces el mismoAPKrespondeSuccesslas 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 mismatches contenido modificado —«alguien tocó el fichero»—;Signature stripped?yMissing META-INF/MANIFEST.MFson 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.loadLibraryfalla condlopen failed: empty/missing DT_HASH/DT_GNU_HASH in "…" (new hash type from the future?); con unp_alignde 8 KiB, peor: carga y muere en la primera llamada conFatal signal 4 (SIGILL). - Android 16: carga en el modo de compatibilidad, con un aviso al usuario y
pageSizeCompat=4endumpsys package. Solo si el manifiesto lo desactiva, o si elp_alignno 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
79b6338del 10 de agosto de 2026 —ramamain, que es la 3.x— y en los tagsv2.7.0yv2.9.3para 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 azipalign, a alineación de página ni a 16.384. Registra qué entradas vanstoredendoNotCompressy las escribe sin comprimir, pero nunca las alinea:zipalignes estrictamente externo. --copy-originalsolo sirve para v1 (FAQ), y--match-originalimpide reconstruir: la ayuda dice literalmenteKeep files closest to original as possible (prevents rebuild).- Hay
APKque 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
e738a26del 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.
- 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@hidede todos, y la comprobación de queINSTALL_FAILED_MULTI_ARCH_NOT_SUPPORTEDno existe. - 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 deNOT_APK,BAD_MANIFEST,UNEXPECTED_EXCEPTIONyMANIFEST_MALFORMED);core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java(las reglas devalidateName()y el límite de 223 caracteres);core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.javajunto acore/java/android/content/pm/parsing/result/ParseInput.java(el error diferidoRESOURCES_ARSC_COMPRESSED, suChangeId 132742131con@EnabledAfter(targetSdkVersion = Q)y el texto del error);core/java/android/util/apk/ApkSignatureVerifier.java(NO_CERTIFICATESeINCONSISTENT_CERTIFICATES); yservices/core/java/com/android/server/pm/conPackageInstallerSession.java,InstallPackageHelper.javayPackageManagerServiceUtils.java(MISSING_SPLIT,UPDATE_INCOMPATIBLEyVERSION_DOWNGRADE, incluido elTODOdel propio AOSP sobre la reutilización del segundo). - jadx — código fuente, commit
e738a26del 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.javayjadx-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». - 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.
- Apktool — código fuente, commit
79b6338del 10 de agosto de 2026 (ramamain, 3.x), repositorio clonado · https://github.com/iBotPeaches/Apktool/tree/main/brut.apktool/apktool-lib/src/main/java/brut/androlib/exceptions ybrut.j.util/src/main/java/brut/util/OS.java· la tabla de «§5.2 · Las excepciones de brut.androlib (3.x)», el mensajeExecution 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»). - Apktool — tags
v2.7.0yv2.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». - 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-sizese verificó sobre los 2.376 commits de todas las ramas y sobre https://apktool.org/docs/cli-parameters/. - Apktool — FAQ oficial · https://apktool.org/docs/faq/ · las citas literales de «§5.4 · Lo que apktool no hace, y hay que saber».
- Ejecuciones locales sobre el muestrario — 12 de agosto de 2026 con
build-tools37.0.0 (apksigner0.9,zipalign,aapt22.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 deorg.fossify.calculator_1.4.0.apky sobrecom.netflix.mediaclient.sai.zipen el scratchpad. En ningún momento se ha escrito en el muestrario. - 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. DePackageManagerServiceUtils.java(verifySignatures()) eInstallPackageHelper.java(preparePackage()) sale el mensajesignatures do not match newer version; ignoring!(«sección 2.4 · Fallos de instalación»); dePackageInstallerSession.java(validateApkInstallLocked()) yApkLiteParseUtils.java, que los atributos de split sobrantes danFull install must include a base packageoMissing split fory noMANIFEST_MALFORMED(secciones «2.3» y «10»); y decore/jni/android_content_res_ApkAssets.cppjunto aparseApkLiteInner(), que unAXMLcorrupto daUNEXPECTED_EXCEPTION(«sección 8.3 · AXML malformado (reproducido)»). - Ejecución local — 25 de septiembre de 2026,
unzip -lsobreorg.fossify.calculator_1.4.0.apk: lleva entradas enMETA-INF/(85.versionde AndroidX, entre otras) y ninguna de firma v1, así quegrep META-INFno sirve para detectar la firma («sección 3.2 · Bloque desaparecido frente a contenido modificado (reproducido)»).