protobuf / EVIDENCE.md
testamentaria's picture
Upload 11 files
33b540b verified
|
Raw
History Blame Contribute Delete
9.61 kB
# 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.