Fix use-after-free in V3LinkDotIfaceCapture (#7943)

This commit is contained in:
Nick Brereton 2026-08-03 10:55:30 -04:00 committed by GitHub
parent 3c47e5735a
commit 1356c15e44
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 354 additions and 286 deletions

View File

@ -36,14 +36,14 @@
// 3. TARGET RESOLUTION (finalizeIfaceCapture, after V3Param):
// Runs after all cloning is complete and cell pointers are wired
// to the correct interface clones. For each entry, walks cellPath
// starting from the entry's owner module (using findOwnerModule(refp)
// for clone entries) to find the correct target module, then locates
// the PARAMTYPEDTYPE / TYPEDEF by name and applies it to the REFDTYPE.
// starting from the stored owner module to find the correct target,
// then uses targetKind and the captured name to apply the replacement
// without inspecting the REFDTYPE's inherited target pointers.
// ** This is the ONLY place that resolves targets and mutates AST. **
//
// KEY INVARIANT: The path {ownerModName, refName, cellPath, cloneCellPath}
// is the sole identity. No clonep(), no pointer matching. The path IS
// the disambiguation.
// plus targetKind is the stable identity. Inherited target pointers are
// not used for final resolution.
//
// Template entries have cloneCellPath = ""; clone entries get it set by
// propagateClone. TemplateKey (ownerModName, refName, cellPath) matches
@ -87,7 +87,6 @@ void V3LinkDotIfaceCapture::reset() {
namespace {
struct StmtNameMap final {
std::unordered_map<string, std::vector<AstNode*>> m_byName;
std::unordered_map<string, std::vector<AstNodeDType*>> m_byPrettyName;
};
std::unordered_map<AstNodeModule*, StmtNameMap> s_moduleCache;
@ -98,10 +97,6 @@ const StmtNameMap& getOrBuild(AstNodeModule* modp) {
for (AstNode* stmtp = modp->stmtsp(); stmtp; stmtp = stmtp->nextp()) {
const string& nm = stmtp->name();
if (!nm.empty()) cache.m_byName[nm].push_back(stmtp);
if (AstNodeDType* const dtp = VN_CAST(stmtp, NodeDType)) {
const string pn = dtp->prettyName();
if (!pn.empty()) cache.m_byPrettyName[pn].push_back(dtp);
}
}
return cache;
}
@ -187,12 +182,40 @@ AstParamTypeDType* V3LinkDotIfaceCapture::findParamTypeInModule(AstNodeModule* m
return resultp;
}
AstNodeDType* V3LinkDotIfaceCapture::findDTypeByPrettyName(AstNodeModule* modp,
const string& prettyName) {
const StmtNameMap& cache = getOrBuild(modp);
const auto it = cache.m_byPrettyName.find(prettyName);
if (it == cache.m_byPrettyName.end()) return nullptr;
return it->second.front();
bool V3LinkDotIfaceCapture::retargetRefToModule(const CapturedEntry& entry,
AstNodeModule* targetModp) {
if (!entry.refp || !targetModp) return false;
if (entry.targetKind == TargetKind::PARAM_TYPE) {
AstParamTypeDType* const paramTypep
= findParamTypeInModule(targetModp, entry.refp->name());
if (!paramTypep) return false;
const auto retarget = [&](AstRefDType* refp) {
if (!refp) return;
refp->refDTypep(paramTypep);
refp->dtypep(paramTypep);
};
retarget(entry.refp);
for (AstRefDType* const refp : entry.extraRefps) retarget(refp);
return true;
}
AstTypedef* const typedefp = findTypedefInModule(targetModp, entry.refp->name());
if (!typedefp) return false;
AstNodeDType* const dtypep = typedefp->subDTypep();
const auto retarget = [&](AstRefDType* refp) {
if (!refp) return;
refp->typedefp(typedefp);
// An incomplete typedef is still a successful name resolution, but
// must not erase type links that a later width pass can complete.
if (dtypep) {
refp->refDTypep(dtypep);
refp->dtypep(dtypep);
}
};
retarget(entry.refp);
for (AstRefDType* const refp : entry.extraRefps) retarget(refp);
return true;
}
AstNodeModule* V3LinkDotIfaceCapture::findCloneViaHierarchy(AstNodeModule* containingModp,
@ -218,18 +241,60 @@ AstNodeModule* V3LinkDotIfaceCapture::findCloneViaHierarchy(AstNodeModule* conta
return nullptr;
}
namespace {
using LiveNodes = std::unordered_set<const AstNode*>;
// A scoped snapshot of every node currently in the tree. V3Broken::isLinkable()
// cannot serve this role: its table is populated only while V3Broken::brokenAll()
// runs and is cleared before it returns, and brokenAll() would itself assert on
// the dangling cross-links this pass has yet to repair.
LiveNodes collectLiveNodes() {
LiveNodes liveNodes;
v3Global.rootp()->foreach([&](AstNode* nodep) { liveNodes.insert(nodep); });
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.
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<uintptr_t>(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 (AstNodeModule* const modp = VN_CAST(curp, NodeModule)) return modp;
}
return nullptr;
}
AstNodeModule* findOwnerModuleIfLive(AstNode* nodep, const LiveNodes& liveNodes) {
return findOwnerModuleImpl(nodep, &liveNodes);
}
bool moduleMatchesOwner(const AstNodeModule* modp, const string& ownerName) {
if (!modp || ownerName.empty()) return false;
return modp->name() == ownerName || modp->origName() == ownerName;
}
} // namespace
int V3LinkDotIfaceCapture::fixDeadRefs(AstRefDType* refp, AstNodeModule* containingModp,
const char* location) {
const char* location, const LiveNodes& liveNodes) {
int fixed = 0;
// Fix typedefp pointing to dead module
if (refp->typedefp()) {
AstNodeModule* const typedefModp = findOwnerModule(refp->typedefp());
AstTypedef* const oldTypedefp = refp->typedefp();
if (oldTypedefp && liveNodes.count(oldTypedefp)) {
AstNodeModule* const typedefModp = findOwnerModuleIfLive(oldTypedefp, liveNodes);
if (typedefModp && typedefModp->dead()) {
AstNodeModule* cloneModp = nullptr;
if (containingModp) { cloneModp = findCloneViaHierarchy(containingModp, typedefModp); }
if (cloneModp) {
const string& tdName = refp->typedefp()->name();
const string& tdName = oldTypedefp->name();
if (AstTypedef* const newTdp = findTypedefInModule(cloneModp, tdName)) {
UINFO(9, "iface capture finalizeCapture ("
<< location << "): fixing typedefp refp=" << refp << " dead="
@ -241,75 +306,24 @@ int V3LinkDotIfaceCapture::fixDeadRefs(AstRefDType* refp, AstNodeModule* contain
}
}
// Fix refDTypep pointing to dead module
if (refp->refDTypep()) {
AstNodeModule* const targetModp = findOwnerModule(refp->refDTypep());
if (targetModp && targetModp->dead()) {
AstNodeModule* cloneModp = nullptr;
if (containingModp) { cloneModp = findCloneViaHierarchy(containingModp, targetModp); }
bool foundByName = false;
if (cloneModp) {
const string& targetName = refp->refDTypep()->prettyName();
if (AstNodeDType* const newDtp = findDTypeByPrettyName(cloneModp, targetName)) {
UINFO(9, "iface capture finalizeCapture ("
<< location << "): fixing refDTypep refp=" << refp
<< " dead=" << targetModp->name() << " -> " << cloneModp->name());
refp->refDTypep(newDtp);
++fixed;
foundByName = true;
}
}
// If name-based search failed, try to derive refDTypep from
// the already-fixed typedefp chain. The typedefp was fixed
// above to point to the clone's typedef, so its subDTypep()
// returns a live dtype (type-table entry or clone-owned).
// This avoids setting refDTypep to nullptr which would force
// V3Width to re-walk the dtype tree under TYPETABLE where
// module provenance is lost, triggering spurious warnings.
if (!foundByName) {
AstNodeDType* derivedp = nullptr;
if (refp->typedefp() && refp->typedefp()->subDTypep()) {
derivedp = refp->typedefp()->subDTypep();
AstNodeModule* const derivedOwnerp = findOwnerModule(derivedp);
if (derivedOwnerp && derivedOwnerp->dead()) { derivedp = nullptr; }
}
UINFO(9, "iface capture finalizeCapture ("
<< location << "): deriving refDTypep from typedefp refp=" << refp
<< " dead=" << targetModp->name() << " derived=" << derivedp);
refp->refDTypep(derivedp);
++fixed;
}
}
// refDTypep is retargeted for captured refs by resolveCapturedRefs, and no
// non-captured ref resolves refDTypep into a template that then dies, so it
// never survives pointing at a dead module here (verifyNoDeadRefs re-checks).
AstNodeDType* const oldRefDTypep = refp->refDTypep();
if (oldRefDTypep && liveNodes.count(oldRefDTypep)) {
AstNodeModule* const targetModp = findOwnerModuleIfLive(oldRefDTypep, liveNodes);
UASSERT_OBJ(!targetModp || !targetModp->dead(), refp,
"refDTypep of '" << refp->prettyNameQ() << "' points to dead module '"
<< (targetModp ? targetModp->name() : "") << "'");
}
// Fix base-class dtypep() - V3Broken checks this pointer, and V3Width
// may have set it to a node in the dead template module. Derive from
// the (already fixed) typedefp chain when possible.
if (refp->dtypep()) {
AstNodeModule* const dtOwnerp = findOwnerModule(refp->dtypep());
if (dtOwnerp && dtOwnerp->dead()) {
AstNodeDType* newDtp = nullptr;
// Derive from the fixed typedef's subDTypep. This always succeeds
// because the typedefp was fixed above to point to a clone's typedef
// whose subDTypep is a live type-table entry or clone-owned dtype.
// If this fires, either typedefp was not fixed or subDTypep is stale.
// Dump refp->typedefp() and dtOwnerp to diagnose.
if (refp->typedefp() && refp->typedefp()->subDTypep()) {
newDtp = refp->typedefp()->subDTypep();
AstNodeModule* const newDtOwnerp = findOwnerModule(newDtp);
if (newDtOwnerp && newDtOwnerp->dead()) newDtp = nullptr;
}
UASSERT_OBJ(newDtp, refp,
"fixDeadRefs dtypep: could not derive live dtypep for "
<< refp->prettyNameQ() << " dead owner=" << dtOwnerp->name()
<< " typedefp="
<< (refp->typedefp() ? refp->typedefp()->name() : "<null>"));
UINFO(9, "iface capture finalizeCapture ("
<< location << "): fixing dtypep refp=" << refp
<< " dead=" << dtOwnerp->name() << " -> " << newDtp);
refp->dtypep(newDtp);
++fixed;
}
// dtypep (checked later by V3Broken) likewise never points at a dead module.
AstNodeDType* const oldDTypep = refp->dtypep();
if (oldDTypep && liveNodes.count(oldDTypep)) {
AstNodeModule* const dtOwnerp = findOwnerModuleIfLive(oldDTypep, liveNodes);
UASSERT_OBJ(!dtOwnerp || !dtOwnerp->dead(), refp,
"dtypep of '" << refp->prettyNameQ() << "' points to dead module '"
<< (dtOwnerp ? dtOwnerp->name() : "") << "'");
}
return fixed;
@ -332,26 +346,23 @@ AstNodeModule* V3LinkDotIfaceCapture::findLiveCloneOf(AstNodeModule* deadTargetM
}
AstNodeModule* V3LinkDotIfaceCapture::findOwnerModule(AstNode* nodep) {
for (AstNode* curp = nodep; curp; curp = curp->backp()) {
// Guard against corrupted backp() chains (e.g. freed memory,
// low addresses like 0x1) from nodes unlinked by linkDotParamed.
if (reinterpret_cast<uintptr_t>(curp) < 0x1000) return nullptr;
if (AstNodeModule* const modp = VN_CAST(curp, NodeModule)) return modp;
return findOwnerModuleImpl(nodep, nullptr);
}
void V3LinkDotIfaceCapture::nullStaleLedgerRefs(const std::unordered_set<const AstNode*>& live) {
for (auto& kv : s_map) {
kv.second.foreachLink([&](AstNode*& nodep) {
if (nodep && !live.count(nodep)) nodep = nullptr;
});
}
return nullptr;
}
void V3LinkDotIfaceCapture::purgeStaleRefs() {
if (!s_enabled || s_map.empty() || !v3Global.rootp()) return;
// Collect every live AstNode* in the AST so we can detect stale pointers
// in the ledger (refp, ownerModp, typedefp, paramTypep, etc.).
std::unordered_set<const AstNode*> liveNodes;
v3Global.rootp()->foreach([&](AstNode* np) { liveNodes.insert(np); });
for (auto& kv : s_map) {
kv.second.foreachLink([&](AstNode*& nodep) {
if (nodep && !liveNodes.count(nodep)) nodep = nullptr;
});
}
const LiveNodes liveNodes = collectLiveNodes();
nullStaleLedgerRefs(liveNodes);
}
void V3LinkDotIfaceCapture::dumpEntries(const string& label) {
@ -404,10 +415,18 @@ void V3LinkDotIfaceCapture::add(AstRefDType* refp, const string& cellPath,
<< refp->name() << " cellPath='" << cellPath << "'" << " ownerMod="
<< ownerModName << " extraRefps.size=" << it->second.extraRefps.size());
} else {
s_map[key] = CapturedEntry{
CaptureType::IFACE, refp, cellPath,
/*cloneCellPath=*/"",
/*origClassp=*/nullptr, ownerModp, typedefp, nullptr, tdOwnerName, ifacePortVarp, {}};
s_map[key] = CapturedEntry{CaptureType::IFACE,
TargetKind::TYPEDEF,
refp,
cellPath,
/*cloneCellPath=*/"",
/*origClassp=*/nullptr,
ownerModp,
typedefp,
nullptr,
tdOwnerName,
ifacePortVarp,
{}};
UINFO(9, "iface capture add: refp=" << refp->name() << " cellPath='" << cellPath << "'"
<< " ownerMod=" << ownerModName << " typedefp="
<< (typedefp ? typedefp->name() : "<null>")
@ -428,9 +447,18 @@ void V3LinkDotIfaceCapture::addClass(AstRefDType* refp, AstClass* origClassp,
UASSERT_OBJ(!cellPath.empty(), origClassp, "addClass() produced empty cellPath");
const string ownerModName = ownerModp->name();
const CaptureKey key{ownerModName, refp->name(), cellPath, ""};
s_map[key] = CapturedEntry{CaptureType::CLASS, refp, cellPath,
/*cloneCellPath=*/"", origClassp, ownerModp, typedefp, nullptr,
tdOwnerName, nullptr, {}};
s_map[key] = CapturedEntry{CaptureType::CLASS,
TargetKind::TYPEDEF,
refp,
cellPath,
/*cloneCellPath=*/"",
origClassp,
ownerModp,
typedefp,
nullptr,
tdOwnerName,
nullptr,
{}};
UINFO(9, "iface capture addClass: refp=" << refp->name() << " cellPath='" << cellPath << "'"
<< " ownerMod="
<< (ownerModp ? ownerModp->name() : "<null>"));
@ -518,8 +546,10 @@ AstNodeModule* V3LinkDotIfaceCapture::followCellPath(AstNodeModule* startModp,
// after all cell pointers are wired to the correct interface clones.
// See header ARCHITECTURE comment for the full picture.
void V3LinkDotIfaceCapture::propagateClone(const TemplateKey& tkey, AstRefDType* newRefp,
AstNodeModule* newOwnerModp,
const string& cloneCellPath) {
UASSERT(newRefp, "propagateClone() called with null newRefp");
UASSERT(newOwnerModp, "propagateClone() called with null newOwnerModp");
// Find the template entry by exact key. The entry was captured during
// the primary LinkDot pass, so it must exist. If this fires, either the
// capture was missed or the key components (ownerModName, refName,
@ -536,6 +566,7 @@ void V3LinkDotIfaceCapture::propagateClone(const TemplateKey& tkey, AstRefDType*
// where cell pointers are already wired to the correct interface clones.
CapturedEntry newEntry = it->second;
newEntry.refp = newRefp;
newEntry.ownerModp = newOwnerModp;
newEntry.cellPath = tkey.cellPath;
newEntry.cloneCellPath = cloneCellPath;
newEntry.clearStaleRefs();
@ -696,6 +727,7 @@ void V3LinkDotIfaceCapture::captureInnerParamTypeRefs(AstParamTypeDType* paramTy
<< innerRefp << " refDTypep owner=" << refOwnerModp->name()
<< " nestedCellName='" << nestedCellName << "'");
s_map[innerKey] = CapturedEntry{CaptureType::IFACE,
TargetKind::TYPEDEF,
innerRefp,
nestedCellName.empty() ? cellPath : nestedCellName,
/*cloneCellPath=*/"",
@ -746,11 +778,18 @@ void V3LinkDotIfaceCapture::addParamType(AstRefDType* refp, const string& cellPa
<< refp->name() << " cellPath='" << cellPath << "'" << " ownerMod="
<< ownerModName << " extraRefps.size=" << it->second.extraRefps.size());
} else {
s_map[key]
= CapturedEntry{CaptureType::IFACE, refp, cellPath,
/*cloneCellPath=*/"",
/*origClassp=*/nullptr, ownerModp, nullptr, paramTypep, ptOwnerName,
ifacePortVarp, {}};
s_map[key] = CapturedEntry{CaptureType::IFACE,
TargetKind::PARAM_TYPE,
refp,
cellPath,
/*cloneCellPath=*/"",
/*origClassp=*/nullptr,
ownerModp,
nullptr,
paramTypep,
ptOwnerName,
ifacePortVarp,
{}};
}
// Also capture REFDTYPEs inside the PARAMTYPEDTYPE's subDTypep chain.
@ -766,6 +805,7 @@ void V3LinkDotIfaceCapture::addParamType(AstRefDType* refp, const string& cellPa
// Handles both AstRefDType (direct typedef references) and AstMemberDType
// (struct/union member types) in a single traversal for efficiency.
class TypeTableDeadRefVisitor final : public VNVisitor {
const LiveNodes& m_liveNodes;
int m_fixed = 0;
void visit(AstRefDType* refp) override {
@ -776,26 +816,27 @@ class TypeTableDeadRefVisitor final : public VNVisitor {
AstNodeModule* deadTargetModp = nullptr;
// Check BOTH typedefp and refDTypep for dead owners.
// Either (or both) may point to a dead module.
if (refp->typedefp()) {
AstNodeModule* const tdOwnerp
= V3LinkDotIfaceCapture::findOwnerModule(refp->typedefp());
if (refp->typedefp() && m_liveNodes.count(refp->typedefp())) {
AstNodeModule* const tdOwnerp = findOwnerModuleIfLive(refp->typedefp(), m_liveNodes);
if (tdOwnerp && tdOwnerp->dead()) deadTargetModp = tdOwnerp;
}
if (!deadTargetModp && refp->refDTypep()) {
AstNodeModule* const rdOwnerp
= V3LinkDotIfaceCapture::findOwnerModule(refp->refDTypep());
if (!deadTargetModp && refp->refDTypep() && m_liveNodes.count(refp->refDTypep())) {
AstNodeModule* const rdOwnerp = findOwnerModuleIfLive(refp->refDTypep(), m_liveNodes);
if (rdOwnerp && rdOwnerp->dead()) deadTargetModp = rdOwnerp;
}
if (deadTargetModp) {
V3LinkDotIfaceCapture::findLiveCloneOf(deadTargetModp, &containingModp);
}
m_fixed += V3LinkDotIfaceCapture::fixDeadRefs(refp, containingModp, "type table");
m_fixed
+= V3LinkDotIfaceCapture::fixDeadRefs(refp, containingModp, "type table", m_liveNodes);
}
void visit(AstMemberDType* memberp) override {
iterateChildren(memberp);
if (!memberp->dtypep()) return;
AstNodeModule* const dtOwnerp = V3LinkDotIfaceCapture::findOwnerModule(memberp->dtypep());
UASSERT_OBJ(m_liveNodes.count(memberp->dtypep()), memberp,
"MemberDType has a dangling dtypep");
AstNodeModule* const dtOwnerp = findOwnerModuleIfLive(memberp->dtypep(), m_liveNodes);
if (!dtOwnerp || !dtOwnerp->dead()) return;
// Try to find the clone of the dead module
AstNodeModule* const cloneModp = V3LinkDotIfaceCapture::findLiveCloneOf(dtOwnerp);
@ -830,29 +871,33 @@ class TypeTableDeadRefVisitor final : public VNVisitor {
public:
int fixed() const { return m_fixed; }
explicit TypeTableDeadRefVisitor(AstNode* nodep) { iterate(nodep); }
TypeTableDeadRefVisitor(AstNode* nodep, const LiveNodes& liveNodes)
: m_liveNodes{liveNodes} {
iterate(nodep);
}
};
int V3LinkDotIfaceCapture::fixDeadRefsInTypeTable() {
int V3LinkDotIfaceCapture::fixDeadRefsInTypeTable(const LiveNodes& liveNodes) {
if (!v3Global.rootp()->typeTablep()) return 0;
const TypeTableDeadRefVisitor visitor{v3Global.rootp()->typeTablep()};
const TypeTableDeadRefVisitor visitor{v3Global.rootp()->typeTablep(), liveNodes};
return visitor.fixed();
}
int V3LinkDotIfaceCapture::fixDeadRefsInModules() {
int V3LinkDotIfaceCapture::fixDeadRefsInModules(const LiveNodes& liveNodes) {
int fixed = 0;
for (AstNode* nodep = v3Global.rootp()->modulesp(); nodep; nodep = nodep->nextp()) {
if (AstNodeModule* const modp = VN_CAST(nodep, NodeModule)) {
if (modp->dead()) continue;
const string modName = modp->name();
modp->foreach(
[&](AstRefDType* refp) { fixed += fixDeadRefs(refp, modp, modName.c_str()); });
modp->foreach([&](AstRefDType* refp) {
fixed += fixDeadRefs(refp, modp, modName.c_str(), liveNodes);
});
}
}
return fixed;
}
int V3LinkDotIfaceCapture::fixWrongCloneRefs() {
int V3LinkDotIfaceCapture::resolveCapturedRefs() {
int fixed = 0;
// TARGET RESOLUTION - the ONLY place that resolves targets and
@ -865,78 +910,58 @@ int V3LinkDotIfaceCapture::fixWrongCloneRefs() {
forEach([&](const CapturedEntry& entry) {
AstRefDType* const refp = entry.refp;
if (!refp) return;
// For clone entries the stored ownerModp is the template (stale cells).
// Use the actual module containing the REFDTYPE - its cells are wired
// to the correct interface clones by this point.
// findOwnerModule handles corrupted backp() chains gracefully.
AstNodeModule* const ownerModp
= !entry.cloneCellPath.empty() ? findOwnerModule(refp) : entry.ownerModp;
// Parameterized class typedefs are relinked by
// ParamClassRefDTypeRelinkVisitor. Their class name is not a cell path.
if (entry.captureType == CaptureType::CLASS) return;
AstNodeModule* const ownerModp = entry.ownerModp;
if (!ownerModp || ownerModp->dead() || VN_IS(ownerModp, Package)) return;
AstNodeModule* const rdOwnerBefore
= (refp->refDTypep() ? findOwnerModule(refp->refDTypep()) : nullptr);
UINFO(9,
"finalizeIfaceCapture Phase3 entry: refp="
<< refp->name() << " (" << cvtToHex(refp) << ")"
<< " ownerMod=" << ownerModp->name() << " (dead=" << ownerModp->dead() << ")"
<< " storedOwnerMod=" << (entry.ownerModp ? entry.ownerModp->name() : "<null>")
<< " cellPath='" << entry.cellPath << "' cloneCellPath='" << entry.cloneCellPath
<< "' typedefp=" << (refp->typedefp() ? refp->typedefp()->name() : "<null>")
<< " refDTypep=" << (refp->refDTypep() ? refp->refDTypep()->name() : "<null>")
<< " refDTypepOwner=" << (rdOwnerBefore ? rdOwnerBefore->name() : "<null>")
<< " refDTypepDead=" << (rdOwnerBefore ? rdOwnerBefore->dead() : 0));
<< "' targetKind="
<< (entry.targetKind == TargetKind::PARAM_TYPE ? "param type" : "typedef"));
// Determine the correct target module using cellPath
// Prefer the owner itself when its stable template identity matches the
// captured target owner. Otherwise resolve and validate the cell path.
AstNodeModule* correctModp = nullptr;
if (!entry.cellPath.empty()) {
if (moduleMatchesOwner(ownerModp, entry.typedefOwnerModName)) {
correctModp = ownerModp;
} else {
// A non-matching owner always carries a cell path to the target owner.
UASSERT_OBJ(!entry.cellPath.empty(), refp,
"captured ref '"
<< refp->prettyNameQ() << "' owner '" << ownerModp->prettyNameQ()
<< "' does not match target owner '" << entry.typedefOwnerModName
<< "' and has no cell path");
correctModp = followCellPath(ownerModp, entry.cellPath);
UINFO(9, " followCellPath('"
<< ownerModp->name() << "', '" << entry.cellPath
<< "') = " << (correctModp ? correctModp->name() : "<null>")
<< (correctModp ? (correctModp->dead() ? " (DEAD)" : " (live)") : ""));
if (correctModp && correctModp->dead()) { correctModp = nullptr; }
UASSERT_OBJ(correctModp && !correctModp->dead()
&& moduleMatchesOwner(correctModp, entry.typedefOwnerModName),
refp,
"captured ref '" << refp->prettyNameQ() << "' cell path '"
<< entry.cellPath << "' did not resolve to live owner '"
<< entry.typedefOwnerModName << "'");
}
// Proactive target resolution: when cellPath resolved to a valid
// correctModp, find the PARAMTYPEDTYPE or TYPEDEF by name and apply.
if (correctModp) {
const string& refName = refp->name();
bool resolved = false;
if (AstParamTypeDType* const ptdp = findParamTypeInModule(correctModp, refName)) {
refp->refDTypep(ptdp);
refp->user3(true);
resolved = true;
UINFO(9, "finalizeIfaceCapture Phase3: resolved paramTypep '"
<< refName << "' in " << correctModp->name() << " for refp in "
<< ownerModp->name() << " cloneCellPath='" << entry.cloneCellPath
<< "'");
} else if (AstTypedef* const tdp = findTypedefInModule(correctModp, refName)) {
refp->typedefp(tdp);
refp->user3(true);
resolved = true;
UINFO(9, "finalizeIfaceCapture Phase3: resolved typedefp '"
<< refName << "' in " << correctModp->name() << " for refp in "
<< ownerModp->name() << " cloneCellPath='" << entry.cloneCellPath
<< "'");
}
if (resolved) {
++fixed;
return;
}
}
// Note: the structural disambiguation infrastructure (collectReachable,
// findCorrectClone, wrong-clone fixup blocks) was removed. All captured
// entries have non-empty cellPath, and disambiguateTarget always returns
// nullptr through the "cellPath unresolved" path. The wrong-clone fixup
// blocks were dead code - CI-CD with v3fatalSrc asserts confirmed this.
// Unresolved entries are fixed by fixDeadRefs in a later phase.
UASSERT_OBJ(
retargetRefToModule(entry, correctModp), refp,
"could not retarget captured "
<< (entry.targetKind == TargetKind::PARAM_TYPE ? "parameter type " : "typedef ")
<< refp->prettyNameQ() << " in " << correctModp->prettyNameQ());
refp->user3(true);
++fixed;
});
return fixed;
}
void V3LinkDotIfaceCapture::verifyNoDeadRefs() {
void V3LinkDotIfaceCapture::verifyNoDeadRefs(const LiveNodes& liveNodes) {
// Assert: no REFDTYPE in any live module should have typedefp or refDTypep
// pointing to a dead module.
for (AstNode* nodep = v3Global.rootp()->modulesp(); nodep; nodep = nodep->nextp()) {
@ -944,40 +969,26 @@ void V3LinkDotIfaceCapture::verifyNoDeadRefs() {
if (modp->dead()) continue;
modp->foreach([&](AstRefDType* refp) {
if (refp->typedefp()) {
AstNodeModule* const ownerModp = findOwnerModule(refp->typedefp());
// Diagnostic block: only entered when fixup logic has a bug
// and leaves a typedefp pointing to a dead module.
if (ownerModp && ownerModp->dead()) { // LCOV_EXCL_START
bool inLedger = false;
forEach([&](const CapturedEntry& e) {
if (e.refp == refp) inLedger = true;
});
UINFO(9, "VERIFY FAIL typedefp: refp="
<< refp->name() << " (" << cvtToHex(refp) << ")" << " in mod="
<< modp->name() << " typedefp->owner=" << ownerModp->name()
<< " inLedger=" << inLedger);
} // LCOV_EXCL_STOP
UASSERT_OBJ(!ownerModp || !ownerModp->dead(), refp, // LCOV_EXCL_LINE
UASSERT_OBJ(liveNodes.count(refp->typedefp()), refp,
"REFDTYPE '" << refp->prettyNameQ() << "' in live module '"
<< modp->prettyNameQ()
<< "' has a dangling typedefp");
AstNodeModule* const ownerModp
= findOwnerModuleIfLive(refp->typedefp(), liveNodes);
UASSERT_OBJ(!ownerModp || !ownerModp->dead(), refp,
"REFDTYPE '" << refp->prettyNameQ() << "' in live module '"
<< modp->prettyNameQ()
<< "' has typedefp pointing to dead module '"
<< ownerModp->prettyNameQ() << "'");
}
if (refp->refDTypep()) {
AstNodeModule* const ownerModp = findOwnerModule(refp->refDTypep());
// Diagnostic block: only entered when fixup logic has a bug
// and leaves a refDTypep pointing to a dead module.
if (ownerModp && ownerModp->dead()) { // LCOV_EXCL_START
bool inLedger = false;
forEach([&](const CapturedEntry& e) {
if (e.refp == refp) inLedger = true;
});
UINFO(9, "VERIFY FAIL refDTypep: refp="
<< refp->name() << " (" << cvtToHex(refp) << ")" << " in mod="
<< modp->name() << " refDTypep->owner=" << ownerModp->name()
<< " inLedger=" << inLedger);
} // LCOV_EXCL_STOP
UASSERT_OBJ(!ownerModp || !ownerModp->dead(), refp, // LCOV_EXCL_LINE
UASSERT_OBJ(liveNodes.count(refp->refDTypep()), refp,
"REFDTYPE '" << refp->prettyNameQ() << "' in live module '"
<< modp->prettyNameQ()
<< "' has a dangling refDTypep");
AstNodeModule* const ownerModp
= findOwnerModuleIfLive(refp->refDTypep(), liveNodes);
UASSERT_OBJ(!ownerModp || !ownerModp->dead(), refp,
"REFDTYPE '" << refp->prettyNameQ() << "' in live module '"
<< modp->prettyNameQ()
<< "' has refDTypep pointing to dead module '"
@ -991,20 +1002,24 @@ void V3LinkDotIfaceCapture::verifyNoDeadRefs() {
nodep = nodep->nextp()) {
nodep->foreach([&](AstRefDType* refp) {
if (refp->typedefp()) {
AstNodeModule* const ownerModp = findOwnerModule(refp->typedefp());
// Bug-only assertion: fires only if fixup logic fails to
// resolve a type-table typedefp away from a dead module.
UASSERT_OBJ(!ownerModp || !ownerModp->dead(), refp, // LCOV_EXCL_LINE
UASSERT_OBJ(liveNodes.count(refp->typedefp()), refp,
"REFDTYPE '" << refp->prettyNameQ()
<< "' in type table has a dangling typedefp");
AstNodeModule* const ownerModp
= findOwnerModuleIfLive(refp->typedefp(), liveNodes);
UASSERT_OBJ(!ownerModp || !ownerModp->dead(), refp,
"REFDTYPE '"
<< refp->prettyNameQ()
<< "' in type table has typedefp pointing to dead module '"
<< ownerModp->prettyNameQ() << "'");
}
if (refp->refDTypep()) {
AstNodeModule* const ownerModp = findOwnerModule(refp->refDTypep());
// Bug-only assertion: fires only if fixup logic fails to
// resolve a type-table refDTypep away from a dead module.
UASSERT_OBJ(!ownerModp || !ownerModp->dead(), refp, // LCOV_EXCL_LINE
UASSERT_OBJ(liveNodes.count(refp->refDTypep()), refp,
"REFDTYPE '" << refp->prettyNameQ()
<< "' in type table has a dangling refDTypep");
AstNodeModule* const ownerModp
= findOwnerModuleIfLive(refp->refDTypep(), liveNodes);
UASSERT_OBJ(!ownerModp || !ownerModp->dead(), refp,
"REFDTYPE '"
<< refp->prettyNameQ()
<< "' in type table has refDTypep pointing to dead module '"
@ -1021,14 +1036,23 @@ void V3LinkDotIfaceCapture::finalizeIfaceCapture() {
if (!v3Global.rootp()) return;
clearModuleCache(); // Ensure fresh view after all cloning/widthing
const int typeTableFixed = fixDeadRefsInTypeTable();
const int moduleFixed = fixDeadRefsInModules();
// purgeStaleRefs() snapshotted liveness at the end of V3Param::param() and
// discarded it; linkDotParamed, finalizeDeferredParams, linkLValue and linkWith
// have since mutated the tree. Take a fresh snapshot and reuse it here both to
// purge the ledger and to guard every legacy cross-link inspection.
const LiveNodes liveNodes = collectLiveNodes();
nullStaleLedgerRefs(liveNodes);
// Resolve live captured refs from stable path metadata before inspecting
// any inherited target pointers, which may refer to replaced template nodes.
const int capturedFixed = resolveCapturedRefs();
UINFO(4, "finalizeIfaceCapture: structurally resolved " << capturedFixed << " captured refs");
const int typeTableFixed = fixDeadRefsInTypeTable(liveNodes);
const int moduleFixed = fixDeadRefsInModules(liveNodes);
UINFO(4, "finalizeIfaceCapture: fixed " << typeTableFixed << " in type table, " << moduleFixed
<< " in modules (dead refs)");
const int wrongCloneFixed = fixWrongCloneRefs();
UINFO(4, "finalizeIfaceCapture: fixed " << wrongCloneFixed << " wrong-live-clone pointers");
if (debug() >= 9) dumpEntries("after finalizeIfaceCapture");
// Emit statistics for --stats
@ -1046,8 +1070,10 @@ void V3LinkDotIfaceCapture::finalizeIfaceCapture() {
V3Stats::addStat("IfaceCapture, Entries cloned", clones);
V3Stats::addStat("IfaceCapture, Dead refs fixed in type table", typeTableFixed);
V3Stats::addStat("IfaceCapture, Dead refs fixed in modules", moduleFixed);
V3Stats::addStat("IfaceCapture, Wrong-clone refs fixed", wrongCloneFixed);
V3Stats::addStat("IfaceCapture, Captured refs resolved", capturedFixed);
verifyNoDeadRefs();
// Independent debug-only audit of the repairs above; kept separate from the
// repair traversal so it can catch omissions in that repair.
if (debug() >= 9) verifyNoDeadRefs(liveNodes);
reset();
}

View File

@ -28,6 +28,7 @@
#include <functional>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
class VSymEnt;
@ -35,6 +36,7 @@ class VSymEnt;
class V3LinkDotIfaceCapture final {
public:
enum class CaptureType : uint8_t { IFACE, CLASS };
enum class TargetKind : uint8_t { TYPEDEF, PARAM_TYPE };
// Path-based map key: no pointers, only stable strings.
// {ownerModName, refName, cellPath, cloneCellPath} uniquely identifies
@ -70,6 +72,8 @@ public:
struct CapturedEntry final {
CaptureType captureType = CaptureType::IFACE;
// Semantic target identity, retained when template pointers are cleared.
TargetKind targetKind = TargetKind::TYPEDEF;
AstRefDType* refp = nullptr;
string cellPath; // Template path (e.g. "cca_io.tlb_io") - immutable key component
string cloneCellPath; // Instance-specific path (e.g. "cca_io1.tlb_io") - set by
@ -131,19 +135,20 @@ private:
static void clearModuleCache();
static AstIfaceRefDType* ifaceRefFromVarDType(AstNodeDType* dtypep);
static string extractIfacePortName(const string& dotText);
static AstNodeDType* findDTypeByPrettyName(AstNodeModule* modp, const string& prettyName);
static AstNodeModule* findCloneViaHierarchy(AstNodeModule* containingModp,
AstNodeModule* deadTargetModp, int depth = 0);
static AstNodeModule* findLiveCloneOf(AstNodeModule* deadTargetModp,
AstNodeModule** containerp = nullptr);
static int fixDeadRefs(AstRefDType* refp, AstNodeModule* containingModp, const char* location);
static int fixDeadRefs(AstRefDType* refp, AstNodeModule* containingModp, const char* location,
const std::unordered_set<const AstNode*>& liveNodes);
static void captureInnerParamTypeRefs(AstParamTypeDType* paramTypep, AstRefDType* refp,
const string& cellPath, const string& ownerModName,
const string& ptOwnerName);
static int fixDeadRefsInTypeTable();
static int fixDeadRefsInModules();
static int fixWrongCloneRefs();
static void verifyNoDeadRefs();
static void nullStaleLedgerRefs(const std::unordered_set<const AstNode*>& liveNodes);
static int fixDeadRefsInTypeTable(const std::unordered_set<const AstNode*>& liveNodes);
static int fixDeadRefsInModules(const std::unordered_set<const AstNode*>& liveNodes);
static int resolveCapturedRefs();
static void verifyNoDeadRefs(const std::unordered_set<const AstNode*>& liveNodes);
template <typename T_FilterFn, typename T_Fn>
static void forEachImpl(T_FilterFn&& filter, T_Fn&& fn);
@ -156,6 +161,8 @@ public:
static AstNodeDType* findDTypeInModule(AstNodeModule* modp, const string& name, VNType type);
// Find a ParamTypeDType by name in a module's top-level statements
static AstParamTypeDType* findParamTypeInModule(AstNodeModule* modp, const string& name);
// Retarget every live RefDType in an entry using only stable capture metadata.
static bool retargetRefToModule(const CapturedEntry& entry, AstNodeModule* targetModp);
static void add(AstRefDType* refp, const string& cellPath, AstNodeModule* ownerModp,
AstTypedef* typedefp = nullptr, const string& typedefOwnerModName = "",
AstVar* ifacePortVarp = nullptr);
@ -182,7 +189,7 @@ public:
// Ledger-only: no target lookup or AST mutation. Target resolution
// happens later in finalizeIfaceCapture where cell pointers are wired up.
static void propagateClone(const TemplateKey& tkey, AstRefDType* newRefp,
const string& cloneCellPath);
AstNodeModule* newOwnerModp, const string& cloneCellPath);
static void captureTypedefContext(AstRefDType* refp, const char* stageLabel, int dotPos,
bool dotIsFinal, const std::string& dotText,
@ -190,8 +197,8 @@ public:
AstNode* nodep,
const std::function<std::string()>& indentFn);
// Null out ledger refp entries that point to freed nodes (not in the live AST).
// Called once after V3Param completes, before any code touches the ledger.
// Null out ledger entries that point to freed nodes (not in the live AST).
// Called at pass boundaries before code dereferences ledger pointers.
static void purgeStaleRefs();
// Debug: dump all captured entries

View File

@ -850,54 +850,6 @@ class ParamProcessor final {
return resolvedp == expectModp;
}
// Retarget entry.refp (and extraRefps) to the typedef/paramType found
// in targetModp. Returns true if anything was retargeted.
static bool retargetRefToModule(const V3LinkDotIfaceCapture::CapturedEntry& entry,
AstNodeModule* targetModp) {
if (entry.refp->typedefp()) {
if (AstTypedef* const tdp = V3LinkDotIfaceCapture::findTypedefInModule(
targetModp, entry.refp->typedefp()->name())) {
entry.refp->typedefp(tdp);
if (tdp->subDTypep()) {
entry.refp->refDTypep(tdp->subDTypep());
entry.refp->dtypep(tdp->subDTypep());
}
for (AstRefDType* const xrefp : entry.extraRefps) {
xrefp->typedefp(tdp);
if (tdp->subDTypep()) {
xrefp->refDTypep(tdp->subDTypep());
xrefp->dtypep(tdp->subDTypep());
}
}
return true;
}
} else if (entry.paramTypep) {
if (AstParamTypeDType* const ptp = V3LinkDotIfaceCapture::findParamTypeInModule(
targetModp, entry.paramTypep->name())) {
entry.refp->refDTypep(ptp);
entry.refp->dtypep(ptp);
for (AstRefDType* const xrefp : entry.extraRefps) {
xrefp->refDTypep(ptp);
xrefp->dtypep(ptp);
}
return true;
}
} else if (!entry.cloneCellPath.empty()) {
// Clone entry has no paramTypep stored; look up the type by name.
if (AstParamTypeDType* const ptp
= V3LinkDotIfaceCapture::findParamTypeInModule(targetModp, entry.refp->name())) {
entry.refp->refDTypep(ptp);
entry.refp->dtypep(ptp);
for (AstRefDType* const xrefp : entry.extraRefps) {
xrefp->refDTypep(ptp);
xrefp->dtypep(ptp);
}
return true;
}
}
return false;
}
// Fix cross-module REFDTYPE pointers in newModp after cloneTree.
// Phase A: path-based fixup using ledger entries with cellPath.
// Phase B: reachable-set fallback for remaining REFDTYPEs.
@ -915,7 +867,10 @@ class ParamProcessor final {
V3LinkDotIfaceCapture::forEach([&](const V3LinkDotIfaceCapture::CapturedEntry& entry) {
if (!entry.refp) return;
if (entry.cloneCellPath != cloneCP) return;
if (!entry.ownerModp || entry.ownerModp->name() != srcName) return;
UASSERT_OBJ(
entry.ownerModp
&& (entry.ownerModp == newModp || entry.ownerModp->name() == srcName),
entry.refp, "clone ledger entry for '" << cloneCP << "' has unexpected owner");
if (entry.cellPath.empty()) return;
AstRefDType* const refp = entry.refp;
@ -1048,17 +1003,24 @@ class ParamProcessor final {
if (AstRefDType* const clonedRefp = entry.refp->clonep()) {
// Use newname (unique specialized module name) as cloneCellPath.
const string cloneCP = newname;
// A cloned captured ref lives inside srcModp's tree, so its owner
// is srcModp (SV has no nested module definitions).
UASSERT_OBJ(
entry.ownerModp == srcModp, clonedRefp,
"cloned captured RefDType owner is not the specialized module");
AstNodeModule* const clonedOwnerp = newModp;
const V3LinkDotIfaceCapture::TemplateKey tkey{
entry.ownerModp ? entry.ownerModp->name() : "", entry.refp->name(),
entry.cellPath};
V3LinkDotIfaceCapture::propagateClone(tkey, clonedRefp, cloneCP);
V3LinkDotIfaceCapture::propagateClone(tkey, clonedRefp, clonedOwnerp,
cloneCP);
} else if (entry.ownerModp != srcModp) {
// REFDTYPE lives in a parent module; clonep() is null.
AstNodeModule* const actualOwnerp
= V3LinkDotIfaceCapture::findOwnerModule(entry.refp);
if (actualOwnerp && actualOwnerp->hasGParam()) return;
// Owner won't be cloned - directly retarget now.
if (retargetRefToModule(entry, newModp)) {
if (V3LinkDotIfaceCapture::retargetRefToModule(entry, newModp)) {
UINFO(9, "iface capture direct retarget: " << entry.refp << " -> "
<< newModp->prettyNameQ());
}
@ -1078,7 +1040,7 @@ class ParamProcessor final {
&& !cellPathMatchesClone(entry.cellPath, cloneCellp, actualOwnerp, m_modp)) {
return;
}
if (retargetRefToModule(entry, newModp)) {
if (V3LinkDotIfaceCapture::retargetRefToModule(entry, newModp)) {
UINFO(9, "iface capture clone-entry retarget: " << entry.refp << " -> "
<< newModp->prettyNameQ());
}
@ -2185,7 +2147,7 @@ public:
&& !(ownerp == nullptr && entry.cloneCellPath == parentModp->name())) {
return;
}
if (retargetRefToModule(entry, correctModp)) {
if (V3LinkDotIfaceCapture::retargetRefToModule(entry, correctModp)) {
UINFO(9,
"retargetIfaceRefs: " << entry.refp << " -> " << correctModp->prettyNameQ());
}

View File

@ -0,0 +1,18 @@
#!/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.compile(v_flags2=["--binary"])
test.execute()
test.passes()

View File

@ -0,0 +1,55 @@
// DESCRIPTION: Verilator: Test interface typedef capture after parameter cloning
//
// 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
// verilator lint_off DECLFILENAME
// verilog_format: off
`define stop $stop
`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d (%s !== %s)\n", `__FILE__,`__LINE__, (gotv), (expv), `"gotv`", `"expv`"); `stop; end while(0)
// verilog_format: on
interface avmm_if #(
parameter int DW = 32
);
typedef struct packed {
logic [DW-1:0] writedata;
logic write;
} request_t;
request_t req;
modport master(output req);
modport slave(input req);
endinterface
module avmm_autopipeline (
avmm_if.slave master,
avmm_if.master slave
);
typedef master.request_t request_t;
request_t internal_req;
assign internal_req = master.req;
always_comb slave.req = internal_req;
endmodule
module t;
for (genvar g = 0; g < 2; ++g) begin : gen_block
avmm_if #(.DW(32 * (g + 1))) m_if ();
avmm_if #(.DW(32 * (g + 1))) s_if ();
avmm_autopipeline pipe (.master(m_if), .slave(s_if));
end
initial begin
#1;
`checkd($bits(gen_block[0].pipe.internal_req), 33);
`checkd($bits(gen_block[1].pipe.internal_req), 65);
$write("*-* All Finished *-*\n");
$finish;
end
endmodule

View File

@ -22,7 +22,7 @@ test.file_grep(test.stats, r'IfaceCapture, Entries total\s+(\d+)', 18)
test.file_grep(test.stats, r'IfaceCapture, Entries template\s+(\d+)', 8)
test.file_grep(test.stats, r'IfaceCapture, Entries cloned\s+(\d+)', 10)
test.file_grep(test.stats, r'IfaceCapture, Ledger fixups in V3Param\s+(\d+)', 8)
test.file_grep(test.stats, r'IfaceCapture, Wrong-clone refs fixed\s+(\d+)', 10)
test.file_grep(test.stats, r'IfaceCapture, Captured refs resolved\s+(\d+)', 10)
test.file_grep(test.stats, r'IfaceCapture, Dead refs fixed in modules\s+(\d+)', 0)
test.execute()

View File

@ -22,7 +22,7 @@ test.file_grep(test.stats, r'IfaceCapture, Entries total\s+(\d+)', 25)
test.file_grep(test.stats, r'IfaceCapture, Entries template\s+(\d+)', 11)
test.file_grep(test.stats, r'IfaceCapture, Entries cloned\s+(\d+)', 14)
test.file_grep(test.stats, r'IfaceCapture, Ledger fixups in V3Param\s+(\d+)', 5)
test.file_grep(test.stats, r'IfaceCapture, Wrong-clone refs fixed\s+(\d+)', 10)
test.file_grep(test.stats, r'IfaceCapture, Captured refs resolved\s+(\d+)', 16)
test.file_grep(test.stats, r'IfaceCapture, Dead refs fixed in modules\s+(\d+)', 0)
test.execute()

View File

@ -22,7 +22,7 @@ test.file_grep(test.stats, r'IfaceCapture, Entries total\s+(\d+)', 20)
test.file_grep(test.stats, r'IfaceCapture, Entries template\s+(\d+)', 8)
test.file_grep(test.stats, r'IfaceCapture, Entries cloned\s+(\d+)', 12)
test.file_grep(test.stats, r'IfaceCapture, Ledger fixups in V3Param\s+(\d+)', 8)
test.file_grep(test.stats, r'IfaceCapture, Wrong-clone refs fixed\s+(\d+)', 14)
test.file_grep(test.stats, r'IfaceCapture, Captured refs resolved\s+(\d+)', 14)
test.file_grep(test.stats, r'IfaceCapture, Dead refs fixed in modules\s+(\d+)', 0)
test.execute()