Fix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor] - #126854

Merged
rzikm merged 2 commits into
mainfrom
copilot/fix-tarreader-sparsefiletests-reflection
Apr 14, 2026
Merged

Fix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor]#126854
rzikm merged 2 commits into
mainfrom
copilot/fix-tarreader-sparsefiletests-reflection

Conversation

CopilotAI commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

TarReader_SparseFileTests.WriteSparseEntry used reflection to inject GNU.sparse.realsize into TarEntry._header.ExtendedAttributes after construction (to bypass validation for negative-value test cases). Under Native AOT outerloop runs, the trimmer removes metadata for these internal members, causing NullReferenceException.

Changes

  • Replaced reflection (GetField/GetProperty/GetValue) in WriteSparseEntry with [UnsafeAccessor], which is AOT-safe and statically analyzable by the trimmer.
  • Approach: access the mutable Dictionary<string, string> by calling the public PaxTarEntry.ExtendedAttributes property (which lazily initializes _header._ea) and then unwrapping the ReadOnlyDictionary<string, string> via a generic accessor class targeting the stable m_dictionary field.
  • AddedReadOnlyDictionaryAccessors<TKey, TValue> private nested class (required pattern for [UnsafeAccessor] on a generic target type):
privatestaticclassReadOnlyDictionaryAccessors<TKey,TValue>whereTKey:notnull{[UnsafeAccessor(UnsafeAccessorKind.Field,Name="m_dictionary")]publicstaticexternrefIDictionary<TKey,TValue>GetInnerDictionary(ReadOnlyDictionary<TKey,TValue>d);}// usage:varea=(Dictionary<string,string>)ReadOnlyDictionaryAccessors<string,string>.GetInnerDictionary((ReadOnlyDictionary<string,string>)entry.ExtendedAttributes);ea["GNU.sparse.realsize"]=realSize.ToString();
  • Removed using System.Reflection; added using System.Collections.ObjectModel and using System.Runtime.CompilerServices.
Original prompt

Problem

PR #125283 introduced TarReader_SparseFileTests that use reflection to access internal members, which breaks under Native AOT outerloop runs because the trimmer removes the reflection metadata.

The failing test:

[FAIL] System.Formats.Tar.Tests.TarReader_SparseFileTests.CorruptedSparseMap_InvalidDataException(sparseMapContent: "abc\n0\n256\n", useAsync: False)
System.NullReferenceException : Object reference not set to an instance of an object.
at System.Formats.Tar.Tests.TarReader_SparseFileTests.WriteSparseEntry(TarWriter writer, String realName, Int64 realSize, Byte[] rawSparseData) in /_/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs:line 42

Root Cause

In src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs, the WriteSparseEntry method uses reflection at lines 40-43:

varheaderField=typeof(TarEntry).GetField("_header",BindingFlags.NonPublic|BindingFlags.Instance)!;varheader=headerField.GetValue(entry)!;vareaProp=header.GetType().GetProperty("ExtendedAttributes",BindingFlags.NonPublic|BindingFlags.Instance)!;varea=(Dictionary<string,string>)eaProp.GetValue(header)!;ea["GNU.sparse.realsize"]=realSize.ToString();

Under Native AOT, GetField and GetProperty return null because reflection metadata for these internal members is not preserved. The ! operator then causes a NullReferenceException.

Required Fix

Replace the reflection-based access with [UnsafeAccessor] attribute, which works under Native AOT. The internal types/members that need to be accessed:

  1. TarEntry._header - an internal field of type TarHeader on the abstract class TarEntry (in src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs, line 17: internal TarHeader _header;)

  2. TarHeader.ExtendedAttributes - an internal property on the sealed class TarHeader that returns Dictionary<string, string>. It's defined as:

    internalDictionary<string,string>ExtendedAttributes=> _ea ??=new Dictionary<string,string>();

    The backing field is private Dictionary<string, string>? _ea;

The TarHeader class is internal sealed partial class TarHeader in namespace System.Formats.Tar.

Implementation

In the test file src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs:

  1. Add using System.Runtime.CompilerServices; at the top.

  2. Remove using System.Reflection; (should no longer be needed).

  3. Add [UnsafeAccessor] static extern methods to the test class (or as file-scoped static methods). For example:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_header")]staticexternrefTarHeaderGetHeader(TarEntryentry);

Note: TarHeader is internal, so the test project needs to have access to it. Check if the test project already has InternalsVisibleTo or uses [assembly: InternalsVisibleTo]. If not, you might need to access the _ea field directly instead of going through the ExtendedAttributes property:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_ea")]staticexternrefDictionary<string,string>?GetEa(TarHeaderheader);

However, since UnsafeAccessor with UnsafeAccessorKind.Field returns a ref to the field, and _ea might be null, you'd need to handle the initialization. An alternative approach: since the ExtendedAttributes property initializes _ea via ??=, you could use the _ea field ref but it might be simpler to just set it.

Actually, the simplest approach: The test only needs to add a key to the internal extended attributes dictionary. Since TarHeader is internal, check if the test assembly can see it. Look for InternalsVisibleTo in the product assembly. If the tests can already see TarHeader, then [UnsafeAccessor] only needs to access _header on TarEntry (since _header is internal but the class TarHeader itself is also internal).

Important: [UnsafeAccessor] can access internal types from other assemblies. The field accessor returns a ref, so you can directly work with it. The method should look something like:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_ea")]staticexternrefDictionary<string,string>?GetEa(TarHeaderheader);

But since TarHeader is internal to the product assembly and can't be named in the test assembly's source code directly... you need to check if test projects have InternalsVisibleTo.

  1. Replace lines 40-44 in WriteSparseEntry with calls to the [UnsafeAccessor] methods to get the _header field and then the ExtendedAttributes/_ea field, and set ea["GNU.sparse.realsize"] = realSize.ToString();.

Please check whether the test project has access to internal types (via InternalsVisibleTo or similar) and choose the simplest working approach. The key requirement is that the code must work under Native AOT without relying on reflection APIs (`Type....

This pull request was created from Copilot chat.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-formats-tar
See info in area-owners.md if you want to be subscribed.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/26cfdebb-c9b4-43c7-908e-b1d9ce2f1cd6
Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 13, 2026 22:42
CopilotAI changed the title [WIP] Fix TarReader sparse file tests for Native AOT compatibilityFix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor]Apr 13, 2026
@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).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes TarReader_SparseFileTests failing under Native AOT outerloop by removing reflection-based access to internal TAR header state (which is trim-sensitive) and replacing it with [UnsafeAccessor]-based access that is AOT-/trimmer-friendly.

Changes:

  • Replace reflection (GetField/GetProperty/GetValue) in WriteSparseEntry with [UnsafeAccessor].
  • Unwrap ReadOnlyDictionary<TKey,TValue> to its backing IDictionary<TKey,TValue> via a generic ReadOnlyDictionaryAccessors<TKey,TValue> helper to inject GNU.sparse.realsize.
  • Update usings to remove System.Reflection and add required namespaces for ReadOnlyDictionary + UnsafeAccessor.

@MichalStrehovsky

Copy link
Copy Markdown
Member

Copilot came up with a creative replacement that I scratched my head about for a bit. We can ask it to keep using reflection, just rewrite it into typeof(SomeType).GetField(...) or Type.GetType("SomeStringLiteral").GetField(...) (that are still reflection, but at least it can now be statically analyzed). I don't have a preference, leave this up to the maintainers.

@rzikmrzikm left a comment

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.

LGTM (if that works, that is), thanks!

@rzikm

Copy link
Copy Markdown
Member

/azp list

@azure-pipelines

Copy link
Copy Markdown
CI/CD Pipelines for this repository:

@rzikm

Copy link
Copy Markdown
Member

oh, then nativeAoT was already run

----- start Tue Apr 14 01:53:23 AM UTC 2026 =============== To repro directly: =====================================================
pushd .
chmod +rwx System.Formats.Tar.Tests ^&^& ./System.Formats.Tar.Tests -notrait category=IgnoreForCI -notrait category=OuterLoop -notrait category=failing -xml testResults.xml popd
===========================================================================================================
/root/helix/work/workitem/e /root/helix/work/workitem/e
Running assembly:System.Formats.Tar.Tests, Version=11.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
[SKIP] System.Formats.Tar.Tests.TarEntry_ExtractToFile_Tests_Unix.Extract_SpecialFiles_Async
[SKIP] System.Formats.Tar.Tests.TarEntry_ExtractToFile_Tests_Unix.Extract_SpecialFiles
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_BlockDevice
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentUser
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_CharacterDevice
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_Fifo
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentGroup
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentGroupAndUser
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_BlockDevice_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentGroup_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentUser_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentGroupAndUser_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_Fifo_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_CharacterDevice_Async
Finished System.Formats.Tar.Tests, Version=11.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
Tests run: 5868, Errors: 0, Failures: 0, Skipped: 14. Time: 3.645058s

Seems like the fix works, thanks again

@rzikm
rzikm enabled auto-merge (squash) April 14, 2026 06:44
@rzikm

Copy link
Copy Markdown
Member

/ba-g test failure is #126867

@rzikm
rzikm merged commit 4f21ff0 into mainApr 14, 2026
98 of 118 checks passed
@rzikm
rzikm deleted the copilot/fix-tarreader-sparsefiletests-reflection branch April 14, 2026 06:48
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 14, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@MichalStrehovsky@rzikm
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor] - #126854

Merged
rzikm merged 2 commits into
mainfrom
copilot/fix-tarreader-sparsefiletests-reflection
Apr 14, 2026
Merged

Fix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor]#126854
rzikm merged 2 commits into
mainfrom
copilot/fix-tarreader-sparsefiletests-reflection

Conversation

CopilotAI commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

TarReader_SparseFileTests.WriteSparseEntry used reflection to inject GNU.sparse.realsize into TarEntry._header.ExtendedAttributes after construction (to bypass validation for negative-value test cases). Under Native AOT outerloop runs, the trimmer removes metadata for these internal members, causing NullReferenceException.

Changes

  • Replaced reflection (GetField/GetProperty/GetValue) in WriteSparseEntry with [UnsafeAccessor], which is AOT-safe and statically analyzable by the trimmer.
  • Approach: access the mutable Dictionary<string, string> by calling the public PaxTarEntry.ExtendedAttributes property (which lazily initializes _header._ea) and then unwrapping the ReadOnlyDictionary<string, string> via a generic accessor class targeting the stable m_dictionary field.
  • AddedReadOnlyDictionaryAccessors<TKey, TValue> private nested class (required pattern for [UnsafeAccessor] on a generic target type):
privatestaticclassReadOnlyDictionaryAccessors<TKey,TValue>whereTKey:notnull{[UnsafeAccessor(UnsafeAccessorKind.Field,Name="m_dictionary")]publicstaticexternrefIDictionary<TKey,TValue>GetInnerDictionary(ReadOnlyDictionary<TKey,TValue>d);}// usage:varea=(Dictionary<string,string>)ReadOnlyDictionaryAccessors<string,string>.GetInnerDictionary((ReadOnlyDictionary<string,string>)entry.ExtendedAttributes);ea["GNU.sparse.realsize"]=realSize.ToString();
  • Removed using System.Reflection; added using System.Collections.ObjectModel and using System.Runtime.CompilerServices.
Original prompt

Problem

PR #125283 introduced TarReader_SparseFileTests that use reflection to access internal members, which breaks under Native AOT outerloop runs because the trimmer removes the reflection metadata.

The failing test:

[FAIL] System.Formats.Tar.Tests.TarReader_SparseFileTests.CorruptedSparseMap_InvalidDataException(sparseMapContent: "abc\n0\n256\n", useAsync: False)
System.NullReferenceException : Object reference not set to an instance of an object.
at System.Formats.Tar.Tests.TarReader_SparseFileTests.WriteSparseEntry(TarWriter writer, String realName, Int64 realSize, Byte[] rawSparseData) in /_/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs:line 42

Root Cause

In src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs, the WriteSparseEntry method uses reflection at lines 40-43:

varheaderField=typeof(TarEntry).GetField("_header",BindingFlags.NonPublic|BindingFlags.Instance)!;varheader=headerField.GetValue(entry)!;vareaProp=header.GetType().GetProperty("ExtendedAttributes",BindingFlags.NonPublic|BindingFlags.Instance)!;varea=(Dictionary<string,string>)eaProp.GetValue(header)!;ea["GNU.sparse.realsize"]=realSize.ToString();

Under Native AOT, GetField and GetProperty return null because reflection metadata for these internal members is not preserved. The ! operator then causes a NullReferenceException.

Required Fix

Replace the reflection-based access with [UnsafeAccessor] attribute, which works under Native AOT. The internal types/members that need to be accessed:

  1. TarEntry._header - an internal field of type TarHeader on the abstract class TarEntry (in src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs, line 17: internal TarHeader _header;)

  2. TarHeader.ExtendedAttributes - an internal property on the sealed class TarHeader that returns Dictionary<string, string>. It's defined as:

    internalDictionary<string,string>ExtendedAttributes=> _ea ??=new Dictionary<string,string>();

    The backing field is private Dictionary<string, string>? _ea;

The TarHeader class is internal sealed partial class TarHeader in namespace System.Formats.Tar.

Implementation

In the test file src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs:

  1. Add using System.Runtime.CompilerServices; at the top.

  2. Remove using System.Reflection; (should no longer be needed).

  3. Add [UnsafeAccessor] static extern methods to the test class (or as file-scoped static methods). For example:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_header")]staticexternrefTarHeaderGetHeader(TarEntryentry);

Note: TarHeader is internal, so the test project needs to have access to it. Check if the test project already has InternalsVisibleTo or uses [assembly: InternalsVisibleTo]. If not, you might need to access the _ea field directly instead of going through the ExtendedAttributes property:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_ea")]staticexternrefDictionary<string,string>?GetEa(TarHeaderheader);

However, since UnsafeAccessor with UnsafeAccessorKind.Field returns a ref to the field, and _ea might be null, you'd need to handle the initialization. An alternative approach: since the ExtendedAttributes property initializes _ea via ??=, you could use the _ea field ref but it might be simpler to just set it.

Actually, the simplest approach: The test only needs to add a key to the internal extended attributes dictionary. Since TarHeader is internal, check if the test assembly can see it. Look for InternalsVisibleTo in the product assembly. If the tests can already see TarHeader, then [UnsafeAccessor] only needs to access _header on TarEntry (since _header is internal but the class TarHeader itself is also internal).

Important: [UnsafeAccessor] can access internal types from other assemblies. The field accessor returns a ref, so you can directly work with it. The method should look something like:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_ea")]staticexternrefDictionary<string,string>?GetEa(TarHeaderheader);

But since TarHeader is internal to the product assembly and can't be named in the test assembly's source code directly... you need to check if test projects have InternalsVisibleTo.

  1. Replace lines 40-44 in WriteSparseEntry with calls to the [UnsafeAccessor] methods to get the _header field and then the ExtendedAttributes/_ea field, and set ea["GNU.sparse.realsize"] = realSize.ToString();.

Please check whether the test project has access to internal types (via InternalsVisibleTo or similar) and choose the simplest working approach. The key requirement is that the code must work under Native AOT without relying on reflection APIs (`Type....

This pull request was created from Copilot chat.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-formats-tar
See info in area-owners.md if you want to be subscribed.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/26cfdebb-c9b4-43c7-908e-b1d9ce2f1cd6
Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 13, 2026 22:42
CopilotAI changed the title [WIP] Fix TarReader sparse file tests for Native AOT compatibilityFix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor]Apr 13, 2026
@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).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes TarReader_SparseFileTests failing under Native AOT outerloop by removing reflection-based access to internal TAR header state (which is trim-sensitive) and replacing it with [UnsafeAccessor]-based access that is AOT-/trimmer-friendly.

Changes:

  • Replace reflection (GetField/GetProperty/GetValue) in WriteSparseEntry with [UnsafeAccessor].
  • Unwrap ReadOnlyDictionary<TKey,TValue> to its backing IDictionary<TKey,TValue> via a generic ReadOnlyDictionaryAccessors<TKey,TValue> helper to inject GNU.sparse.realsize.
  • Update usings to remove System.Reflection and add required namespaces for ReadOnlyDictionary + UnsafeAccessor.

@MichalStrehovsky

Copy link
Copy Markdown
Member

Copilot came up with a creative replacement that I scratched my head about for a bit. We can ask it to keep using reflection, just rewrite it into typeof(SomeType).GetField(...) or Type.GetType("SomeStringLiteral").GetField(...) (that are still reflection, but at least it can now be statically analyzed). I don't have a preference, leave this up to the maintainers.

@rzikmrzikm left a comment

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.

LGTM (if that works, that is), thanks!

@rzikm

Copy link
Copy Markdown
Member

/azp list

@azure-pipelines

Copy link
Copy Markdown
CI/CD Pipelines for this repository:

@rzikm

Copy link
Copy Markdown
Member

oh, then nativeAoT was already run

----- start Tue Apr 14 01:53:23 AM UTC 2026 =============== To repro directly: =====================================================
pushd .
chmod +rwx System.Formats.Tar.Tests ^&^& ./System.Formats.Tar.Tests -notrait category=IgnoreForCI -notrait category=OuterLoop -notrait category=failing -xml testResults.xml popd
===========================================================================================================
/root/helix/work/workitem/e /root/helix/work/workitem/e
Running assembly:System.Formats.Tar.Tests, Version=11.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
[SKIP] System.Formats.Tar.Tests.TarEntry_ExtractToFile_Tests_Unix.Extract_SpecialFiles_Async
[SKIP] System.Formats.Tar.Tests.TarEntry_ExtractToFile_Tests_Unix.Extract_SpecialFiles
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_BlockDevice
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentUser
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_CharacterDevice
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_Fifo
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentGroup
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentGroupAndUser
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_BlockDevice_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentGroup_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentUser_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentGroupAndUser_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_Fifo_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_CharacterDevice_Async
Finished System.Formats.Tar.Tests, Version=11.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
Tests run: 5868, Errors: 0, Failures: 0, Skipped: 14. Time: 3.645058s

Seems like the fix works, thanks again

@rzikm
rzikm enabled auto-merge (squash) April 14, 2026 06:44
@rzikm

Copy link
Copy Markdown
Member

/ba-g test failure is #126867

@rzikm
rzikm merged commit 4f21ff0 into mainApr 14, 2026
98 of 118 checks passed
@rzikm
rzikm deleted the copilot/fix-tarreader-sparsefiletests-reflection branch April 14, 2026 06:48
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 14, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@MichalStrehovsky@rzikm
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor] - #126854

Merged
rzikm merged 2 commits into
mainfrom
copilot/fix-tarreader-sparsefiletests-reflection
Apr 14, 2026
Merged

Fix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor]#126854
rzikm merged 2 commits into
mainfrom
copilot/fix-tarreader-sparsefiletests-reflection

Conversation

CopilotAI commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

TarReader_SparseFileTests.WriteSparseEntry used reflection to inject GNU.sparse.realsize into TarEntry._header.ExtendedAttributes after construction (to bypass validation for negative-value test cases). Under Native AOT outerloop runs, the trimmer removes metadata for these internal members, causing NullReferenceException.

Changes

  • Replaced reflection (GetField/GetProperty/GetValue) in WriteSparseEntry with [UnsafeAccessor], which is AOT-safe and statically analyzable by the trimmer.
  • Approach: access the mutable Dictionary<string, string> by calling the public PaxTarEntry.ExtendedAttributes property (which lazily initializes _header._ea) and then unwrapping the ReadOnlyDictionary<string, string> via a generic accessor class targeting the stable m_dictionary field.
  • AddedReadOnlyDictionaryAccessors<TKey, TValue> private nested class (required pattern for [UnsafeAccessor] on a generic target type):
privatestaticclassReadOnlyDictionaryAccessors<TKey,TValue>whereTKey:notnull{[UnsafeAccessor(UnsafeAccessorKind.Field,Name="m_dictionary")]publicstaticexternrefIDictionary<TKey,TValue>GetInnerDictionary(ReadOnlyDictionary<TKey,TValue>d);}// usage:varea=(Dictionary<string,string>)ReadOnlyDictionaryAccessors<string,string>.GetInnerDictionary((ReadOnlyDictionary<string,string>)entry.ExtendedAttributes);ea["GNU.sparse.realsize"]=realSize.ToString();
  • Removed using System.Reflection; added using System.Collections.ObjectModel and using System.Runtime.CompilerServices.
Original prompt

Problem

PR #125283 introduced TarReader_SparseFileTests that use reflection to access internal members, which breaks under Native AOT outerloop runs because the trimmer removes the reflection metadata.

The failing test:

[FAIL] System.Formats.Tar.Tests.TarReader_SparseFileTests.CorruptedSparseMap_InvalidDataException(sparseMapContent: "abc\n0\n256\n", useAsync: False)
System.NullReferenceException : Object reference not set to an instance of an object.
at System.Formats.Tar.Tests.TarReader_SparseFileTests.WriteSparseEntry(TarWriter writer, String realName, Int64 realSize, Byte[] rawSparseData) in /_/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs:line 42

Root Cause

In src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs, the WriteSparseEntry method uses reflection at lines 40-43:

varheaderField=typeof(TarEntry).GetField("_header",BindingFlags.NonPublic|BindingFlags.Instance)!;varheader=headerField.GetValue(entry)!;vareaProp=header.GetType().GetProperty("ExtendedAttributes",BindingFlags.NonPublic|BindingFlags.Instance)!;varea=(Dictionary<string,string>)eaProp.GetValue(header)!;ea["GNU.sparse.realsize"]=realSize.ToString();

Under Native AOT, GetField and GetProperty return null because reflection metadata for these internal members is not preserved. The ! operator then causes a NullReferenceException.

Required Fix

Replace the reflection-based access with [UnsafeAccessor] attribute, which works under Native AOT. The internal types/members that need to be accessed:

  1. TarEntry._header - an internal field of type TarHeader on the abstract class TarEntry (in src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs, line 17: internal TarHeader _header;)

  2. TarHeader.ExtendedAttributes - an internal property on the sealed class TarHeader that returns Dictionary<string, string>. It's defined as:

    internalDictionary<string,string>ExtendedAttributes=> _ea ??=new Dictionary<string,string>();

    The backing field is private Dictionary<string, string>? _ea;

The TarHeader class is internal sealed partial class TarHeader in namespace System.Formats.Tar.

Implementation

In the test file src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs:

  1. Add using System.Runtime.CompilerServices; at the top.

  2. Remove using System.Reflection; (should no longer be needed).

  3. Add [UnsafeAccessor] static extern methods to the test class (or as file-scoped static methods). For example:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_header")]staticexternrefTarHeaderGetHeader(TarEntryentry);

Note: TarHeader is internal, so the test project needs to have access to it. Check if the test project already has InternalsVisibleTo or uses [assembly: InternalsVisibleTo]. If not, you might need to access the _ea field directly instead of going through the ExtendedAttributes property:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_ea")]staticexternrefDictionary<string,string>?GetEa(TarHeaderheader);

However, since UnsafeAccessor with UnsafeAccessorKind.Field returns a ref to the field, and _ea might be null, you'd need to handle the initialization. An alternative approach: since the ExtendedAttributes property initializes _ea via ??=, you could use the _ea field ref but it might be simpler to just set it.

Actually, the simplest approach: The test only needs to add a key to the internal extended attributes dictionary. Since TarHeader is internal, check if the test assembly can see it. Look for InternalsVisibleTo in the product assembly. If the tests can already see TarHeader, then [UnsafeAccessor] only needs to access _header on TarEntry (since _header is internal but the class TarHeader itself is also internal).

Important: [UnsafeAccessor] can access internal types from other assemblies. The field accessor returns a ref, so you can directly work with it. The method should look something like:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_ea")]staticexternrefDictionary<string,string>?GetEa(TarHeaderheader);

But since TarHeader is internal to the product assembly and can't be named in the test assembly's source code directly... you need to check if test projects have InternalsVisibleTo.

  1. Replace lines 40-44 in WriteSparseEntry with calls to the [UnsafeAccessor] methods to get the _header field and then the ExtendedAttributes/_ea field, and set ea["GNU.sparse.realsize"] = realSize.ToString();.

Please check whether the test project has access to internal types (via InternalsVisibleTo or similar) and choose the simplest working approach. The key requirement is that the code must work under Native AOT without relying on reflection APIs (`Type....

This pull request was created from Copilot chat.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-formats-tar
See info in area-owners.md if you want to be subscribed.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/26cfdebb-c9b4-43c7-908e-b1d9ce2f1cd6
Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 13, 2026 22:42
CopilotAI changed the title [WIP] Fix TarReader sparse file tests for Native AOT compatibilityFix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor]Apr 13, 2026
@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).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes TarReader_SparseFileTests failing under Native AOT outerloop by removing reflection-based access to internal TAR header state (which is trim-sensitive) and replacing it with [UnsafeAccessor]-based access that is AOT-/trimmer-friendly.

Changes:

  • Replace reflection (GetField/GetProperty/GetValue) in WriteSparseEntry with [UnsafeAccessor].
  • Unwrap ReadOnlyDictionary<TKey,TValue> to its backing IDictionary<TKey,TValue> via a generic ReadOnlyDictionaryAccessors<TKey,TValue> helper to inject GNU.sparse.realsize.
  • Update usings to remove System.Reflection and add required namespaces for ReadOnlyDictionary + UnsafeAccessor.

@MichalStrehovsky

Copy link
Copy Markdown
Member

Copilot came up with a creative replacement that I scratched my head about for a bit. We can ask it to keep using reflection, just rewrite it into typeof(SomeType).GetField(...) or Type.GetType("SomeStringLiteral").GetField(...) (that are still reflection, but at least it can now be statically analyzed). I don't have a preference, leave this up to the maintainers.

@rzikmrzikm left a comment

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.

LGTM (if that works, that is), thanks!

@rzikm

Copy link
Copy Markdown
Member

/azp list

@azure-pipelines

Copy link
Copy Markdown
CI/CD Pipelines for this repository:

@rzikm

Copy link
Copy Markdown
Member

oh, then nativeAoT was already run

----- start Tue Apr 14 01:53:23 AM UTC 2026 =============== To repro directly: =====================================================
pushd .
chmod +rwx System.Formats.Tar.Tests ^&^& ./System.Formats.Tar.Tests -notrait category=IgnoreForCI -notrait category=OuterLoop -notrait category=failing -xml testResults.xml popd
===========================================================================================================
/root/helix/work/workitem/e /root/helix/work/workitem/e
Running assembly:System.Formats.Tar.Tests, Version=11.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
[SKIP] System.Formats.Tar.Tests.TarEntry_ExtractToFile_Tests_Unix.Extract_SpecialFiles_Async
[SKIP] System.Formats.Tar.Tests.TarEntry_ExtractToFile_Tests_Unix.Extract_SpecialFiles
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_BlockDevice
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentUser
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_CharacterDevice
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_Fifo
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentGroup
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentGroupAndUser
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_BlockDevice_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentGroup_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentUser_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentGroupAndUser_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_Fifo_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_CharacterDevice_Async
Finished System.Formats.Tar.Tests, Version=11.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
Tests run: 5868, Errors: 0, Failures: 0, Skipped: 14. Time: 3.645058s

Seems like the fix works, thanks again

@rzikm
rzikm enabled auto-merge (squash) April 14, 2026 06:44
@rzikm

Copy link
Copy Markdown
Member

/ba-g test failure is #126867

@rzikm
rzikm merged commit 4f21ff0 into mainApr 14, 2026
98 of 118 checks passed
@rzikm
rzikm deleted the copilot/fix-tarreader-sparsefiletests-reflection branch April 14, 2026 06:48
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 14, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@MichalStrehovsky@rzikm
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor] - #126854

Merged
rzikm merged 2 commits into
mainfrom
copilot/fix-tarreader-sparsefiletests-reflection
Apr 14, 2026
Merged

Fix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor]#126854
rzikm merged 2 commits into
mainfrom
copilot/fix-tarreader-sparsefiletests-reflection

Conversation

CopilotAI commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

TarReader_SparseFileTests.WriteSparseEntry used reflection to inject GNU.sparse.realsize into TarEntry._header.ExtendedAttributes after construction (to bypass validation for negative-value test cases). Under Native AOT outerloop runs, the trimmer removes metadata for these internal members, causing NullReferenceException.

Changes

  • Replaced reflection (GetField/GetProperty/GetValue) in WriteSparseEntry with [UnsafeAccessor], which is AOT-safe and statically analyzable by the trimmer.
  • Approach: access the mutable Dictionary<string, string> by calling the public PaxTarEntry.ExtendedAttributes property (which lazily initializes _header._ea) and then unwrapping the ReadOnlyDictionary<string, string> via a generic accessor class targeting the stable m_dictionary field.
  • AddedReadOnlyDictionaryAccessors<TKey, TValue> private nested class (required pattern for [UnsafeAccessor] on a generic target type):
privatestaticclassReadOnlyDictionaryAccessors<TKey,TValue>whereTKey:notnull{[UnsafeAccessor(UnsafeAccessorKind.Field,Name="m_dictionary")]publicstaticexternrefIDictionary<TKey,TValue>GetInnerDictionary(ReadOnlyDictionary<TKey,TValue>d);}// usage:varea=(Dictionary<string,string>)ReadOnlyDictionaryAccessors<string,string>.GetInnerDictionary((ReadOnlyDictionary<string,string>)entry.ExtendedAttributes);ea["GNU.sparse.realsize"]=realSize.ToString();
  • Removed using System.Reflection; added using System.Collections.ObjectModel and using System.Runtime.CompilerServices.
Original prompt

Problem

PR #125283 introduced TarReader_SparseFileTests that use reflection to access internal members, which breaks under Native AOT outerloop runs because the trimmer removes the reflection metadata.

The failing test:

[FAIL] System.Formats.Tar.Tests.TarReader_SparseFileTests.CorruptedSparseMap_InvalidDataException(sparseMapContent: "abc\n0\n256\n", useAsync: False)
System.NullReferenceException : Object reference not set to an instance of an object.
at System.Formats.Tar.Tests.TarReader_SparseFileTests.WriteSparseEntry(TarWriter writer, String realName, Int64 realSize, Byte[] rawSparseData) in /_/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs:line 42

Root Cause

In src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs, the WriteSparseEntry method uses reflection at lines 40-43:

varheaderField=typeof(TarEntry).GetField("_header",BindingFlags.NonPublic|BindingFlags.Instance)!;varheader=headerField.GetValue(entry)!;vareaProp=header.GetType().GetProperty("ExtendedAttributes",BindingFlags.NonPublic|BindingFlags.Instance)!;varea=(Dictionary<string,string>)eaProp.GetValue(header)!;ea["GNU.sparse.realsize"]=realSize.ToString();

Under Native AOT, GetField and GetProperty return null because reflection metadata for these internal members is not preserved. The ! operator then causes a NullReferenceException.

Required Fix

Replace the reflection-based access with [UnsafeAccessor] attribute, which works under Native AOT. The internal types/members that need to be accessed:

  1. TarEntry._header - an internal field of type TarHeader on the abstract class TarEntry (in src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs, line 17: internal TarHeader _header;)

  2. TarHeader.ExtendedAttributes - an internal property on the sealed class TarHeader that returns Dictionary<string, string>. It's defined as:

    internalDictionary<string,string>ExtendedAttributes=> _ea ??=new Dictionary<string,string>();

    The backing field is private Dictionary<string, string>? _ea;

The TarHeader class is internal sealed partial class TarHeader in namespace System.Formats.Tar.

Implementation

In the test file src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs:

  1. Add using System.Runtime.CompilerServices; at the top.

  2. Remove using System.Reflection; (should no longer be needed).

  3. Add [UnsafeAccessor] static extern methods to the test class (or as file-scoped static methods). For example:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_header")]staticexternrefTarHeaderGetHeader(TarEntryentry);

Note: TarHeader is internal, so the test project needs to have access to it. Check if the test project already has InternalsVisibleTo or uses [assembly: InternalsVisibleTo]. If not, you might need to access the _ea field directly instead of going through the ExtendedAttributes property:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_ea")]staticexternrefDictionary<string,string>?GetEa(TarHeaderheader);

However, since UnsafeAccessor with UnsafeAccessorKind.Field returns a ref to the field, and _ea might be null, you'd need to handle the initialization. An alternative approach: since the ExtendedAttributes property initializes _ea via ??=, you could use the _ea field ref but it might be simpler to just set it.

Actually, the simplest approach: The test only needs to add a key to the internal extended attributes dictionary. Since TarHeader is internal, check if the test assembly can see it. Look for InternalsVisibleTo in the product assembly. If the tests can already see TarHeader, then [UnsafeAccessor] only needs to access _header on TarEntry (since _header is internal but the class TarHeader itself is also internal).

Important: [UnsafeAccessor] can access internal types from other assemblies. The field accessor returns a ref, so you can directly work with it. The method should look something like:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_ea")]staticexternrefDictionary<string,string>?GetEa(TarHeaderheader);

But since TarHeader is internal to the product assembly and can't be named in the test assembly's source code directly... you need to check if test projects have InternalsVisibleTo.

  1. Replace lines 40-44 in WriteSparseEntry with calls to the [UnsafeAccessor] methods to get the _header field and then the ExtendedAttributes/_ea field, and set ea["GNU.sparse.realsize"] = realSize.ToString();.

Please check whether the test project has access to internal types (via InternalsVisibleTo or similar) and choose the simplest working approach. The key requirement is that the code must work under Native AOT without relying on reflection APIs (`Type....

This pull request was created from Copilot chat.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-formats-tar
See info in area-owners.md if you want to be subscribed.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/26cfdebb-c9b4-43c7-908e-b1d9ce2f1cd6
Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 13, 2026 22:42
CopilotAI changed the title [WIP] Fix TarReader sparse file tests for Native AOT compatibilityFix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor]Apr 13, 2026
@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).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes TarReader_SparseFileTests failing under Native AOT outerloop by removing reflection-based access to internal TAR header state (which is trim-sensitive) and replacing it with [UnsafeAccessor]-based access that is AOT-/trimmer-friendly.

Changes:

  • Replace reflection (GetField/GetProperty/GetValue) in WriteSparseEntry with [UnsafeAccessor].
  • Unwrap ReadOnlyDictionary<TKey,TValue> to its backing IDictionary<TKey,TValue> via a generic ReadOnlyDictionaryAccessors<TKey,TValue> helper to inject GNU.sparse.realsize.
  • Update usings to remove System.Reflection and add required namespaces for ReadOnlyDictionary + UnsafeAccessor.

@MichalStrehovsky

Copy link
Copy Markdown
Member

Copilot came up with a creative replacement that I scratched my head about for a bit. We can ask it to keep using reflection, just rewrite it into typeof(SomeType).GetField(...) or Type.GetType("SomeStringLiteral").GetField(...) (that are still reflection, but at least it can now be statically analyzed). I don't have a preference, leave this up to the maintainers.

@rzikmrzikm left a comment

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.

LGTM (if that works, that is), thanks!

@rzikm

Copy link
Copy Markdown
Member

/azp list

@azure-pipelines

Copy link
Copy Markdown
CI/CD Pipelines for this repository:

@rzikm

Copy link
Copy Markdown
Member

oh, then nativeAoT was already run

----- start Tue Apr 14 01:53:23 AM UTC 2026 =============== To repro directly: =====================================================
pushd .
chmod +rwx System.Formats.Tar.Tests ^&^& ./System.Formats.Tar.Tests -notrait category=IgnoreForCI -notrait category=OuterLoop -notrait category=failing -xml testResults.xml popd
===========================================================================================================
/root/helix/work/workitem/e /root/helix/work/workitem/e
Running assembly:System.Formats.Tar.Tests, Version=11.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
[SKIP] System.Formats.Tar.Tests.TarEntry_ExtractToFile_Tests_Unix.Extract_SpecialFiles_Async
[SKIP] System.Formats.Tar.Tests.TarEntry_ExtractToFile_Tests_Unix.Extract_SpecialFiles
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_BlockDevice
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentUser
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_CharacterDevice
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_Fifo
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentGroup
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentGroupAndUser
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_BlockDevice_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentGroup_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentUser_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentGroupAndUser_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_Fifo_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_CharacterDevice_Async
Finished System.Formats.Tar.Tests, Version=11.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
Tests run: 5868, Errors: 0, Failures: 0, Skipped: 14. Time: 3.645058s

Seems like the fix works, thanks again

@rzikm
rzikm enabled auto-merge (squash) April 14, 2026 06:44
@rzikm

Copy link
Copy Markdown
Member

/ba-g test failure is #126867

@rzikm
rzikm merged commit 4f21ff0 into mainApr 14, 2026
98 of 118 checks passed
@rzikm
rzikm deleted the copilot/fix-tarreader-sparsefiletests-reflection branch April 14, 2026 06:48
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 14, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@MichalStrehovsky@rzikm
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor] - #126854

Merged
rzikm merged 2 commits into
mainfrom
copilot/fix-tarreader-sparsefiletests-reflection
Apr 14, 2026
Merged

Fix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor]#126854
rzikm merged 2 commits into
mainfrom
copilot/fix-tarreader-sparsefiletests-reflection

Conversation

CopilotAI commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

TarReader_SparseFileTests.WriteSparseEntry used reflection to inject GNU.sparse.realsize into TarEntry._header.ExtendedAttributes after construction (to bypass validation for negative-value test cases). Under Native AOT outerloop runs, the trimmer removes metadata for these internal members, causing NullReferenceException.

Changes

  • Replaced reflection (GetField/GetProperty/GetValue) in WriteSparseEntry with [UnsafeAccessor], which is AOT-safe and statically analyzable by the trimmer.
  • Approach: access the mutable Dictionary<string, string> by calling the public PaxTarEntry.ExtendedAttributes property (which lazily initializes _header._ea) and then unwrapping the ReadOnlyDictionary<string, string> via a generic accessor class targeting the stable m_dictionary field.
  • AddedReadOnlyDictionaryAccessors<TKey, TValue> private nested class (required pattern for [UnsafeAccessor] on a generic target type):
privatestaticclassReadOnlyDictionaryAccessors<TKey,TValue>whereTKey:notnull{[UnsafeAccessor(UnsafeAccessorKind.Field,Name="m_dictionary")]publicstaticexternrefIDictionary<TKey,TValue>GetInnerDictionary(ReadOnlyDictionary<TKey,TValue>d);}// usage:varea=(Dictionary<string,string>)ReadOnlyDictionaryAccessors<string,string>.GetInnerDictionary((ReadOnlyDictionary<string,string>)entry.ExtendedAttributes);ea["GNU.sparse.realsize"]=realSize.ToString();
  • Removed using System.Reflection; added using System.Collections.ObjectModel and using System.Runtime.CompilerServices.
Original prompt

Problem

PR #125283 introduced TarReader_SparseFileTests that use reflection to access internal members, which breaks under Native AOT outerloop runs because the trimmer removes the reflection metadata.

The failing test:

[FAIL] System.Formats.Tar.Tests.TarReader_SparseFileTests.CorruptedSparseMap_InvalidDataException(sparseMapContent: "abc\n0\n256\n", useAsync: False)
System.NullReferenceException : Object reference not set to an instance of an object.
at System.Formats.Tar.Tests.TarReader_SparseFileTests.WriteSparseEntry(TarWriter writer, String realName, Int64 realSize, Byte[] rawSparseData) in /_/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs:line 42

Root Cause

In src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs, the WriteSparseEntry method uses reflection at lines 40-43:

varheaderField=typeof(TarEntry).GetField("_header",BindingFlags.NonPublic|BindingFlags.Instance)!;varheader=headerField.GetValue(entry)!;vareaProp=header.GetType().GetProperty("ExtendedAttributes",BindingFlags.NonPublic|BindingFlags.Instance)!;varea=(Dictionary<string,string>)eaProp.GetValue(header)!;ea["GNU.sparse.realsize"]=realSize.ToString();

Under Native AOT, GetField and GetProperty return null because reflection metadata for these internal members is not preserved. The ! operator then causes a NullReferenceException.

Required Fix

Replace the reflection-based access with [UnsafeAccessor] attribute, which works under Native AOT. The internal types/members that need to be accessed:

  1. TarEntry._header - an internal field of type TarHeader on the abstract class TarEntry (in src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs, line 17: internal TarHeader _header;)

  2. TarHeader.ExtendedAttributes - an internal property on the sealed class TarHeader that returns Dictionary<string, string>. It's defined as:

    internalDictionary<string,string>ExtendedAttributes=> _ea ??=new Dictionary<string,string>();

    The backing field is private Dictionary<string, string>? _ea;

The TarHeader class is internal sealed partial class TarHeader in namespace System.Formats.Tar.

Implementation

In the test file src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs:

  1. Add using System.Runtime.CompilerServices; at the top.

  2. Remove using System.Reflection; (should no longer be needed).

  3. Add [UnsafeAccessor] static extern methods to the test class (or as file-scoped static methods). For example:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_header")]staticexternrefTarHeaderGetHeader(TarEntryentry);

Note: TarHeader is internal, so the test project needs to have access to it. Check if the test project already has InternalsVisibleTo or uses [assembly: InternalsVisibleTo]. If not, you might need to access the _ea field directly instead of going through the ExtendedAttributes property:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_ea")]staticexternrefDictionary<string,string>?GetEa(TarHeaderheader);

However, since UnsafeAccessor with UnsafeAccessorKind.Field returns a ref to the field, and _ea might be null, you'd need to handle the initialization. An alternative approach: since the ExtendedAttributes property initializes _ea via ??=, you could use the _ea field ref but it might be simpler to just set it.

Actually, the simplest approach: The test only needs to add a key to the internal extended attributes dictionary. Since TarHeader is internal, check if the test assembly can see it. Look for InternalsVisibleTo in the product assembly. If the tests can already see TarHeader, then [UnsafeAccessor] only needs to access _header on TarEntry (since _header is internal but the class TarHeader itself is also internal).

Important: [UnsafeAccessor] can access internal types from other assemblies. The field accessor returns a ref, so you can directly work with it. The method should look something like:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_ea")]staticexternrefDictionary<string,string>?GetEa(TarHeaderheader);

But since TarHeader is internal to the product assembly and can't be named in the test assembly's source code directly... you need to check if test projects have InternalsVisibleTo.

  1. Replace lines 40-44 in WriteSparseEntry with calls to the [UnsafeAccessor] methods to get the _header field and then the ExtendedAttributes/_ea field, and set ea["GNU.sparse.realsize"] = realSize.ToString();.

Please check whether the test project has access to internal types (via InternalsVisibleTo or similar) and choose the simplest working approach. The key requirement is that the code must work under Native AOT without relying on reflection APIs (`Type....

This pull request was created from Copilot chat.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-formats-tar
See info in area-owners.md if you want to be subscribed.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/26cfdebb-c9b4-43c7-908e-b1d9ce2f1cd6
Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 13, 2026 22:42
CopilotAI changed the title [WIP] Fix TarReader sparse file tests for Native AOT compatibilityFix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor]Apr 13, 2026
@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).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes TarReader_SparseFileTests failing under Native AOT outerloop by removing reflection-based access to internal TAR header state (which is trim-sensitive) and replacing it with [UnsafeAccessor]-based access that is AOT-/trimmer-friendly.

Changes:

  • Replace reflection (GetField/GetProperty/GetValue) in WriteSparseEntry with [UnsafeAccessor].
  • Unwrap ReadOnlyDictionary<TKey,TValue> to its backing IDictionary<TKey,TValue> via a generic ReadOnlyDictionaryAccessors<TKey,TValue> helper to inject GNU.sparse.realsize.
  • Update usings to remove System.Reflection and add required namespaces for ReadOnlyDictionary + UnsafeAccessor.

@MichalStrehovsky

Copy link
Copy Markdown
Member

Copilot came up with a creative replacement that I scratched my head about for a bit. We can ask it to keep using reflection, just rewrite it into typeof(SomeType).GetField(...) or Type.GetType("SomeStringLiteral").GetField(...) (that are still reflection, but at least it can now be statically analyzed). I don't have a preference, leave this up to the maintainers.

@rzikmrzikm left a comment

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.

LGTM (if that works, that is), thanks!

@rzikm

Copy link
Copy Markdown
Member

/azp list

@azure-pipelines

Copy link
Copy Markdown
CI/CD Pipelines for this repository:

@rzikm

Copy link
Copy Markdown
Member

oh, then nativeAoT was already run

----- start Tue Apr 14 01:53:23 AM UTC 2026 =============== To repro directly: =====================================================
pushd .
chmod +rwx System.Formats.Tar.Tests ^&^& ./System.Formats.Tar.Tests -notrait category=IgnoreForCI -notrait category=OuterLoop -notrait category=failing -xml testResults.xml popd
===========================================================================================================
/root/helix/work/workitem/e /root/helix/work/workitem/e
Running assembly:System.Formats.Tar.Tests, Version=11.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
[SKIP] System.Formats.Tar.Tests.TarEntry_ExtractToFile_Tests_Unix.Extract_SpecialFiles_Async
[SKIP] System.Formats.Tar.Tests.TarEntry_ExtractToFile_Tests_Unix.Extract_SpecialFiles
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_BlockDevice
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentUser
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_CharacterDevice
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_Fifo
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentGroup
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentGroupAndUser
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_BlockDevice_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentGroup_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentUser_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentGroupAndUser_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_Fifo_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_CharacterDevice_Async
Finished System.Formats.Tar.Tests, Version=11.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
Tests run: 5868, Errors: 0, Failures: 0, Skipped: 14. Time: 3.645058s

Seems like the fix works, thanks again

@rzikm
rzikm enabled auto-merge (squash) April 14, 2026 06:44
@rzikm

Copy link
Copy Markdown
Member

/ba-g test failure is #126867

@rzikm
rzikm merged commit 4f21ff0 into mainApr 14, 2026
98 of 118 checks passed
@rzikm
rzikm deleted the copilot/fix-tarreader-sparsefiletests-reflection branch April 14, 2026 06:48
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 14, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@MichalStrehovsky@rzikm
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor] - #126854

Merged
rzikm merged 2 commits into
mainfrom
copilot/fix-tarreader-sparsefiletests-reflection
Apr 14, 2026
Merged

Fix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor]#126854
rzikm merged 2 commits into
mainfrom
copilot/fix-tarreader-sparsefiletests-reflection

Conversation

CopilotAI commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

TarReader_SparseFileTests.WriteSparseEntry used reflection to inject GNU.sparse.realsize into TarEntry._header.ExtendedAttributes after construction (to bypass validation for negative-value test cases). Under Native AOT outerloop runs, the trimmer removes metadata for these internal members, causing NullReferenceException.

Changes

  • Replaced reflection (GetField/GetProperty/GetValue) in WriteSparseEntry with [UnsafeAccessor], which is AOT-safe and statically analyzable by the trimmer.
  • Approach: access the mutable Dictionary<string, string> by calling the public PaxTarEntry.ExtendedAttributes property (which lazily initializes _header._ea) and then unwrapping the ReadOnlyDictionary<string, string> via a generic accessor class targeting the stable m_dictionary field.
  • AddedReadOnlyDictionaryAccessors<TKey, TValue> private nested class (required pattern for [UnsafeAccessor] on a generic target type):
privatestaticclassReadOnlyDictionaryAccessors<TKey,TValue>whereTKey:notnull{[UnsafeAccessor(UnsafeAccessorKind.Field,Name="m_dictionary")]publicstaticexternrefIDictionary<TKey,TValue>GetInnerDictionary(ReadOnlyDictionary<TKey,TValue>d);}// usage:varea=(Dictionary<string,string>)ReadOnlyDictionaryAccessors<string,string>.GetInnerDictionary((ReadOnlyDictionary<string,string>)entry.ExtendedAttributes);ea["GNU.sparse.realsize"]=realSize.ToString();
  • Removed using System.Reflection; added using System.Collections.ObjectModel and using System.Runtime.CompilerServices.
Original prompt

Problem

PR #125283 introduced TarReader_SparseFileTests that use reflection to access internal members, which breaks under Native AOT outerloop runs because the trimmer removes the reflection metadata.

The failing test:

[FAIL] System.Formats.Tar.Tests.TarReader_SparseFileTests.CorruptedSparseMap_InvalidDataException(sparseMapContent: "abc\n0\n256\n", useAsync: False)
System.NullReferenceException : Object reference not set to an instance of an object.
at System.Formats.Tar.Tests.TarReader_SparseFileTests.WriteSparseEntry(TarWriter writer, String realName, Int64 realSize, Byte[] rawSparseData) in /_/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs:line 42

Root Cause

In src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs, the WriteSparseEntry method uses reflection at lines 40-43:

varheaderField=typeof(TarEntry).GetField("_header",BindingFlags.NonPublic|BindingFlags.Instance)!;varheader=headerField.GetValue(entry)!;vareaProp=header.GetType().GetProperty("ExtendedAttributes",BindingFlags.NonPublic|BindingFlags.Instance)!;varea=(Dictionary<string,string>)eaProp.GetValue(header)!;ea["GNU.sparse.realsize"]=realSize.ToString();

Under Native AOT, GetField and GetProperty return null because reflection metadata for these internal members is not preserved. The ! operator then causes a NullReferenceException.

Required Fix

Replace the reflection-based access with [UnsafeAccessor] attribute, which works under Native AOT. The internal types/members that need to be accessed:

  1. TarEntry._header - an internal field of type TarHeader on the abstract class TarEntry (in src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs, line 17: internal TarHeader _header;)

  2. TarHeader.ExtendedAttributes - an internal property on the sealed class TarHeader that returns Dictionary<string, string>. It's defined as:

    internalDictionary<string,string>ExtendedAttributes=> _ea ??=new Dictionary<string,string>();

    The backing field is private Dictionary<string, string>? _ea;

The TarHeader class is internal sealed partial class TarHeader in namespace System.Formats.Tar.

Implementation

In the test file src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs:

  1. Add using System.Runtime.CompilerServices; at the top.

  2. Remove using System.Reflection; (should no longer be needed).

  3. Add [UnsafeAccessor] static extern methods to the test class (or as file-scoped static methods). For example:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_header")]staticexternrefTarHeaderGetHeader(TarEntryentry);

Note: TarHeader is internal, so the test project needs to have access to it. Check if the test project already has InternalsVisibleTo or uses [assembly: InternalsVisibleTo]. If not, you might need to access the _ea field directly instead of going through the ExtendedAttributes property:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_ea")]staticexternrefDictionary<string,string>?GetEa(TarHeaderheader);

However, since UnsafeAccessor with UnsafeAccessorKind.Field returns a ref to the field, and _ea might be null, you'd need to handle the initialization. An alternative approach: since the ExtendedAttributes property initializes _ea via ??=, you could use the _ea field ref but it might be simpler to just set it.

Actually, the simplest approach: The test only needs to add a key to the internal extended attributes dictionary. Since TarHeader is internal, check if the test assembly can see it. Look for InternalsVisibleTo in the product assembly. If the tests can already see TarHeader, then [UnsafeAccessor] only needs to access _header on TarEntry (since _header is internal but the class TarHeader itself is also internal).

Important: [UnsafeAccessor] can access internal types from other assemblies. The field accessor returns a ref, so you can directly work with it. The method should look something like:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_ea")]staticexternrefDictionary<string,string>?GetEa(TarHeaderheader);

But since TarHeader is internal to the product assembly and can't be named in the test assembly's source code directly... you need to check if test projects have InternalsVisibleTo.

  1. Replace lines 40-44 in WriteSparseEntry with calls to the [UnsafeAccessor] methods to get the _header field and then the ExtendedAttributes/_ea field, and set ea["GNU.sparse.realsize"] = realSize.ToString();.

Please check whether the test project has access to internal types (via InternalsVisibleTo or similar) and choose the simplest working approach. The key requirement is that the code must work under Native AOT without relying on reflection APIs (`Type....

This pull request was created from Copilot chat.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-formats-tar
See info in area-owners.md if you want to be subscribed.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/26cfdebb-c9b4-43c7-908e-b1d9ce2f1cd6
Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 13, 2026 22:42
CopilotAI changed the title [WIP] Fix TarReader sparse file tests for Native AOT compatibilityFix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor]Apr 13, 2026
@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).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes TarReader_SparseFileTests failing under Native AOT outerloop by removing reflection-based access to internal TAR header state (which is trim-sensitive) and replacing it with [UnsafeAccessor]-based access that is AOT-/trimmer-friendly.

Changes:

  • Replace reflection (GetField/GetProperty/GetValue) in WriteSparseEntry with [UnsafeAccessor].
  • Unwrap ReadOnlyDictionary<TKey,TValue> to its backing IDictionary<TKey,TValue> via a generic ReadOnlyDictionaryAccessors<TKey,TValue> helper to inject GNU.sparse.realsize.
  • Update usings to remove System.Reflection and add required namespaces for ReadOnlyDictionary + UnsafeAccessor.

@MichalStrehovsky

Copy link
Copy Markdown
Member

Copilot came up with a creative replacement that I scratched my head about for a bit. We can ask it to keep using reflection, just rewrite it into typeof(SomeType).GetField(...) or Type.GetType("SomeStringLiteral").GetField(...) (that are still reflection, but at least it can now be statically analyzed). I don't have a preference, leave this up to the maintainers.

@rzikmrzikm left a comment

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.

LGTM (if that works, that is), thanks!

@rzikm

Copy link
Copy Markdown
Member

/azp list

@azure-pipelines

Copy link
Copy Markdown
CI/CD Pipelines for this repository:

@rzikm

Copy link
Copy Markdown
Member

oh, then nativeAoT was already run

----- start Tue Apr 14 01:53:23 AM UTC 2026 =============== To repro directly: =====================================================
pushd .
chmod +rwx System.Formats.Tar.Tests ^&^& ./System.Formats.Tar.Tests -notrait category=IgnoreForCI -notrait category=OuterLoop -notrait category=failing -xml testResults.xml popd
===========================================================================================================
/root/helix/work/workitem/e /root/helix/work/workitem/e
Running assembly:System.Formats.Tar.Tests, Version=11.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
[SKIP] System.Formats.Tar.Tests.TarEntry_ExtractToFile_Tests_Unix.Extract_SpecialFiles_Async
[SKIP] System.Formats.Tar.Tests.TarEntry_ExtractToFile_Tests_Unix.Extract_SpecialFiles
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_BlockDevice
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentUser
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_CharacterDevice
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_Fifo
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentGroup
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentGroupAndUser
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_BlockDevice_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentGroup_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentUser_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentGroupAndUser_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_Fifo_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_CharacterDevice_Async
Finished System.Formats.Tar.Tests, Version=11.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
Tests run: 5868, Errors: 0, Failures: 0, Skipped: 14. Time: 3.645058s

Seems like the fix works, thanks again

@rzikm
rzikm enabled auto-merge (squash) April 14, 2026 06:44
@rzikm

Copy link
Copy Markdown
Member

/ba-g test failure is #126867

@rzikm
rzikm merged commit 4f21ff0 into mainApr 14, 2026
98 of 118 checks passed
@rzikm
rzikm deleted the copilot/fix-tarreader-sparsefiletests-reflection branch April 14, 2026 06:48
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 14, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@MichalStrehovsky@rzikm
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor] - #126854

Merged
rzikm merged 2 commits into
mainfrom
copilot/fix-tarreader-sparsefiletests-reflection
Apr 14, 2026
Merged

Fix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor]#126854
rzikm merged 2 commits into
mainfrom
copilot/fix-tarreader-sparsefiletests-reflection

Conversation

CopilotAI commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

TarReader_SparseFileTests.WriteSparseEntry used reflection to inject GNU.sparse.realsize into TarEntry._header.ExtendedAttributes after construction (to bypass validation for negative-value test cases). Under Native AOT outerloop runs, the trimmer removes metadata for these internal members, causing NullReferenceException.

Changes

  • Replaced reflection (GetField/GetProperty/GetValue) in WriteSparseEntry with [UnsafeAccessor], which is AOT-safe and statically analyzable by the trimmer.
  • Approach: access the mutable Dictionary<string, string> by calling the public PaxTarEntry.ExtendedAttributes property (which lazily initializes _header._ea) and then unwrapping the ReadOnlyDictionary<string, string> via a generic accessor class targeting the stable m_dictionary field.
  • AddedReadOnlyDictionaryAccessors<TKey, TValue> private nested class (required pattern for [UnsafeAccessor] on a generic target type):
privatestaticclassReadOnlyDictionaryAccessors<TKey,TValue>whereTKey:notnull{[UnsafeAccessor(UnsafeAccessorKind.Field,Name="m_dictionary")]publicstaticexternrefIDictionary<TKey,TValue>GetInnerDictionary(ReadOnlyDictionary<TKey,TValue>d);}// usage:varea=(Dictionary<string,string>)ReadOnlyDictionaryAccessors<string,string>.GetInnerDictionary((ReadOnlyDictionary<string,string>)entry.ExtendedAttributes);ea["GNU.sparse.realsize"]=realSize.ToString();
  • Removed using System.Reflection; added using System.Collections.ObjectModel and using System.Runtime.CompilerServices.
Original prompt

Problem

PR #125283 introduced TarReader_SparseFileTests that use reflection to access internal members, which breaks under Native AOT outerloop runs because the trimmer removes the reflection metadata.

The failing test:

[FAIL] System.Formats.Tar.Tests.TarReader_SparseFileTests.CorruptedSparseMap_InvalidDataException(sparseMapContent: "abc\n0\n256\n", useAsync: False)
System.NullReferenceException : Object reference not set to an instance of an object.
at System.Formats.Tar.Tests.TarReader_SparseFileTests.WriteSparseEntry(TarWriter writer, String realName, Int64 realSize, Byte[] rawSparseData) in /_/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs:line 42

Root Cause

In src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs, the WriteSparseEntry method uses reflection at lines 40-43:

varheaderField=typeof(TarEntry).GetField("_header",BindingFlags.NonPublic|BindingFlags.Instance)!;varheader=headerField.GetValue(entry)!;vareaProp=header.GetType().GetProperty("ExtendedAttributes",BindingFlags.NonPublic|BindingFlags.Instance)!;varea=(Dictionary<string,string>)eaProp.GetValue(header)!;ea["GNU.sparse.realsize"]=realSize.ToString();

Under Native AOT, GetField and GetProperty return null because reflection metadata for these internal members is not preserved. The ! operator then causes a NullReferenceException.

Required Fix

Replace the reflection-based access with [UnsafeAccessor] attribute, which works under Native AOT. The internal types/members that need to be accessed:

  1. TarEntry._header - an internal field of type TarHeader on the abstract class TarEntry (in src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs, line 17: internal TarHeader _header;)

  2. TarHeader.ExtendedAttributes - an internal property on the sealed class TarHeader that returns Dictionary<string, string>. It's defined as:

    internalDictionary<string,string>ExtendedAttributes=> _ea ??=new Dictionary<string,string>();

    The backing field is private Dictionary<string, string>? _ea;

The TarHeader class is internal sealed partial class TarHeader in namespace System.Formats.Tar.

Implementation

In the test file src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs:

  1. Add using System.Runtime.CompilerServices; at the top.

  2. Remove using System.Reflection; (should no longer be needed).

  3. Add [UnsafeAccessor] static extern methods to the test class (or as file-scoped static methods). For example:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_header")]staticexternrefTarHeaderGetHeader(TarEntryentry);

Note: TarHeader is internal, so the test project needs to have access to it. Check if the test project already has InternalsVisibleTo or uses [assembly: InternalsVisibleTo]. If not, you might need to access the _ea field directly instead of going through the ExtendedAttributes property:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_ea")]staticexternrefDictionary<string,string>?GetEa(TarHeaderheader);

However, since UnsafeAccessor with UnsafeAccessorKind.Field returns a ref to the field, and _ea might be null, you'd need to handle the initialization. An alternative approach: since the ExtendedAttributes property initializes _ea via ??=, you could use the _ea field ref but it might be simpler to just set it.

Actually, the simplest approach: The test only needs to add a key to the internal extended attributes dictionary. Since TarHeader is internal, check if the test assembly can see it. Look for InternalsVisibleTo in the product assembly. If the tests can already see TarHeader, then [UnsafeAccessor] only needs to access _header on TarEntry (since _header is internal but the class TarHeader itself is also internal).

Important: [UnsafeAccessor] can access internal types from other assemblies. The field accessor returns a ref, so you can directly work with it. The method should look something like:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_ea")]staticexternrefDictionary<string,string>?GetEa(TarHeaderheader);

But since TarHeader is internal to the product assembly and can't be named in the test assembly's source code directly... you need to check if test projects have InternalsVisibleTo.

  1. Replace lines 40-44 in WriteSparseEntry with calls to the [UnsafeAccessor] methods to get the _header field and then the ExtendedAttributes/_ea field, and set ea["GNU.sparse.realsize"] = realSize.ToString();.

Please check whether the test project has access to internal types (via InternalsVisibleTo or similar) and choose the simplest working approach. The key requirement is that the code must work under Native AOT without relying on reflection APIs (`Type....

This pull request was created from Copilot chat.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-formats-tar
See info in area-owners.md if you want to be subscribed.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/26cfdebb-c9b4-43c7-908e-b1d9ce2f1cd6
Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 13, 2026 22:42
CopilotAI changed the title [WIP] Fix TarReader sparse file tests for Native AOT compatibilityFix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor]Apr 13, 2026
@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).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes TarReader_SparseFileTests failing under Native AOT outerloop by removing reflection-based access to internal TAR header state (which is trim-sensitive) and replacing it with [UnsafeAccessor]-based access that is AOT-/trimmer-friendly.

Changes:

  • Replace reflection (GetField/GetProperty/GetValue) in WriteSparseEntry with [UnsafeAccessor].
  • Unwrap ReadOnlyDictionary<TKey,TValue> to its backing IDictionary<TKey,TValue> via a generic ReadOnlyDictionaryAccessors<TKey,TValue> helper to inject GNU.sparse.realsize.
  • Update usings to remove System.Reflection and add required namespaces for ReadOnlyDictionary + UnsafeAccessor.

@MichalStrehovsky

Copy link
Copy Markdown
Member

Copilot came up with a creative replacement that I scratched my head about for a bit. We can ask it to keep using reflection, just rewrite it into typeof(SomeType).GetField(...) or Type.GetType("SomeStringLiteral").GetField(...) (that are still reflection, but at least it can now be statically analyzed). I don't have a preference, leave this up to the maintainers.

@rzikmrzikm left a comment

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.

LGTM (if that works, that is), thanks!

@rzikm

Copy link
Copy Markdown
Member

/azp list

@azure-pipelines

Copy link
Copy Markdown
CI/CD Pipelines for this repository:

@rzikm

Copy link
Copy Markdown
Member

oh, then nativeAoT was already run

----- start Tue Apr 14 01:53:23 AM UTC 2026 =============== To repro directly: =====================================================
pushd .
chmod +rwx System.Formats.Tar.Tests ^&^& ./System.Formats.Tar.Tests -notrait category=IgnoreForCI -notrait category=OuterLoop -notrait category=failing -xml testResults.xml popd
===========================================================================================================
/root/helix/work/workitem/e /root/helix/work/workitem/e
Running assembly:System.Formats.Tar.Tests, Version=11.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
[SKIP] System.Formats.Tar.Tests.TarEntry_ExtractToFile_Tests_Unix.Extract_SpecialFiles_Async
[SKIP] System.Formats.Tar.Tests.TarEntry_ExtractToFile_Tests_Unix.Extract_SpecialFiles
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_BlockDevice
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentUser
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_CharacterDevice
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_Fifo
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentGroup
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentGroupAndUser
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_BlockDevice_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentGroup_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentUser_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentGroupAndUser_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_Fifo_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_CharacterDevice_Async
Finished System.Formats.Tar.Tests, Version=11.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
Tests run: 5868, Errors: 0, Failures: 0, Skipped: 14. Time: 3.645058s

Seems like the fix works, thanks again

@rzikm
rzikm enabled auto-merge (squash) April 14, 2026 06:44
@rzikm

Copy link
Copy Markdown
Member

/ba-g test failure is #126867

@rzikm
rzikm merged commit 4f21ff0 into mainApr 14, 2026
98 of 118 checks passed
@rzikm
rzikm deleted the copilot/fix-tarreader-sparsefiletests-reflection branch April 14, 2026 06:48
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 14, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@MichalStrehovsky@rzikm
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor] - #126854

Merged
rzikm merged 2 commits into
mainfrom
copilot/fix-tarreader-sparsefiletests-reflection
Apr 14, 2026
Merged

Fix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor]#126854
rzikm merged 2 commits into
mainfrom
copilot/fix-tarreader-sparsefiletests-reflection

Conversation

CopilotAI commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

TarReader_SparseFileTests.WriteSparseEntry used reflection to inject GNU.sparse.realsize into TarEntry._header.ExtendedAttributes after construction (to bypass validation for negative-value test cases). Under Native AOT outerloop runs, the trimmer removes metadata for these internal members, causing NullReferenceException.

Changes

  • Replaced reflection (GetField/GetProperty/GetValue) in WriteSparseEntry with [UnsafeAccessor], which is AOT-safe and statically analyzable by the trimmer.
  • Approach: access the mutable Dictionary<string, string> by calling the public PaxTarEntry.ExtendedAttributes property (which lazily initializes _header._ea) and then unwrapping the ReadOnlyDictionary<string, string> via a generic accessor class targeting the stable m_dictionary field.
  • AddedReadOnlyDictionaryAccessors<TKey, TValue> private nested class (required pattern for [UnsafeAccessor] on a generic target type):
privatestaticclassReadOnlyDictionaryAccessors<TKey,TValue>whereTKey:notnull{[UnsafeAccessor(UnsafeAccessorKind.Field,Name="m_dictionary")]publicstaticexternrefIDictionary<TKey,TValue>GetInnerDictionary(ReadOnlyDictionary<TKey,TValue>d);}// usage:varea=(Dictionary<string,string>)ReadOnlyDictionaryAccessors<string,string>.GetInnerDictionary((ReadOnlyDictionary<string,string>)entry.ExtendedAttributes);ea["GNU.sparse.realsize"]=realSize.ToString();
  • Removed using System.Reflection; added using System.Collections.ObjectModel and using System.Runtime.CompilerServices.
Original prompt

Problem

PR #125283 introduced TarReader_SparseFileTests that use reflection to access internal members, which breaks under Native AOT outerloop runs because the trimmer removes the reflection metadata.

The failing test:

[FAIL] System.Formats.Tar.Tests.TarReader_SparseFileTests.CorruptedSparseMap_InvalidDataException(sparseMapContent: "abc\n0\n256\n", useAsync: False)
System.NullReferenceException : Object reference not set to an instance of an object.
at System.Formats.Tar.Tests.TarReader_SparseFileTests.WriteSparseEntry(TarWriter writer, String realName, Int64 realSize, Byte[] rawSparseData) in /_/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs:line 42

Root Cause

In src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs, the WriteSparseEntry method uses reflection at lines 40-43:

varheaderField=typeof(TarEntry).GetField("_header",BindingFlags.NonPublic|BindingFlags.Instance)!;varheader=headerField.GetValue(entry)!;vareaProp=header.GetType().GetProperty("ExtendedAttributes",BindingFlags.NonPublic|BindingFlags.Instance)!;varea=(Dictionary<string,string>)eaProp.GetValue(header)!;ea["GNU.sparse.realsize"]=realSize.ToString();

Under Native AOT, GetField and GetProperty return null because reflection metadata for these internal members is not preserved. The ! operator then causes a NullReferenceException.

Required Fix

Replace the reflection-based access with [UnsafeAccessor] attribute, which works under Native AOT. The internal types/members that need to be accessed:

  1. TarEntry._header - an internal field of type TarHeader on the abstract class TarEntry (in src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs, line 17: internal TarHeader _header;)

  2. TarHeader.ExtendedAttributes - an internal property on the sealed class TarHeader that returns Dictionary<string, string>. It's defined as:

    internalDictionary<string,string>ExtendedAttributes=> _ea ??=new Dictionary<string,string>();

    The backing field is private Dictionary<string, string>? _ea;

The TarHeader class is internal sealed partial class TarHeader in namespace System.Formats.Tar.

Implementation

In the test file src/libraries/System.Formats.Tar/tests/TarReader/TarReader.SparseFile.Tests.cs:

  1. Add using System.Runtime.CompilerServices; at the top.

  2. Remove using System.Reflection; (should no longer be needed).

  3. Add [UnsafeAccessor] static extern methods to the test class (or as file-scoped static methods). For example:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_header")]staticexternrefTarHeaderGetHeader(TarEntryentry);

Note: TarHeader is internal, so the test project needs to have access to it. Check if the test project already has InternalsVisibleTo or uses [assembly: InternalsVisibleTo]. If not, you might need to access the _ea field directly instead of going through the ExtendedAttributes property:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_ea")]staticexternrefDictionary<string,string>?GetEa(TarHeaderheader);

However, since UnsafeAccessor with UnsafeAccessorKind.Field returns a ref to the field, and _ea might be null, you'd need to handle the initialization. An alternative approach: since the ExtendedAttributes property initializes _ea via ??=, you could use the _ea field ref but it might be simpler to just set it.

Actually, the simplest approach: The test only needs to add a key to the internal extended attributes dictionary. Since TarHeader is internal, check if the test assembly can see it. Look for InternalsVisibleTo in the product assembly. If the tests can already see TarHeader, then [UnsafeAccessor] only needs to access _header on TarEntry (since _header is internal but the class TarHeader itself is also internal).

Important: [UnsafeAccessor] can access internal types from other assemblies. The field accessor returns a ref, so you can directly work with it. The method should look something like:

[UnsafeAccessor(UnsafeAccessorKind.Field,Name="_ea")]staticexternrefDictionary<string,string>?GetEa(TarHeaderheader);

But since TarHeader is internal to the product assembly and can't be named in the test assembly's source code directly... you need to check if test projects have InternalsVisibleTo.

  1. Replace lines 40-44 in WriteSparseEntry with calls to the [UnsafeAccessor] methods to get the _header field and then the ExtendedAttributes/_ea field, and set ea["GNU.sparse.realsize"] = realSize.ToString();.

Please check whether the test project has access to internal types (via InternalsVisibleTo or similar) and choose the simplest working approach. The key requirement is that the code must work under Native AOT without relying on reflection APIs (`Type....

This pull request was created from Copilot chat.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-formats-tar
See info in area-owners.md if you want to be subscribed.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/26cfdebb-c9b4-43c7-908e-b1d9ce2f1cd6
Co-authored-by: MichalStrehovsky <13110571+MichalStrehovsky@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 13, 2026 22:42
CopilotAI changed the title [WIP] Fix TarReader sparse file tests for Native AOT compatibilityFix TarReader_SparseFileTests reflection breaking under Native AOT by replacing with [UnsafeAccessor]Apr 13, 2026
@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).

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes TarReader_SparseFileTests failing under Native AOT outerloop by removing reflection-based access to internal TAR header state (which is trim-sensitive) and replacing it with [UnsafeAccessor]-based access that is AOT-/trimmer-friendly.

Changes:

  • Replace reflection (GetField/GetProperty/GetValue) in WriteSparseEntry with [UnsafeAccessor].
  • Unwrap ReadOnlyDictionary<TKey,TValue> to its backing IDictionary<TKey,TValue> via a generic ReadOnlyDictionaryAccessors<TKey,TValue> helper to inject GNU.sparse.realsize.
  • Update usings to remove System.Reflection and add required namespaces for ReadOnlyDictionary + UnsafeAccessor.

@MichalStrehovsky

Copy link
Copy Markdown
Member

Copilot came up with a creative replacement that I scratched my head about for a bit. We can ask it to keep using reflection, just rewrite it into typeof(SomeType).GetField(...) or Type.GetType("SomeStringLiteral").GetField(...) (that are still reflection, but at least it can now be statically analyzed). I don't have a preference, leave this up to the maintainers.

@rzikmrzikm left a comment

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.

LGTM (if that works, that is), thanks!

@rzikm

Copy link
Copy Markdown
Member

/azp list

@azure-pipelines

Copy link
Copy Markdown
CI/CD Pipelines for this repository:

@rzikm

Copy link
Copy Markdown
Member

oh, then nativeAoT was already run

----- start Tue Apr 14 01:53:23 AM UTC 2026 =============== To repro directly: =====================================================
pushd .
chmod +rwx System.Formats.Tar.Tests ^&^& ./System.Formats.Tar.Tests -notrait category=IgnoreForCI -notrait category=OuterLoop -notrait category=failing -xml testResults.xml popd
===========================================================================================================
/root/helix/work/workitem/e /root/helix/work/workitem/e
Running assembly:System.Formats.Tar.Tests, Version=11.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
[SKIP] System.Formats.Tar.Tests.TarEntry_ExtractToFile_Tests_Unix.Extract_SpecialFiles_Async
[SKIP] System.Formats.Tar.Tests.TarEntry_ExtractToFile_Tests_Unix.Extract_SpecialFiles
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_BlockDevice
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentUser
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_CharacterDevice
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.Add_Fifo
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentGroup
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntry_File_Tests.CreateEntryFromFileOwnedByNonExistentGroupAndUser
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_BlockDevice_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentGroup_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentUser_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.CreateEntryFromFileOwnedByNonExistentGroupAndUser_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_Fifo_Async
[SKIP] System.Formats.Tar.Tests.TarWriter_WriteEntryAsync_File_Tests.Add_CharacterDevice_Async
Finished System.Formats.Tar.Tests, Version=11.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
Tests run: 5868, Errors: 0, Failures: 0, Skipped: 14. Time: 3.645058s

Seems like the fix works, thanks again

@rzikm
rzikm enabled auto-merge (squash) April 14, 2026 06:44
@rzikm

Copy link
Copy Markdown
Member

/ba-g test failure is #126867

@rzikm
rzikm merged commit 4f21ff0 into mainApr 14, 2026
98 of 118 checks passed
@rzikm
rzikm deleted the copilot/fix-tarreader-sparsefiletests-reflection branch April 14, 2026 06:48
@github-actionsgithub-actionsBot locked and limited conversation to collaborators May 14, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@MichalStrehovsky@rzikm