Skip to content

chore: errdefer audit for allocator cleanup on OOM paths - #169

Merged
apotema merged 1 commit into
mainfrom
chore/75-errdefer-audit
May 22, 2026
Merged

chore: errdefer audit for allocator cleanup on OOM paths#169
apotema merged 1 commit into
mainfrom
chore/75-errdefer-audit

Conversation

@apotema

@apotema apotema commented May 22, 2026

Copy link
Copy Markdown
Contributor

Closes #75

Systematic errdefer audit of script_scanner.zig, main_zig.zig, and root.zig for the alloc-then-hand-off pattern where an OOM in a later step of the same function leaks an earlier allocation.

Leaks found and fixed

src/script_scanner.zig

  • scanDir root-file branch (name_copy ~L126) — the dupe'd name backs both filename and rel_path on the entry; leaked if addEntryWithPath's entries.append OOM'd. Added errdefer self.allocator.free(name_copy).
  • scanDir state-dir branch (subdir_name / dir_states ~L140-143) — subdir_name (dupe) and dir_states (owned slice from parseDirStates) leaked if either shared_subdirs/shared_states append OOM'd. Reworked to ensureUnusedCapacity before the dupe so the appends are infallible; a transferred flag scopes the errdefers to the pre-handoff window.
  • scanPluginDir name_dup (~L178-179) — gemini PR feat(plugins): Controller discovery + ship_from_plugin + two-block scripts #73 finding. name_dup leaked if shared_plugin_names.append OOM'd. Reserve-then-appendAssumeCapacity.
  • scanPluginDir file branch (name_copy / rel_path ~L203-205) — gemini findings ~198/~202. name_copy leaked if the rel_path allocPrint OOM'd; both leaked if addEntryWithPath OOM'd. Added errdefer on each.
  • scanPluginDir state-dir branch (subdir_name / dir_states ~L214-216) — gemini finding ~213. Same fix as scanDir's state-dir branch.
  • scanZigFilesRecursive file branch (name_copy / rel_path ~L299-301) — same alloc-then-handoff leak as the plugin file branch. Added errdefer on each.
  • parseDirStates (~L320) — a mid-loop dupe/append OOM leaked every state string duped so far plus the ArrayList buffer. Added an errdefer freeing both.
  • getEntriesForState (~L272) — the result ArrayList backing buffer leaked on append OOM. Added errdefer result.deinit.
  • Swallowed iterator errorsiter.next(io) catch return in scanDir / scanPluginDir / scanZigFilesRecursive silently truncated the script list on a real I/O error (AccessDenied, SystemResources, …). Now propagated via try; ScanError folds in std.Io.Dir.Iterator.Error.

src/main_zig.zig

  • generateMainZigFromTemplate (~L2491, 18 append sites) — each emitted code block was toOwnedSlice'd into block then allocs.append(block)'d; an OOM in that append leaked block (it never made it into the allocs cleanup list). Pre-reserve allocs capacity (ALLOCS_BLOCK_COUNT = 18) and switch all sites to appendAssumeCapacity, closing the window.

src/root.zig

Audited — no genuine leaks. rgba_path_allocs already has a matching errdefer allocator.free(rgba_rel); loaded_manifests already reserves capacity before its appendAssumeCapacity (and documents exactly this rationale). All scanner.* results have matching defer freeNames.

Tests

Added two std.testing.checkAllAllocationFailures-based regression tests in test/script_scanner_tests.zig (MemoryLeaks struct) that drive scanDir and scanPluginDir once per allocation, failing each in turn, and assert no memory leaks on the OOM error path. These fail against the pre-fix code and pass after.

Build / test

zig build and zig build test both green — 18/18 steps succeeded; 411/415 tests passed (4 skipped).

🤖 Generated with Claude Code

Systematic audit of alloc-then-hand-off patterns where an OOM in a
later step of the same function would leak an earlier allocation.

script_scanner.zig
- scanDir / scanPluginDir / scanZigFilesRecursive: a per-file
  `dupe`'d name (and `allocPrint`'d rel_path) leaked if the
  `addEntryWithPath` append OOM'd. Added `errdefer` on each.
- scanDir / scanPluginDir state-dir branch: `subdir_name` (dupe) and
  `dir_states` (owned slice from parseDirStates) leaked if the
  shared-list append OOM'd. Reworked to reserve list capacity before
  the dupe so the appends are infallible; an explicit `transferred`
  flag scopes the errdefers to the pre-handoff window.
- scanPluginDir: `name_dup` leaked if shared_plugin_names append
  OOM'd (gemini PR #73 finding) — reserve-then-appendAssumeCapacity.
- parseDirStates: a mid-loop dupe/append OOM leaked already-duped
  state strings + the list buffer. Added an errdefer that frees both.
- getEntriesForState: the `result` ArrayList leaked on append OOM.
  Added `errdefer result.deinit`.
- iter.next() errors were swallowed by `catch return`, silently
  truncating the script list. Now propagated; ScanError folds in
  std.Io.Dir.Iterator.Error.

main_zig.zig
- generateMainZigFromTemplate: each emitted block was `toOwnedSlice`'d
  then `allocs.append`'d; an OOM in that append leaked the block.
  Reserve allocs capacity up front and use appendAssumeCapacity for
  all 18 sites, closing the window.

root.zig audited — no genuine leaks (rgba_path_allocs already has a
matching errdefer; loaded_manifests already reserves capacity).

Tests: added two checkAllAllocationFailures-based regression tests
exercising every OOM point in scanDir and scanPluginDir.

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

cursor Bot commented May 22, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches allocator/error-handling paths in main_zig.zig and script_scanner.zig; mistakes could surface as crashes (insufficient reserved capacity) or changed behavior by now propagating directory iteration errors instead of silently returning.

Overview
Fixes several OOM-path memory leaks by making ownership hand-offs explicit: adds errdefer cleanup for intermediate allocations, reserves ArrayList capacity up front, and switches to appendAssumeCapacity where capacity is guaranteed.

Script scanning now propagates directory iterator errors (and widens ScanError accordingly) instead of silently truncating results, and adds regression coverage via checkAllAllocationFailures tests for both scanDir and scanPluginDir.

Reviewed by Cursor Bugbot for commit 1e7ae56. Bugbot is set up for automated code reviews on this repo. Configure here.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request addresses memory leak vulnerabilities and improves OOM robustness across the codebase, specifically targeting issue #75. Key modifications include the use of ensureUnusedCapacity and appendAssumeCapacity to ensure infallible ownership transfers, the addition of errdefer blocks for proper cleanup during partial allocation failures, and the propagation of directory iteration errors. Feedback was provided regarding a hardcoded capacity constant in src/main_zig.zig, which is considered fragile and could lead to runtime panics if new blocks are added without updating the count.

Comment thread src/main_zig.zig
// is a safe upper bound. Reserving makes the appends infallible,
// closing the OOM window where a `toOwnedSlice`'d block is owned but
// not yet in this cleanup list (errdefer audit, #75).
const ALLOCS_BLOCK_COUNT = 18;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The hardcoded constant ALLOCS_BLOCK_COUNT = 18 is fragile and poses a maintainability risk. If a developer adds a new code block and forgets to increment this value, the program will panic at runtime (in debug/release-safe modes) when appendAssumeCapacity is called. While this approach closes the OOM window, consider using ensureUnusedCapacity(allocator, 1) immediately before each toOwnedSlice and append pair to make the code more robust to future changes.

@apotema
apotema merged commit c21fcb5 into main May 22, 2026
4 checks passed
@apotema
apotema deleted the chore/75-errdefer-audit branch May 22, 2026 04:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

infra: systematic errdefer audit for allocator cleanup on OOM paths

1 participant