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

+23 -2
View File
@@ -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
# (防残留污染裸名泛型构造的类型实参推断)
+14 -7
View File
@@ -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
+22 -45
View File
@@ -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)
+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 壳)"""
+6
View File
@@ -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)
+5
View File
@@ -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)。