RefCountedSet.addWithId over-counts living items when the requested id is dead and the value already exists #14064
|
Hi! I'm an outside contributor who has been fuzzing the terminal core in a local build, and I think I found a small accounting bug in Summary
Where
if (items[id].meta.ref == 0) {
// ...
self.deleteItem(base, id, ctx);
const added_id = self.upsert(base, value, id, ctx);
items[added_id].meta.ref += 1;
self.living += 1; // <-- also runs when upsert returned an existing live item
return if (added_id == id) null else added_id;
}
How the state arisesDead slots are reaped lazily (by the trim loop in This is reachable from plain terminal output, no resize needed. Probing the branch with a panic in a debug build, the fuzz case's path was: i.e. a linefeed at the bottom of a scroll region whose incoming row lives on a different page than its destination. ImpactNothing crashes on Reproduction (unit test)test "addWithId dead id for an existing value keeps living count accurate" {
const alloc = testing.allocator;
const TestSet = RefCountedSet(
u32,
u16,
u16,
struct {
pub fn hash(_: *const @This(), value: u32) u64 {
return std.hash.int(value);
}
pub fn eql(_: *const @This(), a: u32, b: u32) bool {
return a == b;
}
},
);
const layout: TestSet.Layout = .init(8);
const buf = try alloc.alignedAlloc(u8, TestSet.base_align, layout.total_size);
defer alloc.free(buf);
var set: TestSet = .init(.init(buf), layout, .{});
const kept = try set.add(buf, 11);
const dead = try set.add(buf, 22);
set.release(buf, dead);
try testing.expectEqual(@as(usize, 1), set.count());
const resolved = try set.addWithId(buf, 11, dead);
try testing.expectEqual(kept, resolved.?);
try testing.expectEqual(@as(u16, 2), set.refCount(buf, kept));
try testing.expectEqual(@as(usize, 1), set.count()); // fails on main: expected 1, found 2
var it = set.iterator(buf);
var live: usize = 0;
while (it.next()) |_| live += 1;
try testing.expectEqual(set.count(), live);
}On current Proposed fixA fresh insert leaves the item at items[added_id].meta.ref += 1;
// upsert resolves to an existing live item when the
// value is already present; only a fresh insert
// (ref went 0 -> 1) adds a living item.
if (items[added_id].meta.ref == 1) self.living += 1;With that change the test passes and the fuzz input that surfaced this no longer trips the cross-check. Commit with the fix and test on top of current An alternative, if you'd rather not lean on the Thanks for Ghostty! Sent with Claude Code Fable |
Thanks #14081