Tracking

Tracking

Memory safety

C allocations

For high performances and very few safety checks, you might want to use the std.heap.c allocator.

Those allocations are not tracked by the Zig runtime, so you will have to use a tool like Valgrind to track memory leaks which can be done via the appropriate valgrind wrappers in the std or simply executing valgrind on the executable.

Running this excecutable code with Valgrind will give you a memory leak error.

  const allocator = std.heap.c_allocator;
  const just_forgotten_variable = try allocator.create(usize);
  // defer allocator.destroy(just_forgotten_variable);
  std.debug.print("Hello, {}!\n", .{just_forgotten_variable.*});
  ==121407== HEAP SUMMARY:
  ==121407== in use at exit: 8 bytes in 1 blocks
  ==121407== total heap usage: 1 allocs, 0 frees, 8 bytes allocated

This tool might be useful when you integrate C code in your Zig project and want to check memory leaks in the C code. Note that it can not detect leaks from other allocators like the GPA.

Zig allocations

The General Purpose Allocator (GPA) is a general-purpose memory allocator that is used by the Zig standard library. Its advantages over the other implementations in the std it that it is designed for safety over performance.

It can in fact detect memory problems like:

  • memory leaks
  • use-after-free
  • double-free

Note that all those problems are given to us at runtime and not compile time.

But it cannot detect things like data races, which you could think it could with the .thread_safe option but it does not.

What .thread_safe does is that it makes the allocator thread-safe, meaning that it can be used in a multi-threaded environment without any problem. If you don't and use the same allocator for multiples threads, the allocator might not behave correctly.

You can have more informations on the topic following a discussion I had on the Zig discord server here.

Memory leaks

In the exemple below, the memory allocated to a is freed (with destroy here), if you were to comment the destroy line, the GPA would detect a memory leak and tell you what memory leaked giving a stacktrace and the memory address of the leaked memory.

  var gpa = std.heap.GeneralPurposeAllocator(.{ .safety = true }){};
  defer {
      const status = gpa.deinit(); // deinit is going to detect memory leaks, calls gpa.detectLeaks() under the hood
  
      if (status == .leak) {
          std.debug.print("Memory leak detected\n", .{});
      } else {
          std.debug.print("No memory leak detected\n", .{});
      }
  }
  const allocator = gpa.allocator();
  
  const a = try allocator.create(u8);
  allocator.destroy(a); // Memory freed
Use after free

Following the previous exemple we are going to try to access the memory after it has been freed, this will result in a use-after-free error.

  var gpa = std.heap.GeneralPurposeAllocator(.{ .safety = true }){};
  defer _ = gpa.deinit(); // deinit is going to detect memory leaks, calls gpa.detectLeaks() under the hood
  
  const allocator = gpa.allocator();
  
  const a = try allocator.create(u8);
  allocator.destroy(a); // Memory freed
  
  a.* = 4; // USE AFTER FREE
Invalid free

By still keeping the previous exemples, we are going to double free and get an error.

  var gpa = std.heap.GeneralPurposeAllocator(.{ .safety = true }){};
  defer _ = gpa.deinit(); // deinit is going to detect memory leaks, calls gpa.detectLeaks() under the hood
  
  const allocator = gpa.allocator();
  
  const a = try allocator.create(u8);
  allocator.destroy(a); // Memory freed
  allocator.destroy(a); // DOUBLE FREE