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

未关闭 适合新手
#137,187 3 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
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 小时
30 天内合并 PR
585

环境准备

在 Codespaces 中打开

在浏览器里用你自己的 GitHub 账号启动这个项目的开发容器。

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

dotnet/runtime 的其他 Issue

查看 dotnet/runtime 的全部 Issue

相似的 Issue

更多 C# Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。