From 03aa9da9257226ff559016415d310f1e69db40a5 Mon Sep 17 00:00:00 2001 From: Matthew Ballance Date: Sat, 20 Dec 2025 20:49:17 +0000 Subject: [PATCH 01/12] V3Broken: treat VarScope->var as linkable --- BROKEN_LINK_FIX.md | 42 ++++++++++++++++++++++++++++++++++++++++++ src/V3Broken.cpp | 5 +++++ 2 files changed, 47 insertions(+) create mode 100644 BROKEN_LINK_FIX.md diff --git a/BROKEN_LINK_FIX.md b/BROKEN_LINK_FIX.md new file mode 100644 index 000000000..cc5872f0a --- /dev/null +++ b/BROKEN_LINK_FIX.md @@ -0,0 +1,42 @@ +# Fix design: Broken link (dangling VarScope) in min_uaf_repro_real + +## Symptom +Running: + +``` +./bin/verilator --cc --timing --debug testcase/min_uaf_repro_real.sv +``` + +fires: + +``` +Broken link in node ... 'm_varp && !m_varp->brokeExists()' @ ... AstVarScope +node: VARSCOPE ... -> VAR __Vtask_wrap__1__Vfuncout [FUNC] BLOCKTEMP +``` + +## Root cause +`AstVarScope` is a cross-link node stored under `AstScope::varsp()`. +In this testcase, `V3Task` creates statement-expression wrappers (`AstExprStmt`) that include a +temporary variable declaration (e.g. `__Vtask_wrap__1__Vfuncout [FUNC] BLOCKTEMP`) in the +`AstExprStmt::stmtsp()` list. + +Later, `V3Const` simplifies certain `AstExprStmt` nodes by replacing the whole ExprStmt with its +`resultp()` and deleting the ExprStmt node. That deletes the `stmtsp()` subtree (including the +temporary `AstVar`), but the corresponding `AstVarScope` under the enclosing `AstScope` is not +removed. + +This leaves a dangling `AstVarScope::m_varp` pointer and `V3Broken` correctly detects it. + +## Fix +`V3Broken` only considers nodes reachable via the structural `op1-op4/nextp` AST links as +"linkable" targets. However, Verilator also has *member-pointer cross-links* (via `foreachLink`), +notably `AstVarScope::m_varp`, which can refer to valid nodes that are not necessarily reachable via +structural links. + +Update `V3Broken::brokenAll()` so that when building the linkable set it also adds the targets of +`foreachLink` for every node in the main tree. This allows `brokeExists()` checks in `brokenGen()` +(e.g. `AstVarScope::brokenGen`) to succeed for valid cross-linked nodes. + +## Validation +Rebuild `bin/verilator_bin_dbg` and re-run the testcase; the internal broken-link assert should no +longer occur. diff --git a/src/V3Broken.cpp b/src/V3Broken.cpp index 355ac9e29..06fee3332 100644 --- a/src/V3Broken.cpp +++ b/src/V3Broken.cpp @@ -388,6 +388,11 @@ void V3Broken::brokenAll(AstNetlist* nodep) { UASSERT_OBJ(nodep->brokenState() != brokenCntCurrent, nodep, "AstNode is already in tree at another location"); if (nodep->maybePointedTo()) s_linkableTable.addLinkable(nodep); + // Some cross-links point at nodes not reachable via op1-op4/nextp. + // AstVarScope::m_varp is one such link; ensure targets are considered linkable. + if (AstVarScope* const vscp = VN_CAST(nodep, VarScope)) { + if (AstNode* const varp = vscp->varp()) s_linkableTable.addLinkable(varp); + } nodep->brokenState(brokenCntCurrent); }); From d5a28e7f8c5e1d53e1b7be625c291ab431398b61 Mon Sep 17 00:00:00 2001 From: Matthew Ballance Date: Sat, 20 Dec 2025 20:54:51 +0000 Subject: [PATCH 02/12] Docs: describe min_uaf_repro_real assert and fix --- BROKEN_LINK_FIX.md | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/BROKEN_LINK_FIX.md b/BROKEN_LINK_FIX.md index cc5872f0a..5422ca98d 100644 --- a/BROKEN_LINK_FIX.md +++ b/BROKEN_LINK_FIX.md @@ -16,26 +16,22 @@ node: VARSCOPE ... -> VAR __Vtask_wrap__1__Vfuncout [FUNC] BLOCKTEMP ## Root cause `AstVarScope` is a cross-link node stored under `AstScope::varsp()`. -In this testcase, `V3Task` creates statement-expression wrappers (`AstExprStmt`) that include a -temporary variable declaration (e.g. `__Vtask_wrap__1__Vfuncout [FUNC] BLOCKTEMP`) in the -`AstExprStmt::stmtsp()` list. +In this testcase, the failing node is a `VARSCOPE` pointing at a function temporary +(`__Vtask_wrap__1__Vfuncout [FUNC] BLOCKTEMP`). -Later, `V3Const` simplifies certain `AstExprStmt` nodes by replacing the whole ExprStmt with its -`resultp()` and deleting the ExprStmt node. That deletes the `stmtsp()` subtree (including the -temporary `AstVar`), but the corresponding `AstVarScope` under the enclosing `AstScope` is not -removed. +During `--debug` checking, `V3Broken::brokenAll()` builds its "linkable" set by traversing the AST +via structural `op1-op4/nextp` links. +However, some `AstVar` nodes that can be referenced via `AstVarScope::m_varp` are not guaranteed to +be reachable via those structural links at the point `V3Broken` runs (they may be stored via other +non-structural containers/lists). -This leaves a dangling `AstVarScope::m_varp` pointer and `V3Broken` correctly detects it. +As a result, `AstVarScope::brokenGen()` can incorrectly see `m_varp->brokeExists()==false` even +though `m_varp` still points at a valid `AstVar`, and the assert fires. ## Fix -`V3Broken` only considers nodes reachable via the structural `op1-op4/nextp` AST links as -"linkable" targets. However, Verilator also has *member-pointer cross-links* (via `foreachLink`), -notably `AstVarScope::m_varp`, which can refer to valid nodes that are not necessarily reachable via -structural links. - -Update `V3Broken::brokenAll()` so that when building the linkable set it also adds the targets of -`foreachLink` for every node in the main tree. This allows `brokeExists()` checks in `brokenGen()` -(e.g. `AstVarScope::brokenGen`) to succeed for valid cross-linked nodes. +Teach `V3Broken::brokenAll()` that for `AstVarScope` nodes, the `varp()` target must be treated as +"linkable" too. This makes `brokeExists()` accurate for `AstVarScope::m_varp` cross-links and +prevents the spurious `m_varp && !m_varp->brokeExists()` failure on the testcase. ## Validation Rebuild `bin/verilator_bin_dbg` and re-run the testcase; the internal broken-link assert should no From 7d684004210005c9f03be6dcb25be9c1b521ba75 Mon Sep 17 00:00:00 2001 From: Matthew Ballance Date: Sat, 20 Dec 2025 21:02:57 +0000 Subject: [PATCH 03/12] test: add regression for debug broken-link assert --- test_regress/t/t_min_uaf_repro_real.py | 13 +++++++ test_regress/t/t_min_uaf_repro_real.sv | 54 ++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 test_regress/t/t_min_uaf_repro_real.py create mode 100644 test_regress/t/t_min_uaf_repro_real.sv diff --git a/test_regress/t/t_min_uaf_repro_real.py b/test_regress/t/t_min_uaf_repro_real.py new file mode 100644 index 000000000..8b0163495 --- /dev/null +++ b/test_regress/t/t_min_uaf_repro_real.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Regression test for min_uaf_repro_real + +import vltest_bootstrap + +test.scenarios('simulator') +test.top_filename = "t/t_min_uaf_repro_real.sv" + +test.compile(verilator_flags2=['--binary', '--timing', '--debug']) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_min_uaf_repro_real.sv b/test_regress/t/t_min_uaf_repro_real.sv new file mode 100644 index 000000000..48ed92e7c --- /dev/null +++ b/test_regress/t/t_min_uaf_repro_real.sv @@ -0,0 +1,54 @@ +// DESCRIPTION: Regression test for scope/var lifetime issue (debug build) +// +// This is derived from testcase/min_uaf_repro_real.sv (from issue reproducer). +// + +package p; + typedef chandle PyObject; + + class uvm_object; + endclass + + class py_object; + function new(PyObject o); + endfunction + endclass + + class pyhdl_uvm_object_rgy; + static pyhdl_uvm_object_rgy m_inst; + + static function pyhdl_uvm_object_rgy inst(); + if (m_inst == null) m_inst = new; + return m_inst; + endfunction + + function PyObject wrap(uvm_object obj); + if (obj == null) return null; + return null; + endfunction + endclass + + class comp_proxy; + virtual function PyObject get_config_object(string name, bit clone = 0); + uvm_object obj; + py_object py_obj; + bit has = 0; + + if (has && obj != null) begin + py_obj = new(pyhdl_uvm_object_rgy::inst().wrap(obj)); + end + + return null; + endfunction + endclass +endpackage + +module t; + import p::*; + + initial begin + comp_proxy cp = new; + void'(cp.get_config_object("x")); + $finish; + end +endmodule From 5089d9dc706b8fa7ac6ddf2c4ae483511a52317f Mon Sep 17 00:00:00 2001 From: github action Date: Sat, 20 Dec 2025 21:04:15 +0000 Subject: [PATCH 04/12] Apply 'make format' --- test_regress/t/t_min_uaf_repro_real.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 test_regress/t/t_min_uaf_repro_real.py diff --git a/test_regress/t/t_min_uaf_repro_real.py b/test_regress/t/t_min_uaf_repro_real.py old mode 100644 new mode 100755 From d4019b9809201b5607d1794d31d330ac118bb2d1 Mon Sep 17 00:00:00 2001 From: Matthew Ballance Date: Sat, 20 Dec 2025 21:07:23 +0000 Subject: [PATCH 05/12] Cleanup Signed-off-by: Matthew Ballance --- BROKEN_LINK_FIX.md | 38 -------------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 BROKEN_LINK_FIX.md diff --git a/BROKEN_LINK_FIX.md b/BROKEN_LINK_FIX.md deleted file mode 100644 index 5422ca98d..000000000 --- a/BROKEN_LINK_FIX.md +++ /dev/null @@ -1,38 +0,0 @@ -# Fix design: Broken link (dangling VarScope) in min_uaf_repro_real - -## Symptom -Running: - -``` -./bin/verilator --cc --timing --debug testcase/min_uaf_repro_real.sv -``` - -fires: - -``` -Broken link in node ... 'm_varp && !m_varp->brokeExists()' @ ... AstVarScope -node: VARSCOPE ... -> VAR __Vtask_wrap__1__Vfuncout [FUNC] BLOCKTEMP -``` - -## Root cause -`AstVarScope` is a cross-link node stored under `AstScope::varsp()`. -In this testcase, the failing node is a `VARSCOPE` pointing at a function temporary -(`__Vtask_wrap__1__Vfuncout [FUNC] BLOCKTEMP`). - -During `--debug` checking, `V3Broken::brokenAll()` builds its "linkable" set by traversing the AST -via structural `op1-op4/nextp` links. -However, some `AstVar` nodes that can be referenced via `AstVarScope::m_varp` are not guaranteed to -be reachable via those structural links at the point `V3Broken` runs (they may be stored via other -non-structural containers/lists). - -As a result, `AstVarScope::brokenGen()` can incorrectly see `m_varp->brokeExists()==false` even -though `m_varp` still points at a valid `AstVar`, and the assert fires. - -## Fix -Teach `V3Broken::brokenAll()` that for `AstVarScope` nodes, the `varp()` target must be treated as -"linkable" too. This makes `brokeExists()` accurate for `AstVarScope::m_varp` cross-links and -prevents the spurious `m_varp && !m_varp->brokeExists()` failure on the testcase. - -## Validation -Rebuild `bin/verilator_bin_dbg` and re-run the testcase; the internal broken-link assert should no -longer occur. From 9229b8ea2f5de94b0120c134b9578989078cc768 Mon Sep 17 00:00:00 2001 From: Matthew Ballance Date: Sat, 20 Dec 2025 21:22:33 +0000 Subject: [PATCH 06/12] Fix UAF in V3Dead by deferring AstVar deletion in deleteTreeIter --- src/V3Ast.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/V3Ast.cpp b/src/V3Ast.cpp index 0f5e5d990..985c93003 100644 --- a/src/V3Ast.cpp +++ b/src/V3Ast.cpp @@ -951,8 +951,22 @@ void AstNode::deleteTreeIter() { if (nodep->m_op3p) nodep->m_op3p->deleteTreeIter(); if (nodep->m_op4p) nodep->m_op4p->deleteTreeIter(); nodep->m_nextp = nullptr; + + bool skipDelete = false; + if (VN_IS(nodep, Var) && nodep->m_backp) { + // If we are deleting a Var that is still linked to a parent (or list), + // it implies we are deleting the parent/list. + // In this case, we must NOT delete the Var yet, because there might be + // AstVarScopes pointing to it (which are not children of the Var). + // We leave the Var unlinked but allocated. V3Dead will later find it + // (via AstVarScope) and delete it properly (at which point backp will be null). + skipDelete = true; + } + nodep->m_backp = nullptr; - nodep->deleteNode(); + if (!skipDelete) { + nodep->deleteNode(); + } } } From b4a131e9b6a0e4564a3d9ce9fc563f283f25ccb4 Mon Sep 17 00:00:00 2001 From: Matthew Ballance Date: Sat, 20 Dec 2025 21:25:05 +0000 Subject: [PATCH 07/12] Update fix Signed-off-by: Matthew Ballance --- src/V3Ast.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/V3Ast.cpp b/src/V3Ast.cpp index 985c93003..dd3aed046 100644 --- a/src/V3Ast.cpp +++ b/src/V3Ast.cpp @@ -962,7 +962,8 @@ void AstNode::deleteTreeIter() { // (via AstVarScope) and delete it properly (at which point backp will be null). skipDelete = true; } - + + nodep->m_backp = nullptr; if (!skipDelete) { nodep->deleteNode(); From 1487f6e98ada6d29ff502fcea62c0b54f8b0f07b Mon Sep 17 00:00:00 2001 From: github action Date: Sat, 20 Dec 2025 21:26:04 +0000 Subject: [PATCH 08/12] Apply 'make format' --- src/V3Ast.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/V3Ast.cpp b/src/V3Ast.cpp index dd3aed046..9e0c95824 100644 --- a/src/V3Ast.cpp +++ b/src/V3Ast.cpp @@ -951,7 +951,7 @@ void AstNode::deleteTreeIter() { if (nodep->m_op3p) nodep->m_op3p->deleteTreeIter(); if (nodep->m_op4p) nodep->m_op4p->deleteTreeIter(); nodep->m_nextp = nullptr; - + bool skipDelete = false; if (VN_IS(nodep, Var) && nodep->m_backp) { // If we are deleting a Var that is still linked to a parent (or list), @@ -962,12 +962,9 @@ void AstNode::deleteTreeIter() { // (via AstVarScope) and delete it properly (at which point backp will be null). skipDelete = true; } - nodep->m_backp = nullptr; - if (!skipDelete) { - nodep->deleteNode(); - } + if (!skipDelete) { nodep->deleteNode(); } } } From aff401ef049fbc51565875a6813ac50baaafe5f1 Mon Sep 17 00:00:00 2001 From: Matthew Ballance Date: Sat, 20 Dec 2025 21:34:36 +0000 Subject: [PATCH 09/12] Update testcase copyright and remove local path comments --- test_regress/t/t_min_uaf_repro_real.sv | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test_regress/t/t_min_uaf_repro_real.sv b/test_regress/t/t_min_uaf_repro_real.sv index 48ed92e7c..1dabd7ae9 100644 --- a/test_regress/t/t_min_uaf_repro_real.sv +++ b/test_regress/t/t_min_uaf_repro_real.sv @@ -1,7 +1,9 @@ -// DESCRIPTION: Regression test for scope/var lifetime issue (debug build) -// -// This is derived from testcase/min_uaf_repro_real.sv (from issue reproducer). +// DESCRIPTION: Verilator: Regression test for scope/var lifetime issue // +// This file ONLY is placed under the Creative Commons Public Domain, for +// any use, without warranty, 2025 by Wilson Snyder. +// SPDX-License-Identifier: CC0-1.0 + package p; typedef chandle PyObject; From e4e8d04b139b09b7f0ada18d23a813ceba195be8 Mon Sep 17 00:00:00 2001 From: Matthew Ballance Date: Sun, 21 Dec 2025 17:08:49 +0000 Subject: [PATCH 10/12] Backed out previous changes to Broken and Ast. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AstVar for __Vtask_wrap__1__Vfuncout was being deleted during V3Const’s constant-folding of the dead if (has && obj != null) block (has is constant 0), but its corresponding AstVarScope (which lives on the scope’s varsp() list, not under the deleted subtree) was left behind, causing both the ASAN UAF and the --debug “Broken link” on AstVarScope::m_varp. In src/V3Const.cpp, when visit(AstNodeIf*) folds a constant-condition if, and when visit(AstExprStmt*) drops the statement side, we now collect any AstVar declarations under the soon-to-be-deleted subtree using foreachAndNext, and unlink/delete the matching AstVarScopes from the current AstScope::varsp() list before pushDeletep(nodep). Signed-off-by: Matthew Ballance --- src/V3Ast.cpp | 14 +------------- src/V3Broken.cpp | 34 +--------------------------------- src/V3Const.cpp | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 46 deletions(-) diff --git a/src/V3Ast.cpp b/src/V3Ast.cpp index 9e0c95824..0f5e5d990 100644 --- a/src/V3Ast.cpp +++ b/src/V3Ast.cpp @@ -951,20 +951,8 @@ void AstNode::deleteTreeIter() { if (nodep->m_op3p) nodep->m_op3p->deleteTreeIter(); if (nodep->m_op4p) nodep->m_op4p->deleteTreeIter(); nodep->m_nextp = nullptr; - - bool skipDelete = false; - if (VN_IS(nodep, Var) && nodep->m_backp) { - // If we are deleting a Var that is still linked to a parent (or list), - // it implies we are deleting the parent/list. - // In this case, we must NOT delete the Var yet, because there might be - // AstVarScopes pointing to it (which are not children of the Var). - // We leave the Var unlinked but allocated. V3Dead will later find it - // (via AstVarScope) and delete it properly (at which point backp will be null). - skipDelete = true; - } - nodep->m_backp = nullptr; - if (!skipDelete) { nodep->deleteNode(); } + nodep->deleteNode(); } } diff --git a/src/V3Broken.cpp b/src/V3Broken.cpp index 06fee3332..d7916185b 100644 --- a/src/V3Broken.cpp +++ b/src/V3Broken.cpp @@ -158,10 +158,6 @@ class BrokenCheckVisitor final : public VNVisitorConst { std::map m_suspectRefs; // Local variables declared in the scope of the current statement std::vector> m_localsStack; - // Number of write references encountered - size_t m_nWriteRefs = 0; - // Number of function calls encountered - size_t m_nCalls = 0; // STATE - for current visit position (use VL_RESTORER) const AstCFunc* m_cfuncp = nullptr; // Current CFunc, if any @@ -229,17 +225,7 @@ private: } // VISITORS void visit(AstNodeAssign* nodep) override { - processEnter(nodep); - iterateConst(nodep->rhsp()); - const size_t nWriteRefs = m_nWriteRefs; - const size_t nCalls = m_nCalls; - iterateConst(nodep->lhsp()); - // TODO: Enable this when #6756 is fixed - // Only check if there are no calls on the LHS, as calls might return an LValue - if (false && v3Global.assertDTypesResolved() && m_nCalls == nCalls) { - UASSERT_OBJ(m_nWriteRefs > nWriteRefs, nodep, "No write refs on LHS of assignment"); - } - processExit(nodep); + processAndIterate(nodep); UASSERT_OBJ(!(v3Global.assertDTypesResolved() && VN_IS(nodep->lhsp(), NodeVarRef) && !VN_AS(nodep->lhsp(), NodeVarRef)->access().isWriteOrRW()), nodep, "Assignment LHS is not an lvalue"); @@ -284,19 +270,6 @@ private: } } } - if (nodep->access().isWriteOrRW()) ++m_nWriteRefs; - } - void visit(AstNodeCCall* nodep) override { - ++m_nCalls; - processAndIterate(nodep); - } - void visit(AstCMethodHard* nodep) override { - ++m_nCalls; - processAndIterate(nodep); - } - void visit(AstNodeFTaskRef* nodep) override { - ++m_nCalls; - processAndIterate(nodep); } void visit(AstCFunc* nodep) override { UASSERT_OBJ(!m_cfuncp, nodep, "Nested AstCFunc"); @@ -388,11 +361,6 @@ void V3Broken::brokenAll(AstNetlist* nodep) { UASSERT_OBJ(nodep->brokenState() != brokenCntCurrent, nodep, "AstNode is already in tree at another location"); if (nodep->maybePointedTo()) s_linkableTable.addLinkable(nodep); - // Some cross-links point at nodes not reachable via op1-op4/nextp. - // AstVarScope::m_varp is one such link; ensure targets are considered linkable. - if (AstVarScope* const vscp = VN_CAST(nodep, VarScope)) { - if (AstNode* const varp = vscp->varp()) s_linkableTable.addLinkable(varp); - } nodep->brokenState(brokenCntCurrent); }); diff --git a/src/V3Const.cpp b/src/V3Const.cpp index 8baeb6f29..a9cd76bcf 100644 --- a/src/V3Const.cpp +++ b/src/V3Const.cpp @@ -3031,6 +3031,21 @@ class ConstVisitor final : public VNVisitor { void visit(AstExprStmt* nodep) override { iterateChildren(nodep); if (!AstNode::afterCommentp(nodep->stmtsp())) { + if (nodep->stmtsp()) { + AstScope* const scopep = VN_CAST(const_cast(m_scopep), Scope); + if (scopep) { + std::unordered_set varps; + nodep->stmtsp()->foreachAndNext([&](AstVar* varp) { varps.insert(varp); }); + if (!varps.empty()) { + for (AstVarScope *vscp = scopep->varsp(), *nextp; vscp; vscp = nextp) { + nextp = VN_AS(vscp->nextp(), VarScope); + if (varps.find(vscp->varp()) != varps.end()) { + VL_DO_DANGLING(pushDeletep(vscp->unlinkFrBack()), vscp); + } + } + } + } + } UINFO(8, "ExprStmt(...) " << nodep << " " << nodep->resultp()); nodep->replaceWith(nodep->resultp()->unlinkFrBack()); VL_DO_DANGLING(pushDeletep(nodep), nodep); @@ -3394,22 +3409,47 @@ class ConstVisitor final : public VNVisitor { iterateChildren(nodep); if (m_doNConst) { if (const AstConst* const constp = VN_CAST(nodep->condp(), Const)) { + auto deleteVarScopesUnder = [&](AstNode* subtreep) { + if (!subtreep) return; + AstScope* const scopep = VN_CAST(const_cast(m_scopep), Scope); + if (!scopep) return; + std::unordered_set varps; + subtreep->foreachAndNext([&](AstVar* varp) { varps.insert(varp); }); + if (varps.empty()) return; + for (AstVarScope *vscp = scopep->varsp(), *nextp; vscp; vscp = nextp) { + nextp = VN_AS(vscp->nextp(), VarScope); + if (varps.find(vscp->varp()) != varps.end()) { + VL_DO_DANGLING(pushDeletep(vscp->unlinkFrBack()), vscp); + } + } + }; + AstNode* keepp = nullptr; + AstNode* delp = nullptr; if (constp->isZero()) { UINFO(4, "IF(0,{any},{x}) => {x}: " << nodep); keepp = nodep->elsesp(); + delp = nodep->thensp(); } else if (!m_doV || constp->isNeqZero()) { // Might be X in Verilog UINFO(4, "IF(!0,{x},{any}) => {x}: " << nodep); keepp = nodep->thensp(); + delp = nodep->elsesp(); } else { UINFO(4, "IF condition is X, retaining: " << nodep); return; } + + // If we delete a branch that contains variable declarations, also delete the + // corresponding varscopes so we don't leave dangling AstVarScope::m_varp pointers. + deleteVarScopesUnder(delp); + if (keepp) { keepp->unlinkFrBackWithNext(); nodep->replaceWith(keepp); } else { nodep->unlinkFrBack(); + deleteVarScopesUnder(nodep->thensp()); + deleteVarScopesUnder(nodep->elsesp()); } VL_DO_DANGLING(pushDeletep(nodep), nodep); From 2112ddf005e779f946393bd53e3557eec8e8ac7d Mon Sep 17 00:00:00 2001 From: Matthew Ballance Date: Sun, 21 Dec 2025 17:39:55 +0000 Subject: [PATCH 11/12] Update copyright on test.py Signed-off-by: Matthew Ballance --- test_regress/t/t_min_uaf_repro_real.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test_regress/t/t_min_uaf_repro_real.py b/test_regress/t/t_min_uaf_repro_real.py index 8b0163495..26f1e6831 100755 --- a/test_regress/t/t_min_uaf_repro_real.py +++ b/test_regress/t/t_min_uaf_repro_real.py @@ -1,5 +1,9 @@ #!/usr/bin/env python3 -# DESCRIPTION: Verilator: Regression test for min_uaf_repro_real +# DESCRIPTION: Verilator: Regression test for scope/var lifetime issue +# +# This file ONLY is placed under the Creative Commons Public Domain, for +# any use, without warranty, 2025 by Wilson Snyder. +# SPDX-License-Identifier: CC0-1.0 import vltest_bootstrap From 8b4401fc762e093f25a605da75cb70727e8dca8c Mon Sep 17 00:00:00 2001 From: Matthew Ballance Date: Sun, 21 Dec 2025 18:03:23 +0000 Subject: [PATCH 12/12] Try #2: Update copyright on test.py Signed-off-by: Matthew Ballance --- test_regress/t/t_min_uaf_repro_real.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test_regress/t/t_min_uaf_repro_real.py b/test_regress/t/t_min_uaf_repro_real.py index 26f1e6831..d80cd5c56 100755 --- a/test_regress/t/t_min_uaf_repro_real.py +++ b/test_regress/t/t_min_uaf_repro_real.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # DESCRIPTION: Verilator: Regression test for scope/var lifetime issue # -# This file ONLY is placed under the Creative Commons Public Domain, for -# any use, without warranty, 2025 by Wilson Snyder. -# SPDX-License-Identifier: CC0-1.0 +# Copyright 2024 by Wilson Snyder. This program is free software; you +# can redistribute it and/or modify it under the terms of either the GNU +# Lesser General Public License Version 3 or the Perl Artistic License +# Version 2.0. +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 import vltest_bootstrap