File size: 9,608 Bytes
33b540b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# 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.