| # 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 coremltools` limpio, tal cual lo |
| obtendria hoy cualquiera (`coremltools==9.0`, arrastra `protobuf==7.35.1`, backend por defecto |
| **upb** (acelerado)). |
| - `targets/coremltools/verify/venv-cve-old/` — mismo `coremltools==9.0`, protobuf forzado a |
| `4.25.7` (**ANTES** del fix de CVE-2025-4565/GHSA-8qvm-5x2c-j2w7). |
| - `targets/coremltools/verify/venv-cve-new/` — mismo `coremltools==9.0`, protobuf forzado a |
| `4.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.py` de `coremltools.proto` (sin `protoc`), |
| 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] -> ...` (ciclo `MILSpec.Operation.blocks`/`Block.operations`) |
| - `nn_branch`: `NeuralNetwork.layers[i].branch.ifBranch` (que es de nuevo un mensaje |
| `NeuralNetwork` completo) encadenado -> ciclo `NeuralNetwork.BranchLayer` |
| - Nota tecnica documentada en el propio script: construir/serializar cadenas muy profundas |
| (>=~2000-5000 niveles) con el backend acelerado `upb` puede 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 llama `ParseFromString`/`load_spec` sobre bytes ya |
| hechos). Para que la fase de CONSTRUCCION nunca sea el cuello de botella bajo prueba, el |
| script fuerza `PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python` + un hilo con pila de 64MB + |
| `sys.setrecursionlimit` alto SOLO para construir los payloads; los bytes resultantes son |
| formato wire estandar, independientes del backend que los genero. |
| - `test_parse.py` — carga un `.mlmodel` con `coremltools.utils.load_spec()` (o, con `--raw`, |
| directamente `Model_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. |
| |