Conversation
4f38e79 to
bd13459
Compare
bd13459 to
b11bf0f
Compare
The tk/utk builders passed back from instruction handlers are never disposed in t2c_trace_ebb. This change make sure them deposed at the end.
Fix memory leak by disposing messages from LLVMGetHostCPUName/Features
- Free branch_tables for all cached blocks - Free fd_map/mem in rv_delete() - Free all live cache entries in cache_free()
b11bf0f to
2138efe
Compare
jserv
left a comment
There was a problem hiding this comment.
Review Summary
All five leak categories are correctly fixed. No double-free, no use-after-free, no race conditions, no regressions. Cross-validated with Gemini and Codex (GPT) — all three reviewers agree the fixes are sound.
Per-commit assessment
-
970f619
Dispose LLVM builders in t2c_trace_ebb— Thetk != *builder/utk != *builderguard is correct; avoids double-disposing the caller's builder. -
72f3e75
Free LLVMGetHostCPU* return strings— Textbook fix. LLVM C API returnsmalloc'd strings;LLVMDisposeMessageis the right cleanup. -
3cbd3f0
Free fd_map/mem in JIT teardown + cache entries—fd_map/memwere only freed in the non-JIT#elsepath. Thecache_free()entry cleanup is also necessary (list nodes were previously leaked). -
2138efe
Free branch_table on runtime block eviction— Covers all three eviction loops.free(NULL)is a no-op per C standard, so calling unconditionally on every IR node is safe and simple.
Ordering correctness
The teardown sequence in rv_delete is the only correct ordering: clear_cache_hot(free_block_branch_tables) → jit_state_exit → cache_free → mpool_destroy. Reversing free_block_branch_tables after mpool_destroy would be use-after-free on IR nodes. The current code is correct.
Pre-existing leak (not introduced by this PR)
In src/t2c_template.c, the hand-written cbeqz (line 1214-1218) and cbnez handlers do not call LLVMDisposeBuilder(builder2/builder3) in their else branches, unlike the macro version (line 343/355) which correctly disposes them. Worth a follow-up fix.
| if (tk && tk != *builder) | ||
| LLVMDisposeBuilder(tk); | ||
| if (utk && utk != *builder) | ||
| LLVMDisposeBuilder(utk); |
There was a problem hiding this comment.
Good. The tk != *builder and utk != *builder guards are necessary and correct — when the branch is taken/untaken within the EBB, tk/utk alias *builder and must not be double-disposed. When they point to distinct builders created by instruction handlers, they are properly cleaned up here.
One subtlety: tk/utk are initialized to NULL and only set by branch instruction handlers. The tk && / utk && null checks handle the case where no branch was encountered. Clean.
| LLVMDisposeTargetMachine(tm); | ||
| LLVMDisposeMessage(triple); | ||
| LLVMDisposeMessage(cpu_name); | ||
| LLVMDisposeMessage(cpu_features); |
There was a problem hiding this comment.
Correct. LLVMGetHostCPUName() and LLVMGetHostCPUFeatures() return malloc'd C strings per LLVM C API contract. LLVMDisposeMessage (which calls free) is the documented cleanup. Previously these were passed as temporaries directly to LLVMCreateTargetMachine, leaking on every T2C compilation.
| ir = next_ir) { | ||
| next_ir = ir->next; | ||
|
|
||
| free(ir->branch_table); |
There was a problem hiding this comment.
Correct. This is the runtime eviction path in block_find_or_translate — when the cache is full and a block is replaced. Without this, blocks with indirect branches (JALR dispatch tables) leaked their branch_table allocation on every eviction cycle. free(NULL) is a no-op for the majority of IR nodes that have no branch table.
| clear_cache_hot(rv->block_cache, t2c_dispose_block_engine); | ||
| #endif | ||
| /* Free branch tables for all remaining blocks before freeing cache */ | ||
| clear_cache_hot(rv->block_cache, free_block_branch_tables); |
There was a problem hiding this comment.
This must remain before mpool_destroy(rv->block_ir_mp) at line 1114. free_block_branch_tables dereferences ir->branch_table through IR nodes allocated from that pool — destroying the pool first would be use-after-free.
Consider adding a brief comment noting this ordering constraint, e.g.:
/* Must precede mpool_destroy(block_ir_mp) -- IR nodes live in that pool */| mpool_destroy(rv->block_ir_mp); | ||
| mpool_destroy(rv->block_mp); | ||
| mpool_destroy(rv->fuse_mp); | ||
| map_delete(attr->fd_map); |
There was a problem hiding this comment.
Good catch. These were previously only freed in the #if !RV32_HAS(JIT) path (line 1081-1082), meaning every JIT-enabled build leaked fd_map and mem on exit.
| #else | ||
| list_for_each_entry_safe (entry, safe, &cache->list, list, cache_entry_t) | ||
| #endif | ||
| free(entry); |
There was a problem hiding this comment.
Nit: The #ifdef __HAVE_TYPEOF / #else pattern appears twice (lines 401-406 for live, 408-414 for ghost). Consider factoring into a helper macro or static function that takes a struct list_head * to reduce duplication. Not a blocker.
| /* Free IRs that main thread skipped during deferred eviction */ | ||
| for (rv_insn_t *ir = block->ir_head, *next_ir; ir; ir = next_ir) { | ||
| next_ir = ir->next; | ||
| free(ir->branch_table); |
There was a problem hiding this comment.
Correct. This deferred eviction path runs under cache_lock (acquired at line 600). The block was evicted by the main thread while T2C was compiling it — now we properly free branch_table before returning the IR node to the pool. Same pattern correctly applied at line 635 for the successful-compilation-but-evicted case.
|
Thank @fourcolor for contributing! |
This PR fixes several ASan-reported memory leaks:
Summary by cubic
Fixes ASan-reported memory leaks in the JIT and teardown for a clean shutdown and lower memory use. Disposes temporary LLVM builders/messages and frees branch tables on eviction/teardown, plus fd_map/mem and cache entries.
Written for commit 2138efe. Summary will update on new commits.