TFLite READ_VARIABLE Static Output Shape Heap Out-of-Bounds Read PoC
malicious_read_variable_shape_16.tflite is a valid, self-contained TFL3 model that triggers a native heap out-of-bounds read during normal CPU inference. The graph is VAR_HANDLE -> ASSIGN_VARIABLE -> READ_VARIABLE. It assigns one FLOAT32 value (1.25, four bytes) to a resource variable, while the serialized READ_VARIABLE output is a non-empty static FLOAT32[16] tensor (64 bytes).
TFLite preserves the attacker-supplied non-empty output shape, type-checks only, and copies output->bytes from the resource buffer. The resource holds four bytes, so inference copies 64 bytes from a four-byte source allocation and returns the first value followed by 60 bytes read past it. The tail is allocator-dependent process memory and can disclose data held adjacent to the resource allocation.
The control model is byte-for-byte equivalent except for that one output-shape value: it declares FLOAT32[1], returns only 1.25, and performs no over-read.
Files
malicious_read_variable_shape_16.tfliteโ uploadable malicious model.control_read_variable_shape_1.tfliteโ matching safe control model.build_and_verify.pyโ regenerates both models and proves normal CPU execution plus the 60-byte copy excess.verify_report.jsonโ fresh CPU verification results; incidental process-memory bytes are deliberately not committed.source_citations.mdโ upstream root-cause links.duplicate_check.mdโ public-duplicate and scope gate.
Reproduction
Run with CPU TensorFlow 2.21.0. No GPU, custom operator, malformed FlatBuffer, external file, or model input is required.
python -m pip install -r requirements.txt
python build_and_verify.py
The verifier runs all three normal loading paths:
- default CPU
tf.lite.Interpreter(model_path=...); - CPU built-ins only (
BUILTIN_WITHOUT_DEFAULT_DELEGATES); - normal
model_content=loading.
For each path, the control allocates and returns four bytes. The malicious model loads, allocates, and invokes successfully; it returns a 64-byte tensor whose first float is 1.25, leaving a 60-byte returned tail. The script prints that tail locally because it is direct evidence of the out-of-bounds read, but omits it from the committed JSON report to avoid publishing incidental host-memory contents.
Root cause
READ_VARIABLE::Prepare() makes only rank-zero outputs dynamic. It leaves every non-empty output shape serialized in the model static. In Eval(), the kernel checks the element type but not the source and destination byte counts or shapes. A static output therefore avoids the resize-to-resource-shape path, after which the kernel calls:
memcpy(output->data.raw, variable_tensor->data.raw, output->bytes);
ASSIGN_VARIABLE stores the one-element value in a ResourceVariable, whose backing allocation is exactly the assigned tensor's bytes value: four bytes. For the malicious static output, output->bytes is 64. The unchecked copy consequently reads 60 bytes past the resource allocation and exposes those bytes through the model output.
See source_citations.md for exact upstream locations and the 2021 regression that introduced the static-output behavior.
Impact
An attacker who can supply a .tflite model can cause a victim to return process-heap data beyond a resource variable during ordinary CPU inference. In a service or application that exposes model outputs, this can disclose adjacent in-process data. This is a native memory-safety issue with a directly observable data-disclosure primitive, not a parser rejection, allocation-only condition, GPU-only path, or crash-only PoC.
- Downloads last month
- 7