You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

YAML Metadata Warning:empty or missing yaml metadata in repo card

Check out the documentation for more information.

ExecuTorch XNNPACK named-data size-confusion PoC

Summary

ExecuTorch 1.3.1 does not validate that the FreeableBuffer returned for an XNNPACK named constant is large enough for the serialized tensor. A crafted .pte can declare a four-byte Program.segments[].size for a 36-byte FP32 weight while retaining attacker-controlled bytes after that declared buffer. The file passes Verification.InternalConsistency, XNNPACK reads the undeclared bytes as weights, and those bytes deterministically alter inference output.

The primary PoC is not a crash-only parser error. With an all-ones 1x3 input, the valid control returns [[6.25, 15.5, 24.75]]. The malicious model keeps the complete declared four-byte buffer unchanged and changes only the 32 bytes outside it; the runtime returns [[5.25, 6.5, 6.75]].

A second model demonstrates the memory-safety boundary directly. It declares four bytes for a 512x512 FP32 weight that requires 1,048,576 bytes and ends the file at the declared segment boundary. The official Windows wheel terminates with access violation 0xC0000005 during load_method("forward"), before inference. The matching large control loads successfully.

Tested on CPU with ExecuTorch 1.3.1, PyTorch 2.12.0, Python 3.12, and the official Windows x86-64 wheels. ExecuTorch tag v1.3.1 is commit e2f18eb23c45bd22ca332b0b8b49a81de304b472.

Files

  • malicious_hidden_weight.pte β€” primary 2,188-byte output-manipulation PoC.
  • baseline_linear_xnn.pte β€” valid control for the primary PoC.
  • malicious_truncated_weight.pte β€” 1,796-byte native out-of-bounds-read/crash PoC.
  • baseline_large_xnn.pte β€” valid 1,050,368-byte control for the crash PoC.
  • build_and_verify.py β€” dependency-free FlatBuffer inspection and byte-exact rebuild.
  • runtime_child.py β€” loads one model with InternalConsistency in an isolated process.
  • run_dynamic_verification.py β€” runs all four dynamic cases and records exit codes/output.
  • verify_report.json and dynamic_verify_report.json β€” captured static and runtime evidence.

Root cause

runtime/executor/pte_data_map.cpp uses the model-controlled segment size when loading named data:

size_t segment_offset = segments_->Get(segment_index)->offset();
size_t segment_size = segments_->Get(segment_index)->size();
return loader_->load(
    segment_base_offset_ + segment_offset,
    segment_size,
    DataLoader::SegmentInfo(DataLoader::SegmentInfo::Type::Constant));

For the primary malicious file, this returns a FreeableBuffer whose logical size is four bytes.

backends/xnnpack/runtime/XNNCompiler.cpp:240-268 then handles a named constant by retrieving that buffer and returning only its pointer:

Result<FreeableBuffer> buffer =
    named_data_map->get_data(data_name.c_str());
...
const uint8_t* data_ptr =
    static_cast<const uint8_t*>(buffer.get().data());
freeable_buffers.push_back(std::move(buffer.get()));
return data_ptr;

The branch never compares buffer.get().size() with ConstantDataOffset.size() or the byte count required by the tensor dimensions and datatype. defineTensor() subsequently passes this pointer together with model-controlled dimensions and datatype to xnn_define_tensor_value(). XNNPACK's weight setup consumes the number of bytes implied by the tensor, not the four-byte FreeableBuffer boundary.

InternalConsistency verifies FlatBuffer structure and segment containment, but it does not enforce this cross-schema invariant between the PTE DataSegment, the XNN delegate constant entry, and the tensor's required storage size.

Static verification

No third-party package is needed:

python build_and_verify.py --report rebuilt_report.json

The script parses the PTE FlatBuffer directly, confirms the ET12 identifier, confirms both malicious segment sizes are four, and rebuilds both malicious files byte-for-byte from their valid controls.

For the primary pair it also verifies that the first four declared bytes are unchanged and that only the 32-byte weight tail outside the declared buffer is replaced. For the large pair it confirms that the file ends exactly four bytes into a weight whose valid control contains 1,048,576 bytes.

Dynamic reproduction

Use the matching ExecuTorch/PyTorch release pair in a clean virtual environment:

python -m pip install -r requirements.txt
python run_dynamic_verification.py

Expected security-relevant results:

small_baseline:   rc 0, output=[[6.25, 15.5, 24.75]]
hidden_weight:    rc 0, output=[[5.25, 6.5, 6.75]]
large_baseline:   rc 0, load_method=OK
truncated_weight: native crash during load_method

On Windows, the truncated model exits with 0xC0000005 (STATUS_ACCESS_VIOLATION). On Unix-like systems the same invalid read may surface as SIGSEGV or SIGBUS, depending on the file-mapping implementation.

No call to the model's forward method is required for the memory-safety case; backend initialization in load_method("forward") is sufficient.

Security impact

This is a native out-of-bounds weight read and a parser/runtime differential, not merely an invalid model rejection or allocation denial of service. An attacker can place model behavior in bytes that the PTE metadata says are outside the named weight buffer. A validator or scanner that honors Program.segments[].size can inspect only the declared bytes while XNNPACK consumes additional bytes and produces attacker-selected inference behavior. Truncating those bytes turns the same primitive into a reliable native access violation during model loading.

The demonstrated impacts are model-integrity compromise, hidden inference-output manipulation, and process availability loss. The PoC does not claim code execution.

Relationship to existing XNNPACK hardening

PRs #19593 and #19595 address an adjacent out-of-bounds read in the same helper, but only for the non-named external constant-data branch:

if (!has_named_key) {
  // validates offset + entry_size against constant_data_size
  ...
} else {
  // named_data_map branch; buffer length is not validated
  ...
}

This report does not reuse the fixed constant_data_ptr + offset path. It controls Program.segments[].size, reaches the has_named_key branch, receives a short FreeableBuffer from PteDataMap, passes full InternalConsistency verification, and demonstrates hidden output manipulation. PR #18820 checks constant indices and null entries but likewise does not validate named-buffer length against tensor storage requirements.

Recommended fix

Before returning a named constant pointer, reject the model unless the returned buffer is at least ConstantDataOffset.size() bytes. Independently validate that the constant entry size is sufficient for the storage required by the tensor dimensions, datatype, quantization layout, and packing rules; otherwise an attacker could reduce both serialized sizes together. Apply equivalent size-aware validation to the weights-cache path. Add regression tests for undersized embedded named segments, undersized external named data, and quantized scale buffers, using both InternalConsistency and normal backend initialization.

Downloads last month
7
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support