From d2f62fda93fa003f9b2860c60069dcaaa6649f92 Mon Sep 17 00:00:00 2001 From: em2machine <92717390+em2machine@users.noreply.github.com> Date: Sat, 15 Aug 2026 12:07:01 +0200 Subject: [PATCH] Fix use-after-free of captured interface typedef reference during parameter cloning (#8076) --- src/V3LinkDotIfaceCapture.cpp | 41 ++++++++++++++++------ src/V3LinkDotIfaceCapture.h | 3 ++ src/V3Width.cpp | 17 +++++---- test_regress/t/t_iface_typedef_bits_uaf.py | 16 +++++++++ test_regress/t/t_iface_typedef_bits_uaf.v | 28 +++++++++++++++ 5 files changed, 88 insertions(+), 17 deletions(-) create mode 100755 test_regress/t/t_iface_typedef_bits_uaf.py create mode 100644 test_regress/t/t_iface_typedef_bits_uaf.v diff --git a/src/V3LinkDotIfaceCapture.cpp b/src/V3LinkDotIfaceCapture.cpp index 28c7ea02b..7c230108e 100644 --- a/src/V3LinkDotIfaceCapture.cpp +++ b/src/V3LinkDotIfaceCapture.cpp @@ -254,19 +254,10 @@ LiveNodes collectLiveNodes() { return liveNodes; } -// A live snapshot, when supplied, stops the walk at the first stale back link; -// callers without one fall back to the sentinel guard below. +// Find the module that owns this node; a snapshot, if given, stops the walk at a stale link. AstNodeModule* findOwnerModuleImpl(AstNode* nodep, const LiveNodes* liveNodesp) { for (AstNode* curp = nodep; curp; curp = curp->backp()) { - if (liveNodesp) { - if (!liveNodesp->count(curp)) return nullptr; - } else if (reinterpret_cast(curp) < 0x1000) { - // Legacy callers lack a liveness snapshot; retain the existing guard - // against sentinel values encountered in corrupted backp() chains. - // It cannot prove an arbitrary freed pointer safe - invalidating - // ledger entries at deletion time would make it unnecessary. - return nullptr; - } + if (liveNodesp && !liveNodesp->count(curp)) return nullptr; if (AstNodeModule* const modp = VN_CAST(curp, NodeModule)) return modp; } return nullptr; @@ -365,6 +356,34 @@ void V3LinkDotIfaceCapture::purgeStaleRefs() { nullStaleLedgerRefs(liveNodes); } +void V3LinkDotIfaceCapture::purgeDeletedSubtree(AstNode* nodep) { + if (!s_enabled || s_map.empty() || !nodep) return; + // Only track nodes something could point to, within the subtree being deleted. + std::unordered_set deadps; + nodep->foreach([&](AstNode* np) { + if (np->maybePointedTo()) deadps.insert(np); + }); + for (auto& kv : s_map) { + CapturedEntry& entry = kv.second; + // If the main reference is dying, promote a live one so consumers + // (which skip an entry with a null reference) still retarget the rest. + if (entry.refp && deadps.count(entry.refp)) { + entry.refp = nullptr; + for (AstRefDType*& xrefp : entry.extraRefps) { + if (xrefp && !deadps.count(xrefp)) { + entry.refp = xrefp; + xrefp = nullptr; + break; + } + } + } + // Null every remaining link into the deleted subtree. + entry.foreachLink([&](AstNode*& np) { + if (np && deadps.count(np)) np = nullptr; + }); + } +} + void V3LinkDotIfaceCapture::dumpEntries(const string& label) { UINFO(9, "========== iface capture dumpEntries: " << label << " (entries=" << s_map.size() << ") =========="); diff --git a/src/V3LinkDotIfaceCapture.h b/src/V3LinkDotIfaceCapture.h index bce20aff8..8abb54e93 100644 --- a/src/V3LinkDotIfaceCapture.h +++ b/src/V3LinkDotIfaceCapture.h @@ -201,6 +201,9 @@ public: // Called at pass boundaries before code dereferences ledger pointers. static void purgeStaleRefs(); + // Remove any saved references that point into a subtree, just before it is deleted. + static void purgeDeletedSubtree(AstNode* nodep); + // Debug: dump all captured entries static void dumpEntries(const string& label); diff --git a/src/V3Width.cpp b/src/V3Width.cpp index 401618cbc..ad9003064 100644 --- a/src/V3Width.cpp +++ b/src/V3Width.cpp @@ -2190,6 +2190,11 @@ class WidthVisitor final : public VNVisitor { if (nodep->stmtsp()) nodep->addNextHere(nodep->stmtsp()->unlinkFrBack()); VL_DO_DANGLING(nodep->unlinkFrBack()->deleteTree(), nodep); } + // Delete a subtree after removing any saved references that point into it. + static void deleteTreeCaptured(AstNode* nodep) { + V3LinkDotIfaceCapture::purgeDeletedSubtree(nodep); + nodep->deleteTree(); + } void visit(AstAttrOf* nodep) override { VL_RESTORER(m_attrp); m_attrp = nodep; @@ -2206,7 +2211,7 @@ class WidthVisitor final : public VNVisitor { = (nodep->attrType() == VAttrType::DIM_UNPK_DIMENSIONS ? dim.second : (dim.first + dim.second)); nodep->replaceWith(new AstConst(nodep->fileline(), AstConst::Signed32{}, val)); - VL_DO_DANGLING(nodep->deleteTree(), nodep); + VL_DO_DANGLING(deleteTreeCaptured(nodep), nodep); break; } case VAttrType::DIM_BITS_OR_NUMBER: { @@ -2249,7 +2254,7 @@ class WidthVisitor final : public VNVisitor { case VAttrType::DIM_LOW: { AstNode* const newp = new AstConst(nodep->fileline(), AstConst::Signed32{}, 0); nodep->replaceWith(newp); - VL_DO_DANGLING(nodep->deleteTree(), nodep); + VL_DO_DANGLING(deleteTreeCaptured(nodep), nodep); break; } case VAttrType::DIM_RIGHT: @@ -2271,7 +2276,7 @@ class WidthVisitor final : public VNVisitor { AstNodeExpr* const newp = new AstConst(nodep->fileline(), AstConst::Signed32{}, -1); nodep->replaceWith(newp); - VL_DO_DANGLING(nodep->deleteTree(), nodep); + VL_DO_DANGLING(deleteTreeCaptured(nodep), nodep); break; } case VAttrType::DIM_BITS: { @@ -2304,14 +2309,14 @@ class WidthVisitor final : public VNVisitor { AstConst* const newp = dimensionValue(nodep->fileline(), baseDTypep, nodep->attrType(), dim); nodep->replaceWith(newp); - VL_DO_DANGLING(nodep->deleteTree(), nodep); + VL_DO_DANGLING(deleteTreeCaptured(nodep), nodep); } } else if (VN_IS(nodep->dimp(), Const)) { const int dim = VN_AS(nodep->dimp(), Const)->toSInt(); AstConst* const newp = dimensionValue(nodep->fileline(), dtypep, nodep->attrType(), dim); nodep->replaceWith(newp); - VL_DO_DANGLING(nodep->deleteTree(), nodep); + VL_DO_DANGLING(deleteTreeCaptured(nodep), nodep); } else { // Need a runtime lookup table. Yuk. UASSERT_OBJ(nodep->fromp() && dtypep, nodep, "Unsized expression"); AstVar* const varp = dimensionVarp(dtypep, nodep->attrType(), msbdim); @@ -2319,7 +2324,7 @@ class WidthVisitor final : public VNVisitor { AstNodeExpr* const newp = new AstArraySel{nodep->fileline(), newVarRefDollarUnit(varp), dimp}; nodep->replaceWith(newp); - VL_DO_DANGLING(nodep->deleteTree(), nodep); + VL_DO_DANGLING(deleteTreeCaptured(nodep), nodep); } } break; diff --git a/test_regress/t/t_iface_typedef_bits_uaf.py b/test_regress/t/t_iface_typedef_bits_uaf.py new file mode 100755 index 000000000..363cbe649 --- /dev/null +++ b/test_regress/t/t_iface_typedef_bits_uaf.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# 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-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +test.lint() + +test.passes() diff --git a/test_regress/t/t_iface_typedef_bits_uaf.v b/test_regress/t/t_iface_typedef_bits_uaf.v new file mode 100644 index 000000000..f114006a4 --- /dev/null +++ b/test_regress/t/t_iface_typedef_bits_uaf.v @@ -0,0 +1,28 @@ +// DESCRIPTION: Verilator: interface typedef $bits() capture use-after-free +// +// UAF: the $bits fold computes the correct constant +// before freeing, so no value is wrong. It therefore fails under +// --enable-dev-asan (heap-use-after-free), and -- once the findOwnerModule +// address guard is removed -- as a deterministic SIGSEGV in a --debug build. +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +typedef struct packed {int unsigned W;} cfg_t; + +interface types_if #(parameter cfg_t cfg = '{default: 0}); + typedef logic [cfg.W-1:0] rq_t; +endinterface + +module body #(parameter cfg_t cfg = '{default: 0}); + types_if #(cfg) types(); + localparam int DW = $bits(types.rq_t); // AstAttrOf(DIM_BITS) over captured ref + logic [DW-1:0] v; + initial v = '0; +endmodule + +module t; + body #('{W: 8}) b0(); + body #('{W: 16}) b1(); +endmodule