atproto utils for zig zat.dev
atproto sdk zig
26
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix CAR reader allocation safety: errdefer + header arena

Add errdefer cleanup for roots, blocks, and block_index in
readWithOptions so partial allocations are freed on error. Use a
temporary ArenaAllocator for the header CBOR decode so the header's
Value tree is always freed (it's only needed to extract version + roots).

Note: checkAllAllocationFailures cannot verify CAR read/write because
readWithOptions uses an internal ArenaAllocator whose page-level
allocations are non-deterministic from the tool's perspective. The
errdefer + arena pattern provides equivalent safety.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

jcalabro 4c1f212d ece81a96

+19 -3
+12 -3
src/internal/repo/car.zig
··· 73 73 const header_end = pos + header_len_usize; 74 74 if (header_end > data.len) return error.UnexpectedEof; 75 75 76 - // decode header (DAG-CBOR map with "version" and "roots") 76 + // decode header using a temporary arena (the header's Value tree is only 77 + // needed to extract version + roots, then discarded). this avoids leaking 78 + // the header's CBOR allocations if a later allocation fails. 79 + var header_arena = std.heap.ArenaAllocator.init(allocator); 80 + defer header_arena.deinit(); 77 81 const header_bytes = data[pos..header_end]; 78 - const header = cbor.decodeAll(allocator, header_bytes) catch return error.InvalidHeader; 82 + const header = cbor.decodeAll(header_arena.allocator(), header_bytes) catch return error.InvalidHeader; 79 83 80 84 // validate version == 1 81 85 const version = header.getUint("version") orelse return error.InvalidHeader; 82 86 if (version != 1) return error.InvalidHeader; 83 87 84 - // extract roots (array of CID links) — CAR v1 requires at least one root 88 + // extract roots (array of CID links) — CAR v1 requires at least one root. 89 + // CID.raw slices point into the input `data` (zero-copy), so they outlive 90 + // the header arena. 85 91 var roots: std.ArrayList(cbor.Cid) = .empty; 92 + errdefer roots.deinit(allocator); 86 93 const root_values = header.getArray("roots") orelse return error.InvalidHeader; 87 94 for (root_values) |root_val| { 88 95 switch (root_val) { ··· 96 103 97 104 // read blocks 98 105 var blocks: std.ArrayList(Block) = .empty; 106 + errdefer blocks.deinit(allocator); 99 107 var block_index: std.StringHashMapUnmanaged([]const u8) = .empty; 108 + errdefer block_index.deinit(allocator); 100 109 101 110 while (pos < data.len) { 102 111 // block: [varint total_len] [CID bytes] [data bytes]
+7
src/internal/repo/car_test.zig
··· 321 321 try std.testing.expectEqual(@as(usize, 1), parsed.roots.len); 322 322 try std.testing.expectEqual(@as(usize, 0), parsed.blocks.len); 323 323 } 324 + 325 + // note: checkAllAllocationFailures cannot be used for car.read because 326 + // readWithOptions uses an internal ArenaAllocator for header CBOR parsing, 327 + // which creates non-deterministic page-level allocations from the backing 328 + // allocator. the errdefer cleanup on roots/blocks/block_index in 329 + // readWithOptions ensures no leaks on OOM; the header arena's defer deinit 330 + // ensures the header Value tree is always freed.