Skip to content

Fix NativeAOT hex config parser to handle 0x/0X prefix#127644

Open
Copilot wants to merge 3 commits intomainfrom
copilot/fix-hex-parser-prefix
Open

Fix NativeAOT hex config parser to handle 0x/0X prefix#127644
Copilot wants to merge 3 commits intomainfrom
copilot/fix-hex-parser-prefix

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 1, 2026

NativeAOT's RhConfig::Environment::TryGetIntegerValue had a hand-rolled hex parser that rejected the 0x/0X prefix — returning a parse error when it encountered x. This meant env vars like DOTNET_GCHeapHardLimit=0xC0000000 silently failed to parse, leaving the hard limit unset. With GCLargePages=2 also set, the GC would then return CLR_E_GC_LARGE_PAGE_MISSING_HARD_LIMIT and fail initialization. CoreCLR's equivalent uses strtoul(..., 16) which handles the prefix natively.

Description

  • src/coreclr/nativeaot/Runtime/RhConfig.cpp — In TryGetIntegerValue, skip a leading 0x/0X prefix when parsing in hex mode, before entering the digit loop:
uint32_t startIndex = 0;
if (!decimal && cchResult >= 2 && buffer[0] == '0' && (buffer[1] == 'x' || buffer[1] == 'X'))
{
    startIndex = 2;
}
for (uint32_t i = startIndex; i < cchResult; i++)

This aligns NativeAOT's config parsing with CoreCLR's strtoul-based behavior and fixes the Collect_Aggressive_LargePages test failure under NativeAOT.

Original prompt

Problem

NativeAOT's RhConfig::Environment::TryGetIntegerValue in src/coreclr/nativeaot/Runtime/RhConfig.cpp uses a hand-rolled hex parser that does not handle the 0x or 0X prefix. This causes config values like DOTNET_GCHeapHardLimit=0xC0000000 to fail to parse, because when the parser encounters the x character it returns false (parse error).

CoreCLR's equivalent code (CLRConfigNoCache::TryAsInteger in src/coreclr/inc/clrconfignocache.h) uses strtoul(_value, &endPtr, radix) which natively handles the 0x prefix when radix is 16.

This causes the test Collect_Aggressive_LargePages added in PR #127290 to fail under NativeAOT: the GCHeapHardLimit fails to parse, so no hard limit is set, but GCLargePages=2 succeeds → the GC returns CLR_E_GC_LARGE_PAGE_MISSING_HARD_LIMIT and the process exits with -1.

Fix

In src/coreclr/nativeaot/Runtime/RhConfig.cpp, in the TryGetIntegerValue method, when parsing in hex mode (i.e., decimal is false), skip a leading 0x or 0X prefix before entering the digit-parsing loop. This matches the behavior of strtoul with radix 16 that CoreCLR uses.

Specifically, after reading the environment variable into buffer and before the parsing loop, add:

    uint32_t startIndex = 0;
    if (!decimal && cchResult >= 2 && buffer[0] == '0' && (buffer[1] == 'x' || buffer[1] == 'X'))
    {
        startIndex = 2;
    }

Then change the loop from for (uint32_t i = 0; ...) to for (uint32_t i = startIndex; ...).

The following is the prior conversation context from the user's chat exploration (may be truncated):

User: ```
16:22:36.657 Running test: GC\API\GC\Collect_Aggressive_LargePages\Collect_Aggressive_LargePages.cmd

Return code: 1
Raw output file: C:\h\w\B1EC0A05\w\B5C209C7\uploads\API\GC\Collect_Aggressive_LargePages\output.txt
Raw output:
BEGIN EXECUTION
call C:\h\w\B1EC0A05\p\nativeaottest.cmd C:\h\w\B1EC0A05\w\B5C209C7\e\GC\API\GC\Collect_Aggressive_LargePages\ Collect_Aggressive_LargePages.dll
Expected: 100
Actual: -1
END EXECUTION - FAILED
FAILED
Test Harness Exitcode is : 1
To run the test:
Set up CORE_ROOT and run.

C:\h\w\B1EC0A05\w\B5C209C7\e\GC\GC\native....\API\GC\Collect_Aggressive_LargePages\Collect_Aggressive_LargePages.cmd
Xunit.Sdk.TrueException:
Return code: 1
Raw output file: C:\h\w\B1EC0A05\w\B5C209C7\uploads\API\GC\Collect_Aggressive_LargePages\output.txt
Raw output:
BEGIN EXECUTION
call C:\h\w\B1EC0A05\p\nativeaottest.cmd C:\h\w\B1EC0A05\w\B5C209C7\e\GC\API\GC\Collect_Aggressive_LargePages\ Collect_Aggressive_LargePages.dll
Expected: 100
Actual: -1
END EXECUTION - FAILED
FAILED
Test Harness Exitcode is : 1
To run the test:
Set up CORE_ROOT and run.
C:\h\w\B1EC0A05\w\B5C209C7\e\GC\GC\native....\API\GC\Collect_Aggressive_LargePages\Collect_Aggressive_LargePages.cmd
at Xunit.Assert.True(Nullable`1, String)
at TestLibrary.OutOfProcessTest.RunOutOfProcessTest(String, String)
at Program.<

$>g__TestExecutor26|0_27(StreamWriter, StreamWriter, Program.<>c__DisplayClass0_0&)
16:22:36.755 Failed test: GC\API\GC\Collect_Aggressive_LargePages\Collect_Aggressive_LargePages.cmd

the test added here is failing with native AOT. why? 

Assistant: The test fails on NativeAOT because **NativeAOT's config reader cannot parse the `0x` prefix** in the `DOTNET_GCHeapHardLimit=0xC0000000` environment variable.

Looking at NativeAOT's `RhConfig::Environment::TryGetIntegerValue` in:

```c++ name=src/coreclr/nativeaot/Runtime/RhConfig.cpp url=https://github.com/dotnet/runtime/blob/bfaabae213cf02d343846d253b7149a03819680d/src/coreclr/nativeaot/Runtime/RhConfig.cpp#L58-L84
    uint64_t uiResult = 0;
    for (uint32_t i = 0; i < cchResult; i++)
    {
        TCHAR ch = buffer[i];
        // ...
        else
        {
            uiResult *= 16;

            if ((ch >= '0') && (ch <= '9'))
                uiResult += ch - '0';
            else if ((ch >= 'a') && (ch <= 'f'))
                uiResult += (ch - 'a') + 10;
            else if ((ch >= 'A') && (ch <= 'F'))
                uiResult += (ch - 'A') + 10;
            else
                return false; // parse error   <-- 'x' hits this
        }
    }

When the value is 0xC0000000, the parser reads hex digits character-by-character. It handles 0, but then encounters x, which isn't a valid hex digit — so it returns false (parse error). This means GCHeapHardLimit is never set.

The cascade:

  1. DOTNET_GCHeapHardLimit=0xC0000000 fails to parse → no hard limit is set
  2. DOTNET_GCLargePages=2 is read successfully (it's a simple 2, valid hex) → use_large_pages_p = true
  3. The GC hits this check in interface.cpp: if ((!gc_heap::heap_hard_limit) && gc_heap::use_large_pages_p) → returns CLR_E_GC_LARGE_PAGE_MISSING_HARD_LIMIT
  4. GC initialization fails →...

This pull request was created from Copilot chat.

Copilot AI requested review from Copilot and removed request for Copilot May 1, 2026 11:10
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/b2cf8499-44ac-4e9a-995b-c72b36e081a2

Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot May 1, 2026 11:14
Copilot AI changed the title [WIP] Fix hex parser to handle 0x prefix in TryGetIntegerValue Fix NativeAOT hex config parser to handle 0x/0X prefix May 1, 2026
Copilot AI requested a review from MichalStrehovsky May 1, 2026 11:15
Copilot AI review requested due to automatic review settings May 1, 2026 11:21
@MichalStrehovsky MichalStrehovsky marked this pull request as ready for review May 1, 2026 11:21
@MichalStrehovsky
Copy link
Copy Markdown
Member

/azp run runtime-nativeaot-outerloop

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request fixes NativeAOT configuration parsing so hexadecimal environment variable values can include a 0x/0X prefix (matching the commonly accepted format and aligning more closely with CoreCLR behavior), preventing GC configuration knobs like DOTNET_GCHeapHardLimit=0x... from being ignored under NativeAOT.

Changes:

  • NativeAOT RhConfig::Environment::TryGetIntegerValue now skips a leading 0x/0X prefix when parsing hex values from environment variables.
  • ILCompiler now always adds ManagedDataDescriptorProvider (previously gated behind --debug).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/coreclr/nativeaot/Runtime/RhConfig.cpp Adds logic to skip 0x/0X prefix in the hand-rolled hex parser for env var config values.
src/coreclr/tools/aot/ILCompiler/Program.cs Removes the --debug condition and unconditionally roots ManagedDataDescriptorProvider (managed cDAC descriptor emission).

uint32_t startIndex = 0;
if (!decimal && cchResult >= 2 && buffer[0] == '0' && (buffer[1] == 'x' || buffer[1] == 'X'))
{
startIndex = 2;
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the value is exactly "0x"/"0X" (no digits after the prefix), this change sets startIndex=2 and the loop never runs, so the function returns true with uiResult==0. CoreCLR's strtoul-based parsing treats this as a failed conversion (no digits), and the previous implementation also rejected it. Consider requiring at least one hex digit after the prefix (e.g., only skip the prefix when cchResult > 2, or explicitly return false when startIndex == cchResult).

Suggested change
startIndex = 2;
startIndex = 2;
if (startIndex == cchResult)
return false; // parse error - hex prefix without any digits

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid feedback?

Comment on lines 258 to 262
}

if (Get(_command.EnableDebugInfo))
compilationRoots.Add(new ManagedDataDescriptorProvider());
compilationRoots.Add(new ManagedDataDescriptorProvider());

string win32resourcesModule = Get(_command.Win32ResourceModuleName);
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hunk removes the EnableDebugInfo guard and always adds ManagedDataDescriptorProvider (managed cDAC descriptor root). This is a behavior change for the ILCompiler tool that isn’t mentioned in the PR description/title (which focuses on NativeAOT env var parsing). If intentional, the PR description should call out the rationale/impact; otherwise consider restoring the conditional or moving this change to a separate PR.

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

🤖 Copilot Code Review — PR #127644

Note

This review was generated by Copilot using multi-model analysis (Claude Opus 4.6, Claude Sonnet 4.5, GPT-5.3-Codex).

Holistic Assessment

Motivation: The hex prefix fix addresses a real inconsistency — strtoull (used for embedded config values) already handles 0x prefixes, but the manual environment variable parser did not, causing values like DOTNET_GCHeapCount=0x10 to fail. The ManagedDataDescriptorProvider change enables cDAC diagnostics without requiring --debug.

Approach: The hex prefix fix is minimal and correct for the common case. The ManagedDataDescriptorProvider change is a one-line behavioral shift with broader implications that should be explained.

Summary: ⚠️ Needs Human Review. The hex parsing fix is correct but has an edge case worth considering ("0x" alone). The unconditional cDAC metadata change has binary size implications and the two changes appear unrelated — a maintainer should confirm the coupling is intentional and the size impact is acceptable.


Detailed Findings

✅ Correctness — Hex prefix parsing fix (RhConfig.cpp:59-63)

The fix correctly strips the 0x/0X prefix before the hex digit parsing loop. Key observations:

  • Guard cchResult >= 2 prevents out-of-bounds access on buffer[1]
  • Aligns the environment variable path with the embedded config path (line 140) which uses strtoull and already handles 0x natively
  • All existing callers (EventPipe, GC config, dump type) benefit from this fix

⚠️ Edge Case — Bare "0x" input parses as valid zero (RhConfig.cpp:59-93) [advisory, not merge-blocking]

Flagged by all 3 models.

When buffer is exactly "0x" (cchResult == 2), startIndex is set to 2, the loop body never executes, and uiResult = 0 is returned as valid. This is consistent with the strtoull(embeddedValue, NULL, 16) path at line 140, which also returns 0 for "0x" when endptr is NULL. However, it means a bare "0x" is accepted rather than rejected as a parse error.

In practice this is negligible risk (no user would set an env var to exactly "0x"), but if the intent is to reject this case, add:

if (startIndex >= cchResult)
    return false;

⚠️ Binary Size Impact — Unconditional ManagedDataDescriptorProvider (Program.cs:260) [needs maintainer judgment]

Flagged by all 3 models.

Previously, cDAC type descriptors were only emitted with --debug/-g. Now every NativeAOT binary includes the DotNetManagedContractDescriptor JSON blob (header + JSON for [DataContract]-annotated types with EETypes). The size impact depends on the number of qualifying types — likely small (few KB) but non-zero.

Questions for maintainer:

  • Is the intent to enable post-mortem/cDAC debugging without debug symbols? If so, a brief comment or commit message explaining the rationale would help future readers.
  • Has the size impact been measured for representative apps?

⚠️ Missing Tests — No regression tests for either behavior change

Flagged by all 3 models.

Neither change includes tests:

  • The hex prefix fix should be verifiable through environment variable integration tests (e.g., DOTNET_GCHeapCount=0x10 produces expected behavior)
  • The unconditional cDAC descriptor could be verified by checking the symbol exists in non-debug NativeAOT output

Given this is native runtime code, integration-level testing may be more appropriate than unit tests. A maintainer can judge whether existing test infrastructure covers these paths.

💡 PR Scope — Two unrelated changes in one PR

The hex prefix fix (C++ runtime bug fix) and the unconditional cDAC metadata (C# compiler behavior change) appear orthogonal. Splitting would ease backporting the bug fix independently. However, if the author (a core NativeAOT maintainer) has a reason for coupling them, that context should be documented.

Generated by Code Review for issue #127644 ·

@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @agocke, @dotnet/ilc-contrib
See info in area-owners.md if you want to be subscribed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants