We are migrating the bug tracker to github Issues. This is now the preferred way to report NASM bugs.

Self-registration is disabled due to spam issue (mail gorcunov@gmail.com or hpa@zytor.com to create an account)

Bug 3392892 - [Patch] Major memory leak when using many %rep loops with nontrivial content
Summary: [Patch] Major memory leak when using many %rep loops with nontrivial content
Status: CLOSED FIXED
Alias: None
Product: NASM
Classification: Unclassified
Component: Assembler (show other bugs)
Version: 3.00.xx
Hardware: All All
: Medium blocker
Assignee: nobody
URL:
Depends on:
Blocks:
 
Reported: 2023-07-20 13:05 PDT by E. C. Masloch
Modified: 2024-04-12 14:59 PDT (History)
5 users (show)

Obtained from: Built from git using configure
Generated by: Human
Bug category: Memory leak - cumulative, Performance problem on valid input
Observed for: Development code
Regression: ---
Regression since:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description E. C. Masloch 2023-07-20 13:05:49 PDT
During development of a new check feature for my 86-DOS debugger [1] I encountered reliably reproducable OOM killings of NASM. The feature in question was to use an mmacro for the call instruction that would expand to dozens of lines, including three %rep loops, to check every call instruction for potential cross-section calls that were not marked properly. I don't know how many call instructions there are in the debugger sources but it must be hundreds of them.

I found that the assembler appears to have some leaks, which I suspected were causing my problems. First I changed the #define TOKEN_BLOCKSIZE 4096 [3] to 0 in order to make the preprocessor call nasm_free when deleting tokens. Next I used valgrind --leak-check=full nasm to look around and find leaks. It pointed, among others, to the nasm_new call in the preprocessor handling of starting a %rep loop [4].

Eventually, I ended up checking how the global variable named "defining" is used, and how it is assigned a pointer to an allocated structure and how that structure is freed. After some false starts I found the culprit: Once they are no longer used, the structures pointed to by "defining" are not freed properly. In particular, %rep structures are affected and seem to be leaked entirely.

In my tests, the %rep loop bodies have to contain some nontrivial amount of text to cause the OOM killing, though I suspect the exact contents don't actually matter. The space used up by this content probably does matter.

This is the fix for the big memory leak and another, smaller leak. The big leak already had a FIXME note [5] linking to another bug reported earlier [6]. Unlike expected the %rep leak is significant and causes the OOM killings I observed. In my fix I took care to only free the structure if it has no name, that is it belongs to a %rep rather than an mmacro being expanded. Diff:

$ git diff
diff --git a/asm/preproc.c b/asm/preproc.c
index ac42131e..f131d655 100644
--- a/asm/preproc.c
+++ b/asm/preproc.c
@@ -7640,10 +7640,12 @@ static Token *pp_tokline(void)
 #endif
                     {
                         nasm_free(m->params);
+                        nasm_free(m->iname);
                         free_tlist(m->iline);
                         nasm_free(m->paramlen);
                         fm->in_progress = 0;
                        m->params = NULL;
+                       m->iname = NULL;
                        m->iline = NULL;
                        m->paramlen = NULL;
                     }
@@ -7673,8 +7675,8 @@ static Token *pp_tokline(void)
                  *
                  * https://bugzilla.nasm.us/show_bug.cgi?id=3392414
                  */
-#if 0
-                else
+#if 1
+                if (!m->name)
                     free_mmacro(m);
 #endif
             }


Contents of the session log [2]:

test/20230720$ cat testchk2.asm
        %imacro call 1.nolist
%push
 %assign %$size 2
 %assign %$isreg 0
 %assign %$exit 0
 %rep 2
  %ifn %$exit
   %if %$size == 2
    %define %$regnames "ax  cx  dx  bx  sp  bp  si  di  "
   %elif %$size == 4
    %define %$regnames "eax ecx edx ebx esp ebp esi edi "
   %endif
   %assign %$index 0
   %rep 8
    %ifn %$exit
     %substr %$reg %$regnames %$index * 4 + 1, 4
     %deftok %$reg %$reg
     %ifnempty %$reg
      %ifidni %$reg, %1
       %assign %$isreg %$size
       %assign %$exit 1
       %exitrep
      %endif
     %endif
    %endif
    %assign %$index %$index + 1
   %endrep
   %if %$exit
    %exitrep
   %endif
   %assign %$size %$size * 2
  %endif
 %endrep

 %assign %$ismulti 0
 %assign %$ismem 0
 %defstr %$string %1
 %strlen %$length %$string
 %assign %$ii 0
 %rep %$length
  %substr %$point %$string %$ii + 1, 1
  %if %$point == 32 || %$point == 9
   %assign %$ismulti 1
  %endif
  %ifidn %$point, '['
   %assign %$ismem 1
  %endif
  %assign %$ii %$ii + 1
 %endrep
%pop
        %endmacro


%ifndef _OUTER
 %assign _OUTER 100
%endif
%ifndef _INNER
 %assign _INNER 50
%endif

%rep _OUTER
call cx
call near [0]
call .
 %rep _INNER
call label
call labelfoo
call labelbar
call labelbaz
 %endrep
%endrep
test/20230720$ orignasm -v
NASM version 2.17rc0 compiled on Jul 20 2023
test/20230720$ orignasm testchk2.asm -D_INNER=1
test/20230720$ orignasm testchk2.asm
Killed
test/20230720$ nasm -v
NASM version 2.17rc0 compiled on Jul 20 2023
test/20230720$ nasm testchk2.asm -D_INNER=1
test/20230720$ nasm testchk2.asm
test/20230720$

[4435708.583745] OOM killed process 1845493 (orignasm) total-vm:2435924kB, anon-rss:2432680kB, file-rss:80kB


[1]: https://hg.pushbx.org/ecm/ldebug/rev/7d94a90c607b
[2]: https://pushbx.org/ecm/test/20230720/testchk2.txt
[3]: https://github.com/netwide-assembler/nasm/blob/a916e4127b2eaa3bf40bddf3de9b0ceefc0d98a4/asm/preproc.c#L1763
[4]: https://github.com/netwide-assembler/nasm/blob/a916e4127b2eaa3bf40bddf3de9b0ceefc0d98a4/asm/preproc.c#L4743
[5]: https://github.com/netwide-assembler/nasm/blob/a916e4127b2eaa3bf40bddf3de9b0ceefc0d98a4/asm/preproc.c#L7670
[6]: https://bugzilla.nasm.us/show_bug.cgi?id=3392414
Comment 1 E. C. Masloch 2023-07-20 14:21:02 PDT
The debugger, being quite a large project assembled all at once, did already feature many uses of %rep blocks. I assume this is the reason that this patch greatly reduces the memory use of the assembler even when not using the new feature that I mentioned (_CHECKSECTION=0). Here's a comparison:

/usr/bin/time --format="%M\n" orignasm -I ../../l
macros/ -I ../../scanptab/ debug.asm
...
570216

/usr/bin/time --format="%M\n" nasm -I ../../l
macros/ -I ../../scanptab/ debug.asm
...
41896

570 MiB vs 41 MiB.

As mentioned in another bug report: https://bugzilla.nasm.us/show_bug.cgi?id=3392774#c1

> The /usr/bin/time executable is GNU time. Its %M format code lists the maximum amount of KiB reserved to the process.
Comment 2 H. Peter Anvin 2023-10-11 13:59:26 PDT
Fix checked in to the 2.16.xx branch.
Huge thanks for actually tracking this one down correctly!