arm64 ReadyToRun/NativeAOT: in a method over 1 MB, loads of the method's own read-only data (IF_LARGELDC / IF_LARGEADR) are emitted without relocations and read the wrong bytes
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 45/100
調査の方向性
Start in src/coreclr/jit/emitarm64.cpp, in emitOutputLJ, and compare the IF_LARGELDC and IF_LARGEADR handling with emitOutputLoadLabel. Reproduce with the supplied generator and ReadyToRun or NativeAOT on arm64; inspect the emitted relocations for the constant loads and switch table. Done means the long-form loads address the method's read-only data correctly in both AOT paths.
索引モデルが issue の本文から書いたものです。
説明
Description
When crossgen2 (ReadyToRun) or ILC (NativeAOT) compiles a method whose arm64 code is larger than 1 MB, the JIT reaches the method's read-only data (RWD constants: Vector128 and double constants, switch jump tables) from points more than 1 MB away with the long forms IF_LARGELDC (adrp + ldr, or adrp + add + ld1 for 16 bytes) and IF_LARGEADR (adrp + add). In emitOutputLJ these are encoded from the addresses of the JIT's temporary code and data buffers:
// src/coreclr/jit/emitarm64.cpp, emitOutputLJ, iiaIsJitDataOffset() path, loadConstant && !idjShort
ssize_t relPageAddr = computeRelPageAddr((size_t)dstAddr, (size_t)srcAddr);
...
ssize_t imm12 = (ssize_t)dstAddr & 0xFFF; // 12 bits
No relocation is recorded for them. The data path for IF_LARGEADR goes through emitOutputLoadLabel, which does record ARM64_PAGEBASE_REL21/ARM64_PAGEOFFSET_12A, but only when id->idIsReloc(), and that is not set for JIT data. adrp depends on the page offset (address & 0xFFF) of the instruction and of its target. The page offset of the buffer the AOT compiler hands the JIT is unrelated to where the method ends up in the image, so the final adrp+offset points at the wrong address. In practice that is usually somewhere else in the code, and the load reads instruction bytes. A switch through a wrong jump table branches to a wrong address and crashes.
With the runtime JIT the buffer is the final code address, so the same encoding is correct. Loads within 1 MB of the data use the short pc-relative ldr/adr, which are position-independent and correct. x64 is unaffected.
Reproduction Steps
A generator writes one method, Huge.Run, with N straight-line calls. It loads a Vector128<ulong> constant, a double constant and (optionally) a switch jump table at its start, and another vector and double at its end.
gen.py:
import sys
n = int(sys.argv[1])
switch = "--no-switch" not in sys.argv[2:]
out = []
w = out.append
w("using System.Runtime.CompilerServices;")
w("using System.Runtime.Intrinsics;")
w("public static class Huge")
w("{")
w(" [MethodImpl(MethodImplOptions.NoInlining)] static int F(int x, int k) => x ^ k;")
w(" [MethodImpl(MethodImplOptions.NoInlining)] static void Show(string what, Vector128<ulong> v) => System.Console.WriteLine($\"{what}: {v.GetElement(0):X16} {v.GetElement(1):X16}\");")
w(" [MethodImpl(MethodImplOptions.NoInlining)] static void Show(string what, double d) => System.Console.WriteLine($\"{what}: {d:R}\");")
w(" public static int Run(int x)")
w(" {")
w(" Show(\"start vector\", Vector128.Create(0x0123456789ABCDEFUL, 0xFEDCBA9876543210UL));")
w(" Show(\"start double\", 3.141592653589793);")
if switch: w(" switch (x) { case 1: Show(\"start switch\", 1.0); break; case 2: Show(\"start switch\", 2.0); break; case 3: Show(\"start switch\", 3.0); break; case 4: Show(\"start switch\", 4.0); break; case 5: Show(\"start switch\", 5.0); break; default: Show(\"start switch\", 0.0); break; }")
for i in range(n):
w(" x = F(x, %d);" % (i * 7919 % 1000003))
w(" Show(\"end vector \", Vector128.Create(0x1111111122222222UL, 0x3333333344444444UL));")
w(" Show(\"end double \", 2.718281828459045);")
w(" return x;")
w(" }")
w("}")
print("\n".join(out))
Program.cs:
System.Console.WriteLine(System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription + " " +
System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture);
System.Console.WriteLine("result: " + Huge.Run(1));
Repro.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0;net11.0</TargetFrameworks>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
</Project>
Build on linux-x64 and run under arm64 emulation:
python3 gen.py 32000 --no-switch > Huge.cs # Huge.Run: 416 KB IL, 1.09 MB arm64 code
dotnet publish -c Release -f net10.0 -r linux-arm64 --self-contained -p:PublishReadyToRun=true -o out \
"-p:PublishReadyToRunCrossgen2ExtraArgs=--map%3B--codegenopt:JitDisasm=Run%3B--codegenopt:JitStdOutFile=$PWD/disasm.txt"
docker run --rm --platform linux/arm64 -v $PWD/out:/app debian:13-slim /app/Repro
docker run --rm --platform linux/arm64 -v $PWD/out:/app -e DOTNET_ReadyToRun=0 debian:13-slim /app/Repro
Expected behavior
The same output with and without the R2R code:
.NET 10.0.15 Arm64
start vector: 0123456789ABCDEF FEDCBA9876543210
start double: 3.141592653589793
end vector : 1111111122222222 3333333344444444
end double : 2.718281828459045
result: 591691
Actual behavior
With the R2R code, the two constants loaded at the start of the method (more than 1 MB before the data) are wrong. The ones at the end (short pc-relative ldr) are right:
.NET 10.0.15 Arm64
start vector: 0001355620020010 0200100600000128
start double: 1.60796364073817E-309
end vector : 1111111122222222 3333333344444444
end double : 2.718281828459045
result: 591691
.NET 11.0.0 gives the same result (start vector: 0001280001544520 7334200200100600, start double: 1.7713159171880902E-231).
With the switch (python3 gen.py 32000), the jump table is also read from the wrong address. The process dies with SIGSEGV on .NET 10.0.15 and 11.0.0 after printing the two wrong constants. At N=30000 it threw a NullReferenceException in Huge.Run instead.
The JIT's listing (crossgen2 10.0.15, JitDisasm=Run) shows the long forms at the start and the short form at the end:
; Assembly listing for method Huge:Run(int):int (FullOpts)
; ReadyToRun compilation
G_M000_IG02: ;; offset=0x0048
adrp x0, [HIGH RELOC #0x420210]
add x0, x0, [LOW RELOC #0x420210]
ldr x0, [x0]
ldr x0, [x0]
ldr q0, (LARGELDC)[@RWD00]
...
ldr d0, (LARGELDC)[@RWD16]
...
adr x11, (LARGEADR)[@RWD24]
ldr w11, [x11, x0, LSL #2]
...
ldr q0, [@RWD48] ; near the end: short pc-relative ldr
...
RWD00 dq 0123456789ABCDEFh, FEDCBA9876543210h
RWD16 dq 400921FB54442D18h ; 3.14159265
RWD24 dd 00000120h ; case G_M000_IG08
Decoding the instructions in the published Repro.dll (RVA and size of Huge.Run from crossgen2 --map: 0x00023500 | 0x117AA8 | .text | Repro_Huge__Run (MethodWithGCInfo)) shows where these pairs point:
method RVA 0x23500..0x13AFA8 (1145512 bytes of code + read-only data)
constant 0123456789ABCDEF is at RVA 0x13AF70 (method+0x117A70)
constant 400921FB54442D18 is at RVA 0x13AF80 (method+0x117A80)
method+0x000058: adrp x11, +280 pages; add x11, x11, #0x2D0 -> RVA 0x13B2D0 (.text)
method+0x000084: adrp x11, +280 pages; ldr x11, [x11, #0x2E0] -> RVA 0x13B2E0 (.text)
The data is at RVA 0x13AF70, but the pair computes 0x13B2D0. The low 12 bits (0x2D0) are those of the JIT's buffer, not of the image. The decoder is a short Python script that searches for adrp xN followed by an add/ldr on xN, and I can attach it. Every other adrp in the method points at its import cell in .data, through a relocation.
NativeAOT (ILC 10.0.15 and 11.0.0, linux-arm64) has the same bug. ILC's code is smaller, so it needs N=45000 (Repro_Huge__Run, 0x143988 bytes). In Repro.o, every adrp in Huge.Run has an R_AARCH64_ADR_PREL_PG_HI21 relocation except the three for the vector, the double and the jump table:
Run+0x048 adrp x0 reloc: None
Run+0x054 adrp x0 reloc: R_AARCH64_ADR_PREL_PG_HI21 __Str_start_vector_...
Run+0x060 adrp x0 reloc: None
Run+0x06C adrp x0 reloc: R_AARCH64_ADR_PREL_PG_HI21 __Str_start_double_...
Run+0x088 adrp x1 reloc: None
The linked binary prints start vector: 72A0018152920841 B901D7A097FAF23C and start double: 6.97014894305068E+89, then dies with SIGSEGV (.NET 10.0.15; .NET 11.0.0 is the same).
Threshold: the long forms appear as soon as the data is more than 1 MB (the ±1 MB range of ldr-literal/adr) from the load. N=29500 (1,041,592 bytes of code) is correct with no LARGE* instruction. N=30000 (1,062,428 bytes) uses them and fails.
Regression?
Not known. 10.0.15 and 11.0.0 both fail. Older versions were not tested.
Known Workarounds
- Keep methods under about 1 MB of arm64 code.
- Leave such methods to the runtime JIT, for example by keeping them out of the R2R image.
DOTNET_ReadyToRun=0makes the repro correct. The runtime JIT (10.0.15) emits the sameLARGELDC/LARGEADRforms (Tier0, 1,144,064 bytes; also withDOTNET_TieredCompilation=0) but encodes them from the real code address.
Configuration
- crossgen2 (microsoft.netcore.app.crossgen2.linux-x64) 10.0.15 and 11.0.0. ILC 10.0.15 and 11.0.0. SDKs 10.0.401 and 11.0.100.
- Target linux-arm64, self-contained. Run in
debian:13-slim(arm64) under qemu-user 10.0.13 on a linux-x64 Debian 13 host. - Also seen with osx-arm64 R2R on Apple Silicon, in the project below.
- x64 is not affected. With .NET 11.0.0 linux-x64 R2R,
Huge.Runis precompiled, its data is a separateMethodReadOnlyDataNodereached through relocations, and the output is correct. On .NET 10, x64 crossgen2 does not precompileHuge.Runat all ("Abi unstable type Vector128`1").
Other information
The likely fix is in emitOutputLJ in src/coreclr/jit/emitarm64.cpp (checked at 425a54eb9ab03e2d7c7c3e539aca6800ce0c5d7a). When emitComp->opts.compReloc is set, the IF_LARGELDC path (and IF_LARGEADR on JIT data) should record ARM64_PAGEBASE_REL21 and ARM64_PAGEOFFSET_12A/12L relocations against the data, as emitOutputLoadLabel does for idIsReloc() labels. Alternatively, crossgen2 and ILC could keep the method's data at the page offset the JIT assumed.
Found in IronRuby (https://github.com/Largo/ironruby). Its ahead-of-time startup library compiles the top level of IronRuby's Ruby prelude into one generated method of 484 KB IL (2.1 MB arm64 code), saved with PersistedAssemblyBuilder and then precompiled with crossgen2. In the R2R build, 73 of that method's 103 adrp-based constant loads pointed into code. At run time a symbol name built from one of these constants came out as the bytes of an arm64 epilogue.
- 主要言語
- C#
- スター
- 18.4k
- フォーク
- 5.8k
- 平均マージ
- 2日 19時間
- マージ済み PR(30日)
- 590
環境構築
このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
dotnet/runtime のほかの issue
-
area-Setup untriaged
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
dotnet/runtime#137376 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
area-System.Memory untriaged
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
dotnet/runtime#137334 · コメント 4 件 ·
メンテナーはふだん 1 日以内に返信
-
ImmutableSortedSet<T>.Builder: enumerating through IEnumerable<T> doesn't throw on modification and can return wrong items対応中かも @aw0lid が 3 日前に担当しました。 オープンarea-System.Collections
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
dotnet/runtime#137167 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
DbTransaction.SupportsSavepoints docs say Release/ReleaseAsync are expected to throw, but they are no-ops by design対応中かも @alaricd が 7 日前に担当しました。 オープンarea-System.Transactions untriaged
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
dotnet/runtime#137040 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
FileSystemGlobbing: Matcher throws ArgumentOutOfRangeException when the rootDir is the same value as the file対応中かも @makazeu が 7 日前に担当しました。 オープンarea-Extensions-FileSystem
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
dotnet/runtime#136989 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
似ている issue
-
bug emitter:client:csharp
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
microsoft/typespec#12572 · コメント 1 件 · リアクション 1 件 ·
メンテナーはふだん 1 日以内に返信
-
needs-area-label
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 2 日以内に返信
-
AWS Lambda: KeyNotFoundException drops HTTP-triggered transactions when allowAllHeaders is disabledオープンbug
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
newrelic/newrelic-dotnet-agent#3949 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
area-networking
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
dotnet/aspnetcore#70545 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 87/100
メンテナーはふだん 1 日以内に返信