Skip to content

Native memory leaks in Widget enumeration and TextPage.Search #256

Description

@Adash-nitro

Hello MuPDF Team, we knew about #248 fixing a ton but we've found out that the are still some leaks, at least in certain code paths we use the library for. So we've managed to pin it to 2 separate features.

Leak 1 - enumerating widgets (~0.2–0.8 KB per widget/doc)
Leak 2 - TextPage.Search (~0.5 KB per call with a hit)

Although small, a leak is a leak. An example that would process 10000 docs/hours means for us a ~200MB every hour added that eventually lead to memory exhaustion.

Minimal repro: loop opening a PDF, enumerate page.GetWidgets() (Leak 1) or run TextPage.Search for a string that matches (Leak 2), Dispose/Close each iteration, force full GC -> watch RSS (Resident Set Size) climb.

A 🤖's investigation:

Leak 1 - enumerating widgets (~0.2–0.8 KB per widget/doc), in Widget.SyncFromNative():

  • Widget.FieldName calls pdf_load_field_name, whose returned char* is never freed. pdf_load_field_name2 is the freeing variant.
  • The border/color reads go through Helpers.PdfObjBorrowed, which drops C# ownership of pdf_dict_get's result without calling pdf_drop_obj, leaking the kept reference each call.

Leak 2 - TextPage.Search (~0.5 KB per call with a hit), in Helpers.BorrowStextBlock:

  • It allocates a native FzStextBlock via new_FzStextBlock__SWIG_2(...) but wraps it with cMemOwn: false, so delete_FzStextBlock is never called. Leaks one native block per FirstStextLinePtr call.

Suggested fixes: (1) FieldName → pdf_load_field_name2; (2) read the border/color dict entries with owning wrappers disposed after use rather than PdfObjBorrowed; (3) make BorrowStextBlock own the block and dispose it.

Environment: MuPDF.NET 3.28.2 (also 3.2.17.9) · MuPDF.NativeAssets.Linux.x64 1.28.2 · net10.0, workstation GC · Linux x64 (but applies to windows too)

Example repro below.
Use TestDocuments/.../WidgetTest/test_widget_parse.pdf for widgets, and any text PDF (e.g. TestDocuments/Demo/columns.pdf with search the) for search.

using System;
using System.IO;
using System.Linq;
using MuPDF.NET;

// Leak 1:  dotnet run -- <pdf-with-form-fields>  widgets
// Leak 2:  dotnet run -- <pdf-with-text>         search <word-that-occurs>
class Program
{
    static void Main(string[] args)
    {
        var data = File.ReadAllBytes(args[0]);
        var mode = args.Length > 1 ? args[1] : "widgets";
        var needle = args.Length > 2 ? args[2] : "the";

        Report(0);
        for (int i = 1; i <= 3000; i++)
        {
            using var doc = new Document(stream: data);
            for (int p = 0; p < doc.PageCount; p++)
            {
                var page = doc[p];
                if (mode == "widgets")
                {
                    // Leak 1: constructing each Widget retains native memory.
                    foreach (var w in page.GetWidgets()) { _ = w.FieldName; _ = w.Rect; }
                }
                else // search
                {
                    var tp = page.GetTextPage();
                    _ = tp.ExtractText();
                    for (int k = 0; k < 20; k++)
                    {
                        var quads = TextPage.Search(tp, needle, hitMax: 1); // leaks per call that finds a hit
                        if (quads.Count > 0) { _ = quads[0].Rect; }
                    }
                    tp.Dispose();
                }
                page.Dispose();
            }
            doc.Close();
            if (i % 500 == 0) Report(i);
        }
    }

    // Native footprint after a forced full GC + finalizers. Managed heap stays flat; RSS keeps climbing.
    // use `Process.GetCurrentProcess().WorkingSet64` on Windows
    static void Report(int i)
    {
        GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
        long rssKb = long.Parse(File.ReadLines("/proc/self/status")
            .First(l => l.StartsWith("VmRSS:"))
            .Split(' ', StringSplitOptions.RemoveEmptyEntries)[1]);
        Console.WriteLine($"iter={i,-5} managedMB={GC.GetTotalMemory(true) >> 20}  RSS_MB={rssKb / 1024}");
    }
}

Activity

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