CVE-2024-32886 (published 2024-05-08T14:15Z)

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.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/vitessio/vitess
commit_details: repo_url: https://github.com/vitessio/vitess
author: Dirkjan Bussink
author_date: 2024-05-08 12:21:33+02:00
msg: 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>
repository: repo_url: https://github.com/vitessio/vitess
repo_name: vitessio/vitess
description: Vitess is a database clustering system for horizontal scaling of MySQL.
repo_language: Go
owner: vitessio
file_changes: filename: convert.go
change_type: ModificationType.MODIFY
diff: @@ -72,7 +72,7 @@ func convertSlow(dst []byte, dstCharset Charset, src []byte, srcCharset Charset)
 
 	for len(src) > 0 {
 		cp, width := srcCharset.DecodeRune(src)
-		if cp == utf8.RuneError && width < 3 {
+		if cp == utf8.RuneError {
 			failed++
 			cp = '?'
 		}

programming_language: Go
filename: helpers.go
change_type: ModificationType.MODIFY
diff: @@ -41,7 +41,7 @@ func Validate(charset Charset, input []byte) bool {
 	}
 	for len(input) > 0 {
 		r, size := charset.DecodeRune(input)
-		if r == RuneError && size < 2 {
+		if r == RuneError {
 			return false
 		}
 		input = input[size:]

programming_language: Go
filename: utf16.go
change_type: ModificationType.MODIFY
diff: @@ -67,7 +67,7 @@ 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
@@ -129,7 +129,7 @@ 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
@@ -185,7 +185,7 @@ func (Charset_ucs2) EncodeRune(dst []byte, r rune) int {
 
 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
 }

programming_language: Go
filename: utf32.go
change_type: ModificationType.MODIFY
diff: @@ -49,7 +49,7 @@ func (Charset_utf32) EncodeRune(dst []byte, r rune) int {
 
 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
 }

programming_language: Go
filename: compiler_asm.go
change_type: ModificationType.MODIFY
diff: @@ -5068,7 +5068,8 @@ func (asm *assembler) Fn_REGEXP_REPLACE_slow(merged collations.TypedCollation, f
 
 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

programming_language: Go
filename: compiler_test.go
change_type: ModificationType.MODIFY
diff: @@ -699,6 +699,30 @@ func TestCompilerSingle(t *testing.T) {
 			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")

programming_language: Go
filename: expr_collate.go
change_type: ModificationType.MODIFY
diff: @@ -18,6 +18,8 @@ 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"
@@ -131,20 +133,48 @@ func (expr *CollateExpr) compile(c *compiler) (ctype, error) {
 
 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

programming_language: Go
filename: translate.go
change_type: ModificationType.MODIFY
diff: @@ -373,7 +373,7 @@ func (ast *astCompiler) translateIntroducerExpr(introduced *sqlparser.Introducer
 		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
 			}

programming_language: Go