Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

[mono] Native memory leak on the first invoke of every open-instance delegate to a virtual method

Abierto Apto para principiantes
#136,284 0 comentarios 6 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
2/5
Tiempo estimado
1-3 horas
Aptitud para principiantes
72/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
csharp
Área
backend

Línea de trabajo

Empieza en src/mono/mono/metadata/marshal.c y lee mono_marshal_get_delegate_invoke_internal, centrándote en los dos retornos por acierto en la caché de wrappers que se producen después de duplicar target_method_sig. Comprueba quién es responsable de esa asignación y cómo se libera en cada ruta; después, ejecuta las pruebas pertinentes del runtime de Mono. El trabajo estará terminado cuando los delegates open-instance repetidos a métodos virtuales ya no aumenten la memoria nativa y las pruebas existentes pasen.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

area-VM-meta-mono untriaged
Description

[Mono] Native leak in mono_marshal_get_delegate_invoke_internal: target_method_sig is never freed on a wrapper cache hit

Repository: dotnet/runtime. Area label: area-VM-meta-mono. OS: Android (Mono runtime).

Description

On Mono, every new open-instance delegate over a virtual method (including a property that implements an interface) leaks a duplicated MonoMethodSignature on its first invoke. Code that creates such delegates repeatedly leaks native memory without bound, and it shows up nowhere in the managed heap.

In src/mono/mono/metadata/marshal.c, mono_marshal_get_delegate_invoke_internal (tag v10.0.12, unchanged on main):

if (subtype == WRAPPER_SUBTYPE_DELEGATE_INVOKE_VIRTUAL) {
    /*
     * We don't want to use target_method's signature because it can be freed early
     */
    target_method_sig = mono_metadata_signature_dup_delegate_invoke_to_target (invoke_sig);   // g_malloc (image == NULL)

    closed_over_null = sig->param_count == target_method_sig->param_count;
}

/*
 * Check cache
 */
if (ctx) {
    ...
    res = check_generic_delegate_wrapper_cache (cache, orig_method, method, ctx);
    if (res)
        return res;                 // target_method_sig leaked
    ...
} else if (callvirt) {
    ...
    res = (MonoMethod *)g_hash_table_lookup (cache, invoke_sig);
    mono_marshal_unlock ();
    if (res)
        return res;                 // target_method_sig leaked
    ...
}

mono_metadata_signature_dup_delegate_invoke_to_target calls mono_metadata_signature_dup_full (NULL, sig), which allocates with g_malloc. Nothing frees target_method_sig on the cache-hit returns (or anywhere else in the function). Since mono_delegate_trampoline disables caching for callvirt delegates, every new delegate instance of this kind reaches this code once, and after the first one the wrapper is always cached.

The copy was introduced between 7.0 and 8.0: v7.0.20 has no call to mono_metadata_signature_dup_delegate_invoke_to_target, v8.0.18, v9.0.7 and v10.0.12 do.

Reproduction Steps

On a .NET for Android app (Mono), e.g. net10.0-android, Release, on a device:

public interface IFoo { int Value { get; set; } }
public class Foo : IFoo { public int Value { get; set; } } // implements IFoo, so set_Value is virtual
public class Bar { public int Value { get; set; } } // control: non-virtual

static double LeakPerDelegate<T>() where T : new()
{
    var setter = typeof(T).GetProperty("Value")!.GetSetMethod()!;
    var target = new T();
    for (var i = 0; i < 1_000; i++) // warm up: wrapper is now cached
        ((Action<T, int>)Delegate.CreateDelegate(typeof(Action<T, int>), null, setter))(target, i);

    GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
    var before = Android.OS.Debug.NativeHeapAllocatedSize;
    for (var i = 0; i < 100_000; i++)
        ((Action<T, int>)Delegate.CreateDelegate(typeof(Action<T, int>), null, setter))(target, i);
    GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
    return (Android.OS.Debug.NativeHeapAllocatedSize - before) / 100_000.0;
}
Expected behavior

Native heap does not grow per delegate once the invoke wrapper is cached (as for Bar).

Actual behavior

Measured on a Samsung SM-X306B, Android 16, .NET for Android workload 36.1.69, Mono runtime pack Microsoft.NETCore.App.Runtime.Mono.android-arm64 10.0.12 (libmonosgen-2.0.so build id 54924a38a8872da3a0bb85f533898e89895508b7):

Target Native heap growth per delegate
Foo (virtual setter) 80 bytes
Bar (non-virtual setter) 0 bytes

A Perfetto heapprofd profile of the real app attributes the unreleased memory to:

monoeg_malloc
mono_metadata_signature_dup_internal
mono_metadata_signature_dup_full
mono_metadata_signature_dup_delegate_invoke_to_target
mono_marshal_get_delegate_invoke_internal
mono_delegate_trampoline
<generated code>
Regression?

Yes: not present in .NET 6/7 Mono, present from .NET 8.

Known Workarounds

Avoid creating open-instance delegates over virtual methods repeatedly (cache them), or make the target methods non-virtual.

Configuration

.NET 10.0.12 Mono runtime, Android arm64. Not specific to Android as far as the code goes. Any Mono target should be affected.

Other information

Real-world impact: sqlite-net-pcl (1.10.196-beta and master) creates one such delegate per mapped column on every query (FastColumnSetter.CreateTypedSetterDelegate → Delegate.CreateDelegate(typeof(Action<ObjectType, ColumnMemberType>), null, setter)), so any app whose entities implement interfaces leaks ~64 bytes per column per query. In our app that is ~100 KB per user action.

Suggested fix: free the copy on the early returns, or only make it on the cache-miss path, e.g.

if (res) {
    g_free (target_method_sig);
    return res;
}

for both the ctx and callvirt branches (target_method_sig is NULL for the other subtypes, and g_free (NULL) is a no-op).

Lenguaje dominante
C#
Estrellas
18.4k
Forks
5.7k
Merge medio
2 d 19 h
PR fusionados (30 d)
649

Preparar el entorno

Abrir en Codespaces

Inicia el contenedor de desarrollo del proyecto en tu navegador, con tu propia cuenta de GitHub.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de dotnet/runtime

Todos los issues de dotnet/runtime

Issues similares

Más issues de C#

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.