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.

TFLite READ_VARIABLE out-of-bounds read: kernel memcpy's output->bytes from an undersized resource-variable buffer with only a type check

Summary

READ_VARIABLE in TensorFlow Lite copies a resource variable's backing buffer into the op's output tensor using memcpy(output->data.raw, variable_tensor->data.raw, output->bytes). The only consistency guard is a scalar type check. The source length is never validated against the destination length. Because the output tensor's byte length (output->bytes) is derived from an attacker-controlled static shape in the untrusted .tflite, while the variable's backing buffer is sized to a separate attacker-controlled ASSIGN_VARIABLE value tensor, a model can force the kernel to memcpy an attacker-chosen large number of bytes out of a tiny (e.g. 4-byte) heap allocation, reading far out of bounds and crashing the process (SIGSEGV) β€” or leaking adjacent heap memory into the readable output tensor.

  • Target: TensorFlow Lite (tensorflow-cpu) β€” verified on 2.21.0 (pip, x86-64 Linux). Code path unchanged on master (read_variable.cc line 81).
  • File: tensorflow/lite/kernels/read_variable.cc, read_variable::Eval
  • Class: Out-of-bounds read (CWE-125) from untrusted model file
  • Attack surface: Loading and invoking a malicious .tflite. All three ops used (ASSIGN_VARIABLE, READ_VARIABLE, plus const initialization) are builtin ops present in the default BuiltinOpResolver β€” no custom ops, no flex delegate required.

Root cause

read_variable::Eval (tensorflow/lite/kernels/read_variable.cc):

TfLiteTensor* variable_tensor = variable->GetTensor();
TfLiteTensor* output;
TF_LITE_ENSURE_OK(context,
                  GetOutputSafe(context, node, kOutputValue, &output));

TF_LITE_ENSURE_TYPES_EQ(context, variable_tensor->type, output->type);  // scalar TYPE check only
// Only resize the output if the op produces dynamic output.
if (IsDynamicTensor(output)) {
  TF_LITE_ENSURE_OK(context, context->ResizeTensor(
                                 context, output,
                                 TfLiteIntArrayCopy(variable_tensor->dims)));
}
memcpy(output->data.raw, variable_tensor->data.raw, output->bytes);   // <-- no size check

Two independent problems combine:

  1. Copy length is the destination's length, not the source's. output->bytes is computed from the output tensor's declared shape at allocation time. It is never compared to variable_tensor->bytes (the source). If output->bytes > variable_tensor->bytes, the memcpy reads past the end of the variable's buffer.

  2. The output is only re-shaped to the variable when it is dynamic. The ResizeTensor that would make output match the variable's real dims runs only inside if (IsDynamicTensor(output)). A model can declare the READ_VARIABLE output with a static shape (rank >= 1 with non-zero dims), which keeps its allocation type kTfLiteArenaRw (not dynamic). The resize is skipped, and output->bytes retains the attacker's large static size while the source stays tiny.

The variable's buffer is sized by ASSIGN_VARIABLE β†’ ResourceVariable::AssignFrom (tensorflow/lite/experimental/resource/resource_variable.cc), which reallocs the backing buffer to exactly the assigned value tensor's byte size. So the source length is the size of the attacker's tiny value tensor, and the destination length is the size of the attacker's large output shape β€” both fully controlled fields in the untrusted flatbuffer.

Proof of Concept

A 488-byte .tflite, built with the bundled TFLite flatbuffer schema (tensorflow.lite.python.schema_py_generated), containing one subgraph with two ops executed in order:

  1. ASSIGN_VARIABLE(resource_id = const int32[1] = 0, value = const int32[1] = 42) β€” stores a 4-byte buffer into resource variable id 0.
  2. READ_VARIABLE(resource_id = 0) whose output tensor is declared as a STATIC int32[8000000] arena tensor (buffer index 0, uninitialized).

At invoke(), READ_VARIABLE memcpy's output->bytes = 32,000,000 bytes from the 4-byte variable buffer β†’ out-of-bounds read β†’ SIGSEGV.

Loaded and invoked via:

tf.lite.Interpreter(
    model_content=data,
    experimental_op_resolver_type=tf.lite.experimental.OpResolverType.BUILTIN_REF,
)

Negative control

The identical model with the READ_VARIABLE output declared int32[1] (so output->bytes == the variable's 4 bytes) exercises the exact same kernel/memcpy path and runs cleanly: allocate_tensors OK, invoke OK, output = 42, exit 0. This isolates the crash to the output-vs-variable size mismatch, not to the op path itself.

Captured evidence (verbatim)

Reproduced live on this run β€” tensorflow-cpu 2.21.0:

--- crash run (N=8000000: memcpy 32MB from 4-byte variable buffer) ---
[load] constructed
[load] allocate_tensors OK
crash exit=139        # 139 = 128 + 11 = SIGSEGV

--- negative control (N=1, output bytes == variable bytes) ---
[load] constructed
[load] allocate_tensors OK
[load] invoke OK
[load] output first/last = 42 42 len 1
neg exit=0

Crash reproduced 3/3 runs in the original capture (exit 139 each).

gdb backtrace (fault in the READ_VARIABLE memcpy)

Thread 1 "python" received signal SIGSEGV, Segmentation fault.
#0  0x00007ffff7dd0720 in ?? () from libc.so.6            (memmove/memcpy)
#1  tflite::ops::builtin::read_variable::Eval(TfLiteContext*, TfLiteNode*)
#2  tflite::Subgraph::InvokeImpl()
#3  tflite::Subgraph::Invoke()
#4  tflite::interpreter_wrapper::InterpreterWrapper::Invoke(int)
rsi(src)=0x462bd40   rdi(dst)=0x7fff9beaf080

rsi (memcpy source) = 0x462bd40 is the small heap-allocated variable buffer, being walked off its end; rdi (destination) is the large arena output tensor.

Impact

Loading and invoking an untrusted .tflite triggers an out-of-bounds heap read of attacker-controlled length. Minimally a denial of service (SIGSEGV). Because the read destination is the model's own output tensor, out-of-bounds heap contents adjacent to the undersized variable buffer can be copied into a tensor the caller reads back, giving a potential heap information-disclosure primitive in addition to the crash. No custom ops or delegates are needed; the whole thing is reachable through the default builtin resolver.

Reproduction steps

pip install tensorflow-cpu==2.21.0
python mk_readvar.py 8000000 readvar_crash.tflite   # build crash model
python mk_readvar.py 1        readvar_neg.tflite     # build negative-control model
python load.py readvar_crash.tflite   # -> exit 139 (SIGSEGV)
python load.py readvar_neg.tflite     # -> exit 0, output 42

Suggested fix

Before the memcpy, validate that the source and destination byte lengths agree (and/or always resize the output to the variable's dims, not only when dynamic). For example:

TF_LITE_ENSURE_EQ(context, variable_tensor->bytes, output->bytes);

Dedup / prior-art note

This is distinct from the other TFLite kernel OOB findings in this account (stablehlo-gather, sparse-densify, dequantize-qdim, getminimumruntime): the vulnerable code is read_variable::Eval and the trigger is a resource-variable size mismatch between ASSIGN_VARIABLE and a statically-shaped READ_VARIABLE output. No CVE currently tracks this specific READ_VARIABLE output-vs-variable length mismatch; the missing size guard is still present on master (read_variable.cc line 81) at the time of writing.

Artifacts

  • mk_readvar.py β€” flatbuffer model builder (crash + negative control)
  • load.py β€” load/invoke harness
  • readvar_crash.tflite β€” 488-byte crash PoC (static int32[8000000] output)
  • readvar_neg.tflite β€” negative control (static int32[1] output)
  • poc_crash_evidence.log β€” captured run output
  • gdbcmds.txt β€” gdb command script for the backtrace
  • read_variable.cc β€” vulnerable source (2.21.0; identical path on master)
  • resource_variable.cc β€” AssignFrom (variable buffer sizing)
Downloads last month
-
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support