.NET 10 single-file publishing silently produces empty executables above 2 GiB due to integer overflow in BinaryUtils.WriteToStream

Aperta Adatta ai principianti
#137,187 3 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
2/5
Tempo stimato
1-3 ore
Idoneità per principianti
68/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
csharp
Ambito
build-system

Direzione di ricerca

Il bug si trova in WriteToStream in src/installer/managed/Microsoft.NET.HostModel/AppHost/BinaryUtils.cs, dove la lunghezza long viene ridotta a int prima del calcolo della dimensione del blocco. Inizia da lì e verifica come sono tipizzati pos e bytesRequested. Esegui il repro con reflection dell'issue, che dovrebbe stampare firstWrite=16384 per il caso 2 GiB+1 una volta corretto. È completato quando quel caso raggiunge la prima scrittura sulla destinazione e un unit test copre lunghezze superiori a Int32.MaxValue.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

area-Host untriaged
Description

Publishing a single-file executable larger than 2 GiB can produce a 0-byte executable while reporting success.

The issue appears to be a narrowing conversion in BinaryUtils.WriteToStream:
https://github.com/dotnet/runtime/blob/6aa72daa24f66c01c3d7b6a32519e9d21396deec/src/installer/managed/Microsoft.NET.HostModel/AppHost/BinaryUtils.cs#L186

For lengths between 2 GiB and 4 GiB, converting length to int produces a negative value. On the first iteration, bytesRequested <= 0, so the method returns without writing anything or throwing an exception.

The .NET 10 bundler creates/truncates the output file immediately before calling this method, leaving an empty executable. dotnet publish nevertheless exits successfully.

Reproduction Steps

Use .NET SDK 10.0.403 with a 64-bit process.
The following dependency-free console application invokes the actual internal SDK method through reflection. It does not substitute or patch the implementation.
To avoid generating a multi-gigabyte installer, it uses a large virtual mapping and stops at the first destination write. It only needs to read one chunk, and no large output file is created.

  1. Create Repro.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <None Include="$(NetCoreRoot)sdk\$(NETCoreSdkVersion)\Microsoft.NET.HostModel.dll"
          Link="Microsoft.NET.HostModel.dll"
          CopyToOutputDirectory="Always" />
  </ItemGroup>
</Project>
  1. Create Program.cs
using System.IO.MemoryMappedFiles;
using System.Reflection;

var assembly = Assembly.LoadFrom(
    Path.Combine(AppContext.BaseDirectory, "Microsoft.NET.HostModel.dll"));

var method = assembly
    .GetType("Microsoft.NET.HostModel.AppHost.BinaryUtils", throwOnError: true)
    .GetMethod("WriteToStream", BindingFlags.Static | BindingFlags.NonPublic);

var copy = method.CreateDelegate<
    Action<MemoryMappedViewAccessor, FileStream, long>>();

bool failed = false;

foreach (long length in new[] { 16384L, 2147483649L })
{
    using var map = MemoryMappedFile.CreateNew(null, length);
    using var source = map.CreateViewAccessor();
    using var destination = new FirstWriteStream();

    try
    {
        copy(source, destination, length);
    }
    catch (CopyStartedException)
    {
        // Stop after the first chunk; no multi-gigabyte output file is needed.
    }

    Console.WriteLine(
        $"length={length}, firstWrite={destination.BytesWritten}");

    failed |= destination.BytesWritten == 0;
}

return failed ? 1 : 0;

sealed class FirstWriteStream : FileStream
{
    public int BytesWritten { get; private set; }

    public FirstWriteStream()
        : base(Path.GetTempFileName(), FileMode.Open,
            FileAccess.Write, FileShare.None, 1, FileOptions.DeleteOnClose)
    {
    }

    public override void Write(byte[] buffer, int offset, int count)
        => Stop(count);

    public override void Write(ReadOnlySpan<byte> buffer)
        => Stop(buffer.Length);

    private void Stop(int count)
    {
        BytesWritten = count;
        throw new CopyStartedException();
    }
}

sealed class CopyStartedException : Exception
{
}
  1. run
dotnet run --configuration Release
Expected behavior

WriteToStream should handle valid lengths and positions larger than Int32.MaxValue without truncation.

Both reproduction cases should reach the first destination write. With the current 16 KiB buffer size, the output would be:

length=16384, firstWrite=16384
length=2147483649, firstWrite=16384
Actual behavior

The verified reproduction prints:

length=16384, firstWrite=16384
length=2147483649, firstWrite=0

The reproduction itself exits with code 1 to flag that failure. In contrast, the affected dotnet publish command exits with code 0.

Regression?

This appears to be a publishing-path regression that exposes a pre-existing helper bug.
The narrowing conversion already exists in the .NET 9 helper. However:

  • The .NET 9 bundler writes the bundle directly to the output file.
  • The .NET 10 bundler writes the completed memory-mapped bundle through BinaryUtils.WriteToStream.

Verified with SDK 10.0.403.

Known Workarounds

The relevant implementation is in the function:
https://github.com/dotnet/runtime/blob/6aa72daa24f66c01c3d7b6a32519e9d21396deec/src/installer/managed/Microsoft.NET.HostModel/AppHost/BinaryUtils.cs#L177

pos must be long, as length is long:
https://github.com/dotnet/runtime/blob/6aa72daa24f66c01c3d7b6a32519e9d21396deec/src/installer/managed/Microsoft.NET.HostModel/AppHost/BinaryUtils.cs#L179

-int pos = 0;
+long pos = 0;

Cast to int without overflow:
https://github.com/dotnet/runtime/blob/6aa72daa24f66c01c3d7b6a32519e9d21396deec/src/installer/managed/Microsoft.NET.HostModel/AppHost/BinaryUtils.cs#L186

- int bytesRequested = Math.Min((int)length - pos, bufSize);
+ int bytesRequested = (int)Math.Min(length - pos, (long)bufSize);

This narrows only the bounded chunk size, rather than the total length.

Configuration

No response

Other information

No response

Lingua principale
C#
Stelle
18.4k
Fork
5.8k
Merge medio
2g 14h
PR unite (30g)
585

Preparare l'ambiente

Apri in Codespaces

Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di dotnet/runtime

Tutte le issue di dotnet/runtime

Issue simili

Altre issue su C#

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.