OpenBSD's built-in memory leak detection

Since 2007 I have been working on and off on the malloc(3) implementation in OpenBSD. OpenBSD's malloc is a bit of a different beast compared to most other implementations: it has built-in, always-on randomization, it returns pages to the operating system when it no longer needs them and performs extensive consistency checking. These features help a lot when debugging memory management problems in programs and make various forms of heap based attacks much harder. You can find more details in the sheets of the presentation I gave on EuroBSDCon 2009.
Quote from a private correspondence with a Qualys vulnerability researcher (used with permission):
> Interesting (great malloc by the way, clean design and implementation
> and lots of security checks)! 

Additionally, there are a few features that can be switched on runtime that implement even more strict checks, at the cost of performance and/or increased memory usage. You can read more about that in the man page of malloc(3) and my EuroBSDCon 2023 presentation slides or video.

Around 2011 worked on improving the speed of the allocation of small chunks. Working on that, I really got into the mood to return to the old goal of adding a feature that is very useful to developers: memory leak detection. Once I realized I could use a compiler feature to determine the address of the code malloc has been called from, I got on a roll. While the original code worked, it had some drawbacks and for various reasons the leak detection code was never compiled by default until I rewrote it in 2023. OpenBSD 7.4, released in 2023 has leak detection available, switched off by default. In this page I'd like to show how to use this feature.

Leak detection

If a program is run with MALLOC_OPTIONS=D malloc will produce utrace records that can be caught and stored using ktrace.
  $ MALLOC_OPTIONS=D ktrace -tu ./a.out
This will produce a file called ktrace.out. The output then can be viewed using kdump To shows the malloc utrace records in in a nice way, use the argument -u malloc:
 $ kdump -u malloc
I made a little leaky program to illustrate some points.
  $ cat -n x.c
     1	#include <stdlib.h>
     2	#include <stdio.h>
     3	
     4	int
     5	main() 
     6	{
     7	  void *p;
     8	  int i;
     9	
    10	  for (i = 0; i < 3; i++)
    11		  printf("%p\n", p = malloc(10240));
    12	  free(p);
    13	  for (i = 0; i < 5; i++)
    14		  printf("%p\n", malloc(1000));
    15	}
  $ cc -g x.c
  $ MALLOC_OPTIONS=D ktrace -tu ./a.out
  0x1098469000
  0x114c10b000
  0x11447c8000
  0x1112865800
  0x1112865000
  0x111283c800
  0x1112865400
  0x111283c400
  [otto@macmini:9]$ kdump -u malloc
  ******** Start dump a.out *******
  M=8 I=1 F=0 U=0 J=1 R=0 X=0 C=0 cache=64 G=0
  Leak report:
                   f     sum      #    avg
                 0x0    4096      4   1024 addr2line -e . 0x0
         0xc5e0d0a90   20480      2  10240 addr2line -e ./a.out 0x10a90
         0xc5e0d0ad8    1024      1   1024 addr2line -e ./a.out 0x10ad8
        0x110bb84d18   65536      1  65536 addr2line -e /usr/lib/libc.so.97.1 0x47d18
  
  ******** End dump a.out *******
  $
The Leak report shows information for all leaks grouped by the value of f, which is the call site of the function call that allocated memory that was never freed. If the value of f is 0x0, it means no caller data is available, due to the sampling nature of the leak detection. Current and the upcoming 7.5 release drops this sampling and has full info for all leaks, including the ability to show backtraces.

The sum column shows the total number of bytes leaked, the # column shows the number of calls to an allocation that leaked and the avg column shows the average size of the allocations. We can conclude that this program has 5 leaks of size 1024, two leaks of size 10240 and one leak of size 65536. For the smaller allocations the size is rounded up to 1024, while malloc was called wit a size argument of 1000.

So where did my program leak?

Executing the suggested addr2line command lines gives us this:
  $ addr2line -e ./a.out 0x10a90
  /home/otto/x.c:11
  $ addr2line -e ./a.out 0x10ad8
  /home/otto/x.c:14
  $
This shows that both instances of malloc calls introduced leaks. Fixing the leaks is left as an exercise to the reader.

The third case is a bit different: the leak originates from libc and on my arm64 I get:

  $ addr2line -e /usr/lib/libc.so.97.1 0x47d18 
  BFD: Dwarf Error: found dwarf version '4', this reader only handles version 2 information.
  ...
  $
This is nasty, the addr2line command in base cannot read the debug info. Luckily there's a workaround (which is needed on e.g. arm64 machines): install the package binutils and use gaddr2line:
  $ doas pkg_add binutils
  $ gaddr2line -e /usr/lib/libc.so.97.1 0x47d18
  /usr/src/lib/libc/stdio/makebuf.c:62
  $
If you look at makebuf.c and the rest of the standard io library you can learn that on this line a buffer used by the stdout stream is allocated. This is strictly speaking a leak, but it is not harmful. When leak hunting we are more interested in leaks that grow with the process doing work.

Some hints