--- library_name: pytorch tags: - security - model-format-vulnerability - torch-export - pt2 - denial-of-service --- # Torch Export zero-byte raw tensor allocation PoC This repository contains a minimal proof of concept for a Torch Export `.pt2` resource-exhaustion issue. ## Summary Torch Export PT2 archives store raw tensor payload metadata separately from the archive record that contains the tensor bytes. When `torch.export.load()` encounters an empty raw tensor record, PyTorch treats the empty bytes as a special case and allocates a zero-filled tensor using the shape declared in `model_weights_config.json`. The included malicious archive is 5,558 bytes on disk and declares a float32 tensor with shape `[134217728]`. Loading it causes PyTorch to allocate a 512 MiB zero tensor during normal `torch.export.load()`. This PoC does not use pickle, AOTInductor native libraries, or model execution. It exercises the raw tensor zero-byte fallback path. ## Affected - PyTorch Torch Export PT2 loader - Source reviewed: `pytorch/pytorch` commit `c7656354cff2e2c4f9aee5695d3e7f37e3006dd4` - Runtime used for verification: `torch==2.12.1+cpu` ## Reproduction Install PyTorch with Torch Export support, then run: ```bash python -c 'import torch; obj=torch.export.load("control.pt2"); print(obj.state_dict["p"].shape)' ``` Expected control result: ```text torch.Size([1]) ``` Now load the malicious PT2: ```bash python -c 'import torch; obj=torch.export.load("zero-payload-512m.pt2"); print(obj.state_dict["p"].shape)' ``` Expected uncapped result: ```text torch.Size([134217728]) ``` That shape is a 512 MiB float32 tensor allocated from a 5,558-byte archive. The included measurement script also demonstrates control/candidate separation under address-space caps: ```bash python mutate_and_measure_pt2_zero_payload.py ``` Observed locally: - Control loads under 700 MiB, 900 MiB, and 1200 MiB address-space caps. - Candidate fails under 700 MiB and 900 MiB with: ```text DefaultCPUAllocator: can't allocate memory: you tried to allocate 536870912 bytes ``` - Candidate succeeds under 1200 MiB and returns shape `(134217728,)`. ## Files - `control.pt2` - benign control archive, SHA256 `cb79c7913524f08255f74214f37b8bce500ac80b4bf6f2d6f3979116c42287c1` - `zero-payload-512m.pt2` - malicious 5,558-byte PT2 archive, SHA256 `7e4c2c3ab37ac28f6ad4e307b77ae75fd2d655b15f9cbeb6832ac02702e2b18a` - `mutate_and_measure_pt2_zero_payload.py` - generator/measurement script, SHA256 `d1c3887e4f7d612c428c1a66c2ecada91d9b7bdcf42d17cc383303818b6f0690` - `zero_payload_measurements_latest.json` - local measurement report, SHA256 `33ee2b71e6964773228a693aba4696679167a49a9f806df844ec69bafe8ed311` ## Root Cause In `torch/export/pt2_archive/_package.py`, the PT2 loader reads `model_weights_config.json` and the referenced raw tensor record. For non-empty records it validates byte alignment before mapping tensor storage. For empty records, it logs that `torch.frombuffer()` cannot operate on empty bytes and creates a zero tensor as a workaround: ```text torch.zeros(size, dtype=dtype, device=device) ``` The `size` value comes from archive-controlled tensor metadata, so a tiny PT2 archive can force a large allocation during load. ## Suggested Fix Reject zero-byte raw tensor payloads unless the declared tensor has zero elements. The loader should verify that the raw archive record size matches the declared dtype/shape storage requirement before allocating, and should not synthesize attacker-sized tensors from empty records during deserialization.