fix(obs): close reference-return and ownership soundness holes

Reference returns are checked structurally instead of relying on the frame tag, so `return &x` and `return x` for a by-value parameter are rejected while a reference parameter, `self`, and a call still pass. Borrows created by a call result now register the borrowed arguments, so the owner cannot be deleted while the reference lives. `own` rejects `new memgr(...)` storage (pool memory is not freed by `free`) and raw-pointer adoption; `own` arguments reject memgr too. Overwriting an `own` field releases the value it held (not during construction), and `del` of an `own` field is rejected. Reference parameters registered under their source name as well, so passing a borrow to an `&T` parameter no longer trips the stack-escape check. The LSP mirrors the new return and adoption rules.
This commit is contained in:
TermiNexus committed 2026-11-05 16:43:48 +08:00
1 parent a2a04f567e
commit 1d668497a5
5 files changed
+187 -33

No files matched your search

+138 -31
View File
@@ -1430,14 +1430,12 @@ novirtual class AssignGenMixin:
if GenApi.is_nonlocal_name(trans, onm.id) != 0: return GenTypes.LIFE_UNKNOWN
slot_lo: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, onm.id)
if slot_lo is not None:
# `&ownedPtr` yields the heap object pointer (one load), not the frame slot.
# `&ownedPtr` / `&heapPtr` yields the heap object pointer (one load),
# not the frame slot; a plain value/parameter is still frame-local.
if trans.StrictOBS != 0 and slot_lo.Ty is not None:
match slot_lo.Ty:
case mlirlite.LLVMType.Ptr(lo1):
match lo1:
case mlirlite.LLVMType.Ptr(_): return GenTypes.LIFE_HEAP
case _: pass
case _: pass
if self.Gen.VarGen._SlifeOwned(slot_lo) != 0 \
or self.Gen.VarGen.get_var_life(trans.SymTab, onm.id) == GenTypes.LIFE_HEAP:
return GenTypes.LIFE_HEAP
return GenTypes.LIFE_FRAME
return GenTypes.LIFE_STATIC
return GenTypes.LIFE_UNKNOWN
@@ -1644,8 +1642,9 @@ novirtual class AssignGenMixin:
fd: ast.AST* = self.Gen.StructGen.NllCurFunc
if fd is None: return 0
# OBS has no `@life` contracts: a returned borrow must come from a reference parameter or
# `self`, which is the elided lifetime rule for reference return types.
if GenApi.strict_obs() != 0:
# `self`, which is the elided lifetime rule for reference return types. Use the translator's
# per-file flag: the module-level one can be clobbered by another file's vpi pass.
if trans.StrictOBS != 0:
return self._check_return_borrow_obs(trans, ast.FunctionDef*(fd), value)
rv_ty: mlirlite.LLVMType* = None
if trans._cur_func is not None: rv_ty = mlirlite.function_get_ret_ty(trans._cur_func)
@@ -1709,6 +1708,9 @@ novirtual class AssignGenMixin:
def _check_return_borrow_obs(self, trans: HT.Translator*, fd: ast.FunctionDef*, value: ast.AST*) -> int:
"""Reject a frame-local return in an OBS file; borrow elision replaces `@life`."""
if trans is None or fd is None or value is None: return 0
ret_rk: int = self.Gen.TypeGenResolve.ref_kind(fd.returns)
if ret_rk != 0:
return self._check_ref_return_obs(trans, fd, value)
rv_ty: mlirlite.LLVMType* = None
if trans._cur_func is not None: rv_ty = mlirlite.function_get_ret_ty(trans._cur_func)
life_r: int = self.life_of_expr(trans, value, rv_ty)
@@ -1727,30 +1729,62 @@ novirtual class AssignGenMixin:
if n_pure is not None and n_pure.id is not None: nm_pure = n_pure.id
self.Gen.TypeGenErr.fatal_error(value, f"pure value escapes: `{nm_pure}` is borrowed from the caller; pure results must be owned value copies. Return a newly allocated value instead", "LifetimeError")
return 0
ret_rk: int = self.Gen.TypeGenResolve.ref_kind(fd.returns)
pi: int = self.Gen.StructGen._ParamIndex(fd, value)
nm_txt: str = "expression"
nm_pre: str = ""
self.Gen.TypeGenErr.fatal_error(value, "stack address escapes: this value is frame-local and OBS files have no `@life(ret(...))` contracts. Return a heap allocation (`new ...`), a reference parameter, or a value copy", "LifetimeError")
return 0
# _ParamIndexByName - parameter position by name (None when not a parameter).
def _ParamIndexByName(self, fd: ast.FunctionDef*, name: str) -> int:
if fd is None or name is None: return -1
agr: ast.Arguments* = ast.Arguments*(fd.args)
if agr is None or agr.args is None: return -1
i: int = 0
while i < int(len(agr.args)):
pa: ast.AST* = agr.args.get(sizet(i))
if pa is not None and pa.kind() == ast.ASTKind.Arg:
ar: ast.Arg* = ast.Arg*(pa)
if ar.arg is not None and string.strcmp(ar.arg, name) == 0: return i
i += 1
return -1
# _check_ref_return_obs - a `-> &T` result may only borrow a reference parameter or `self`.
def _check_ref_return_obs(self, trans: HT.Translator*, fd: ast.FunctionDef*, value: ast.AST*) -> int:
"""Structural borrow-elision check for reference-returning OBS functions."""
if trans is None or fd is None or value is None: return 0
if value.kind() == ast.ASTKind.Call: return 0
if value.kind() == ast.ASTKind.Name:
n2: ast.Name* = ast.Name*(value)
if n2.id is not None: nm_txt = n2.id
elif value.kind() == ast.ASTKind.UnaryOp:
u2: ast.UnaryOp* = ast.UnaryOp*(value)
if u2.operand is not None and u2.operand.kind() == ast.ASTKind.Name:
n3: ast.Name* = ast.Name*(u2.operand)
if n3.id is not None:
nm_txt = n3.id
nm_pre = "&"
if ret_rk != 0 and pi >= 0:
# Borrow elision: returning a reference parameter borrows the caller's value.
par: ast.Arg* = self._ParamAt(fd, pi)
if par is not None and self.Gen.TypeGenResolve.ref_kind(par.annotation) != 0: return 0
self.Gen.TypeGenErr.fatal_error(value, f"cannot return `{nm_txt}` from a reference-returning function: parameter '{nm_txt}' is stored by value in this frame, so the result would dangle. Declare the parameter as `&T` / `&mut T`", "LifetimeError")
nm: ast.Name* = ast.Name*(value)
if nm.id is None: return 0
if string.strcmp(nm.id, "self") == 0: return 0
pi: int = self._ParamIndexByName(fd, nm.id)
if pi >= 0:
par: ast.Arg* = self._ParamAt(fd, pi)
if par is not None and self.Gen.TypeGenResolve.ref_kind(par.annotation) != 0: return 0
self.Gen.TypeGenErr.fatal_error(value, f"cannot return `{nm.id}` from a reference-returning function: parameter '{nm.id}' is stored by value in this frame, so the result would dangle. Declare the parameter as `&T` / `&mut T`", "LifetimeError")
return 0
# A reference binding: allowed when the object it borrows outlives this frame.
if self.Gen.VarGen.var_ref_kind(trans.SymTab, nm.id) != 0:
own_r: str = self.Gen.StmtGen._BorrowOwnerOf(trans, nm.id)
if own_r is not None:
pi_o: int = self._ParamIndexByName(fd, own_r)
if pi_o >= 0:
par_o: ast.Arg* = self._ParamAt(fd, pi_o)
if par_o is not None and self.Gen.TypeGenResolve.ref_kind(par_o.annotation) != 0: return 0
else:
slot_o: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, own_r)
if slot_o is not None and (self.Gen.VarGen._SlifeOwned(slot_o) != 0 \
or self.Gen.VarGen.get_var_life(trans.SymTab, own_r) == GenTypes.LIFE_HEAP):
return 0
self.Gen.TypeGenErr.fatal_error(value, f"cannot return `{nm.id}`: it lives in this frame, and a reference result may only borrow a reference parameter or `self` (borrow elision)", "LifetimeError")
return 0
if ret_rk != 0:
self.Gen.TypeGenErr.fatal_error(value, f"cannot return `{nm_pre}{nm_txt}`: it lives in this frame, and a reference result may only borrow a reference parameter or `self` (borrow elision). Allocate the value with `new` and return an owned binding instead", "LifetimeError")
if value.kind() == ast.ASTKind.Attribute:
at_r: ast.Attribute* = ast.Attribute*(value)
if at_r.value is not None and at_r.value.kind() == ast.ASTKind.Name:
bn: ast.Name* = ast.Name*(at_r.value)
if bn.id is not None and string.strcmp(bn.id, "self") == 0: return 0
if value.kind() == ast.ASTKind.UnaryOp:
self.Gen.TypeGenErr.fatal_error(value, "cannot return the address of a frame binding from a reference-returning function: return the reference parameter itself (`return p`) or the stored reference (`return self.field`)", "LifetimeError")
return 0
self.Gen.TypeGenErr.fatal_error(value, f"stack address escapes: `{nm_pre}{nm_txt}` is frame-local and OBS files have no `@life(ret(...))` contracts. Return a heap allocation (`new ...`), a reference parameter, or a value copy", "LifetimeError")
self.Gen.TypeGenErr.fatal_error(value, "cannot return this expression from a reference-returning function: a reference result must borrow a reference parameter, `self`, or another reference", "LifetimeError")
return 0
# _ParamAt - parameter node at a 0-based position (None when out of range).
@@ -1772,6 +1806,38 @@ novirtual class AssignGenMixin:
self.Gen.VarGen._SlifeNoteStore(pool, builder, sh, 0)
return 1
# _in_ctor - 1 when the current function is a constructor (`__init__` / `__before_init__`).
def _in_ctor(self, trans: HT.Translator*) -> int:
if trans is None or trans._cur_func is None: return 0
fname: str = mlirlite.function_get_name(trans._cur_func)
if fname is None: return 0
if string.strstr(fname, ".__init__") is not None: return 1
if string.strstr(fname, ".__before_init__") is not None: return 1
return 0
# _release_own_slot - free the value an `own` field holds before the field is overwritten.
def _release_own_slot(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*,
mod: mlirlite.LLVMModule*, trans: HT.Translator*,
field_ptr: mlirlite.Value*) -> int:
if pool is None or builder is None or mod is None or trans is None or field_ptr is None: return 0
if field_ptr.Ty is None or field_ptr.Ty.Pointee == 0: return 0
fty: mlirlite.LLVMType* = field_ptr.Ty.Pointee
if self.Gen.ExprGenBase.is_ptr_type(fty) == 0: return 0
cur: mlirlite.Value* = mlirlite.build_load(builder, fty, field_ptr)
if cur is None: return 0
nulp: mlirlite.Value* = mlirlite.ConstNull(pool, fty, "null")
if nulp is None: return 0
live: mlirlite.Value* = mlirlite.build_icmp(builder, mlirlite.ICMP_NE, cur, nulp)
if live is None: return 0
one: mlirlite.Value* = mlirlite.const_int(pool, 8, 1)
zero: mlirlite.Value* = mlirlite.const_int(pool, 8, 0)
if one is None or zero is None: return 0
flag: mlirlite.Value* = mlirlite.build_select(builder, live, one, zero)
sh: mlirlite.Value* = mlirlite.build_alloca(builder, mlirlite.Int8(pool))
if flag is None or sh is None: return 0
mlirlite.build_store(builder, flag, sh)
return self.Gen.VarGen._SlifeEmitGuardedFree(pool, builder, mod, trans, field_ptr, sh)
# _slife_note_assign - M3 赋值处的所有权记账(§4.2 RAII / §4.3 编译器独占释放)。
# 只在**本地 Name 目标**的赋值落地处调用;目标槽 target_slot 即该变量的 alloca。
# ① RHS = `new heap slife T(...)`:登记该槽(首次建影子标志)→ 覆盖释放旧拥有对象 → 影子置 1;
@@ -1781,6 +1847,20 @@ novirtual class AssignGenMixin:
def _slife_note_assign(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, target_slot: mlirlite.Value*, rhs_node: ast.AST*, target_name: str = None) -> int:
"""M3:赋值处的 slife 所有权记账;返回 1=该槽此刻是编译器拥有的 slife 绑定"""
if target_slot is None or rhs_node is None or trans is None: return 0
# An `own` target only takes an owned value: a move from another `own` binding, `new ...`,
# or a call that returns ownership. A raw pointer does not transfer ownership, so adopting
# it would free memory the program still treats as borrowed/aliased.
if target_name is not None and self.Gen.VarGen.var_is_own(trans.SymTab, target_name) != 0 \
and rhs_node.kind() == ast.ASTKind.Name \
and GenApi.is_none_literal(rhs_node) == 0:
rn_own: ast.Name* = ast.Name*(rhs_node)
if rn_own.id is not None:
src_own: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, rn_own.id)
src_is_own: int = self.Gen.VarGen.var_is_own(trans.SymTab, rn_own.id)
if src_own is not None and self.Gen.VarGen._SlifeOwned(src_own) != 0: src_is_own = 1
if src_is_own == 0:
self.Gen.TypeGenErr.fatal_error(rhs_node, f"`{target_name}` is `own`: it can only take an owned value (move another `own` binding, allocate with `new heap hlife ...`, or call a function returning `own T`); a raw `{rn_own.id}` does not transfer ownership", "LifetimeError")
return 0
# pure_str: the owned resource is the payload field; ownership uses the same
# shadow mechanism, but release goes through _pure_free.
if self._pure_str_slot(pool, target_slot) != 0:
@@ -1818,7 +1898,12 @@ novirtual class AssignGenMixin:
# `own T`: explicit exclusive ownership (independent of the NLL demotion predicate).
if target_name is not None and self.Gen.VarGen.var_is_own(trans.SymTab, target_name) != 0:
if nn.Source == ast.NEW_SRC_STACK:
self.Gen.TypeGenErr.fatal_error(rhs_node, "`own T` stores an exclusively owned heap object; `new stack` dies with the frame, use `new` (heap) or `new memgr(...)`", "LifetimeError")
self.Gen.TypeGenErr.fatal_error(rhs_node, "`own T` stores an exclusively owned heap object; `new stack` dies with the frame, use `new heap hlife ...`", "LifetimeError")
return 0
if nn.Source == ast.NEW_SRC_MEMGR:
# Pool memory is reclaimed by its manager, not by `free`; ownership would
# release it through the wrong allocator.
self.Gen.TypeGenErr.fatal_error(rhs_node, "`own T` cannot take a `new memgr(...)` allocation: that storage is reclaimed by the pool manager, not by `free`. Use `new heap hlife ...`, or keep the pool pointer as a plain `T*`", "LifetimeError")
return 0
owned = 1
if owned != 0:
@@ -2826,6 +2911,24 @@ novirtual class AssignGenMixin:
if o_ref is not None: owner_b = o_ref
if owner_b is not None and src_rk != 0:
self.Gen.StmtGen._BorrowAdd(trans, owner_b, nm.id, src_rk, aa.value)
# A call returning `&T` borrows one of its arguments; until a precise
# return-borrow summary exists, every borrowed argument is treated as borrowed
# by the result (conservative: `del` of any of them is blocked).
if aa.value is not None and aa.value.kind() == ast.ASTKind.Call \
and self.Gen.TypeGenResolve.ref_expr_kind(aa.value) == 0:
c_args: list[ast.AST*]* = (ast.Call*)(aa.value).args
if c_args is not None:
ai_b: int = 0
while ai_b < int(len(c_args)):
arg_b: ast.AST* = c_args.get(sizet(ai_b))
ai_b += 1
ob: str = self._BorrowOwnerName(arg_b)
if ob is None and arg_b is not None and arg_b.kind() == ast.ASTKind.Name:
an_b: ast.Name* = ast.Name*(arg_b)
if an_b.id is not None and self.Gen.VarGen.var_ref_kind(trans.SymTab, an_b.id) != 0:
ob = self.Gen.StmtGen._BorrowOwnerOf(trans, an_b.id)
if ob is not None:
self.Gen.StmtGen._BorrowAdd(trans, ob, nm.id, src_rk, arg_b)
# A call returning `own T` transfers ownership to this binding.
if GenApi.annotation_is_own(annot) != 0 and aa.value is not None \
and aa.value.kind() == ast.ASTKind.Call:
@@ -4162,6 +4265,10 @@ novirtual class AssignGenMixin:
self.Gen.TypeGenErr.fatal_error(target, "assign a reference field through a named object (`obj.field = &value`); a reference held by a temporary cannot be tracked", "TypeError")
else:
self.Gen.StmtGen._BorrowAdd(trans, own_bf, brw_bf, src_rf, asgn.value)
# Overwriting an `own` field releases the value it held. Inside a
# constructor the memory is still uninitialized, so nothing is released.
if fe_rf is not None and fe_rf.IsOwn != 0 and self._in_ctor(trans) == 0:
self._release_own_slot(pool, builder, mod, trans, field_ptr)
mlirlite.build_store(builder, store_val, field_ptr, vol_fw)
own_tgt: int = 0
if fe_rf is not None and fe_rf.IsOwn != 0: own_tgt = 1
+4
View File
@@ -200,6 +200,10 @@ novirtual class CallGenLib(CallGenBase.CallGenBase):
self.Gen.VarGen._FreedMark(src_o)
continue
self.Gen.TypeGenErr.fatal_error(an_o, f"cannot pass '{nm_o.id}' to an `own` parameter: it is not owned here; move an `own` binding or pass `new ...`", "LifetimeError")
elif an_o.kind() == ast.ASTKind.New:
nw_o: ast.New* = ast.New*(an_o)
if nw_o is not None and nw_o.Source == ast.NEW_SRC_MEMGR:
self.Gen.TypeGenErr.fatal_error(an_o, "cannot pass a `new memgr(...)` allocation to an `own` parameter: pool memory is reclaimed by its manager, not by `free`", "LifetimeError")
elif an_o.kind() == ast.ASTKind.UnaryOp:
self.Gen.TypeGenErr.fatal_error(an_o, "cannot pass a borrow to an `own` parameter: the callee takes ownership", "LifetimeError")
# Reference parameters: a mutable reference only accepts `&mut value` (or a `&mut`
+3
View File
@@ -103,6 +103,9 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
rc_n += 1
ref_buf[rc_n] = char(0)
self.Gen.CallGenBase._register_ref_params(name, ref_buf, rc_n)
# Bare source name too: some call-site checks resolve the callee by its written name.
if fd.name is not None and string.strcmp(fd.name, name) != 0:
self.Gen.CallGenBase._register_ref_params(fd.name, ref_buf, rc_n)
stdlib.free(ref_buf)
return 1
+6
View File
@@ -389,6 +389,12 @@ novirtual class StmtGenMixin(StmtGenDeclMixin.StmtGenDeclMixin):
elif tgt.kind() == ast.ASTKind.Attribute:
del_at: ast.Attribute* = ast.Attribute*(tgt)
if del_at is not None:
# Compiler-managed `own` fields are released with their object; a manual `del`
# would free them twice.
fe_del: GenTypes.FieldEntry* = self.Gen.ExprGenAttr._resolve_attr_field_entry(pool, trans, del_at)
if fe_del is not None and fe_del.IsOwn != 0:
self.Gen.TypeGenErr.fatal_error(del_at, f"cannot `del` the `own` field '.{del_at.attr}': it is released together with its object; assign a new owned value instead", "LifetimeError")
continue
del_prop: int = self.Gen.CallGenProp._try_property_deleter(pool, builder, mod, del_at, trans)
if del_prop == 0:
del_av: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, del_at, None, 0, trans)
+36 -2
View File
@@ -1652,6 +1652,34 @@ def _ParamRefFlags(pool: memhub.MemManager*, detail: str) -> str:
return str(buf)
def _CheckRefReturn(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
a: LspSymbols.LspAnalysis*, ret_ann: str, value: ast.AST*,
errors: list[ast.ParseError*]*) -> None:
"""A `-> &T` result may only borrow a reference binding, `self`, or a call."""
if value is None: return
k: int = value.kind()
if k == ast.ASTKind.Call: return
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(value)
if nm.id is None: return
if string.strcmp(nm.id, "self") == 0: return
det: str = _SymDetailAt(pool, a, value)
if _RefDetail(det) != 0 or _DetailIsOwn(det) != 0: return
_EmitTypeError(pool, doc, value,
f"cannot return `{nm.id}` from a reference-returning function: it lives in this frame; return a reference parameter, `self`, or a call", errors)
return
if k == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(value)
if at.value is not None and at.value.kind() == ast.ASTKind.Name \
and string.strcmp((ast.Name*)(at.value).id, "self") == 0: return
_EmitTypeError(pool, doc, value,
"cannot return a field of a frame-local value from a reference-returning function", errors)
return
if k == ast.ASTKind.UnaryOp:
_EmitTypeError(pool, doc, value,
"cannot return the address of a frame binding from a reference-returning function: return the reference parameter itself (`return p`)", errors)
def _ClassSymbolByName(a: LspSymbols.LspAnalysis*, name: str) -> LspSymbols.LspSymbol*:
"""Class symbol (with its own scope) declared in this document, if any."""
if a is None or name is None or a.symbols is None:
@@ -1919,8 +1947,12 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
if own_t != 0: _OwnAdd(pool, tn.id)
if own_t != 0 and an.value is not None and an.value.kind() == ast.ASTKind.Name:
sn: ast.Name* = ast.Name*(an.value)
if sn.id is not None and _IsOwnBinding(pool, a, an.value) != 0:
_MovedAdd(pool, sn.id)
if sn.id is not None:
if _IsOwnBinding(pool, a, an.value) != 0:
_MovedAdd(pool, sn.id)
else:
_EmitTypeError(pool, doc, an.value,
f"`{tn.id}` is `own`: it can only take an owned value (move another `own` binding, `new heap hlife ...`, or a call returning `own T`); a raw pointer does not transfer ownership", errors)
_MovedClear(tn.id)
# `r: &T = &x`: the binding holds a borrow, so `x` must outlive it.
_BorrowDrop(tn.id)
@@ -2132,6 +2164,8 @@ def _WalkChecks(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
continue
if k == ast.ASTKind.Return:
rt: ast.Return* = ast.Return*(s)
if rt.value is not None and _RefKindOfText(ret_ann) != 0:
_CheckRefReturn(pool, doc, a, ret_ann, rt.value, errors)
if rt.value is not None and ret_ann is not None \
and _NullableDetail(ret_ann) == 0 \
and _NullableDetail(_SymDetailAt(pool, a, rt.value)) != 0: