Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

Ordinal.EqualsIgnoreCaseUtf8_Scalar reads 2 bytes past both buffers when tail is 3 bytes with non-ASCII data

Cerrado Apto para principiantes
#134,840 4 comentarios 1 reacción 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
2/5
Tiempo estimado
1-3 horas
Aptitud para principiantes
76/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
csharp
Área
backend

Línea de trabajo

Comienza en src/libraries/System.Private.CoreLib/src/System/Globalization/Ordinal.Utf8.cs e inspecciona el manejo final de length == 3 en EqualsIgnoreCaseUtf8_Scalar y StartsWithIgnoreCaseUtf8_Scalar. Reproduce el problema con los spans UTF-8 de double.TryParse proporcionados y verifica que ambas entradas de 3 bytes que no coinciden devuelvan false independientemente de los bytes de tráiler, sin leer más allá de ninguno de los dos búferes.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

area-System.Memory untriaged
Description

Ordinal.EqualsIgnoreCaseUtf8_Scalar (and StartsWithIgnoreCaseUtf8_Scalar) in Ordinal.Utf8.cs read 2 bytes past the end of both input buffers when the remaining tail after the widening loops is exactly 3 bytes and contains non-ASCII data.

Root cause: The length == 3 tail branch advances byteOffset by 2 to compose the third byte into a uint:

if (length == 3)
{
    valueAu32 = Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref charA, byteOffset));
    valueBu32 = Unsafe.ReadUnaligned<ushort>(ref Unsafe.AddByteOffset(ref charB, byteOffset));

    byteOffset += 2;                        // ← advances the shared offset

    valueAu32 |= (uint)(Unsafe.AddByteOffset(ref charA, byteOffset) << 16);
    valueBu32 |= (uint)(Unsafe.AddByteOffset(ref charB, byteOffset) << 16);
}

When the composed value contains non-ASCII bytes, execution jumps to NonAscii32 → NonAscii, which computes the fallback pointer and length:

NonAscii:
    range -= length;
    return EqualsStringIgnoreCaseUtf8(
        ref Unsafe.AddByteOffset(ref charA, byteOffset), lengthA - range,
        ref Unsafe.AddByteOffset(ref charB, byteOffset), lengthB - range);

byteOffset is now 2 past the start of the tail (due to the += 2), but range -= length still yields the number of bytes consumed by the main loops only (not the tail). The fallback gets a pointer 2 bytes too far with a length 2 bytes too long — reading past both buffers.

The length == 1 and length == 2 branches don't modify byteOffset, so they're not affected.

Suggested fix: Don't mutate byteOffset in the tail — use an inline offset:

     byteOffset += 2;
-
-    valueAu32 |= (uint)(Unsafe.AddByteOffset(ref charA, byteOffset) << 16);
-    valueBu32 |= (uint)(Unsafe.AddByteOffset(ref charB, byteOffset) << 16);
+    valueAu32 |= (uint)(Unsafe.AddByteOffset(ref charA, byteOffset + 2) << 16);
+    valueBu32 |= (uint)(Unsafe.AddByteOffset(ref charB, byteOffset + 2) << 16);

Apply in both EqualsIgnoreCaseUtf8_Scalar and StartsWithIgnoreCaseUtf8_Scalar.

Observable impact: double.TryParse(ReadOnlySpan<byte>, NumberStyles, IFormatProvider, out double) with a NumberFormatInfo whose infinity/NaN symbol encodes to a UTF-8 length ≡ 3 (mod 4) with non-ASCII characters gives incorrect results that depend on memory past the input span. The default ICU infinity symbol "\u221E" (∞ = E2 88 9E, 3 bytes) hits this on every culture that uses it.

Repro:

using System.Globalization;
using System.Text;

var nfi = (NumberFormatInfo)CultureInfo.InvariantCulture.NumberFormat.Clone();
nfi.PositiveInfinitySymbol = "\u221E"; // ∞, 3 bytes UTF-8: E2 88 9E

// U+301E (〞) encodes as E3 80 9E — different character, same byte length.
// The UTF-16 char TryParse correctly rejects it:
Console.WriteLine(double.TryParse("\u301E", NumberStyles.Float, nfi, out _)); // False ✓

// The UTF-8 byte TryParse result depends on what follows the span in memory:
byte[] withNulTrailer = [0xE3, 0x80, 0x9E, 0x00, 0x00];
byte[] withAsciiTrailer = [0xE3, 0x80, 0x9E, 0x41, 0x41];

// Both parse exactly the same 3-byte span — only the trailer bytes differ
Console.WriteLine(double.TryParse(withNulTrailer.AsSpan(0, 3), NumberStyles.Float, nfi, out _));
// True ✗ — wrong! Accepted because OOB NULs matched the symbol cache's NUL terminator

Console.WriteLine(double.TryParse(withAsciiTrailer.AsSpan(0, 3), NumberStyles.Float, nfi, out _));
// False — different OOB bytes, different result

Expected: Both UTF-8 TryParse calls return false (U+301E ≠ U+221E), matching the char overload.

Actual: The first returns true (accepts a non-matching input as ∞), the second returns false. The result depends on bytes outside the input span.

Configuration

Reproduced on:

  • .NET 10.0.12 (10.0.401 SDK), Windows 11 x64
  • Expected to affect .NET 8.x and 9.x (any version with Ordinal.Utf8.cs widening-read scalar paths)
Regression?

Introduced with the Ordinal.Utf8.cs widening-read scalar paths (the TChar-generic / IUtf8SpanParsable rewrite). Not present in .NET 7 or earlier (no UTF-8 TryParse overloads).

Other information

Found via coverage-guided fuzzing (SharpFuzz) of double.TryParse(ReadOnlySpan<byte>) with randomised NumberFormatInfo symbols.

The EqualsIgnoreCaseUtf8_Vector128 path has a separate (correctness-only, no OOB) issue: at line 145 it passes (int)(lengthU - i) as both lengthA and lengthB to the scalar fallback, discarding the actual lengths — so the scalar function's lengthA != lengthB check is dead and it can return true when the original inputs had different lengths.

Lenguaje dominante
C#
Estrellas
18.4k
Forks
5.7k
Merge medio
2 d 21 h
PR fusionados (30 d)
657

Preparar el entorno

Abrir en Codespaces

Inicia el contenedor de desarrollo del proyecto en tu navegador, con tu propia cuenta de GitHub.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de dotnet/runtime

Todos los issues de dotnet/runtime

Issues similares

Más issues de C#

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.