Zaman - Comptime lifetime annotations for Zig

Zaman


Zaman (زمان in Persian) is a memory safety tool for Zig that tries to prevent use-after-free and ifetime confusion at compile time by encoding arena lifetimes directly into pointer types.

Why does this exist?

I have several die-hard Rustacean friends who were constantly bragging about their nice memory-safety features, so I decided to end the discussion by implementing their beloved lifetimes using Zig’s comptime

Why should you care?

  • Manual memory management in Zig is manageable - but sometimes you need stronger guarantees
  • There are safe usage patterns that are not implementable in Rust
  • Usually the attack surface is limited compared to the entire codebase, so you don’t need to bear the burden of Rust’s rules for its entirety
  • For long-running applications, Zaman might reduce memory fragmentation and improve allocation throughput at the cost of slightly higher memory consumption, compared to many allocation strategies including Rust’s (you can optimize this balance as you wish)
  • This is a benchmark of what Zig’s comptime is capable of and you might apply these ideas to your own use-cases

Usage

const La = Lifetime(@src(), .{});
defer La.deinit();

const x = try La.create(i32);
var y = try La.create(i32);

// x and y share the Lifetime La

const Lb = Lifetime(@src(), .{});
defer Lb.deinit();

const z = try Lb.create(i32);
y = z; // compilation error!
// z has the Lifetime Lb != La

What about use-after-free?

const La = Lifetime(@src(), .{});
defer La.deinit();

var use_after_free: La.Bound(*u32) = undefined;
{
    const Lb = Lifetime(@src(), .{});
    defer Lb.deinit();

    const x = try Lb.create(u32);
    x.set(10);
    use_after_free = x;
}
std.debug.print("{}\n", use_after_free.get());

compilation output

src/uaf.zig:14:26: error: expected type 'lifetime.Bounded(*u32,"uaf |zaman> uaf.zig:4:25"[0..24])',
                                  found 'lifetime.Bounded(*u32,"uaf |zaman> uaf.zig:9:29"[0..24])'
        use_after_free = x;
                         ^
src/lifetime.zig:84:12: note: struct declared here (2 times)
    return struct {
           ^~~~~~

Supported Zig versions

This package doesn’t use any version specific std/language feature so it could be used in any zig version with zero or minimal modification

  • master directly supports from zig-0.15.0 to zig-master
17 Likes

How’s the codegen? Will it hurt performance at all? Might be nice to mention in the README either way it goes!

Zig users will do literally anything but use Rust.

1 Like

how does this interact with passing things as parameters

edit: ah, I guess you could just take the Lifetime as a comptime parameter

I plan to benchmark it, also if anyone could help me do these benchmarks that would be very nice and appreciated

here is a comparison between Lifetime and raw Arena codegen using ReleaseSmall optimization (so it would be easier to compare the two programs)

basically the only difference I see is that Lifetime use a threadlocal var static variable for storing the arena while the other code stores the arena directly on the stack

also I plan to add several other strategies for storing the Arenas, one with MemoryPool and one with on-stack storage of the arena

Lifetime generated a smaller code but Arena used better(?) vector instructions so we should benchmark them and compare their performance in the real-world

Also the other thing to note is that I used the BoundedAllocator for the function thing and Debug mode didn’t merge the 2 instantiations of thing (one for each Lifetime) but because of the identical code generated, it was easy for LLVM to collapse them into a single function

You would do something like these:

// Using the raw Lifetimes
fn foo(La: type, bounded_input: La.Bound(*u32), ...) La.Bound(*u32) // lifetime bounded output
// Using the BoundedAllocator
fn foo(La: type, allocator: La.Allocator, bounded_input: La.Bound(*u32), ...) La.Bound(*u32)
// or
fn foo(allocator: anytype, bounded_input: @TypeOf(allocator).Bound(*u32), ...) @TypeOf(allocator).Bound(*u32)

You can also refer to the rust comparison section

:joy:
well… that is the exact reason this exists! I can use this and prove my point of “zig being safety capable” :joy:

2 Likes