Evidencia β focus-02-protobuf-mlmodel-deserialization
Verificacion empirica realizada 2026-07-22 por verify-agent, FASE 4. Todo lo de aqui se ejecuto
en targets/coremltools/verify/venv-* (venvs locales, pip install coremltools/pip install protobuf==X), nunca contra infraestructura de Apple ni de terceros. Ningun dato real de vendor
implicado β todos los .mlmodel son sintaticamente validos pero triviales, construidos por script.
Entorno
targets/coremltools/verify/venv-protobuf/βpip install coremltoolslimpio, tal cual lo obtendria hoy cualquiera (coremltools==9.0, arrastraprotobuf==7.35.1, backend por defecto upb (acelerado)).targets/coremltools/verify/venv-cve-old/β mismocoremltools==9.0, protobuf forzado a4.25.7(ANTES del fix de CVE-2025-4565/GHSA-8qvm-5x2c-j2w7).targets/coremltools/verify/venv-cve-new/β mismocoremltools==9.0, protobuf forzado a4.25.8(DESPUES del fix).- Python 3.12.10 (
py -3.12), Windows 11.
Confirmacion de backend/version (Paso 2 del plan del operador):
$ ./verify/venv-protobuf/Scripts/python.exe -m pip show coremltools protobuf
Name: coremltools Version: 9.0
Name: protobuf Version: 7.35.1
$ ./verify/venv-protobuf/Scripts/python.exe -c "from google.protobuf.internal import api_implementation; print(api_implementation.Type())"
upb
-> Confirmado: un pip install coremltools normal hoy (2026-07-22) resuelve protobuf 7.35.1 con
el backend acelerado upb por defecto. Esto es la condicion mas comun para cualquier usuario que
instale desde PyPI en una plataforma con wheel prebuilt (x86_64/arm64, Windows/Linux/macOS
estandar).
Scripts de PoC
build_payloads.pyβ construye, con los stubs*_pb2.pydecoremltools.proto(sinprotoc), tres familias de payload con profundidad de anidamiento configurable:pipeline:Model.pipeline.models[0] -> Model -> pipeline.models[0] -> ...mil_block:Function.block_specializations["CoreML5"].operations[i] -> Operation.blocks[j] -> Block.operations[i] -> ...(cicloMILSpec.Operation.blocks/Block.operations)nn_branch:NeuralNetwork.layers[i].branch.ifBranch(que es de nuevo un mensajeNeuralNetworkcompleto) encadenado -> cicloNeuralNetwork.BranchLayer- Nota tecnica documentada en el propio script: construir/serializar cadenas muy profundas
(>=~2000-5000 niveles) con el backend acelerado
upbpuede hacer CRASHEAR nativamente el proceso Python entero (sin traceback) durante la CONSTRUCCION, un fenomeno DISTINTO al que se esta verificando (que es el comportamiento del PARSEO/deserializacion sobre bytes ya serializados, que es el escenario real: el atacante construye el fichero una vez, offline, por el medio que sea, y la victima solo llamaParseFromString/load_specsobre bytes ya hechos). Para que la fase de CONSTRUCCION nunca sea el cuello de botella bajo prueba, el script fuerzaPROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python+ un hilo con pila de 64MB +sys.setrecursionlimitalto SOLO para construir los payloads; los bytes resultantes son formato wire estandar, independientes del backend que los genero.
test_parse.pyβ carga un.mlmodelconcoremltools.utils.load_spec()(o, con--raw, directamenteModel_pb2.Model().ParseFromString()para aislar el parseo puro) en un hilo worker con pila de tamano POR DEFECTO (deliberadamente NO agrandada, para reproducir fielmente lo que le pasaria a un hilo normal de una aplicacion que procese un fichero de un atacante), e imprime backend/version de protobuf activos y el resultado exacto (OK /DecodeError/RecursionError/ excepcion generica).
Payloads generados (out/*.mlmodel): profundidades 1, 5, 10, 20, 30, 40, 45, 48, 49, 50, 150,
1000, 5000 para pipeline; 50, 150, 1000, 5000 para mil_block y nn_branch. Todos entre 400
bytes y ~102KB β confirma la afirmacion del hallazgo original: "una cadena de unos pocos
cientos/miles de mensajes es minuscula en bytes serializados".
Resultado A β Backend por defecto (upb, protobuf 7.35.1) β caso comun pip install
Log completo: results/A_default_backend_upb.log y busqueda de frontera exacta en
results/A2_boundary_search.log.
| depth (pipeline) | resultado |
|---|---|
| 1..49 | OK, parsea sin error |
| 50 | DecodeError: ... Exceeded upb_DecodeOptions_MaxDepth |
| 150, 1000, 5000 | mismo DecodeError manejable |
Mismo resultado (DecodeError: Exceeded upb_DecodeOptions_MaxDepth) en las 3 familias
(pipeline, mil_block, nn_branch) en todas las profundidades >=50.
-> Confirmado exactamente lo que predecia el hallazgo: el backend acelerado impone un limite
de anidamiento de facto en torno a 100 niveles del arbol protobuf (aqui, como cada "depth" de mi
constructor produce 2 niveles de anidamiento -- el campo pipeline/branch y el mensaje
Model/NeuralNetwork anidado -- la frontera cae en depth=50, es decir, ~99-100 niveles reales),
y la excepcion resultante (google.protobuf.message.DecodeError) es manejable/capturable por
cualquier codigo llamador razonable. No hay DoS real en este escenario, que es el mas comun.
Resultado B β Backend puro-Python FORZADO + protobuf 4.25.7 (VULNERABLE, antes del fix)
Log completo: results/B_vulnerable_protobuf_4.25.7_pure_python.log.
| depth | pipeline | mil_block | nn_branch |
|---|---|---|---|
| 50 | OK | OK | OK |
| 150 | OK | OK | OK |
| 1000 | RecursionError: maximum recursion depth exceeded | RecursionError | RecursionError |
| 5000 | RecursionError: maximum recursion depth exceeded | RecursionError | RecursionError |
-> Confirmado el mecanismo central del hallazgo: con el backend puro-Python (forzado via
PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python) y una version de protobuf sin el fix de
CVE-2025-4565, load_spec() deja escapar un RecursionError no capturado por protobuf ni por
coremltools (no es subclase de DecodeError; ningun except DecodeError en codigo de la
victima lo atrapa) en las 3 familias de ciclo recursivo identificadas en el hallazgo original.
Control adicional (results/E_raw_parsefromstring_vs_load_spec.log): el mismo RecursionError se
reproduce con Model_pb2.Model().ParseFromString() puro, sin pasar por load_spec() β confirma
que coremltools no aΓ±ade ninguna mitigacion propia (ni try/except, ni limite de profundidad
propio) alrededor del parseo.
Resultado C β Backend puro-Python FORZADO + protobuf 4.25.8 (PARCHEADO, con el fix)
Log completo: results/C_patched_protobuf_4.25.8_pure_python.log.
| depth | pipeline | mil_block | nn_branch |
|---|---|---|---|
| 50 | DecodeError: too many levels of nesting |
idem | idem |
| 150 | idem | idem | idem |
| 1000 | idem | idem | idem |
| 5000 | idem | idem | idem |
-> Control positivo confirmado: la MISMA condicion (backend puro-Python forzado) pero con
protobuf 4.25.8 convierte SIEMPRE el RecursionError no capturado en un DecodeError manejable,
incluso a profundidad 50 (el limite DEFAULT_RECURSION_LIMIT=100 del parche cuenta los niveles de
forma distinta al backend upb, pero el resultado -- excepcion manejable -- es el mismo). Esto
aisla la causa raiz exactamente donde predecia el hallazgo: el fix vive en la libreria protobuf
(decoder.py), no en coremltools, y coremltools no hace nada propio para garantizarlo (no fija
una version minima de protobuf, no fuerza el backend acelerado).
Resultado D β Backend por defecto (upb) es indiferente a la version de protobuf
Log completo: results/D_default_backend_control_both_protobuf_versions.log.
Con protobuf==4.25.7 (vulnerable) Y protobuf==4.25.8 (parcheado), SIN forzar la variable de
entorno, ambos venvs resuelven backend upb y ambos producen el mismo DecodeError manejable a
depth=1000. Confirma la afirmacion de la advisory oficial: los wheels binarios (backend
acelerado) nunca estuvieron afectados por CVE-2025-4565, independientemente del numero de
version exacto de protobuf instalado β lo que importa es el BACKEND, no la version, para el caso
comun.
Resumen de la matriz completa
| Backend | Version protobuf | Resultado a profundidad alta (1000/5000) | Manejable? |
|---|---|---|---|
upb (acelerado, DEFAULT de pip install) |
7.35.1 (hoy) | DecodeError a partir de depth~50 |
SI |
| upb (acelerado) | 4.25.7 (vulnerable) | DecodeError (control D) |
SI |
| upb (acelerado) | 4.25.8 (parcheado) | DecodeError (control D) |
SI |
| puro-Python (forzado) | 4.25.7 (vulnerable) | RecursionError NO capturado |
NO β crash/excepcion no manejada |
| puro-Python (forzado) | 4.25.8 (parcheado) | DecodeError |
SI |
Las 3 familias de ciclo recursivo (pipeline, mil_block/MILSpec, nn_branch/NeuralNetwork) se
comportan de forma IDENTICA en todas las filas de la matriz β el mecanismo no es especifico de un
solo esquema, es generico del parser protobuf subyacente.
Codigo de coremltools: sin mitigacion propia
grep -rn "RecursionError\|SetRecursionLimit\|recursion" coremltools/ (excluyendo proto/) no
encuentra ninguna proteccion relacionada con el parseo de specs. La unica ocurrencia relevante,
converters/mil/frontend/tensorflow/tf_graph_pass/delete_asserts.py:14: sys.setrecursionlimit(5000), hace justo lo contrario de mitigar: SUBE el limite global de Python
(para poder convertir grafos TensorFlow grandes), lo que en el escenario vulnerable ampliaria aun
mas la profundidad necesaria para disparar el RecursionError, no lo evita.