From 5909690de1d942c8b846dfd51c8a26e510436ade Mon Sep 17 00:00:00 2001 From: i999rri Date: Mon, 7 Sep 2026 01:31:42 +0900 Subject: [PATCH] config: rebuild RepeatableCommand's C mirror on clone Cloning value_c copied Command.C structs whose string pointers still referenced the source config's memory; once the source was freed, an embedded host reading the command list after a config replace (ghostty_config_clone + ghostty_config_free of the old one) hit use-after-free, caught by ASan. Rebuild the mirror from the cloned commands with the same cval path parseCLI uses, and add a regression test asserting the clone's C strings do not alias the source's. --- src/config/Config.zig | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index bf4e75f86..877c43724 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -8918,9 +8918,19 @@ pub const RepeatableCommand = struct { item.* = try item.clone(alloc); } + // Cloning value_c directly would copy Command.C structs + // whose string pointers still reference the source config's + // memory — the clone must stay valid after the source is + // freed. + var value_c: std.ArrayListUnmanaged(inputpkg.Command.C) = .empty; + try value_c.ensureTotalCapacityPrecise(alloc, value.items.len); + for (value.items) |item| { + value_c.appendAssumeCapacity(try item.cval(alloc)); + } + return .{ .value = value, - .value_c = try self.value_c.clone(alloc), + .value_c = value_c, }; } @@ -9009,6 +9019,26 @@ pub const RepeatableCommand = struct { try testing.expectEqual(inputpkg.command.defaults.len, list.value.items.len); } + test "RepeatableCommand clone rebuilds the C mirror" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: RepeatableCommand = .{}; + try list.parseCLI(alloc, "title:Foo,description:bar,action:new_tab"); + + const copy = try list.clone(alloc); + try testing.expectEqual(list.value_c.items.len, copy.value_c.items.len); + // The clone's C strings must not alias the source's — the + // source config can be freed while the clone lives on. + try testing.expect(list.value_c.items[0].title != copy.value_c.items[0].title); + try testing.expectEqualStrings( + std.mem.span(list.value_c.items[0].title), + std.mem.span(copy.value_c.items[0].title), + ); + } + test "RepeatableCommand formatConfig empty" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator);