Downloads · 30 days
0
pantumanolo/onnx_parsedata_oob_poc
onnx_parsedata_oob_poc is a machine learning model from pantumanolo. Use it for the machine learning task on the model card, and read the license before you ship it in a product. The card lists the license as mit.
A model passed to the documented onnx.versionconverter.convertversion API triggers a heap out-of-bounds write in native ONNX code. The native ParseData<T helper sizes its output vector by integer floor division of the…
Downloads · 30 days
0
Access
Public
Updated May 31, 2026
Repo size
16.3 KB
Likes
0
Public
Click a slice to open those files.
.txt8.5 KB · 27%
From the Hugging Face model README
A model passed to the documented onnx.version_converter.convert_version API triggers a heap out-of-bounds write in native ONNX code. The native ParseData<T> helper sizes its output vector by integer floor division of the raw_data byte length by the element size, then memcpys the full byte length, so a raw_data length that is not a multiple of the element size writes the remainder bytes past the heap allocation. The crafted model passes onnx.checker.check_model at both default and full_check=True, so this is memory corruption, not merely an invalid model.
onnx (PyPI). Confirmed on onnx 1.21.0, which was the current PyPI release at the time of testing. The 1.22 in online docs is the in-development version, not released on PyPI.Out-of-bounds Write, CWE-787 (heap), in a native extension loaded into the Python process.
onnx.version_converter.convert_version(model, target_opset) is the public Python API to convert a model between opsets. The Upsample opset 9 to 8 adapter (onnx/version_converter/adapters/upsample_9_8.h) calls ParseData<float> on the scales initializer. The converter reaches this adapter without a checker rejection or an equivalent raw_data length validation. Trigger: a model with an Upsample-9 node and a float scales initializer whose raw_data length is not a multiple of 4, then convert_version(model, 8). The caller touches no internal C++ API; the Python-level trigger is only loading or constructing the ModelProto and passing it to convert_version. The corruption occurs during conversion, not during a plain onnx.load.
onnx/defs/tensor_util.cc, the ParseData overload for the in-memory Tensor:
res.resize(raw_data.size() / sizeof(T)); // floor division -> element count
memcpy(res.data(), raw_data.c_str(), raw_data.size()); // copies the FULL byte length
When raw_data.size() is not a multiple of sizeof(T), res holds floor(size / sizeof(T)) * sizeof(T) bytes but memcpy writes size bytes, so size % sizeof(T) bytes land past the allocation.
element size = 4
raw_data length = 8003
vector elements = 8003 / 4 = 2000 (integer floor division)
vector capacity = 2000 * 4 = 8000 bytes
memcpy length = 8003 bytes
overflow = 8003 - 8000 = 3 bytes
The 3 overflowing bytes are the last 3 bytes of attacker-controlled raw_data.
python -m venv onnx-poc-venv
onnx-poc-venv/bin/pip install onnx==1.21.0
onnx-poc-venv/bin/python build_poc.py
onnx-poc-venv/bin/python -c "import onnx; print(onnx.__version__, onnx.__file__)"
# 1.21.0 .../onnx-poc-venv/lib/python3.12/site-packages/onnx/__init__.py
valgrind onnx-poc-venv/bin/python run_convert.py
Valgrind on the stock wheel (valgrind_release_wheel_output.txt):
Invalid write of size 1
by 0x64A4738: onnx::version_conversion::ConvertVersion(onnx::ModelProto const&, int)
(in .../site-packages/onnx/onnx_cpp2py_export.abi3.so)
Address 0x1f051440 is 0 bytes after a block of size 8,000 alloc'd
by 0x64A4738: onnx::version_conversion::ConvertVersion(onnx::ModelProto const&, int)
ERROR SUMMARY: 3 errors from 1 contexts
On a normal (non-instrumented) run the 3-byte write lands in allocator slack and the call returns without crashing, so the corruption is silent without a memory checker. Platform: Ubuntu 24.04.4 LTS, x86_64, Python 3.12.3, onnx 1.21.0 manylinux wheel, Valgrind 3.22.0.
Note: Valgrind reports Invalid write of size 1 because it instruments each byte access inside the memcpy individually. The ASan harness (section 8) reports the full memcpy size of 8003 bytes, confirming the destination is 8000 bytes and the overwrite is 3 bytes past the allocation.
asan_harness.cc (built with build_asan_harness.sh) compiles the unmodified upstream tensor_util.cc and calls ParseData<float> on a Tensor whose raw_data is 8003 bytes. It is included only to isolate the root cause; the end-to-end trigger is the Valgrind run above against the released wheel. The reported WRITE of size 8003 is the length of the intercepted memcpy (the full source buffer); for this input the destination is 8000 bytes, so the actual overwrite past the allocation is 3 bytes. Output (asan_standalone_output.txt):
[harness] raw_data length = 8003 (mod 4 = 3)
ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 8003 at 0x525000009840 thread T0
#2 onnx::ParseData<float>(onnx::Tensor const*) /root/onnx-src/onnx/defs/tensor_util.cc:57
0x525000009840 is located 0 bytes after 8000-byte region
... std::vector<float>::resize(unsigned long)
... onnx::ParseData<float>(...) tensor_util.cc:57
convert_version runs no validation itself. Independently, the crafted model passes the official validator at both checker modes (checker_vs_converter_output.txt):
onnx version: 1.21.0
scales initializer: data_type=FLOAT, dims= [4] raw_data bytes= 8003 (mod 4 = 3 )
checker (full_check=False): ACCEPTED, no error
checker (full_check=True): ACCEPTED, no error
convert_version(9 -> 8): returned without raising; the native ParseData path is reached
This shows that, for this model, the official checker does not enforce the raw_data multiple-of-element-size invariant. The vulnerable conversion path later uses the in-memory Tensor overload of ParseData, where no multiple-of-element-size check is applied before memcpy, so the checker does not enforce the invariant needed to make this copy safe. Even if maintainers consider this raw_data length unusual or semantically invalid, the safe behaviour is rejection with a validation error, not native heap corruption.
Demonstrated impact: a 3-byte attacker-controlled heap overwrite in the float path, during convert_version on the official wheel. The overwritten bytes are the trailing bytes of attacker-supplied raw_data, not arbitrary process memory. General primitive: for element size N, a raw_data length not divisible by N can write up to N-1 bytes past the allocation, so double, int64 and uint64 could allow up to 7 bytes, but the demonstrated public path is float with 3 bytes. Native memory corruption in an extension loaded into the Python process; at minimum a denial of service. A successful normal run does not make it safe: the overwrite lands in allocator slack for this heap layout, while Valgrind and ASan confirm the native write crosses the allocation boundary. Suggested severity: Medium, possibly High.
Reject raw_data when raw_data size modulo the element size is nonzero. This check should happen before deriving the vector size and before memcpy. After that check, compute the element count and copy exactly element_count * sizeof(element) bytes. Where tensor shape metadata is available, also verify the decoded element count matches the declared count.
No published ONNX advisory found covering ParseData, tensor_util.cc, or a version-converter heap overflow. The public advisories found relate to other classes (external data handling, object state corruption). Confirm against current pending and resolved reports.
build_poc.py — builds upsample9.onnx (scales raw_data 8003 bytes; output shape matches scales so the model passes full_check)run_convert.py — loads the model and calls convert_version(9, 8); run under valgrindchecker_vs_converter.py — shows check_model accepts in both modes while convert_version reaches the pathupsample9.onnx — the modelasan_harness.cc, build_asan_harness.sh — root-cause ASan harness on unmodified tensor_util.ccvalgrind_release_wheel_output.txt, asan_standalone_output.txt, checker_vs_converter_output.txt — evidence