Repository: goccy/go-json (go)

Issue:
Slice out of bounds panic when decoding truncated JSON
Hi,

I'm getting the following panic when decoding `{"\u` which is invalid but shouldn't cause a panic:

```
testing.go:1356: panic: runtime error: slice bounds out of range [:8] with capacity 5
    goroutine 9 [running]:
    runtime/debug.Stack()
    	/usr/lib/go-1.19/src/runtime/debug/stack.go:24 +0xdb
    testing.tRunner.func1()
    	/usr/lib/go-1.19/src/testing/testing.go:1356 +0x1f2
    panic({0x724b60, 0xc00001c150})
    	/usr/lib/go-1.19/src/runtime/panic.go:884 +0x212
    github.com/goccy/go-json/internal/decoder.decodeKeyCharByUnicodeRune({0xc0000181e4, 0x5, 0x5}, 0x4)
    	/home/gbe/go/pkg/mod/github.com/goccy/go-json@v0.10.1-0.20221201165857-a2149a5b2591/internal/decoder/struct.go:165 +0x818
    github.com/goccy/go-json/internal/decoder.decodeKeyCharByEscapedChar({0xc0000181e4, 0x5, 0x5}, 0x3)
    	/home/gbe/go/pkg/mod/github.com/goccy/go-json@v0.10.1-0.20221201165857-a2149a5b2591/internal/decoder/struct.go:201 +0x54f
    github.com/goccy/go-json/internal/decoder.decodeKeyByBitmapUint8(0xc0000000a0, {0xc0000181e4, 0x5, 0x5}, 0x1)
    	/home/gbe/go/pkg/mod/github.com/goccy/go-json@v0.10.1-0.20221201165857-a2149a5b2591/internal/decoder/struct.go:245 +0x649
    github.com/goccy/go-json/internal/decoder.(*structDecoder).Decode(0xc0000000a0, 0xc0000720c0, 0x0, 0x0, 0xc0000181e0)
    	/home/gbe/go/pkg/mod/github.com/goccy/go-json@v0.10.1-0.20221201165857-a2149a5b2591/internal/decoder/struct.go:760 +0x8da
    github.com/goccy/go-json.unmarshal({0xc0000181b8, 0x4, 0xc00007e150?}, {0x6f9a20, 0xc0000181e0}, {0x0, 0x0, 0xc0001317d0?})
    	/home/gbe/go/pkg/mod/github.com/goccy/go-json@v0.10.1-0.20221201165857-a2149a5b2591/decode.go:47 +0x327
    github.com/goccy/go-json.Unmarshal(...)
    	/home/gbe/go/pkg/mod/github.com/goccy/go-json@v0.10.1-0.20221201165857-a2149a5b2591/json.go:276
    json-u.Fuzz_JSON_Unmarshal.func1(0xc000007520, {0xc0000181b8, 0x4, 0x8})
    	/tmp/json-u/jsonu_test.go:17 +0xa8
    reflect.Value.call({0x6ffba0?, 0x746a68?, 0x13?}, {0x735410, 0x4}, {0xc00007e120, 0x2, 0x2?})
    	/usr/lib/go-1.19/src/reflect/value.go:584 +0x8c5
    reflect.Value.Call({0x6ffba0?, 0x746a68?, 0x51b?}, {0xc00007e120?, 0x88ed10?, 0x8b8720?})
    	/usr/lib/go-1.19/src/reflect/value.go:368 +0xbc
    testing.(*F).Fuzz.func1.1(0x0?)
    	/usr/lib/go-1.19/src/testing/fuzz.go:337 +0x231
    testing.tRunner(0xc000007520, 0xc000128090)
    	/usr/lib/go-1.19/src/testing/testing.go:1446 +0x10b
    created by testing.(*F).Fuzz.func1
    	/usr/lib/go-1.19/src/testing/fuzz.go:324 +0x5b9
```

This is the fuzz test I used:

```
package foobar

import "testing"
import "strings"
import "github.com/goccy/go-json"

func Fuzz_JSON_Unmarshal(f *testing.F) {
	f.Add([]byte(`{}`))
	f.Add([]byte(`{"n": 123}`))
	f.Add([]byte(`{"n": -123}`))

	f.Fuzz(func(t *testing.T, input []byte) {
		var x struct {
			N int32 `json:"n"`
		}

		err := json.Unmarshal(input, &x)
		if err == nil {
			return
		}

		// I was actually looking for a specific error message and just stumbled upon
		// the panic.
		if strings.Contains(err.Error(), "number u") {
			t.Error("found it")
		}
	})
}
```

There are two closed issues with similar panics, one of them while decoding unicode but in a slightly different code path.

Pull request:
fix: added buffer size check when decoding key
fix #429

Patch:
diff --git a/internal/decoder/struct.go b/internal/decoder/struct.go
index 6d32654..4e14a20 100644
--- a/internal/decoder/struct.go
+++ b/internal/decoder/struct.go
@@ -158,49 +158,53 @@ func (d *structDecoder) tryOptimize() {
 }
 
 // decode from '\uXXXX'
-func decodeKeyCharByUnicodeRune(buf []byte, cursor int64) ([]byte, int64) {
+func decodeKeyCharByUnicodeRune(buf []byte, cursor int64) ([]byte, int64, error) {
 	const defaultOffset = 4
 	const surrogateOffset = 6
 
+	if cursor+defaultOffset >= int64(len(buf)) {
+		return nil, 0, errors.ErrUnexpectedEndOfJSON("escaped string", cursor)
+	}
+
 	r := unicodeToRune(buf[cursor : cursor+defaultOffset])
 	if utf16.IsSurrogate(r) {
 		cursor += defaultOffset
 		if cursor+surrogateOffset >= int64(len(buf)) || buf[cursor] != '\\' || buf[cursor+1] != 'u' {
-			return []byte(string(unicode.ReplacementChar)), cursor + defaultOffset - 1
+			return []byte(string(unicode.ReplacementChar)), cursor + defaultOffset - 1, nil
 		}
 		cursor += 2
 		r2 := unicodeToRune(buf[cursor : cursor+defaultOffset])
 		if r := utf16.DecodeRune(r, r2); r != unicode.ReplacementChar {
-			return []byte(string(r)), cursor + defaultOffset - 1
+			return []byte(string(r)), cursor + defaultOffset - 1, nil
 		}
 	}
-	return []byte(string(r)), cursor + defaultOffset - 1
+	return []byte(string(r)), cursor + defaultOffset - 1, nil
 }
 
-func decodeKeyCharByEscapedChar(buf []byte, cursor int64) ([]byte, int64) {
+func decodeKeyCharByEscapedChar(buf []byte, cursor int64) ([]byte, int64, error) {
 	c := buf[cursor]
 	cursor++
 	switch c {
 	case '"':
-		return []byte{'"'}, cursor
+		return []byte{'"'}, cursor, nil
 	case '\\':
-		return []byte{'\\'}, cursor
+		return []byte{'\\'}, cursor, nil
 	case '/':
-		return []byte{'/'}, cursor
+		return []byte{'/'}, cursor, nil
 	case 'b':
-		return []byte{'\b'}, cursor
+		return []byte{'\b'}, cursor, nil
 	case 'f':
-		return []byte{'\f'}, cursor
+		return []byte{'\f'}, cursor, nil
 	case 'n':
-		return []byte{'\n'}, cursor
+		return []byte{'\n'}, cursor, nil
 	case 'r':
-		return []byte{'\r'}, cursor
+		return []byte{'\r'}, cursor, nil
 	case 't':
-		return []byte{'\t'}, cursor
+		return []byte{'\t'}, cursor, nil
 	case 'u':
 		return decodeKeyCharByUnicodeRune(buf, cursor)
 	}
-	return nil, cursor
+	return nil, cursor, nil
 }
 
 func decodeKeyByBitmapUint8(d *structDecoder, buf []byte, cursor int64) (int64, *structFieldSet, error) {
@@ -242,7 +246,10 @@ func decodeKeyByBitmapUint8(d *structDecoder, buf []byte, cursor int64) (int64,
 					return 0, nil, errors.ErrUnexpectedEndOfJSON("string", cursor)
 				case '\\':
 					cursor++
-					chars, nextCursor := decodeKeyCharByEscapedChar(buf, cursor)
+					chars, nextCursor, err := decodeKeyCharByEscapedChar(buf, cursor)
+					if err != nil {
+						return 0, nil, err
+					}
 					for _, c := range chars {
 						curBit &= bitmap[keyIdx][largeToSmallTable[c]]
 						if curBit == 0 {
@@ -305,7 +312,10 @@ func decodeKeyByBitmapUint16(d *structDecoder, buf []byte, cursor int64) (int64,
 					return 0, nil, errors.ErrUnexpectedEndOfJSON("string", cursor)
 				case '\\':
 					cursor++
-					chars, nextCursor := decodeKeyCharByEscapedChar(buf, cursor)
+					chars, nextCursor, err := decodeKeyCharByEscapedChar(buf, cursor)
+					if err != nil {
+						return 0, nil, err
+					}
 					for _, c := range chars {
 						curBit &= bitmap[keyIdx][largeToSmallTable[c]]
 						if curBit == 0 {


Test patch:
diff --git a/decode_test.go b/decode_test.go
index f139eae..a1a10e9 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -4015,3 +4015,18 @@ func TestIssue408(t *testing.T) {
 		t.Fatal(err)
 	}
 }
+
+func TestIssue429(t *testing.T) {
+	var x struct {
+		N int32
+	}
+	for _, b := range []string{
+		`{"\u"`,
+		`{"\u0"`,
+		`{"\u00"`,
+	} {
+		if err := json.Unmarshal([]byte(b), &x); err == nil {
+			t.Errorf("unexpected success")
+		}
+	}
+}