# Vulnerability Report: TensorFlow GIF Decoder Unbounded Memory Allocation (DoS) ## Target Info - **Target:** TensorFlow (`tensorflow/tensorflow`) - **Component:** `tensorflow/core/kernels/image/decode_image_op.cc` — `DecodeGifV2` - **Vulnerability Type:** CWE-770: Allocation of Resources Without Limits or Throttling - **Impact:** Denial of Service (Memory Exhaustion / OOM Crash) - **CVSS Score:** 7.5 High — `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H` - **Bounty Tier:** $4,000 (TensorFlow Saved Model / Model File Format program) --- ## Executive Summary The TensorFlow `DecodeGifV2` kernel computes `num_frames × height × width × channels` from untrusted GIF file metadata and attempts to allocate that many bytes **without any upper bound check**. A 35-byte crafted GIF file with `width=height=32767` causes TensorFlow to attempt a **~3 GB allocation**, crashing the process with OOM. This is an **inconsistency bug**: the BMP and PNG decoders in the same file enforce hard limits (2³⁰ and 2²⁹ bytes respectively), but `DecodeGifV2` has no such guard. --- ## Root Cause Analysis ### Vulnerable Code **File:** `tensorflow/core/kernels/image/decode_image_op.cc` ```cpp // DecodeGifV2 — NO bounds check on total allocation: void operator()(OpKernelContext* context, StringPiece input_bytes, Tensor* output) override { ... // Reads from GIF header — UNTRUSTED: uint32 num_frames = gif.GetNumFrames(); uint32 height = gif.GetHeight(); // from Logical Screen Header uint32 width = gif.GetWidth(); // from Logical Screen Header uint32 channels = gif_flags.has_value() ? ... : 3; // Allocates num_frames * height * width * channels bytes — NO LIMIT CHECK: OP_REQUIRES_OK(context, context->allocate_output( 0, TensorShape({(int64)num_frames, height, width, channels}), &output)); // ↑ With width=height=32767, channels=3: allocates 3,221,094,411 bytes (~3 GB) ``` ### The Inconsistency: BMP and PNG Decoders Have Limits In the **same file**, `DecodeBmpV2` and `DecodePngV2` enforce explicit size limits: ```cpp // DecodeBmpV2 — HAS bounds check (safe): int64_t total_bytes = static_cast(height) * abs_width * channels; OP_REQUIRES(context, total_bytes < (1LL << 30), errors::InvalidArgument( "Image too large: ", total_bytes, " bytes, max 2^30")); // DecodePngV2 — HAS bounds check (safe): OP_REQUIRES(context, total_pixels < (1LL << 29), errors::InvalidArgument( "PNG image too large: ", total_pixels, " > 2^29")); // DecodeGifV2 — NO BOUNDS CHECK (vulnerable): // Missing: OP_REQUIRES(context, total_pixels < (1LL << 30), ...) ``` | Decoder | Limit | Status | |---------------|----------------|-----------------| | `DecodeBmpV2` | `< 2^30` bytes | ✅ Protected | | `DecodePngV2` | `< 2^29` bytes | ✅ Protected | | `DecodeWebP` | `< 2^32` pixels| ✅ Protected | | `DecodeGifV2` | **None** | ❌ **Vulnerable** | --- ## Proof of Concept ### Step 1: Generate Malicious GIF (35 bytes) ```python import struct # GIF89a header with maximum dimensions header = b'GIF89a' header += struct.pack('(num_frames) * height * width * channels; OP_REQUIRES(context, total_pixels < (1LL << 30), errors::InvalidArgument( "GIF image too large: total_pixels=", total_pixels, " exceeds limit of 2^30. Image dimensions: ", num_frames, "x", height, "x", width, "x", channels)); ``` --- ## References - **Affected file:** `tensorflow/core/kernels/image/decode_image_op.cc` - **Related (fixed) code:** `DecodeBmpV2` in the same file — lines ~350-360 - **CWE-770:** https://cwe.mitre.org/data/definitions/770.html - **Similar TF DoS pattern:** CVE-2022-29191 (TF OOM via crafted model dimensions)