From 4255ab9d32bf6ef90c25adccff15aee29f179fc5 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sun, 27 Sep 2026 14:45:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(fstr):=20f-string=20=E6=94=B9=E5=9B=9E?= =?UTF-8?q?=E6=AF=8F=E7=AB=99=E7=82=B9=E6=A0=88=E7=BC=93=E5=86=B2=20+=20?= =?UTF-8?q?=E7=A6=81=E6=AD=A2=E9=80=83=E9=80=B8=EF=BC=8C=E9=80=83=E9=80=B8?= =?UTF-8?q?=E7=82=B9=E6=94=B9=20stdlib=20=E6=8B=B7=E8=B4=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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。 --- App/lib/core/Gen/AssignGenMixin.vp | 25 ++++++++- App/lib/core/Gen/Expr/ExprGenBase.vp | 21 ++++--- App/lib/core/Gen/Expr/ExprGenFstr.vp | 67 ++++++++--------------- App/lib/core/Gen/GenApi.vp | 70 ++++++++++++++++++++++++ App/lib/core/Gen/Stmt/StmtGenCifMixin.vp | 6 ++ App/lib/core/Gen/TranslatorGenMixin.vp | 5 ++ 6 files changed, 140 insertions(+), 54 deletions(-) diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index 428b407..38465a4 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -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 # (防残留污染裸名泛型构造的类型实参推断) diff --git a/App/lib/core/Gen/Expr/ExprGenBase.vp b/App/lib/core/Gen/Expr/ExprGenBase.vp index 1de7b7a..9ab9dc3 100644 --- a/App/lib/core/Gen/Expr/ExprGenBase.vp +++ b/App/lib/core/Gen/Expr/ExprGenBase.vp @@ -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 diff --git a/App/lib/core/Gen/Expr/ExprGenFstr.vp b/App/lib/core/Gen/Expr/ExprGenFstr.vp index 585ebd8..94d2507 100644 --- a/App/lib/core/Gen/Expr/ExprGenFstr.vp +++ b/App/lib/core/Gen/Expr/ExprGenFstr.vp @@ -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) diff --git a/App/lib/core/Gen/GenApi.vp b/App/lib/core/Gen/GenApi.vp index e905d33..1393a96 100644 --- a/App/lib/core/Gen/GenApi.vp +++ b/App/lib/core/Gen/GenApi.vp @@ -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 壳)""" diff --git a/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp b/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp index 03f9e6f..0776eda 100644 --- a/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp +++ b/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp @@ -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) diff --git a/App/lib/core/Gen/TranslatorGenMixin.vp b/App/lib/core/Gen/TranslatorGenMixin.vp index 7e25f80..5c9c87d 100644 --- a/App/lib/core/Gen/TranslatorGenMixin.vp +++ b/App/lib/core/Gen/TranslatorGenMixin.vp @@ -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)。