Repository navigation
Running from Native AOT app fails to build signature #2187
Description
Activity
Root cause analysis
This is not a missing native library / load failure, and it is not the same as #2160 (static linking for single-file AOT). The native
git2-*binary loads correctly next to the published AOT executable.The failure is in config discovery under Native AOT.
What fails
Configuration.BuildSignatureonly readsuser.name/user.emailfrom the layered config:var name = this.GetValueOrDefault<string>("user.name"); var email = this.GetValueOrDefault<string>("user.email");
When opening a repository, global/system/xdg/programdata paths are resolved via:
Proxy.git_config_find_global() → ConvertPath(NativeMethods.git_config_find_global) → libgit2 fills a git_buf, managed code reads buf.ptrUnder CoreCLR (
dotnet run), those finds succeed and global config is loaded (includinginclude.pathchains that often holduser.name/user.email).Under Native AOT (
dotnet publish+ run the native exe), the same APIs appear to fail:HasConfig(Global)/HasConfig(System)are false, so only local.git/configis present. If identity is not set locally,BuildSignaturereturnsnull.Why it fails under AOT
GitBufis a sequential-layout class passed by value to P/Invoke:[StructLayout(LayoutKind.Sequential)] internal class GitBuf : IDisposable { public IntPtr ptr; public UIntPtr asize; public UIntPtr size; } [DllImport(...)] internal static extern int git_config_find_global(GitBuf global_config_path);
On CoreCLR, blittable sequential classes are typically pinned and native code writes directly into the object fields.
On Native AOT, sequential class marshalling is effectively
[In]-only: native code can fill a temporary buffer and return success, but managedptr/sizeare never updated.ConvertPaththen does:int result = pathRetriever(buf); // often 0 (success) under AOT // ... return LaxFilePathMarshaler.FromNative(buf.ptr); // ptr still IntPtr.Zero
So discovery returns "no path", global/system config is never added, and signature building fails whenever identity lives outside the local repo config.
Minimal probe (struct vs class vs pin)
Calling the same
git_config_find_globalexport aftergit_libgit2_init:Call style CoreCLR Native AOT refblittable structOK ( rc=0, path filled)OK sequential-layout class (current style) OK rc=0butptr=0pinned class ( GCHandle+IntPtr)OK OK That isolates the bug to marshalling of sequential classes under Native AOT, not to libgit2 path logic or environment variables (
USERPROFILE/HOMEare fine; settingHOMEdoes not help).Why "explicit path" workarounds work
Configuration.BuildFrom(localConfigPath, globalConfigPath)
skips
git_config_find_*and still loads includes. That confirms libgit2 config parsing is fine; only discovery viaGitBufclass P/Invoke is broken under AOT.
Proposed fix
Introduce a blittable buffer type and use it for path-returning APIs that currently go through
ConvertPath.1. Add a native-friendly buffer struct
[StructLayout(LayoutKind.Sequential)] internal struct GitBufNative { public IntPtr ptr; public UIntPtr asize; public UIntPtr size; }
Keep the existing
GitBufclass for call sites that still use class marshalling (can migrate later).2. Change path-find P/Invokes to
ref GitBufNativeAt minimum (these feed
Configuration/ repository discovery):git_config_find_globalgit_config_find_systemgit_config_find_xdggit_config_find_programdatagit_repository_discovergit_buf_dispose(ref GitBufNative)overload
3. Update
Proxy.ConvertPathprivate delegate int PathRetriever(ref GitBufNative buf); private static FilePath ConvertPath(PathRetriever pathRetriever) { var buf = new GitBufNative(); try { int result = pathRetriever(ref buf); if (result == (int)GitErrorCode.NotFound) return null; Ensure.ZeroResult(result); return LaxFilePathMarshaler.FromNative(buf.ptr); } finally { NativeMethods.git_buf_dispose(ref buf); } }
Method-group binding stays clean, e.g.
ConvertPath(NativeMethods.git_config_find_global).4. Expected outcome
After this change, Native AOT published apps should again:
- resolve global/system config paths
- honor
include.path - make
repo.Config.BuildSignature(DateTimeOffset.Now)matchdotnet runbehavior whenuser.name/user.emailcome from global config
Follow-up (optional, larger)
Many other APIs still pass class
GitBuffor out-buffers (short ids, messages, opts getters, etc.). Those can misbehave under AOT the same way. Migrating remaining out-git_bufcall sites toref GitBufNativewould make the whole surface AOT-safe; the config-find path above is the high-impact fix for this issue.Not in scope for this fix
- Enable static linking of libgit2 (NativeLibrary / DirectPInvoke) for NativeAOT builds #2160 (static linking / true single-file) is orthogonal: that is about embedding
libgit2, not about config discovery. - No change required in consumer apps once the library fix ships; workarounds like explicit
Configuration.BuildFrom(...)paths are only needed on current NuGet packages.
Happy to open a PR with the surgical
GitBufNative+ConvertPathchange if that approach looks good to maintainers.I promise the above is not just AI slop. I have this issue currently and am looking for a proper fix. I'll submit a PR that does so, but it's written by Grok 4.5.
Reacted by Javier Rojo and Ivan Balan- added a commit that references this issue
on Jul 22, 2026
This is a clean repro:
When running this script from a git repo dir with
dotnet run --file repro.cs, you get the proper signature built from the found config.Doing
dotnet publish repro.csand then running it fromartifacts\repro\repro.exewill result in the error path consistently.The PR #2188 fixes it and can be verified by changing the above repro script to use my CI feed: