TFLite BITCAST INT8-to-INT4 CPU Out-of-Bounds Write PoC
malicious_bitcast_int4_oob.tflite is a structurally valid TFL3 model. During normal CPU inference, its built-in BITCAST operation copies a 4,096-byte INT8 constant into an INT4 output tensor that TFLite allocated as 2,048 packed bytes. The remaining 2,048 bytes write past the destination; in the supplied TensorFlow 2.21 CPU reproduction, they overwrite an adjacent live sentinel allocation.
This is not a malformed FlatBuffer or a crash-only test. The model has a benign, caller-provided INT8 sentinel tensor immediately after the BITCAST output. The proof verifies that exactly its first 2,048 bytes change from -91 to the model-owned constant byte 0x55 (85), while the remainder stays unchanged. The INT8 control is byte-for-byte equivalent in graph structure and differs only in the first BITCAST output type; it leaves the 4,096-byte sentinel untouched.
Files
malicious_bitcast_int4_oob.tfliteโ the uploadable PoC model.control_bitcast_int8.tfliteโ matching non-vulnerable control model.build_and_verify.pyโ regenerates both valid models and proves the overwrite.verify_report.jsonโ concise results from fresh CPU verification.source_citations.mdandduplicate_check.mdโ root-cause and duplicate-gate notes.
Reproduction
Use CPU TensorFlow 2.21.0. No NVIDIA GPU, custom op, malformed model, or external file is required.
python -m pip install -r requirements.txt
python build_and_verify.py
The script runs each model through both the normal default tf.lite.Interpreter path and BUILTIN_WITHOUT_DEFAULT_DELEGATES. Expected assertions:
- Control: output-to-sentinel span = 4,096 bytes; changed sentinel bytes = 0.
- Malicious model: output-to-sentinel span = 2,048 bytes; changed sentinel bytes = 2,048; indices 0 through 2,047 change to
85.
For the direct byte-loading API, run:
python build_and_verify.py --model-content
Root cause
tensorflow/lite/kernels/bitcast.cc compares the input and output logical element sizes through GetSizeOfType() and accepts INT8 and INT4 as equal-size values. It then executes memcpy(output->data.data, input->data.data, input->bytes) without ensuring that the packed INT4 destination has at least input->bytes storage.
TFLite deliberately reports an INT4 logical element as one byte for shape handling, but its separate BytesRequired() allocation routine packs two INT4 elements per byte. Consequently, an INT4 tensor of shape [4096] has a 2,048-byte arena allocation while the INT8 input remains 4,096 bytes. The unchecked memcpy writes 2,048 bytes beyond the destination during inference.
See source_citations.md for exact upstream source locations.
- Downloads last month
- 9