Skip to content

Running from Native AOT app fails to build signature #2187

Description

@kzu

This is a clean repro:

#:package LibGit2Sharp@0.*
#:package Spectre.Console@0.*
using System;
using LibGit2Sharp;
using Spectre.Console;

using var repo = new Repository(Environment.CurrentDirectory);
var signature = repo.Config.BuildSignature(DateTimeOffset.Now);

if (signature == null)
{
    AnsiConsole.MarkupLine("[red] Unexpected error: Git user.name and/or user.email are not configured.[/]");
    return 1;
}
else
{
    AnsiConsole.MarkupLine($"[lime]{signature}[/]");
    return 0;
}

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.cs and then running it from artifacts\repro\repro.exe will 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:

#:package LibGit2Sharp@0.31.1
#:package Spectre.Console@0.*
#:property RestoreSources=https://api.nuget.org/v3/index.json;https://pkg.kzu.app/index.json

Activity

  1. kzu commented on Jul 22, 2026

    @kzu
    ContributorAuthor

    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.BuildSignature only reads user.name / user.email from 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.ptr
    

    Under CoreCLR (dotnet run), those finds succeed and global config is loaded (including include.path chains that often hold user.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/config is present. If identity is not set locally, BuildSignature returns null.

    Why it fails under AOT

    GitBuf is 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 managed ptr / size are never updated.

    ConvertPath then 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_global export after git_libgit2_init:

    Call style CoreCLR Native AOT
    ref blittable struct OK (rc=0, path filled) OK
    sequential-layout class (current style) OK rc=0 but ptr=0
    pinned 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 / HOME are fine; setting HOME does 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 via GitBuf class 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 GitBuf class for call sites that still use class marshalling (can migrate later).

    2. Change path-find P/Invokes to ref GitBufNative

    At minimum (these feed Configuration / repository discovery):

    • git_config_find_global
    • git_config_find_system
    • git_config_find_xdg
    • git_config_find_programdata
    • git_repository_discover
    • git_buf_dispose(ref GitBufNative) overload

    3. Update Proxy.ConvertPath

    private 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) match dotnet run behavior when user.name / user.email come from global config

    Follow-up (optional, larger)

    Many other APIs still pass class GitBuf for out-buffers (short ids, messages, opts getters, etc.). Those can misbehave under AOT the same way. Migrating remaining out-git_buf call sites to ref GitBufNative would 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


    Happy to open a PR with the surgical GitBufNative + ConvertPath change if that approach looks good to maintainers.

  2. kzu commented on Jul 22, 2026

    @kzu
    ContributorAuthor

    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.

  3. added a commit that references this issue on Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions