From c1e616c6cda402c6823d97dba89dde690808f2b7 Mon Sep 17 00:00:00 2001 From: Alessandro De Blasis Date: Mon, 23 Mar 2026 16:54:05 +0100 Subject: [PATCH 1/3] libghostty: add ghostty_free for cross-runtime memory safety On Windows, Zig's built-in libc and MSVC's CRT maintain separate heaps, so calling free() on memory allocated by the library causes undefined behavior. Add ghostty_free() that frees through the same allocator that performed the allocation, making it safe on all platforms. Update format_alloc docs and all examples to use ghostty_free() instead of free(). Co-Authored-By: Claude Opus 4.6 (1M context) --- example/c-vt-cmake-static/src/main.c | 2 +- example/c-vt-cmake/src/main.c | 2 +- example/c-vt-formatter/src/main.c | 2 +- include/ghostty/vt/allocator.h | 25 +++++++++++++++++++++++++ include/ghostty/vt/formatter.h | 7 +++---- src/lib_vt.zig | 1 + src/terminal/c/main.zig | 18 ++++++++++++++++++ 7 files changed, 50 insertions(+), 7 deletions(-) diff --git a/example/c-vt-cmake-static/src/main.c b/example/c-vt-cmake-static/src/main.c index 4821e727e..233bd34d1 100644 --- a/example/c-vt-cmake-static/src/main.c +++ b/example/c-vt-cmake-static/src/main.c @@ -45,7 +45,7 @@ int main() { fwrite(buf, 1, len, stdout); printf("\n"); - free(buf); + ghostty_free(NULL, buf, len); ghostty_formatter_free(formatter); ghostty_terminal_free(terminal); return 0; diff --git a/example/c-vt-cmake/src/main.c b/example/c-vt-cmake/src/main.c index 389be5936..992586451 100644 --- a/example/c-vt-cmake/src/main.c +++ b/example/c-vt-cmake/src/main.c @@ -45,7 +45,7 @@ int main() { fwrite(buf, 1, len, stdout); printf("\n"); - free(buf); + ghostty_free(NULL, buf, len); ghostty_formatter_free(formatter); ghostty_terminal_free(terminal); return 0; diff --git a/example/c-vt-formatter/src/main.c b/example/c-vt-formatter/src/main.c index 5d408b172..56f9d1220 100644 --- a/example/c-vt-formatter/src/main.c +++ b/example/c-vt-formatter/src/main.c @@ -56,7 +56,7 @@ int main() { printf("\n"); // Clean up - free(buf); + ghostty_free(NULL, buf, len); ghostty_formatter_free(formatter); ghostty_terminal_free(terminal); return 0; diff --git a/include/ghostty/vt/allocator.h b/include/ghostty/vt/allocator.h index 4cebe91bb..f1406cf79 100644 --- a/include/ghostty/vt/allocator.h +++ b/include/ghostty/vt/allocator.h @@ -191,6 +191,31 @@ typedef struct GhosttyAllocator { const GhosttyAllocatorVtable *vtable; } GhosttyAllocator; +/** + * Free memory that was allocated by a libghostty-vt function. + * + * Use this to free buffers returned by functions such as + * ghostty_formatter_format_alloc(). Pass the same allocator that was + * used for the allocation, or NULL if the default allocator was used. + * + * On platforms where the library's internal allocator differs from the + * consumer's C runtime (e.g. Windows, where Zig's libc and MSVC's CRT + * maintain separate heaps), calling the standard C free() on memory + * allocated by the library causes undefined behavior. This function + * guarantees the correct allocator is used regardless of platform. + * + * It is safe to pass a NULL pointer; the call is a no-op in that case. + * + * @param allocator Pointer to the allocator that was used to allocate the + * memory, or NULL if the default allocator was used + * @param ptr Pointer to the memory to free (may be NULL) + * @param len Length of the allocation in bytes (must match the original + * allocation size) + * + * @ingroup allocator + */ +void ghostty_free(const GhosttyAllocator* allocator, uint8_t* ptr, size_t len); + /** @} */ #endif /* GHOSTTY_VT_ALLOCATOR_H */ diff --git a/include/ghostty/vt/formatter.h b/include/ghostty/vt/formatter.h index 4beb5fc77..5a9bb524b 100644 --- a/include/ghostty/vt/formatter.h +++ b/include/ghostty/vt/formatter.h @@ -186,10 +186,9 @@ GhosttyResult ghostty_formatter_format_buf(GhosttyFormatter formatter, * * Each call formats the current terminal state. The buffer is allocated * using the provided allocator (or the default allocator if NULL). - * The caller is responsible for freeing the returned buffer. When using - * the default allocator (NULL), the buffer can be freed with `free()`. - * When using a custom allocator, the buffer must be freed using the - * same allocator. + * The caller is responsible for freeing the returned buffer with + * ghostty_free(), passing the same allocator (or NULL for the default) + * that was used for the allocation. * * @param formatter The formatter handle (must not be NULL) * @param allocator Pointer to allocator, or NULL to use the default allocator diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 7a75bb92a..2816befae 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -218,6 +218,7 @@ comptime { @export(&c.grid_ref_graphemes, .{ .name = "ghostty_grid_ref_graphemes" }); @export(&c.grid_ref_style, .{ .name = "ghostty_grid_ref_style" }); @export(&c.build_info, .{ .name = "ghostty_build_info" }); + @export(&c.free_alloc, .{ .name = "ghostty_free" }); // On Wasm we need to export our allocator convenience functions. if (builtin.target.cpu.arch.isWasm()) { diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 11e14f8c7..b1a87e452 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -1,3 +1,6 @@ +const lib_alloc = @import("../../lib/allocator.zig"); +const CAllocator = lib_alloc.Allocator; + const buildpkg = @import("build_info.zig"); pub const cell = @import("cell.zig"); pub const color = @import("color.zig"); @@ -112,6 +115,21 @@ pub const mouse_encoder_encode = mouse_encode.encode; pub const paste_is_safe = paste.is_safe; +/// Free memory that was allocated by a libghostty-vt function. +/// +/// This must be used to free buffers returned by functions like +/// `format_alloc`. Pass the same allocator (or NULL for the default) +/// that was used for the allocation. +pub fn free_alloc( + alloc_: ?*const CAllocator, + ptr: ?[*]u8, + len: usize, +) callconv(.c) void { + const mem = ptr orelse return; + const alloc = lib_alloc.default(alloc_); + alloc.free(mem[0..len]); +} + pub const size_report_encode = size_report.encode; pub const cell_get = cell.get; From 7039f566bb5db987707a9a4fd8637575b595ffaa Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 23 Mar 2026 16:08:25 -0700 Subject: [PATCH 2/3] vt: move free_alloc to dedicated allocator.zig Extract the inline free_alloc function from main.zig into a new allocator.zig module in the C API layer. The function is renamed to alloc_free in main.zig (and free in allocator.zig) for consistency with the other C API naming conventions. Add tests for null pointer, allocated memory, and null allocator fallback. --- src/lib_vt.zig | 2 +- src/terminal/c/allocator.zig | 36 ++++++++++++++++++++++++++++++++++++ src/terminal/c/main.zig | 17 +++-------------- 3 files changed, 40 insertions(+), 15 deletions(-) create mode 100644 src/terminal/c/allocator.zig diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 2816befae..0452d5e31 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -218,7 +218,7 @@ comptime { @export(&c.grid_ref_graphemes, .{ .name = "ghostty_grid_ref_graphemes" }); @export(&c.grid_ref_style, .{ .name = "ghostty_grid_ref_style" }); @export(&c.build_info, .{ .name = "ghostty_build_info" }); - @export(&c.free_alloc, .{ .name = "ghostty_free" }); + @export(&c.alloc_free, .{ .name = "ghostty_free" }); // On Wasm we need to export our allocator convenience functions. if (builtin.target.cpu.arch.isWasm()) { diff --git a/src/terminal/c/allocator.zig b/src/terminal/c/allocator.zig new file mode 100644 index 000000000..b16cf5d25 --- /dev/null +++ b/src/terminal/c/allocator.zig @@ -0,0 +1,36 @@ +const std = @import("std"); +const testing = std.testing; +const lib_alloc = @import("../../lib/allocator.zig"); +const CAllocator = lib_alloc.Allocator; + +/// Free memory that was allocated by a libghostty-vt function. +/// +/// This must be used to free buffers returned by functions like +/// `format_alloc`. Pass the same allocator (or NULL for the default) +/// that was used for the allocation. +pub fn free( + alloc_: ?*const CAllocator, + ptr: ?[*]u8, + len: usize, +) callconv(.c) void { + const mem = ptr orelse return; + const alloc = lib_alloc.default(alloc_); + alloc.free(mem[0..len]); +} + +test "free null pointer" { + free(&lib_alloc.test_allocator, null, 0); +} + +test "free allocated memory" { + const alloc = lib_alloc.default(&lib_alloc.test_allocator); + const mem = try alloc.alloc(u8, 16); + free(&lib_alloc.test_allocator, mem.ptr, mem.len); +} + +test "free with null allocator" { + // null allocator falls back to the default (test allocator in tests) + const alloc = lib_alloc.default(null); + const mem = try alloc.alloc(u8, 8); + free(null, mem.ptr, mem.len); +} diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index b1a87e452..8e6674049 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -2,6 +2,7 @@ const lib_alloc = @import("../../lib/allocator.zig"); const CAllocator = lib_alloc.Allocator; const buildpkg = @import("build_info.zig"); +pub const allocator = @import("allocator.zig"); pub const cell = @import("cell.zig"); pub const color = @import("color.zig"); pub const focus = @import("focus.zig"); @@ -115,20 +116,7 @@ pub const mouse_encoder_encode = mouse_encode.encode; pub const paste_is_safe = paste.is_safe; -/// Free memory that was allocated by a libghostty-vt function. -/// -/// This must be used to free buffers returned by functions like -/// `format_alloc`. Pass the same allocator (or NULL for the default) -/// that was used for the allocation. -pub fn free_alloc( - alloc_: ?*const CAllocator, - ptr: ?[*]u8, - len: usize, -) callconv(.c) void { - const mem = ptr orelse return; - const alloc = lib_alloc.default(alloc_); - alloc.free(mem[0..len]); -} +pub const alloc_free = allocator.free; pub const size_report_encode = size_report.encode; @@ -157,6 +145,7 @@ pub const grid_ref_graphemes = grid_ref.grid_ref_graphemes; pub const grid_ref_style = grid_ref.grid_ref_style; test { + _ = allocator; _ = buildpkg; _ = cell; _ = color; From b819ce0e20c14d649613c8fb39705b5893960b9c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 23 Mar 2026 16:11:16 -0700 Subject: [PATCH 3/3] vt: add ghostty_alloc for buffer allocation Add a ghostty_alloc function that pairs with the existing ghostty_free, giving embedders a symmetric malloc/free-style API for buffer allocation through the libghostty allocator interface. Returns NULL on allocation failure. --- include/ghostty/vt/allocator.h | 33 ++++++++++++++++++++++++++ src/lib_vt.zig | 1 + src/terminal/c/allocator.zig | 43 +++++++++++++++++++++++++++++----- src/terminal/c/main.zig | 1 + 4 files changed, 72 insertions(+), 6 deletions(-) diff --git a/include/ghostty/vt/allocator.h b/include/ghostty/vt/allocator.h index f1406cf79..ed2d36794 100644 --- a/include/ghostty/vt/allocator.h +++ b/include/ghostty/vt/allocator.h @@ -44,6 +44,24 @@ * 2. Create a GhosttyAllocator struct with your vtable and context * 3. Pass the allocator to functions that accept one * + * ## Alloc/Free Helpers + * + * ghostty_alloc() and ghostty_free() provide a simple malloc/free-style + * interface for allocating and freeing byte buffers through the library's + * allocator. These are useful when: + * + * - You need to allocate a buffer to pass into a libghostty-vt function + * (e.g. preparing input data for ghostty_terminal_vt_write()). + * - You need to free a buffer returned by a libghostty-vt function + * (e.g. the output of ghostty_formatter_format_alloc()). + * - You are on a platform where the library's internal allocator differs + * from the consumer's C runtime (e.g. Windows, where Zig's libc and + * MSVC's CRT maintain separate heaps), so calling the standard C + * free() on library-allocated memory would be undefined behavior. + * + * Always use the same allocator (or NULL) for both the allocation and + * the corresponding free. + * * @{ */ @@ -191,6 +209,21 @@ typedef struct GhosttyAllocator { const GhosttyAllocatorVtable *vtable; } GhosttyAllocator; +/** + * Allocate a buffer of `len` bytes. + * + * Uses the provided allocator, or the default allocator if NULL is passed. + * The returned buffer must be freed with ghostty_free() using the same + * allocator. + * + * @param allocator Pointer to the allocator to use, or NULL for the default + * @param len Number of bytes to allocate + * @return Pointer to the allocated buffer, or NULL if allocation failed + * + * @ingroup allocator + */ +uint8_t* ghostty_alloc(const GhosttyAllocator* allocator, size_t len); + /** * Free memory that was allocated by a libghostty-vt function. * diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 0452d5e31..8ccd6bf96 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -218,6 +218,7 @@ comptime { @export(&c.grid_ref_graphemes, .{ .name = "ghostty_grid_ref_graphemes" }); @export(&c.grid_ref_style, .{ .name = "ghostty_grid_ref_style" }); @export(&c.build_info, .{ .name = "ghostty_build_info" }); + @export(&c.alloc_alloc, .{ .name = "ghostty_alloc" }); @export(&c.alloc_free, .{ .name = "ghostty_free" }); // On Wasm we need to export our allocator convenience functions. diff --git a/src/terminal/c/allocator.zig b/src/terminal/c/allocator.zig index b16cf5d25..f16c7237f 100644 --- a/src/terminal/c/allocator.zig +++ b/src/terminal/c/allocator.zig @@ -3,6 +3,20 @@ const testing = std.testing; const lib_alloc = @import("../../lib/allocator.zig"); const CAllocator = lib_alloc.Allocator; +/// Allocate a buffer of `len` bytes using the given allocator +/// (or the default allocator if NULL). +/// +/// Returns a pointer to the allocated buffer, or NULL if the +/// allocation failed. +pub fn alloc( + alloc_: ?*const CAllocator, + len: usize, +) callconv(.c) ?[*]u8 { + const allocator = lib_alloc.default(alloc_); + const buf = allocator.alloc(u8, len) catch return null; + return buf.ptr; +} + /// Free memory that was allocated by a libghostty-vt function. /// /// This must be used to free buffers returned by functions like @@ -14,8 +28,25 @@ pub fn free( len: usize, ) callconv(.c) void { const mem = ptr orelse return; - const alloc = lib_alloc.default(alloc_); - alloc.free(mem[0..len]); + const allocator = lib_alloc.default(alloc_); + allocator.free(mem[0..len]); +} + +test "alloc returns non-null" { + const ptr = alloc(&lib_alloc.test_allocator, 16); + try testing.expect(ptr != null); + free(&lib_alloc.test_allocator, ptr, 16); +} + +test "alloc with null allocator" { + const ptr = alloc(null, 8); + try testing.expect(ptr != null); + free(null, ptr, 8); +} + +test "alloc zero length" { + const ptr = alloc(&lib_alloc.test_allocator, 0); + defer free(&lib_alloc.test_allocator, ptr, 0); } test "free null pointer" { @@ -23,14 +54,14 @@ test "free null pointer" { } test "free allocated memory" { - const alloc = lib_alloc.default(&lib_alloc.test_allocator); - const mem = try alloc.alloc(u8, 16); + const allocator = lib_alloc.default(&lib_alloc.test_allocator); + const mem = try allocator.alloc(u8, 16); free(&lib_alloc.test_allocator, mem.ptr, mem.len); } test "free with null allocator" { // null allocator falls back to the default (test allocator in tests) - const alloc = lib_alloc.default(null); - const mem = try alloc.alloc(u8, 8); + const allocator = lib_alloc.default(null); + const mem = try allocator.alloc(u8, 8); free(null, mem.ptr, mem.len); } diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 8e6674049..7597228d1 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -116,6 +116,7 @@ pub const mouse_encoder_encode = mouse_encode.encode; pub const paste_is_safe = paste.is_safe; +pub const alloc_alloc = allocator.alloc; pub const alloc_free = allocator.free; pub const size_report_encode = size_report.encode;