CVE-2024-32886: Insufficient Information (NVD-CWE-noinfo)
value: Vitess is a database clustering system for horizontal scaling of MySQL. When executing the following simple query, the `vtgate` will go into an endless loop that also keeps consuming memory and eventually will run out of memory. This vulnerability is fixed in 19.0.4, 18.0.5, and 17.0.7.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-05-08T14:15Z
Repository: https://github.com/vitessio/vitess
Language: Go
Files changed: go/mysql/collations/charset/convert.go, go/mysql/collations/charset/helpers.go, go/mysql/collations/charset/unicode/utf16.go, go/mysql/collations/charset/unicode/utf32.go, go/vt/vtgate/evalengine/compiler_asm.go, go/vt/vtgate/evalengine/compiler_test.go, go/vt/vtgate/evalengine/expr_collate.go, go/vt/vtgate/evalengine/translate.go
Commit date: 2024-05-08 12:21:33 +0200
Commit message: Merge pull request from GHSA-649x-hxfx-57j2

* collations: Fix OOM and handle padding for multibyte

This fixes the OOM issue where a simple query can trigger a denial of
service attack. It also ensures we return the right result for these
queries by doing the correct padding.

Signed-off-by: Dirkjan Bussink <d.bussink@gmail.com>

* Address review comments

Signed-off-by: Dirkjan Bussink <d.bussink@gmail.com>

---------

Signed-off-by: Dirkjan Bussink <d.bussink@gmail.com>
Diff: diff --git a/go/mysql/collations/charset/convert.go b/go/mysql/collations/charset/convert.go
index bc51e9b837..261ef7c9b4 100644
--- a/go/mysql/collations/charset/convert.go
+++ b/go/mysql/collations/charset/convert.go
@@ -70,11 +70,11 @@ func convertSlow(dst []byte, dstCharset Charset, src []byte, srcCharset Charset)
 		dst = dst[:cap(dst)]
 	}
 
 	for len(src) > 0 {
 		cp, width := srcCharset.DecodeRune(src)
-		if cp == utf8.RuneError && width < 3 {
+		if cp == utf8.RuneError {
 			failed++
 			cp = '?'
 		}
 		src = src[width:]
 
diff --git a/go/mysql/collations/charset/helpers.go b/go/mysql/collations/charset/helpers.go
index 851ce4bebf..b66a6c77b8 100644
--- a/go/mysql/collations/charset/helpers.go
+++ b/go/mysql/collations/charset/helpers.go
@@ -39,11 +39,11 @@ func Validate(charset Charset, input []byte) bool {
 	if charset, ok := charset.(interface{ Validate([]byte) bool }); ok {
 		return charset.Validate(input)
 	}
 	for len(input) > 0 {
 		r, size := charset.DecodeRune(input)
-		if r == RuneError && size < 2 {
+		if r == RuneError {
 			return false
 		}
 		input = input[size:]
 	}
 	return true
diff --git a/go/mysql/collations/charset/unicode/utf16.go b/go/mysql/collations/charset/unicode/utf16.go
index eb055db738..924c12be7b 100644
--- a/go/mysql/collations/charset/unicode/utf16.go
+++ b/go/mysql/collations/charset/unicode/utf16.go
@@ -65,11 +65,11 @@ func (Charset_utf16be) EncodeRune(dst []byte, r rune) int {
 	}
 }
 
 func (Charset_utf16be) DecodeRune(b []byte) (rune, int) {
 	if len(b) < 2 {
-		return utf8.RuneError, 0
+		return utf8.RuneError, len(b)
 	}
 
 	r1 := uint16(b[1]) | uint16(b[0])<<8
 	if r1 < surr1 || surr3 <= r1 {
 		return rune(r1), 2
@@ -127,11 +127,11 @@ func (Charset_utf16le) EncodeRune(dst []byte, r rune) int {
 	}
 }
 
 func (Charset_utf16le) DecodeRune(b []byte) (rune, int) {
 	if len(b) < 2 {
-		return utf8.RuneError, 0
+		return utf8.RuneError, len(b)
 	}
 
 	r1 := uint16(b[0]) | uint16(b[1])<<8
 	if r1 < surr1 || surr3 <= r1 {
 		return rune(r1), 2
@@ -183,11 +183,11 @@ func (Charset_ucs2) EncodeRune(dst []byte, r rune) int {
 	return -1
 }
 
 func (Charset_ucs2) DecodeRune(p []byte) (rune, int) {
 	if len(p) < 2 {
-		return utf8.RuneError, 0
+		return utf8.RuneError, len(p)
 	}
 	return rune(p[0])<<8 | rune(p[1]), 2
 }
 
 func (Charset_ucs2) SupportsSupplementaryChars() bool {
diff --git a/go/mysql/collations/charset/unicode/utf32.go b/go/mysql/collations/charset/unicode/utf32.go
index 97095bb7f9..6053d7d10f 100644
--- a/go/mysql/collations/charset/unicode/utf32.go
+++ b/go/mysql/collations/charset/unicode/utf32.go
@@ -47,11 +47,11 @@ func (Charset_utf32) EncodeRune(dst []byte, r rune) int {
 	return 4
 }
 
 func (Charset_utf32) DecodeRune(p []byte) (rune, int) {
 	if len(p) < 4 {
-		return utf8.RuneError, 0
+		return utf8.RuneError, len(p)
 	}
 	return (rune(p[0]) << 24) | (rune(p[1]) << 16) | (rune(p[2]) << 8) | rune(p[3]), 4
 }
 
 func (Charset_utf32) SupportsSupplementaryChars() bool {
diff --git a/go/vt/vtgate/evalengine/compiler_asm.go b/go/vt/vtgate/evalengine/compiler_asm.go
index 2cda3ecb34..6c8896bb1f 100644
--- a/go/vt/vtgate/evalengine/compiler_asm.go
+++ b/go/vt/vtgate/evalengine/compiler_asm.go
@@ -5066,11 +5066,12 @@ func (asm *assembler) Fn_REGEXP_REPLACE_slow(merged collations.TypedCollation, f
 	}, "FN REGEXP_REPLACE_SLOW VARCHAR(SP-2), VARCHAR(SP-1)")
 }
 
 func (asm *assembler) Introduce(offset int, t sqltypes.Type, col collations.TypedCollation) {
 	asm.emit(func(env *ExpressionEnv) int {
-		arg := evalToBinary(env.vm.stack[env.vm.sp-offset])
+		var arg *evalBytes
+		arg, env.vm.err = introducerCast(env.vm.stack[env.vm.sp-offset], col.Collation)
 		arg.tt = int16(t)
 		arg.col = col
 		env.vm.stack[env.vm.sp-offset] = arg
 		return 1
 	}, "INTRODUCE (SP-1)")
diff --git a/go/vt/vtgate/evalengine/compiler_test.go b/go/vt/vtgate/evalengine/compiler_test.go
index b2d4ff0c2f..04eb72ad4f 100644
--- a/go/vt/vtgate/evalengine/compiler_test.go
+++ b/go/vt/vtgate/evalengine/compiler_test.go
@@ -697,10 +697,34 @@ func TestCompilerSingle(t *testing.T) {
 				else timestamp'2023-10-24 12:00:00'
 			end`,
 			result:     `DATETIME("2023-10-24 12:00:00.000000")`,
 			typeWanted: evalengine.NewTypeEx(sqltypes.Datetime, collations.CollationBinaryID, false, 6, 0, nil),
 		},
+		{
+			expression: `convert(0xFF using utf16)`,
+			result:     `VARCHAR("ÿ")`,
+		},
+		{
+			expression: `_utf16 0xFF`,
+			result:     `VARCHAR("ÿ")`,
+		},
+		{
+			expression: `convert(0xFF using utf32)`,
+			result:     `NULL`,
+		},
+		{
+			expression: `cast(_utf32 0xFF as binary)`,
+			result:     `VARBINARY("\x00\x00\x00\xff")`,
+		},
+		{
+			expression: `cast(_utf32 0x00FF as binary)`,
+			result:     `VARBINARY("\x00\x00\x00\xff")`,
+		},
+		{
+			expression: `cast(_utf32 0x0000FF as binary)`,
+			result:     `VARBINARY("\x00\x00\x00\xff")`,
+		},
 	}
 
 	tz, _ := time.LoadLocation("Europe/Madrid")
 	venv := vtenv.NewTestEnv()
 	for _, tc := range testCases {
diff --git a/go/vt/vtgate/evalengine/expr_collate.go b/go/vt/vtgate/evalengine/expr_collate.go
index bab0e5e52f..be0eb78882 100644
--- a/go/vt/vtgate/evalengine/expr_collate.go
+++ b/go/vt/vtgate/evalengine/expr_collate.go
@@ -16,10 +16,12 @@ limitations under the License.
 
 package evalengine
 
 import (
 	"vitess.io/vitess/go/mysql/collations"
+	"vitess.io/vitess/go/mysql/collations/charset"
+	"vitess.io/vitess/go/mysql/collations/colldata"
 	"vitess.io/vitess/go/sqltypes"
 	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
 	"vitess.io/vitess/go/vt/vterrors"
 )
 
@@ -129,24 +131,52 @@ func (expr *CollateExpr) compile(c *compiler) (ctype, error) {
 	return ct, nil
 }
 
 var _ IR = (*IntroducerExpr)(nil)
 
+func introducerCast(e eval, col collations.ID) (*evalBytes, error) {
+	if col == collations.CollationBinaryID {
+		return evalToBinary(e), nil
+	}
+
+	var bytes []byte
+	if b, ok := e.(*evalBytes); !ok {
+		bytes = b.ToRawBytes()
+	} else {
+		cs := colldata.Lookup(col).Charset()
+		bytes = b.bytes
+		// We only need to pad here for encodings that have a minimum
+		// character byte width larger than 1, which is all UTF-16
+		// variations and UTF-32.
+		switch cs.(type) {
+		case charset.Charset_utf16, charset.Charset_utf16le, charset.Charset_ucs2:
+			if len(bytes)%2 != 0 {
+				bytes = append([]byte{0}, bytes...)
+			}
+		case charset.Charset_utf32:
+			if mod := len(bytes) % 4; mod != 0 {
+				bytes = append(make([]byte, 4-mod), bytes...)
+			}
+		}
+	}
+	typedcol := collations.TypedCollation{
+		Collation:    col,
+		Coercibility: collations.CoerceCoercible,
+		Repertoire:   collations.RepertoireASCII,
+	}
+	return newEvalText(bytes, typedcol), nil
+}
+
 func (expr *IntroducerExpr) eval(env *ExpressionEnv) (eval, error) {
 	e, err := expr.Inner.eval(env)
 	if err != nil {
 		return nil, err
 	}
 
-	var b *evalBytes
-	if expr.TypedCollation.Collation == collations.CollationBinaryID {
-		b = evalToBinary(e)
-	} else {
-		b, err = evalToVarchar(e, expr.TypedCollation.Collation, false)
-		if err != nil {
-			return nil, err
-		}
+	b, err := introducerCast(e, expr.TypedCollation.Collation)
+	if err != nil {
+		return nil, err
 	}
 	b.flag |= flagExplicitCollation
 	return b, nil
 }
 
diff --git a/go/vt/vtgate/evalengine/translate.go b/go/vt/vtgate/evalengine/translate.go
index 99ffd95651..0091f06a63 100644
--- a/go/vt/vtgate/evalengine/translate.go
+++ b/go/vt/vtgate/evalengine/translate.go
@@ -371,11 +371,11 @@ func (ast *astCompiler) translateIntroducerExpr(introduced *sqlparser.Introducer
 	case *Literal:
 		switch collation {
 		case collations.CollationBinaryID:
 			lit.inner = evalToBinary(lit.inner)
 		default:
-			lit.inner, err = evalToVarchar(lit.inner, collation, false)
+			lit.inner, err = introducerCast(lit.inner, collation)
 			if err != nil {
 				return nil, err
 			}
 		}
 		return expr, nil

Vulnerable code: 		if cp == utf8.RuneError && width < 3 {
		if r == RuneError && size < 2 {
		return utf8.RuneError, 0
		return utf8.RuneError, 0
		return utf8.RuneError, 0
		return utf8.RuneError, 0
		arg := evalToBinary(env.vm.stack[env.vm.sp-offset])
	var b *evalBytes
	if expr.TypedCollation.Collation == collations.CollationBinaryID {
		b = evalToBinary(e)
	} else {
		b, err = evalToVarchar(e, expr.TypedCollation.Collation, false)
		if err != nil {
			return nil, err
		}
			lit.inner, err = evalToVarchar(lit.inner, collation, false)
Fixed code: 		if cp == utf8.RuneError {
		if r == RuneError {
		return utf8.RuneError, len(b)
		return utf8.RuneError, len(b)
		return utf8.RuneError, len(p)
		return utf8.RuneError, len(p)
		var arg *evalBytes
		arg, env.vm.err = introducerCast(env.vm.stack[env.vm.sp-offset], col.Collation)
		{
			expression: `convert(0xFF using utf16)`,
			result:     `VARCHAR("ÿ")`,
		},
		{
			expression: `_utf16 0xFF`,
			result:     `VARCHAR("ÿ")`,
		},
		{
			expression: `convert(0xFF using utf32)`,
			result:     `NULL`,
		},
		{
			expression: `cast(_utf32 0xFF as binary)`,
			result:     `VARBINARY("\x00\x00\x00\xff")`,
		},
		{
			expression: `cast(_utf32 0x00FF as binary)`,
			result:     `VARBINARY("\x00\x00\x00\xff")`,
		},
		{
			expression: `cast(_utf32 0x0000FF as binary)`,
			result:     `VARBINARY("\x00\x00\x00\xff")`,
		},
	"vitess.io/vitess/go/mysql/collations/charset"
	"vitess.io/vitess/go/mysql/collations/colldata"
func introducerCast(e eval, col collations.ID) (*evalBytes, error) {
	if col == collations.CollationBinaryID {
		return evalToBinary(e), nil
	}

	var bytes []byte
	if b, ok := e.(*evalBytes); !ok {
		bytes = b.ToRawBytes()
	} else {
		cs := colldata.Lookup(col).Charset()
		bytes = b.bytes
		// We only need to pad here for encodings that have a minimum
		// character byte width larger than 1, which is all UTF-16
		// variations and UTF-32.
		switch cs.(type) {
		case charset.Charset_utf16, charset.Charset_utf16le, charset.Charset_ucs2:
			if len(bytes)%2 != 0 {
				bytes = append([]byte{0}, bytes...)
			}
		case charset.Charset_utf32:
			if mod := len(bytes) % 4; mod != 0 {
				bytes = append(make([]byte, 4-mod), bytes...)
			}
		}
	}
	typedcol := collations.TypedCollation{
		Collation:    col,
		Coercibility: collations.CoerceCoercible,
		Repertoire:   collations.RepertoireASCII,
	}
	return newEvalText(bytes, typedcol), nil
}

	b, err := introducerCast(e, expr.TypedCollation.Collation)
	if err != nil {
		return nil, err
			lit.inner, err = introducerCast(lit.inner, collation)