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

オープン 初心者向け
#137,187 コメント 3 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
2/5
見積もり時間
1〜3時間
初心者へのやさしさ
68/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
csharp
領域
build-system

調査の方向性

バグは src/installer/managed/Microsoft.NET.HostModel/AppHost/BinaryUtils.cs の WriteToStream にあります。チャンクサイズを計算する前に long の長さが int に縮小されています。まずそこから始めて、pos と bytesRequested の型を確認してください。issue にあるリフレクションによる再現手順を実行してください。修正後は、2 GiB+1 のケースで firstWrite=16384 が出力されるはずです。そのケースが宛先への最初の書き込みに到達し、Int32.MaxValue を超える長さをユニットテストがカバーしていれば完了です。

索引モデルが issue の本文から書いたものです。

説明

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

主要言語
C#
スター
18.4k
フォーク
5.8k
平均マージ
2日 14時間
マージ済み PR(30日)
585

環境構築

Codespaces で開く

このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

dotnet/runtime のほかの issue

dotnet/runtime の issue をすべて見る

似ている issue

C# の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。