PitchHut logo
Memory safety in Zig through compile-time lifetime annotations.
Pitch

Zaman enhances memory safety in Zig by providing compile-time lifetime annotations to prevent issues like use-after-free. By encoding arena lifetimes into pointer types, developers can catch potential memory errors at compile time, leading to safer and more efficient Zig applications.

Description

Zaman: Comptime Lifetime Annotations for Zig

Zaman (زمان in Persian) is a powerful memory safety tool designed for the Zig programming language. It aims to prevent issues like use-after-free and lifetime confusion at compile time by integrating arena lifetimes directly into pointer types.

Key Features

  • Use-After-Free Prevention: Zaman ensures that pointers do not outlive their allocated memory. For example, in the following Zig code:

    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 {
           ^~~~~~
  • Comparative Examples with Rust: Zaman allows for familiar lifetime operations known in Rust, thus making it easier for developers transitioning from Rust to Zig. Below are equivalent examples of function implementation between Rust and Zig.

    Rust Example:

    fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
        if x.len() > y.len() { x } else { y }
    }
    

    Zig Example:

    fn longest(A: type, x: A.Bound([]u8), y: A.Bound([]u8)) A.Bound([]u8) {
        return if (x.len() > y.len()) x else y;
    }
    

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

How does it work?

Each Lifetime(@src(), .{}) call, produces a unique lifetime type that can create bounded pointers. The main property of bounded pointers or L.Bound(P) types is that La.Bound(P) != Lb.Bound(P), thus La.Bound(P) pointers cannot convert into Lb.Bound(P) implicitly, throwing a readable compile error if it happens.

Current Limitations

  • Bounded pointers are currently not safe for multi-threaded scenarios
  • Features like lifetime elision found in Rust cannot be implemented using Zaman
  • There is no implicit detection and transformation of lifetimes

Conclusion

Zaman leverages Zig's comptime features to offer compile-time lifetimes akin to Rust's, providing developers with a robust tool for memory management. This project serves as an illustration of Zig’s capabilities, and its concepts can inspire various use cases in memory management and safety.

0 comments

No comments yet.

Sign in to be the first to comment.