fix(gen): 修 vpi 生成阶段 is_file_func 读到已回收池的悬垂表(ASan 野读崩溃)
现场:TransPyV.exe(ASan)编译 EscapeOkTest 崩在 Phase A-pre-vpi → _GeneratePyiFromAst → _PyiFunction → compute_escape_summary → _ScanEscapes → _PushInductionEntry → is_file_func → HashTable._slot_hash (access-violation on unknown address 0xffffffffffffffff)。 根因(潜伏已久,非本阶段语义改动引起): - FileClassNames / FileFuncNames / FileVarNames / FileModuleSha1 是按文件重建的表, 表与键共用该文件的瞬时池;契约要求跨文件/跨阶段先清空再重建 (原写在 reset_visible_structs 里那四行)。 - Phase A-pre-vpi 为生成 vpi 会重新 lex/parse 每个文件,但不走 register_file_*、 也不清表 → _PushInductionEntry 的 is_file_func 只认 range 的判据,落在 Phase1 最后一个文件的表上,而那个池已被 reset_to(mark1) 回收。非 ASan 下常表现为读到 0/旧值 的静默错答,ASan 下 0xfd 填充 → 垃圾容量/垃圾 __slots__ → 野读崩溃。 修法: - 抽出 reset_file_name_window() 作为文件级名字窗口的唯一开启入口(原四行清空搬进去, reset_visible_structs 改为调它),并在注释里写死契约。 - 新增轻量 register_file_func_names(pool, tree):只重建本文件顶层函数名(核对过 vpi 路径只需这一张;FileClassNames/FileModuleSha1/FileVarNames 的读取点全在翻译期)。 - Phase A-pre-vpi 循环里,在 _GeneratePyiFromAst 之前调用 reset_file_name_window() + register_file_func_names(mb, tree_vp2)。 - register_file_classes / register_file_funcs 改为无条件重建(原来 没有才建 的写法在 表已被别的阶段建过时会静默复用悬垂表)。
This commit is contained in:
1 parent
79d1c86982
commit
41674d125c
3 files changed
+79
-8
No files matched your search
@@ -679,6 +679,12 @@ def RunMultiFileProject(mb: memhub.MemBuddy*, source_dir: str, temp_dir: str, ou
|
||||
continue
|
||||
pyi_buf_vp2: bytes = stdlib.malloc(PYI_BUF_SIZE_P2)
|
||||
if pyi_buf_vp2 is not None:
|
||||
# 文件级名字窗口:本阶段为生成 vpi 重新解析出 tree_vp2,但**不走** register_file_funcs;
|
||||
# 而 _PyiFunction → compute_escape_summary → _ScanEscapes → _PushInductionEntry 会问
|
||||
# is_file_func("range")。不清窗口就会读到"上一个文件的表 + 已被 reset 的池"→
|
||||
# 悬垂指针(ASan access-violation / 非 ASan 静默错答)。见 StructGenMixin.reset_file_name_window。
|
||||
GenApi.reset_file_name_window()
|
||||
GenApi.register_file_func_names(mb, tree_vp2)
|
||||
pyi_pos_vp2: sizet = StubConverter._GeneratePyiFromAst(mb, tree_vp2, ent_vp2.Path, pyi_buf_vp2, PYI_BUF_SIZE_P2)
|
||||
if pyi_pos_vp2 > 0:
|
||||
pyi_path_vp2: str = StubMergerCollect._sliced_path(temp_dir, td_len_vpa, ent_vp2.Sha1, "vpi")
|
||||
|
||||
@@ -279,6 +279,19 @@ def register_file_funcs(pool: memhub.MemBuddy*, tree: ast.AST*) -> None:
|
||||
if _g_gen is not None: _g_gen.StructGen.register_file_funcs(pool, tree)
|
||||
|
||||
|
||||
# ---------------- reset_file_name_window / register_file_func_names ----------------
|
||||
# "文件级名字窗口"的唯一开启入口 + vpi 落盘路径的轻量重建(契约见 StructGenMixin 两处注释)。
|
||||
# 任何文件级入口(翻译 / vpi 落盘 / …)都必须先 reset_file_name_window、再重建对应表。
|
||||
def reset_file_name_window() -> None:
|
||||
"""清空四张按文件重建的名字表(FileClassNames/FileModuleSha1/FileFuncNames/FileVarNames)"""
|
||||
if _g_gen is not None: _g_gen.StructGen.reset_file_name_window()
|
||||
|
||||
|
||||
def register_file_func_names(pool: memhub.MemBuddy*, tree: ast.AST*) -> None:
|
||||
"""只重建"本文件顶层函数名"表(供 vpi 落盘路径的 is_file_func 使用;须先 reset_file_name_window)"""
|
||||
if _g_gen is not None: _g_gen.StructGen.register_file_func_names(pool, tree)
|
||||
|
||||
|
||||
def is_file_func(name: str) -> int:
|
||||
"""Return 1 if name is a top-level function declared in this file, else 0."""
|
||||
if _g_gen is not None: return _g_gen.StructGen.is_file_func(name)
|
||||
|
||||
@@ -236,12 +236,8 @@ novirtual class StructGenMixin:
|
||||
self.StrictVisibility = strict
|
||||
self.CurDefModuleSha1 = None
|
||||
self.CurDefClassName = None
|
||||
# The file-level class name table is rebuilt per file the same way (keys and table share a pool, so no dangling; re-registered by the Translator pre-scan, see register_file_classes).
|
||||
# FileModuleSha1 points to trans.ModuleSha1 (same-pool string), so it must be cleared too to avoid reading reclaimed memory in the next file.
|
||||
self.FileClassNames = None
|
||||
self.FileModuleSha1 = None
|
||||
self.FileFuncNames = None
|
||||
self.FileVarNames = None
|
||||
# 文件级名字窗口:四张按文件重建的表统一由 reset_file_name_window 清空(契约见该方法的注释)
|
||||
self.reset_file_name_window()
|
||||
self.ModuleClasses = hashtable.HashTable(pool)
|
||||
if strict != 0:
|
||||
# Strict mode: create a new HashTable recording visible structs.
|
||||
@@ -250,6 +246,26 @@ novirtual class StructGenMixin:
|
||||
# Lenient mode: everything is visible, no HashTable needed.
|
||||
self.VisibleStructs = None
|
||||
|
||||
# reset_file_name_window - "文件级名字窗口"的**唯一开启入口**:清空四张按文件重建的表。
|
||||
# 契约:
|
||||
# · FileClassNames / FileFuncNames / FileVarNames 的表与键**共用该文件的瞬时池**
|
||||
# (AST 名字是指向 src_buf 的零拷贝指针,故键要拷进同一个池),所以跨文件/跨阶段
|
||||
# 必须**先清空再重建**;
|
||||
# · FileModuleSha1 指向 trans.ModuleSha1(同池字符串),同样必须清,否则下一个文件读到被回收的内存。
|
||||
# ⚠ 任何"文件级入口"都必须在处理该文件前**先调本方法、再重建对应表**;否则
|
||||
# is_file_class / is_file_func / is_file_var 会拿"上一个文件的表 + 已被 reset 的池"
|
||||
# = 悬垂指针(ASan 下 access-violation;非 ASan 下静默错答)。
|
||||
# 现场:Phase A-pre-vpi 只为生成 vpi 重新解析文件、不走 register_file_*,漏开此窗口,
|
||||
# 于是 _PushInductionEntry → is_file_func("range") 落在 Phase1 已回收的表上,
|
||||
# HashTable._slot_hash 野读崩溃。
|
||||
def reset_file_name_window(self) -> int:
|
||||
"""清空四张按文件重建的名字表(文件级入口必须先调);0=完成"""
|
||||
self.FileClassNames = None
|
||||
self.FileModuleSha1 = None
|
||||
self.FileFuncNames = None
|
||||
self.FileVarNames = None
|
||||
return 0
|
||||
|
||||
def add_visible_struct(self, pool: memhub.MemBuddy*, name: str) -> None:
|
||||
"""Record a visible struct name (class definition and from-import)."""
|
||||
if name is None: return
|
||||
@@ -307,7 +323,9 @@ novirtual class StructGenMixin:
|
||||
self.FileModuleSha1 = sha1
|
||||
ch: list[ast.AST*]* = tree.children
|
||||
if ch is None: return
|
||||
if self.FileClassNames is None: self.FileClassNames = hashtable.HashTable(pool)
|
||||
# **无条件重建**(不是"没有才建"):重建即"关窗口",任何先前阶段(如 Phase A-pre-vpi)
|
||||
# 留下的表都会被换掉。若沿用 `if is None` 的旧写法,表非空时会**复用**那张可能已随池回收的表。
|
||||
self.FileClassNames = hashtable.HashTable(pool)
|
||||
if self.FileClassNames is None: return
|
||||
cn: sizet = len(ch)
|
||||
ci: sizet
|
||||
@@ -330,12 +348,46 @@ novirtual class StructGenMixin:
|
||||
if name is None or self.FileClassNames is None: return 0
|
||||
return self.FileClassNames.__contains__(name)
|
||||
|
||||
# register_file_func_names - **只**重建"本文件顶层函数名"集合(vpi 落盘路径专用)。
|
||||
# Phase A-pre-vpi 会为每个文件重新 lex/parse 出独立 AST 再生成 vpi,但不走 register_file_funcs;
|
||||
# 而 compute_escape_summary 的归纳变量判据要问 is_file_func("range")
|
||||
# ("本文件自己定义过 range 就放弃区间推断",见 _PushInductionEntry)。
|
||||
# 核对过消费者:vpi 路径**只需要这一张表**——FileClassNames / FileModuleSha1 / FileVarNames 的
|
||||
# 读取点(TypeGenResolve 的类型归属 / AssignGenMixin 的全局写判据)全部在翻译期;
|
||||
# 另一个 is_file_func 调用点(VpiReader 注入自家 declare 的守卫)标注为 full-translation only。
|
||||
# 调用方必须**先 reset_file_name_window**(本方法只负责"重建"那一半),否则会与既有表叠加。
|
||||
def register_file_func_names(self, pool: memhub.MemBuddy*, tree: ast.AST*) -> None:
|
||||
"""重建"本文件顶层函数名"表(供 vpi 落盘路径的 is_file_func 使用)"""
|
||||
if pool is None or tree is None: return
|
||||
ch: list[ast.AST*]* = tree.children
|
||||
if ch is None: return
|
||||
self.FileFuncNames = hashtable.HashTable(pool)
|
||||
if self.FileFuncNames is None: return
|
||||
cn: sizet = len(ch)
|
||||
ci: sizet = 0
|
||||
while ci < cn:
|
||||
child: ast.AST* = ch.get(ci)
|
||||
ci += 1
|
||||
if child is None or child.kind() != ast.ASTKind.FunctionDef: continue
|
||||
fd: ast.FunctionDef* = ast.FunctionDef*(child)
|
||||
if fd is None or fd.name is None: continue
|
||||
if self.FileFuncNames.__contains__(fd.name) != 0: continue
|
||||
# 键必须与表同池(AST 名字指向 src_buf 的零拷贝指针,跨阶段可能失效)——同 register_file_classes
|
||||
nm_len: sizet = string.strlen(fd.name)
|
||||
nm_buf: str = pool.alloc(nm_len + 1)
|
||||
if nm_buf is None: continue
|
||||
string.strcpy(nm_buf, fd.name)
|
||||
self.FileFuncNames.set_int(nm_buf, 1)
|
||||
|
||||
def register_file_funcs(self, pool: memhub.MemBuddy*, tree: ast.AST*) -> None:
|
||||
"""Pre-register the top-level function names of this file."""
|
||||
if pool is None or tree is None: return
|
||||
ch: list[ast.AST*]* = tree.children
|
||||
if ch is None: return
|
||||
if self.FileFuncNames is None: self.FileFuncNames = hashtable.HashTable(pool)
|
||||
# **无条件重建**:同 register_file_classes——重建即"关窗口",避免复用上一阶段/上一文件
|
||||
# 留下的、其池可能已被 reset 的表(旧写法 `if is None` 在这种情形下会静默复用悬垂表)。
|
||||
self.FileFuncNames = hashtable.HashTable(pool)
|
||||
self.FileVarNames = None # 本函数的后续循环里按需重建(每文件一次)
|
||||
if self.FileFuncNames is None: return
|
||||
# 生命期摘要表:**只分配一次**(PermPool,对照 init_struct_table 的长生命周期表口径),
|
||||
# 此后每文件仅重置 EscCount。表内只存 u64 名哈希,且读只发生在 [0, EscCount) 区间;
|
||||
|
||||
Reference in new issue
Block a user