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:
1 parent
72a3c483e4
commit
4255ab9d32
6 files changed
+140
-54
No files matched your search
@@ -1372,15 +1372,18 @@ novirtual class AssignGenMixin:
|
||||
报文一律用 **f-string 插值**构建:本语言的 str 是 i8*,**没有** `str + str`
|
||||
的拼接语义(`+` 会退化成裸指针 add ⇒ 非法 IR: invalid operand type for
|
||||
instruction),故禁止对 str 用 `+` 拼接。
|
||||
|
||||
返回值会**逃逸本函数**(被 const_assign_fail 传给 fatal_error)⇒ 经
|
||||
GenApi.escape_fstring_copy 显式拷到 stdlib 堆(f-string 本身是栈临时值)。
|
||||
"""
|
||||
decl: str = "(注解含 t.CConst,声明处来源名未记录)"
|
||||
if src_name is not None:
|
||||
decl = f"t.CConst | {src_name}"
|
||||
return f"不能对 const 变量 '{name}' {kind}" \
|
||||
return GenApi.escape_fstring_copy(f"不能对 const 变量 '{name}' {kind}" \
|
||||
f"(声明处类型(含 const):{decl}" \
|
||||
f";修法:删掉该声明里的 const 描述符 / t.CConst,或改为声明时一次赋值、" \
|
||||
f"之后只用非 const 的副本/中间量;const 对象在其作用域内禁止赋值、" \
|
||||
f"增量赋值与元素赋值,且不会降级为警告)"
|
||||
f"增量赋值与元素赋值,且不会降级为警告)")
|
||||
|
||||
def const_assign_fail(self, node: ast.AST*, kind: str,
|
||||
name: str, src_name: str) -> int:
|
||||
@@ -2691,6 +2694,24 @@ novirtual class AssignGenMixin:
|
||||
self.const_assign_fail(tgt_c, "的元素赋值", at_c2.attr,
|
||||
self.Gen.ExprGenAttr.attr_field_decl_src(pool, trans, sub_c.value))
|
||||
|
||||
# f-string 逃逸:f-string 结果是**当前函数内的临时值**(栈缓冲,见
|
||||
# ExprGenFstr._fstr_finalize),写进字段 / 元素 / 全局等函数外可见的位置即
|
||||
# 悬空(调用方之后读到已弹栈的帧)。局部 Name 目标允许——生命周期与 f-string
|
||||
# 缓冲同在当前函数。判据与拷贝出口见 GenApi.fail_if_fstring_escape 头注释。
|
||||
for ti_f in range(len(targets)):
|
||||
tgt_f: ast.AST* = targets.get(ti_f)
|
||||
if tgt_f is None:
|
||||
continue
|
||||
tk_f: int = tgt_f.kind()
|
||||
if tk_f == ast.ASTKind.Attribute:
|
||||
GenApi.fail_if_fstring_escape(asgn.value, "字段赋值")
|
||||
elif tk_f == ast.ASTKind.Subscript:
|
||||
GenApi.fail_if_fstring_escape(asgn.value, "元素赋值")
|
||||
elif tk_f == ast.ASTKind.Name:
|
||||
nm_f: ast.Name* = ast.Name*(tgt_f)
|
||||
if nm_f.id is not None and GenApi.is_global_name(trans, nm_f.id) != 0:
|
||||
GenApi.fail_if_fstring_escape(asgn.value, "全局变量赋值")
|
||||
|
||||
# 翻译 RHS 值
|
||||
# 无注解赋值无目标类型可推断,清空 CurAssignAnnot/CurAssignNode
|
||||
# (防残留污染裸名泛型构造的类型实参推断)
|
||||
|
||||
@@ -9,6 +9,7 @@ import viperlib
|
||||
import lib.core.Gen.Translator as HT
|
||||
import lib.Projectrans.Utils as Utils
|
||||
import lib.core.Gen.Type.TypeSpec as TSpec
|
||||
import lib.core.Gen.GenApi as GenApi
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -326,7 +327,13 @@ novirtual class ExprGenBase:
|
||||
# 由 traceback 打出,列号即可指到具体实参)。
|
||||
# ============================================================
|
||||
def _cast_fail_where(self, node: ast.AST*) -> str:
|
||||
"""取隐式转换失败的定位文本(出错表达式 + 列号)"""
|
||||
"""取隐式转换失败的定位文本(出错表达式 + 列号)
|
||||
|
||||
返回值会**逃逸本函数**(被 coerce_to_type 拼进更大的报文),而 f-string 结果
|
||||
指向当前函数的栈缓冲 ⇒ 每处都经 GenApi.escape_fstring_copy 显式拷到 stdlib 堆
|
||||
(历史事故:直接 return f"..." 让调用方读到已弹栈的帧,ASan
|
||||
stack-buffer-underflow in vsnprintf)。
|
||||
"""
|
||||
if node is None:
|
||||
return "节点未知(无 AST 信息)"
|
||||
k: int = node.kind()
|
||||
@@ -335,22 +342,22 @@ novirtual class ExprGenBase:
|
||||
nm: ast.Name* = ast.Name*(node)
|
||||
if nm.id is not None:
|
||||
pos = nm.col_offset
|
||||
return f"Name '{nm.id}'(列 {pos})"
|
||||
return GenApi.escape_fstring_copy(f"Name '{nm.id}'(列 {pos})")
|
||||
elif k == ast.ASTKind.Attribute:
|
||||
at: ast.Attribute* = ast.Attribute*(node)
|
||||
if at.attr is not None:
|
||||
pos = at.col_offset
|
||||
return f"属性 '.{at.attr}'(列 {pos})"
|
||||
return GenApi.escape_fstring_copy(f"属性 '.{at.attr}'(列 {pos})")
|
||||
elif k == ast.ASTKind.Call:
|
||||
pos = (ast.Call*(node)).col_offset
|
||||
return f"调用结果 Call(列 {pos})"
|
||||
return GenApi.escape_fstring_copy(f"调用结果 Call(列 {pos})")
|
||||
elif k == ast.ASTKind.Subscript:
|
||||
pos = (ast.Subscript*(node)).col_offset
|
||||
return f"下标表达式 Subscript(列 {pos})"
|
||||
return GenApi.escape_fstring_copy(f"下标表达式 Subscript(列 {pos})")
|
||||
elif k == ast.ASTKind.Constant:
|
||||
pos = (ast.Constant*(node)).col_offset
|
||||
return f"常量 Constant(列 {pos})"
|
||||
return f"表达式 kind={k}(列 {pos})"
|
||||
return GenApi.escape_fstring_copy(f"常量 Constant(列 {pos})")
|
||||
return GenApi.escape_fstring_copy(f"表达式 kind={k}(列 {pos})")
|
||||
|
||||
# ============================================================
|
||||
# 类型强制转换:整数 sext/trunc,浮点 fpext/fptrunc,int↔float si2fp/fp2si
|
||||
|
||||
@@ -4,7 +4,6 @@ import ast
|
||||
import llvmlite
|
||||
import memhub
|
||||
import string
|
||||
import viperlib
|
||||
import lib.core.Gen.Translator as HT
|
||||
|
||||
import lib.core.Gen.Expr.ExprGenBase as ExprGenBase
|
||||
@@ -28,8 +27,9 @@ novirtual class ExprGenFstr(ExprGenBase.ExprGenBase):
|
||||
# translate_joined_str - f-string 翻译(对齐 TPC _HandleJoinedStrLlvm)
|
||||
#
|
||||
# f"..."(JoinedStr)→ 拼接字面量与格式说明符 → snprintf 生成 i8*
|
||||
# 模块级缓冲(程序期存活:f-string 结果在全仓被 `return f"..."` 带出函数,
|
||||
# 栈缓冲会悬空——ASan stack-buffer-underflow in vsnprintf)。
|
||||
# 栈缓冲(每站点独立,线程安全);结果是**函数内临时值,禁止逃逸**
|
||||
# (return / 存字段 / 存全局 由 GenApi.fail_if_fstring_escape 拦;
|
||||
# 需逃逸用 GenApi.escape_fstring_copy 显式拷贝)。
|
||||
# 之前 f-string 未翻译导致 print/赋值拿到 NULL 指针
|
||||
# ("(null)"输出 + printf("%s", 垃圾) 崩溃 0xC0000005)。
|
||||
# ============================================================
|
||||
@@ -38,7 +38,7 @@ novirtual class ExprGenFstr(ExprGenBase.ExprGenBase):
|
||||
mod: llvmlite.LLVMModule*,
|
||||
node: ast.AST*,
|
||||
trans: HT.Translator* = None) -> llvmlite.Value*:
|
||||
"""f-string → snprintf 生成 i8* 模块级缓冲(程序期存活,可安全 return)"""
|
||||
"""f-string → snprintf 生成 i8* 栈缓冲(函数内临时值,禁止逃逸)"""
|
||||
if node is None or builder is None or trans is None: return None
|
||||
js: rawptr[ast.JoinedStr] = rawptr[ast.JoinedStr](node)
|
||||
if js is None or js.values is None: return None
|
||||
@@ -145,7 +145,7 @@ novirtual class ExprGenFstr(ExprGenBase.ExprGenBase):
|
||||
arg_tail: llvmlite.Value*,
|
||||
arg_count: int,
|
||||
trans: HT.Translator*) -> llvmlite.Value*:
|
||||
"""f-string 尾部:全局格式串 + 模块级缓冲 + snprintf,返回 i8*(程序期存活)"""
|
||||
"""f-string 尾部:全局格式串 + 每站点栈缓冲 + snprintf,返回 i8*(函数内临时值)"""
|
||||
i32_ty: llvmlite.LLVMType* = llvmlite.Int32(pool)
|
||||
i8_ty: llvmlite.LLVMType* = llvmlite.Int8(pool)
|
||||
if fmt_buf is None or fmt_buf[0] == '\0':
|
||||
@@ -154,47 +154,24 @@ novirtual class ExprGenFstr(ExprGenBase.ExprGenBase):
|
||||
# 格式串 → 全局字符串常量(i8*)
|
||||
fmt_ptr: llvmlite.Value* = self.Gen.ExprGenConst.create_global_string(builder, pool, mod, fmt_buf, trans)
|
||||
if fmt_ptr is None: return None
|
||||
# 结果缓冲:**模块级可写全局**(`internal unnamed_addr global [N x i8]`
|
||||
# zeroinitializer,符号在链接期解析)。
|
||||
# 结果缓冲:**每站点独立栈缓冲**(entry 块 alloca;每次翻译本站点各建一个,
|
||||
# 站点之间不共享 ⇒ 同函数多个 f-string 不再互相覆盖)。
|
||||
#
|
||||
# 为何不是栈 alloca(原实现,真实崩溃 2026-09-27):
|
||||
# 栈缓冲寿命止于当前函数,而 f-string 结果在全仓被大量 `return f"..."` 带出
|
||||
# 函数(ExprGenBase._cast_fail_where / AssignGenMixin.const_assign_msg /
|
||||
# CallGenLib 报文构造 …),返回后再作 %s 喂给 snprintf 就是读**已弹栈的帧**
|
||||
# —— ASan 报 stack-buffer-underflow in vsnprintf(READ size 1,地址落在栈左
|
||||
# 红区);非 asan 构建表现为随机乱码/0xC0000005。且同一函数内多个 f-string
|
||||
# 共用同一个 entry alloca 会互相覆盖(静默串味)。
|
||||
#
|
||||
# 为何不是「编译期池缓冲 + inttoptr 常量地址」:池地址是**编译进程**的宿主地址,
|
||||
# 烘进 .ll 后与产出程序无关(用户程序 exe 里该地址根本不存在)⇒ 绝不可用。
|
||||
# 必须用链接期符号(全局变量)。
|
||||
#
|
||||
# 已知取舍(与 C 的 `static char buf[N]` 惯用法同):同一 f-string 站点被反复
|
||||
# 求值且**同时保留多个结果**(如循环里 append 到表)时,后者覆盖前者;多线程
|
||||
# 并发应避免共享同一站点的结果。要彻底消除需运行时字符串分配器(当前无)。
|
||||
fstr_idx: int = trans._label_counter
|
||||
trans._label_counter += 1
|
||||
arr_ty: llvmlite.LLVMType* = llvmlite.Array(pool, i8_ty, FSTR_BUF_SIZE)
|
||||
buf_name: str = pool.alloc(48)
|
||||
if buf_name is None: return None
|
||||
if trans.ModuleSha1 is not None:
|
||||
viperlib.snprintf(buf_name, 48, ".fstr.buf.%s.%d", trans.ModuleSha1, fstr_idx)
|
||||
else:
|
||||
viperlib.snprintf(buf_name, 48, ".fstr.buf.%d", fstr_idx)
|
||||
buf_gv: llvmlite.GlobalVariable* = llvmlite.new_global_variable(pool, buf_name, arr_ty)
|
||||
if buf_gv is None: return None
|
||||
llvmlite.module_add_global(mod, buf_gv)
|
||||
llvmlite.global_set_unnamed_addr(buf_gv, 1)
|
||||
# 可写(**不**设 constant):snprintf 要往这里写;internal 链接(模块私有)
|
||||
buf_gv.Linkage = "internal"
|
||||
buf_gv.Initializer = "zeroinitializer"
|
||||
arr_ptr_ty: llvmlite.LLVMType* = llvmlite.Ptr(pool, arr_ty)
|
||||
buf_ref_name: str = pool.alloc(64)
|
||||
if buf_ref_name is None: return None
|
||||
viperlib.snprintf(buf_ref_name, 64, "@%s", buf_gv.Name)
|
||||
buf_ref: llvmlite.Value* = llvmlite.SSAValue(pool, arr_ptr_ty, buf_ref_name)
|
||||
i8_ptr_ty: llvmlite.LLVMType* = llvmlite.Ptr(pool, i8_ty)
|
||||
buf_ptr: llvmlite.Value* = llvmlite.build_bitcast(builder, buf_ref, i8_ptr_ty)
|
||||
# 语义(用户 2026-09-27 决定):f-string 结果是**当前函数内的临时值**。
|
||||
# · 栈缓冲 ⇒ 线程安全(每个线程各自一份栈帧,同站点并发也不串味)
|
||||
# + 零运行时依赖(内核没有 C-ABI malloc,也不能假设任何分配器);
|
||||
# · 函数返回即失效 ⇒ **禁止逃逸**:return / 存字段 / 存全局 / 存容器一律
|
||||
# 由 GenApi.fail_if_fstring_escape 在翻译期 fail-fast 拦下。历史事故:
|
||||
# 旧实现让调用方把该栈指针 return 出去,再作 %s 喂 snprintf ⇒
|
||||
# ASan stack-buffer-underflow in vsnprintf(READ size 1,栈左红区)。
|
||||
# · 确需逃逸时**显式分配后拷贝**:GenApi.escape_fstring_copy
|
||||
# (stdlib.malloc + string.strcpy);编译器自身的报文构造即此写法
|
||||
# (ExprGenBase._cast_fail_where / AssignGenMixin.const_assign_msg)。
|
||||
buf_ty: llvmlite.LLVMType* = llvmlite.Array(pool, i8_ty, FSTR_BUF_SIZE)
|
||||
buf_alloca: llvmlite.Value* = self.Gen.VarGen._alloca_at_entry(builder, buf_ty)
|
||||
if buf_alloca is None: return None
|
||||
zero_i32: llvmlite.Value* = llvmlite.const_int32(pool, 0)
|
||||
buf_ptr: llvmlite.Value* = llvmlite.build_gep_array(builder, buf_ty, i8_ty, buf_alloca, zero_i32)
|
||||
if buf_ptr is None: return None
|
||||
# snprintf(buf, FSTR_BUF_SIZE, fmt, args...)
|
||||
size_val: llvmlite.Value* = llvmlite.const_int64(pool, FSTR_BUF_SIZE)
|
||||
|
||||
@@ -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 壳)"""
|
||||
|
||||
@@ -14,6 +14,7 @@ import lib.core.Gen.Translator as HT
|
||||
import lib.core.Gen.Type.TypeSpec as TSpec
|
||||
import lib.core.CompilerFactory as CompilerFactory
|
||||
import lib.Projectrans.Config as Config
|
||||
import lib.core.Gen.GenApi as GenApi
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -461,6 +462,11 @@ novirtual class StmtGenCifMixin:
|
||||
|
||||
val: llvmlite.Value* = None
|
||||
if rt.value is not None:
|
||||
# f-string 逃逸(return f"..."):f-string 指向当前函数的栈缓冲,函数返回即
|
||||
# 失效,返回该指针会让调用方读到已弹栈的帧(历史事故:ASan
|
||||
# stack-buffer-underflow in vsnprintf)。需要带出函数时先显式拷贝,
|
||||
# 见 GenApi.escape_fstring_copy。
|
||||
GenApi.fail_if_fstring_escape(rt.value, "函数返回值")
|
||||
# return a, b → 打包匿名 struct 值(多返回值,函数返回类型 tuple[T1,T2])
|
||||
if rt.value.kind() == ast.ASTKind.Tuple:
|
||||
rtp: ast.Tuple* = ast.Tuple*(rt.value)
|
||||
|
||||
@@ -10,6 +10,7 @@ import viperlib
|
||||
import lib.core.VLogger as VLogger
|
||||
import lib.core.Gen.Translator as HT
|
||||
import lib.core.Gen.GenTypes as GenTypes
|
||||
import lib.core.Gen.GenApi as GenApi
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -711,6 +712,10 @@ novirtual class TranslatorGenMixin:
|
||||
nm: ast.Name* = ast.Name*(aa.target)
|
||||
var_name = nm.id
|
||||
annot_node = aa.annotation
|
||||
# f-string 逃逸:模块级变量的初始值是**全局**(函数外可见),而 f-string
|
||||
# 结果指向当前函数的栈缓冲(且模块级初始化根本没有可用的函数栈)⇒
|
||||
# fail-fast;需要字面量以外的初值请用显式拷贝(GenApi.escape_fstring_copy)。
|
||||
GenApi.fail_if_fstring_escape(aa.value, "全局变量初始化")
|
||||
# static 符号(禁止跨模块使用):模块级变量带 t.CStatic 标记
|
||||
# (`static x: int = 0` 经解析层归一为 `x: t.CStatic | int`;亦覆盖
|
||||
# 手写 `x: t.CStatic | int`)→ 登记 (变量名, 定义模块 SHA1)。
|
||||
|
||||
Reference in new issue
Block a user