fix: drop owned temp passed to a borrow slot at the call site (closes #43)
GREEN side of the owned-RawBuf drop-leak. RED test
raw_buf_owned_drop_balances_rc_stats (47efbdb) now passes:
allocs=2 frees=2 live=0, stdout still "10".
The leak. An owned temporary returned by an Own-ret call and then
passed only to a `borrow`-mode parameter is dead after that call but
was dropped by no one. In the repro, RawBuf.set returns (own RawBuf)
in-place (the intercept emits `ret ptr %b`, the same slab it received),
and that result is passed to RawBuf.get whose RawBuf param is `borrow`,
so get does not consume it and returns an i64. After get the owned
slab is dead. Codegen's two scope-close drop emitters cover only
Term::Let binders (the let gate) and Own fn-params; the general call
lowerer emit_call spliced args without inspecting per-arg modes, so an
owned arg landing in a borrow slot was never dec'd. The leak is
binder-independent — the fully inline form with no `let` at all leaks
byte-identically, which is why the fix lives at the call site, not at
the let gate.
The fix (emit_call, codegen-only, +50 lines, uniqueness.rs untouched).
After a non-tail call, for each argument that is itself an
is_rc_heap_allocated owned temporary (Own-ret call / fresh escaping
ctor / lambda — never a plain Var alias, whose owner is some other
binder dropped elsewhere) AND lands in a Borrow-mode param slot, emit
a drop of that argument's SSA through the same per-type drop symbol the
let gate uses (drop_symbol_for_binder). Per-param modes are read from
the Ail-level Type::Fn.param_modes in module_def_ail_types (which
survives monomorphisation: RawBuf_get__Int -> [Borrow, Implicit]);
note FnSig itself carries only LLVM type strings, not modes.
Why it cannot double-drop or drop a live value:
- set's buffer param is Own, not Borrow, so the rule does not fire for
the new->set link; the single drop lands on the final temp after get.
set is in-place (same pointer in and out), so that one drop frees the
one slab exactly once.
- the rule excludes Var args, so it never races the let/Own-param
emitters for a named binder.
- the dropped SSA is an input arg, always distinct from the fresh call
result, and the rule is gated on !tail, so it can never dec a value
that flows out as the surrounding fn's result.
Known limitation (advisory, no fixture exercises it, stated in a code
comment): a musttail call passing an owned temp into a borrow slot
still leaks — a tail call terminates the block, leaving no post-call
emission point.
Verified: full workspace green; ail e2e 95/95 incl. the 4 raw_buf
fixtures (60/4.0/42/reject) and the now-GREEN drop test; codegen
intercepts_bijection_with_intrinsic_markers green.
This commit is contained in:
@@ -2585,6 +2585,56 @@ impl<'a> Emitter<'a> {
|
||||
module = target_module,
|
||||
name = target_def,
|
||||
));
|
||||
// owned-temp drop at the call site. An argument that is
|
||||
// itself an Own-returning heap allocation (an anonymous owned
|
||||
// temporary, e.g. the in-place result of `RawBuf.set`) and
|
||||
// that lands in a `borrow`-mode parameter slot is borrowed,
|
||||
// not consumed, by the callee. After the call returns it is
|
||||
// dead and no binder owns it, so neither scope-close drop
|
||||
// emitter (the `Term::Let` binder gate, the Own fn-param drop)
|
||||
// covers it. Emit its drop here, routed through the same
|
||||
// per-type drop symbol the let gate uses. Only in the non-tail
|
||||
// path: a tail call terminates the block (the call result IS
|
||||
// the fn result) and there is no post-call point to emit at.
|
||||
//
|
||||
// Scope of the rule (intentionally narrow, per the raw-buf.5
|
||||
// steer — do NOT blanket-drop every owned arg):
|
||||
// - the arg is `is_rc_heap_allocated` (Own-ret call / fresh
|
||||
// escaping ctor / lambda); a plain `Term::Var` arg is an
|
||||
// alias whose owner is some other binder and is dropped
|
||||
// there, never here;
|
||||
// - the matching callee param mode is `Borrow` — `Own` slots
|
||||
// consume the arg (the callee dec's it), `Implicit` is the
|
||||
// back-compat lane that carries no transfer signal;
|
||||
// - the dropped SSA is never the call result `dst` (an input
|
||||
// argument SSA is always distinct from the freshly-minted
|
||||
// result SSA), so this can never dec a value that flows out
|
||||
// as the surrounding fn's result.
|
||||
if !tail {
|
||||
if let Some(Type::Fn { param_modes, .. }) = self
|
||||
.module_def_ail_types
|
||||
.get(target_module)
|
||||
.and_then(|m| m.get(target_def))
|
||||
.cloned()
|
||||
{
|
||||
for (i, (arg, (arg_ssa, arg_ty))) in
|
||||
args.iter().zip(compiled_args.iter()).enumerate()
|
||||
{
|
||||
let is_borrow_slot =
|
||||
matches!(param_modes.get(i), Some(ParamMode::Borrow));
|
||||
if is_borrow_slot
|
||||
&& arg_ty == "ptr"
|
||||
&& arg_ssa != &dst
|
||||
&& self.is_rc_heap_allocated(arg)
|
||||
{
|
||||
let drop_sym = self.drop_symbol_for_binder(arg, arg_ssa);
|
||||
self.body.push_str(&format!(
|
||||
" call void @{drop_sym}(ptr {arg_ssa})\n"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if tail {
|
||||
self.body
|
||||
.push_str(&format!(" ret {ret} {dst}\n", ret = sig.ret));
|
||||
|
||||
Reference in New Issue
Block a user