webp: reject VP8X headers with too-large canvases

RFC 9649 states that the canvas width * height must be
at most 2^32-1. Enforce this.

This avoids creating an invalid image (which will panic
when manipulated) when decoding a too-large image on
32-bit platforms.

https://www.rfc-editor.org/rfc/rfc9649.html#section-2.7-12

Fixes golang/go#78407
Fixes CVE-2026-33813

Change-Id: I7e2b68374681da4f72ee51ebfd8833006a6a6964
Reviewed-on: https://go-review.googlesource.com/c/image/+/759860
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Damien Neil <dneil@google.com>
Reviewed-by: Neal Patel <nealpatel@google.com>
diff --git a/webp/decode.go b/webp/decode.go
index 2371808..15dc0ee 100644
--- a/webp/decode.go
+++ b/webp/decode.go
@@ -134,6 +134,12 @@
 			wantAlpha = (buf[0] & alphaBit) != 0
 			widthMinusOne = uint32(buf[4]) | uint32(buf[5])<<8 | uint32(buf[6])<<16
 			heightMinusOne = uint32(buf[7]) | uint32(buf[8])<<8 | uint32(buf[9])<<16
+			if uint64(widthMinusOne+1)*uint64(heightMinusOne+1) > 1<<32-1 {
+				// The product of _Canvas Width_ and _Canvas Height_ MUST be
+				// at most 2^32 - 1.
+				// https://www.rfc-editor.org/rfc/rfc9649.html#section-2.7-12
+				return nil, image.Config{}, errInvalidFormat
+			}
 			if configOnly {
 				if wantAlpha {
 					return nil, image.Config{
diff --git a/webp/decode_test.go b/webp/decode_test.go
index bb50ddd..56948ef 100644
--- a/webp/decode_test.go
+++ b/webp/decode_test.go
@@ -280,6 +280,53 @@
 	}
 }
 
+func TestVP8XImageTooLarge(t *testing.T) {
+	data := []byte{
+		// WebP file header
+		'R', 'I', 'F', 'F',
+		22, 0, 0, 0, // file size
+		'W', 'E', 'B', 'P',
+		// ChunkHeader('VP8X')
+		'V', 'P', '8', 'X',
+		10, 0, 0, 0, // chunk size
+		// bits + Reserved
+		1 << 4, 0, 0, 0, // alpha bit set
+		// Canvas Width Minus One
+		0xff, 0xff, 0x00,
+		// Canvas Height Minus One
+		0xff, 0xff, 0x00,
+	}
+	_, err := DecodeConfig(bytes.NewReader(data))
+	if err != errInvalidFormat {
+		t.Fatalf("unexpected error: want %q, got %q", errInvalidFormat, err)
+	}
+}
+
+func TestVP8XImageNotQuiteTooLarge(t *testing.T) {
+	data := []byte{
+		// WebP file header
+		'R', 'I', 'F', 'F',
+		22, 0, 0, 0, // file size
+		'W', 'E', 'B', 'P',
+		// ChunkHeader('VP8X')
+		'V', 'P', '8', 'X',
+		10, 0, 0, 0, // chunk size
+		// bits + Reserved
+		1 << 4, 0, 0, 0, // alpha bit set
+		// Canvas Width Minus One
+		0xfe, 0xff, 0x00,
+		// Canvas Height Minus One
+		0xfe, 0xff, 0x00,
+	}
+	cfg, err := DecodeConfig(bytes.NewReader(data))
+	if err != nil {
+		t.Fatalf("unexpected error: want nil, got %q", err)
+	}
+	if cfg.Width != 0xffff || cfg.Height != 0xffff {
+		t.Fatalf("width x height: got %v x %v, want %v x %v", cfg.Width, cfg.Height, 0xffff, 0xffff)
+	}
+}
+
 func benchmarkDecode(b *testing.B, filename string) {
 	data, err := ioutil.ReadFile("../testdata/blue-purple-pink-large." + filename + ".webp")
 	if err != nil {