set GO111MODULE=
set GOARCH=amd64
set GOBIN=
set GOCACHE=C:\Users\Byfron\AppData\Local\go-build
set GOENV=C:\Users\Byfron\AppData\Roaming\go\env
set GOEXE=.exe
set GOEXPERIMENT=
set GOFLAGS=
set GOHOSTARCH=amd64
set GOHOSTOS=windows
set GOINSECURE=
set GOMODCACHE=C:\Users\Byfron\go\pkg\mod
set GONOPROXY=
set GONOSUMDB=
set GOOS=windows
set GOPATH=C:\Users\Byfron\go
set GOPRIVATE=
set GOPROXY=https://proxy.golang.org,direct
set GOROOT=C:\Users\Byfron\scoop\apps\go\current
set GOSUMDB=sum.golang.org
set GOTMPDIR=
set GOTOOLCHAIN=auto
set GOTOOLDIR=C:\Users\Byfron\scoop\apps\go\current\pkg\tool\windows_amd64
set GOVCS=
set GOVERSION=go1.22.4
set GCCGO=gccgo
set GOAMD64=v1
set AR=ar
set CC=gcc
set CXX=g++
set CGO_ENABLED=1
set GOMOD=C:\Users\Byfron\Documents\Code\exp-vector-overflow\go.mod
set GOWORK=
set CGO_CFLAGS=-O2 -g
set CGO_CPPFLAGS=
set CGO_CXXFLAGS=-O2 -g
set CGO_FFLAGS=-O2 -g
set CGO_LDFLAGS=-O2 -g
set PKG_CONFIG=pkg-config
set GOGCCFLAGS=-m64 -mthreads -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=C:\Users\Byfron\AppData\Local\Temp\go-build1421511663=/tmp/go-build -gno-record-gcc-switches
What did you do?
Basically, drawing a vector (any) on an image.RGBA or image.Alpha as dst and a image.Uniform as src with these cases:
Draw a vector at top-left (a few pixels lower/negative).
Draw a vector vertically centered and horizontally right (more than dst's Max.X) or left (less than dst's Min.X)
Draw a vector at bottom-right (a few pixels higher)
package main
import (
"image"
"image/draw"
"image/png"
"os"
"golang.org/x/image/vector"
)
func main() {
img := image.NewRGBA(image.Rect(0, 0, 50, 50))
vec := vector.NewRasterizer(25, 25)
// create a rectangle
vec.LineTo(25, 0) // top right
vec.LineTo(25, 25) // bottom right
vec.LineTo(0, 25) // bottom left
vec.LineTo(0, 0) // top left
// fill the img's background for visibility
draw.Draw(img, img.Rect, image.White, image.Point{}, draw.Over)
// Test cases:
// Replace (0,0) to one of these:
// 1. (-10, -10) Panics
// 2. (35, 35) Panics
// 3. (-10, 15) Draws those -10x pixels on the right and a pixel higher
// 4. (35, 15) Draws those +10x pixels on the left and a pixel lower
addPoint := image.Pt(0, 0)
// Draw an image.Uniform on an image.RGBA
vec.Draw(img, vec.Bounds().Add(addPoint), image.Black, image.Point{})
// Save the image. I avoided the error handling for now.
f, _ := os.OpenFile("go-issue/test-case.png", os.O_CREATE|os.O_WRONLY, os.ModePerm)
png.Encode(f, img)
}
What did you see happen?
Based on the minimal code example's test cases, these happened:
Drawing a 25x25 vector on 50x50 image.RGBA at (-10, -10). panics.
Drawing a 25x25 vector on 50x50 image.RGBA at (35, 35). panics. (different stacktrace)
panic: runtime error: index out of range [2860] with length 2860
goroutine 1 [running]:
golang.org/x/image/vector.(*Rasterizer).rasterizeDstRGBASrcUniformOpOver(0xc000125ee0, 0xc000118040, {{0x19?, 0x0?}, {0xffffffff00125e28?, 0x3200?}}, 0x0, 0x0, 0x0, 0xffff)
C:/Users/Byfron/go/pkg/mod/golang.org/x/image@v0.18.0/vector/vector.go:404 +0x2d2
golang.org/x/image/vector.(*Rasterizer).Draw(0xc000125ee0, {0xe40de0, 0xc000118040}, {{0x0?, 0xe40de0?}, {0xc000118040?, 0x0?}}, {0xe40b10, 0xecd7a0?}, {0x0, ...})
C:/Users/Byfron/go/pkg/mod/golang.org/x/image@v0.18.0/vector/vector.go:287 +0x19d
main.main()
C:/Users/Byfron/Documents/Code/exp-vector-overflow/go-issue/main.go:34 +0x157
exit status 2
Drawing a 25x25 vector on 50x50 image.RGBA at (-10, 15).
Draws the skipped left part of the vector at right side of the image one pixel higher.
Drawing a 25x25 vector on 50x50 image.RGBA at (35, 15).
Draws the skipped right part of the vector at left side of the image one pixel lower.
What did you expect to see?
This issue can be splitted in two parts:
Panicing
Which is totally unexpected as you can't even see any notes for possible panics.
With image.RGBA and DrawOP as draw.Over, it calls rasterizeDstRGBASrcUniformOpOver which tries to get access to dst.Pix's out of range item (both negative and positive here and here)
The same goes for image.Alpha and maybe DrawOP of draw.Src with these two image types. (Didn't dive into it that much)
Invalid drawings
As far as I found out, those overflows happened because rasterizeDstRGBASrcUniformOpOver (same for image.Alpha and maybe DrawOP as draw.Src) writes the pixel colors without taking care of the dst's bounds but just the vector's bounds.
For the third test case, We would expect this:
And for the forth case, We would expect this:
The easiest possible fix I could think of, is to limit the r's bounds to the dst's bounds. if the r.Min is less than dst.Min, skip those differences and start from dst.Min, and same for r.Max and dst.Max.
Here's a basic (of cource not good enough for a stdlib) patch I've made for that single method:
func (z *Rasterizer) rasterizeDstRGBASrcUniformOpOver(dst *image.RGBA, r image.Rectangle, sr, sg, sb, sa uint32) {
z.accumulateMask()
// NEW: Calculate the offsets between dst's Min and r's Min; The goal is to
// start drawing from the dst's Min.
var xOffset, yOffset, pixOffset int
if r.Min.X < dst.Rect.Min.X {
xOffset = dst.Rect.Min.X - r.Min.X
}
if r.Min.Y < dst.Rect.Min.Y {
yOffset = dst.Rect.Min.Y - r.Min.Y
}
// NEW: Calculate the maximum X and Y. Basically, limit the r's Max to the dst's Max.
xMax, yMax := r.Max.X, r.Max.Y
if xMax > dst.Rect.Max.X {
xMax = dst.Rect.Max.X
}
if yMax > dst.Rect.Max.Y {
yMax = dst.Rect.Max.Y
}
// CHANGED: Removed dst.Pix[...] and just kept this. You'll find more details down bellow.
pixOffset = dst.PixOffset(r.Min.X, r.Min.Y)
// CHANGED: Start x and y from the calculated offset.
// CHANGED: Using yMax and xMax instead of r.Min.X and r.Min.Y for y1 and x1.
for y, y1 := yOffset, yMax-r.Min.Y; y < y1; y++ {
for x, x1 := xOffset, xMax-r.Min.X; x < x1; x++ {
ma := z.bufU32[y*z.size.X+x]
// This formula is like rasterizeOpOver's, simplified for the
// concrete dst type and uniform src assumption.
a := 0xffff - (sa * ma / 0xffff)
i := pixOffset + y*dst.Stride + 4*x // CHANGED: added pixOffset
// CHANGED: We can't pre-calculate the pix as it could be negative
// ore more than the dst's pixels. So I just used it directly here.
dst.Pix[i+0] = uint8(((uint32(dst.Pix[i+0])*0x101*a + sr*ma) / 0xffff) >> 8)
dst.Pix[i+1] = uint8(((uint32(dst.Pix[i+1])*0x101*a + sg*ma) / 0xffff) >> 8)
dst.Pix[i+2] = uint8(((uint32(dst.Pix[i+2])*0x101*a + sb*ma) / 0xffff) >> 8)
dst.Pix[i+3] = uint8(((uint32(dst.Pix[i+3])*0x101*a + sa*ma) / 0xffff) >> 8)
}
}
}
This change, fixes drawing a vector with DrawOP of draw.Over on a image.RGBA with a image.Uniform src, but still image.Alpha remains and I couldn't really find out how it works (I lack of knowledge 😅). Also I didn't do any tests on draw.Src, but as they have almost similar code, I think this issue could related to them too.
The repository I mentioned earlier (this) contains everything I could possibly think of and has a little bit more details if it helps.
Go version
go version go1.22.4 windows/amd64
Output of
go env
in your module/workspace:What did you do?
Basically, drawing a vector (any) on an
image.RGBA
orimage.Alpha
asdst
and aimage.Uniform
as src with these cases:A complete code with all test cases: https://github.com/haashemi/exp-vector-overflow/blob/main/main.go
A minimal code:
What did you see happen?
Based on the minimal code example's test cases, these happened:
Draws the skipped left part of the vector at right side of the image one pixel higher.
Draws the skipped right part of the vector at left side of the image one pixel lower.
What did you expect to see?
This issue can be splitted in two parts:
Panicing Which is totally unexpected as you can't even see any notes for possible panics.
With
image.RGBA
andDrawOP
asdraw.Over
, it callsrasterizeDstRGBASrcUniformOpOver
which tries to get access to dst.Pix's out of range item (both negative and positive here and here)The same goes for
image.Alpha
and maybeDrawOP
ofdraw.Src
with these two image types. (Didn't dive into it that much)Invalid drawings
As far as I found out, those overflows happened because
rasterizeDstRGBASrcUniformOpOver
(same forimage.Alpha
and maybeDrawOP
asdraw.Src
) writes the pixel colors without taking care of the dst's bounds but just the vector's bounds.For the third test case, We would expect this:
And for the forth case, We would expect this:
The easiest possible fix I could think of, is to limit the
r
's bounds to thedst
's bounds. if the r.Min is less than dst.Min, skip those differences and start from dst.Min, and same for r.Max and dst.Max.Here's a basic (of cource not good enough for a stdlib) patch I've made for that single method:
This change, fixes drawing a vector with
DrawOP
ofdraw.Over
on aimage.RGBA
with aimage.Uniform
src, but stillimage.Alpha
remains and I couldn't really find out how it works (I lack of knowledge 😅). Also I didn't do any tests ondraw.Src
, but as they have almost similar code, I think this issue could related to them too.The repository I mentioned earlier (this) contains everything I could possibly think of and has a little bit more details if it helps.