fix(fstr): f-string 改回每站点栈缓冲 + 禁止逃逸,逃逸点改 stdlib 拷贝

ExprGenFstr._fstr_finalize:结果缓冲改回每站点栈 alloca(线程安全、零运行时依赖;每站点各建一个,站点之间不再共享),docstring 与文件头写明语义:f-string 结果是当前函数内的临时值。

GenApi 新增唯一判据 fail_if_fstring_escape 与唯一拷贝出口 escape_fstring_copy(stdlib.malloc + string.strcpy);逃逸位置接护栏:translate_return(函数返回值)、translate_assign(字段/元素/全局变量赋值)、handle_module_level_var(模块级 AnnAssign 初始化)。

编译器自身 7 处逃逸点(ExprGenBase._cast_fail_where 6 处、AssignGenMixin.const_assign_msg 1 处)改为 escape_fstring_copy 显式拷贝——正是造成 ASan stack-buffer-underflow in vsnprintf 的那条路径。

残留(已在 GenApi 头注释诚实记录):间接逃逸(把 f-string 传进会保存它的函数、或放进容器后带走容器)静态判不出;类字段默认值路径未接护栏。全仓自查:TransPyV/App、includes、ViperApps 均无 容器/字段/全局 存 f-string 的写法,也无残留 return f-string。
This commit is contained in:
TermiNexus committed 2026-09-27 14:45:28 +08:00
1 parent 72a3c483e4
commit 4255ab9d32
6 files changed
+140 -54

No files matched your search

+70
View File
@@ -4,6 +4,7 @@ import ast
import llvmlite
import memhub
import string
import stdlib
import lib.core.Gen.GenTypes as GenTypes
import lib.core.Gen.Generator as Generator
import lib.core.Gen.Translator as HT
@@ -505,9 +506,78 @@ def forward_declare_functions(trans: HT.Translator*, tree: ast.AST*) -> int:
"""委托到 gen.forward_declare_functions(原 Handles 壳)"""
if _g_gen is not None:
return _g_gen.FuncGenDef.forward_declare_functions(trans, tree)
# ============================================================
# f-string 逃逸规则(判据 + 拷贝出口,本文件为唯一实现)
#
# f-string 的结果指针指向**当前函数的栈缓冲**(见 ExprGenFstr._fstr_finalize):
# · 栈缓冲 ⇒ 线程安全(每线程各自栈帧)+ 零运行时依赖(内核无 C-ABI 分配器);
# · 函数返回即失效 ⇒ 把该指针带出函数的写法都是悬空读。历史事故:调用方
# `return f"..."` 后由上层当 %s 喂 snprintf ⇒ ASan stack-buffer-underflow
# in vsnprintf(READ size 1,地址落在栈左红区)。
#
# 因此:逃逸位置一律 fail-fast;确需逃逸时用 escape_fstring_copy 显式拷贝。
#
# 覆盖范围与残留(诚实记录):本判据覆盖 return / 赋值到字段、元素、全局这类
# **直接逃逸**;把 f-string 传进"会保存它"的函数、或放进容器后带走容器,属于
# 间接逃逸,静态判不出,仍靠规则约束(用 escape_fstring_copy 显式拷贝)。
# ============================================================
def fail_if_fstring_escape(node: ast.AST*, where: str) -> int:
"""value 位置是 f-string(JoinedStr / 裸 FormattedValue,含元组元素)却处于逃逸位置 → fail-fast
where: 逃逸位置描述(如 "函数返回值" / "字段赋值" / "全局变量初始化")。
恒返回 0(命中即终止编译,不返回非 0 值)。
"""
if node is None:
return 0
hit: int = 0
k: int = node.kind()
if k == ast.ASTKind.JoinedStr or k == ast.ASTKind.FormattedValue:
hit = 1
elif k == ast.ASTKind.Tuple:
tp: ast.Tuple* = ast.Tuple*(node)
if tp.elts is not None:
i: sizet = 0
en: sizet = len(tp.elts)
while i < en:
e: ast.AST* = tp.elts.get(i)
if e is not None:
ek: int = e.kind()
if ek == ast.ASTKind.JoinedStr or ek == ast.ASTKind.FormattedValue:
hit = 1
break
i += 1
if hit == 0:
return 0
if _g_gen is not None:
_g_gen.TypeGenErr.fatal_error(node,
f"f-string 结果不能用于{where}(逃逸):f-string 指向**当前函数的栈缓冲**,"
f"函数返回即失效,把这个指针带出函数会读到已弹栈的帧"
f"(历史事故:ASan stack-buffer-underflow in vsnprintf)。"
f"需要带出函数时显式分配后拷贝:"
f"tmp: str = f\"...\";out: str = GenApi.escape_fstring_copy(tmp);return out。",
"TypeError")
return 0
def escape_fstring_copy(src: str) -> str:
"""把 f-string 结果拷贝到 stdlib 堆并返回(f-string 逃逸的**唯一**出口)。
f-string 本身是函数内临时值(栈缓冲);要把它带出函数(return / 存字段 /
存全局 / 放进长期容器)必须显式拷贝。分配用 stdlib.malloc(CRT),
生命周期由调用方决定——编译器自身的报文构造只在错误路径使用,允许不回收。
"""
if src is None:
return None
n: sizet = string.strlen(src)
out: str = stdlib.malloc(n + 1)
if out is None:
return None
string.strcpy(out, src)
return out
# ---------------- create_wrapper_main(委托 TranslatorGenMixin.create_wrapper_main) ----------------
def create_wrapper_main(trans: HT.Translator*, tree: ast.AST*, i32_ty: llvmlite.LLVMType*) -> int:
"""委托到 gen.create_wrapper_main(原 Handles 壳)"""