| //------------------------------------------------------------------------------------------------------- |
| // Copyright (C) Microsoft. All rights reserved. |
| // Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. |
| //------------------------------------------------------------------------------------------------------- |
| #include "RecyclerChecker.h" |
| |
| MainVisitor::MainVisitor( |
| CompilerInstance& compilerInstance, ASTContext& context, bool fix) |
| : _compilerInstance(compilerInstance), _context(context), |
| _fix(fix), _fixed(false), _diagEngine(context.getDiagnostics()), |
| _barrierTypeDefined(false) |
| { |
| if (_fix) |
| { |
| _rewriter.setSourceMgr(compilerInstance.getSourceManager(), |
| compilerInstance.getLangOpts()); |
| } |
| |
| #define SWB_WIKI \ |
| "https://github.com/microsoft/ChakraCore/wiki/Software-Write-Barrier#coding-rules" |
| |
| _diagUnbarrieredField = _diagEngine.getCustomDiagID( |
| DiagnosticsEngine::Error, |
| "Unbarriered field, see " SWB_WIKI); |
| _diagIllegalBarrierCast = _diagEngine.getCustomDiagID( |
| DiagnosticsEngine::Error, |
| "Illegal casting away of write barrier, see " SWB_WIKI); |
| #undef SWB_WIKI |
| } |
| |
| void MainVisitor::ReportUnbarriedField(SourceLocation location) |
| { |
| DiagReport(location, _diagUnbarrieredField); |
| } |
| |
| void MainVisitor::ReportIllegalBarrierCast(SourceLocation location) |
| { |
| DiagReport(location, _diagIllegalBarrierCast); |
| } |
| |
| void MainVisitor::DiagReport(SourceLocation location, unsigned diagId) |
| { |
| _diagEngine.Report(location, diagId); |
| } |
| |
| bool MainVisitor::VisitCXXRecordDecl(CXXRecordDecl* recordDecl) |
| { |
| if (Log::GetLevel() < Log::LogLevel::Verbose) |
| { |
| return true; // At least Verbose level, otherwise this not needed |
| } |
| |
| std::string typeName = recordDecl->getQualifiedNameAsString(); |
| |
| // Ignore (system/non-GC types) before seeing "Memory::NoWriteBarrierField" |
| if (!_barrierTypeDefined) |
| { |
| if (typeName != "Memory::NoWriteBarrierField") |
| { |
| return true; |
| } |
| |
| _barrierTypeDefined = true; |
| } |
| |
| if (!recordDecl->hasDefinition()) |
| { |
| return true; |
| } |
| |
| bool hasUnbarrieredPointer = false; |
| bool hasBarrieredField = false; |
| |
| for (auto field : recordDecl->fields()) |
| { |
| const QualType qualType = field->getType(); |
| const Type* type = qualType.getTypePtr(); |
| |
| auto fieldTypeName = qualType.getAsString(); |
| if (StartsWith(fieldTypeName, "typename WriteBarrierFieldTypeTraits") || |
| StartsWith(fieldTypeName, "const typename WriteBarrierFieldTypeTraits")) |
| { |
| // Note this only indicates the class is write-barrier annotated |
| hasBarrieredField = true; |
| } |
| else if (type->isPointerType()) |
| { |
| hasUnbarrieredPointer = true; |
| } |
| else if (type->isCompoundType()) |
| { |
| // If the field is a compound type, |
| // check if it is a fully barriered type or |
| // has unprotected pointer fields |
| if (Contains(_pointerClasses, fieldTypeName)) |
| { |
| hasUnbarrieredPointer = true; |
| } |
| else if (Contains(_barrieredClasses, fieldTypeName)) |
| { |
| hasBarrieredField = true; |
| } |
| } |
| } |
| |
| if (hasUnbarrieredPointer) |
| { |
| _pointerClasses.insert(typeName); |
| } |
| else if (hasBarrieredField) |
| { |
| _barrieredClasses.insert(typeName); |
| } |
| |
| return true; |
| } |
| |
| template <class PushFieldType> |
| void MainVisitor::ProcessUnbarrieredFields( |
| CXXRecordDecl* recordDecl, const PushFieldType& pushFieldType) |
| { |
| std::string typeName = recordDecl->getQualifiedNameAsString(); |
| if (typeName == "Memory::WriteBarrierPtr") |
| { |
| return; // Skip WriteBarrierPtr itself |
| } |
| |
| const auto& sourceMgr = _compilerInstance.getSourceManager(); |
| |
| for (auto field : recordDecl->fields()) |
| { |
| const QualType qualType = field->getType(); |
| string fieldTypeName = qualType.getAsString(); |
| string fieldName = field->getNameAsString(); |
| |
| if (StartsWith(fieldTypeName, "WriteBarrierPtr<") || // WriteBarrierPtr fields |
| Contains(fieldTypeName, "_no_write_barrier_policy, ")) // FieldNoBarrier |
| { |
| continue; // skip |
| } |
| |
| // If an annotated field type is struct/class/union (RecordType), the |
| // field type in turn should likely be annotated. |
| if (fieldTypeName.back() != '*' // not "... *" |
| && |
| ( |
| StartsWith(fieldTypeName, "typename WriteBarrierFieldTypeTraits") || |
| StartsWith(fieldTypeName, "WriteBarrierFieldTypeTraits") || |
| StartsWith(fieldTypeName, "const typename WriteBarrierFieldTypeTraits") || |
| StartsWith(fieldTypeName, "const WriteBarrierFieldTypeTraits") || |
| fieldName.length() == 0 // anonymous union/struct |
| )) |
| { |
| auto originalType = qualType->getUnqualifiedDesugaredType(); |
| if (auto arrayType = dyn_cast<ArrayType>(originalType)) |
| { |
| originalType = arrayType->getElementType()->getUnqualifiedDesugaredType(); |
| } |
| |
| string originalTypeName = QualType(originalType, 0).getAsString(); |
| if (isa<RecordType>(originalType) && |
| !StartsWith(originalTypeName, "class Memory::WriteBarrierPtr<")) |
| { |
| if (pushFieldType(originalType)) |
| { |
| Log::verbose() << "Queue field type: " << originalTypeName |
| << " (" << typeName << "::" << fieldName << ")\n"; |
| } |
| } |
| } |
| else |
| { |
| SourceLocation location = field->getLocStart(); |
| if (this->_fix) |
| { |
| const char* begin = sourceMgr.getCharacterData(location); |
| const char* end = begin; |
| |
| if (MatchType(fieldTypeName, begin, &end)) |
| { |
| _rewriter.ReplaceText( |
| location, end - begin, |
| GetFieldTypeAnnotation(qualType) + string(begin, end) + |
| (*end == ' ' ? ")" : ") ")); |
| _fixed = true; |
| continue; |
| } |
| |
| Log::errs() << "Fail to fix: " << fieldTypeName << " " |
| << fieldName << "\n"; |
| } |
| |
| ReportUnbarriedField(location); |
| } |
| } |
| } |
| |
| static bool SkipSpace(const char*& p) |
| { |
| if (*p == ' ') |
| { |
| ++p; |
| return true; |
| } |
| |
| return false; |
| } |
| |
| template <size_t N> |
| static bool SkipPrefix(const char*& p, const char (&prefix)[N]) |
| { |
| if (StartsWith(p, prefix)) |
| { |
| p += N - 1; // skip |
| return true; |
| } |
| |
| return false; |
| } |
| |
| static bool SkipPrefix(const char*& p, const string& prefix) |
| { |
| if (StartsWith(p, prefix)) |
| { |
| p += prefix.length(); // skip |
| return true; |
| } |
| |
| return false; |
| } |
| |
| static bool SkipTemplateParameters(const char*& p) |
| { |
| if (*p == '<') |
| { |
| ++p; |
| int left = 1; |
| |
| while (left && *p) |
| { |
| switch (*p++) |
| { |
| case '<': ++left; break; |
| case '>': --left; break; |
| } |
| } |
| |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool MainVisitor::MatchType(const string& type, const char* source, const char** pSourceEnd) |
| { |
| // try match type in source directly (clang "bool" type is "_Bool") |
| if (SkipPrefix(source, type) || (type == "_Bool" && SkipPrefix(source, "bool"))) |
| { |
| *pSourceEnd = source; |
| return true; |
| } |
| |
| const char* p = type.c_str(); |
| while (*p && *source) |
| { |
| if (SkipSpace(p) || SkipSpace(source)) |
| { |
| continue; |
| } |
| |
| #define SKIP_EITHER_PREFIX(prefix) \ |
| (SkipPrefix(p, prefix) || SkipPrefix(source, prefix)) |
| if (SKIP_EITHER_PREFIX("const ") || |
| SKIP_EITHER_PREFIX("class ") || |
| SKIP_EITHER_PREFIX("struct ") || |
| SKIP_EITHER_PREFIX("union ") || |
| SKIP_EITHER_PREFIX("enum ")) |
| { |
| continue; |
| } |
| #undef SKIP_EITHER_PREFIX |
| |
| // type may contain [...] array specifier, while source has it after field name |
| if (*p == '[') |
| { |
| while (*p && *p++ != ']'); |
| continue; |
| } |
| |
| // skip <...> in both |
| if (SkipTemplateParameters(p) || SkipTemplateParameters(source)) |
| { |
| continue; |
| } |
| |
| // type may contain fully qualified name but source may or may not |
| const char* pSkipScopeType = strstr(p, "::"); |
| if (pSkipScopeType && !memchr(p, ' ', pSkipScopeType - p)) |
| { |
| pSkipScopeType += 2; |
| if (strncmp(source, p, pSkipScopeType - p) == 0) |
| { |
| source += pSkipScopeType - p; |
| } |
| p = pSkipScopeType; |
| continue; |
| } |
| |
| if (*p == *source) |
| { |
| while (*p && *source && *p == *source && !strchr("<>", *p)) |
| { |
| ++p, ++source; |
| } |
| continue; |
| } |
| |
| if (*p != *source) |
| { |
| return false; // mismatch |
| } |
| } |
| |
| if (!*p && *source) // type match completed and having remaining source |
| { |
| while (*(source - 1) == ' ') --source; // try to stop after a non-space char |
| *pSourceEnd = source; |
| return true; |
| } |
| |
| return false; |
| } |
| |
| const char* MainVisitor::GetFieldTypeAnnotation(QualType qtype) |
| { |
| if (qtype->isPointerType()) |
| { |
| auto type = qtype->getUnqualifiedDesugaredType()->getPointeeType().getTypePtr(); |
| const auto& i = _allocationTypes.find(type); |
| if (i != _allocationTypes.end() |
| && i->second == AllocationTypes::NonRecycler) |
| { |
| return "FieldNoBarrier("; |
| } |
| } |
| |
| return "Field("; |
| } |
| |
| bool MainVisitor::VisitFunctionDecl(FunctionDecl* functionDecl) |
| { |
| if (functionDecl->hasBody()) |
| { |
| CheckAllocationsInFunctionVisitor visitor(this, functionDecl); |
| |
| visitor.TraverseDecl(functionDecl); |
| } |
| |
| return true; |
| } |
| |
| void MainVisitor::RecordAllocation(QualType qtype, AllocationTypes allocationType) |
| { |
| auto type = qtype->getCanonicalTypeInternal().getTypePtr(); |
| _allocationTypes[type] |= allocationType; |
| } |
| |
| void MainVisitor::RecordRecyclerAllocation(const string& allocationFunction, const string& type) |
| { |
| _allocatorTypeMap[allocationFunction].insert(type); |
| } |
| |
| template <class Set, class DumpItemFunc> |
| void MainVisitor::dump(const char* name, const Set& set, const DumpItemFunc& func) |
| { |
| Log::verbose() << "-------------------------\n\n"; |
| Log::verbose() << name << "\n"; |
| Log::verbose() << "-------------------------\n\n"; |
| for (auto item : set) |
| { |
| func(Log::verbose(), item); |
| } |
| Log::verbose() << "-------------------------\n\n"; |
| } |
| |
| template <class Item> |
| void MainVisitor::dump(const char* name, const set<Item>& set) |
| { |
| dump(name, set, [](raw_ostream& out, const Item& item) |
| { |
| out << " " << item << "\n"; |
| }); |
| } |
| |
| void MainVisitor::dump(const char* name, const unordered_set<const Type*> set) |
| { |
| dump(name, set, [&](raw_ostream& out, const Type* type) |
| { |
| out << " " << QualType(type, 0).getAsString() << "\n"; |
| }); |
| } |
| |
| void MainVisitor::Inspect() |
| { |
| #define Dump(coll) dump(#coll, _##coll) |
| Dump(pointerClasses); |
| Dump(barrieredClasses); |
| |
| Log::verbose() << "Recycler allocations\n"; |
| for (auto item : _allocatorTypeMap) |
| { |
| dump(item.first.c_str(), item.second); |
| } |
| |
| std::queue<const Type*> queue; // queue of types to check |
| std::unordered_set<const Type*> barrierTypes; // set of types queued |
| auto pushBarrierType = [&](const Type* type) -> bool |
| { |
| if (barrierTypes.insert(type).second) |
| { |
| queue.push(type); |
| return true; |
| } |
| return false; |
| }; |
| |
| for (auto item : _allocationTypes) |
| { |
| if (item.second & AllocationTypes::WriteBarrier) |
| { |
| pushBarrierType(item.first); |
| } |
| } |
| dump("WriteBarrier allocation types", barrierTypes); |
| |
| // Examine all barrierd types. They should be fully wb annotated. |
| while (!queue.empty()) |
| { |
| auto type = queue.front(); |
| queue.pop(); |
| |
| auto r = type->getCanonicalTypeInternal()->getAsCXXRecordDecl(); |
| if (r) |
| { |
| auto typeName = r->getQualifiedNameAsString(); |
| ProcessUnbarrieredFields(r, pushBarrierType); |
| |
| // queue the type's base classes |
| for (const auto& base: r->bases()) |
| { |
| if (pushBarrierType(base.getType().getTypePtr())) |
| { |
| Log::verbose() << "Queue base type: " << base.getType().getAsString() |
| << " (base of " << typeName << ")\n"; |
| } |
| } |
| } |
| } |
| |
| #undef Dump |
| } |
| |
| bool MainVisitor::ApplyFix() |
| { |
| return _fixed ? _rewriter.overwriteChangedFiles() : false; |
| } |
| |
| static AllocationTypes CheckAllocationType(const CXXStaticCastExpr* castNode) |
| { |
| QualType targetType = castNode->getTypeAsWritten(); |
| if (const IdentifierInfo* info = targetType.getBaseTypeIdentifier()) |
| { |
| return info->getName().equals("Recycler") ? |
| AllocationTypes::Recycler : AllocationTypes::NonRecycler; |
| } |
| else |
| { |
| // Unknown template dependent allocator types |
| return AllocationTypes::Unknown; |
| } |
| } |
| |
| template <class A0, class A1, class T> |
| void CheckAllocationsInFunctionVisitor::VisitAllocate( |
| const A0& getArg0, const A1& getArg1, const T& getAllocType) |
| { |
| const Expr* firstArgNode = getArg0(); |
| |
| // Check if the first argument (to new or AllocateArray) is a static cast |
| // AllocatorNew/AllocateArray in Chakra always does a static_cast to the AllocatorType |
| const CXXStaticCastExpr* castNode = nullptr; |
| if (firstArgNode != nullptr && |
| (castNode = dyn_cast<CXXStaticCastExpr>(firstArgNode))) |
| { |
| QualType allocatedType = getAllocType(); |
| string allocatedTypeStr = allocatedType.getAsString(); |
| |
| auto allocationType = CheckAllocationType(castNode); |
| if (allocationType == AllocationTypes::Recycler) // Recycler allocation |
| { |
| const Expr* secondArgNode = getArg1(); |
| |
| // Chakra has two types of allocating functions- throwing and non-throwing |
| // However, recycler allocations are always throwing, so the second parameter |
| // should be the address of the allocator function |
| auto unaryNode = cast<UnaryOperator>(secondArgNode); |
| if (unaryNode != nullptr && unaryNode->getOpcode() == UnaryOperatorKind::UO_AddrOf) |
| { |
| Expr* subExpr = unaryNode->getSubExpr(); |
| if (DeclRefExpr* declRef = cast<DeclRefExpr>(subExpr)) |
| { |
| auto declNameInfo = declRef->getNameInfo(); |
| auto allocationFunctionStr = declNameInfo.getName().getAsString(); |
| _mainVisitor->RecordRecyclerAllocation(allocationFunctionStr, allocatedTypeStr); |
| |
| if (!Contains(allocationFunctionStr, "Leaf")) |
| { |
| // Recycler write barrier allocation -- unless "Leaf" in allocFunc |
| allocationType = AllocationTypes::RecyclerWriteBarrier; |
| } |
| } |
| else |
| { |
| Log::errs() << "ERROR: (internal) Expected DeclRefExpr:\n"; |
| subExpr->dump(); |
| } |
| } |
| else if (auto mExpr = cast<MaterializeTemporaryExpr>(secondArgNode)) |
| { |
| auto name = mExpr->GetTemporaryExpr()->IgnoreImpCasts()->getType().getAsString(); |
| if (StartsWith(name, "InfoBitsWrapper<")) // && Contains(name, "WithBarrierBit")) |
| { |
| // RecyclerNewEnumClass, RecyclerNewWithInfoBits -- always have WithBarrier varients |
| allocationType = AllocationTypes::RecyclerWriteBarrier; |
| } |
| } |
| else |
| { |
| Log::errs() << "ERROR: (internal) Expected unary node or MaterializeTemporaryExpr:\n"; |
| secondArgNode->dump(); |
| } |
| } |
| |
| if (allocationType & AllocationTypes::WriteBarrier) |
| { |
| Log::verbose() << "In \"" << _functionDecl->getQualifiedNameAsString() << "\"\n"; |
| Log::verbose() << " Allocating \"" << allocatedTypeStr << "\" in write barriered memory\n"; |
| } |
| |
| _mainVisitor->RecordAllocation(allocatedType, allocationType); |
| } |
| } |
| |
| bool CheckAllocationsInFunctionVisitor::VisitCXXNewExpr(CXXNewExpr* newExpr) |
| { |
| if (newExpr->getNumPlacementArgs() > 1) |
| { |
| VisitAllocate( |
| [=]() { return newExpr->getPlacementArg(0); }, |
| [=]() { return newExpr->getPlacementArg(1); }, |
| [=]() { return newExpr->getAllocatedType(); } |
| ); |
| } |
| |
| return true; |
| } |
| |
| bool CheckAllocationsInFunctionVisitor::VisitCallExpr(CallExpr* callExpr) |
| { |
| // Check callExpr for AllocateArray |
| auto callee = callExpr->getDirectCallee(); |
| if (callExpr->getNumArgs() == 3 && |
| callee && |
| callee->getName().equals("AllocateArray")) |
| { |
| VisitAllocate( |
| [=]() { return callExpr->getArg(0); }, |
| [=]() { return callExpr->getArg(1); }, |
| [=]() |
| { |
| auto retType = callExpr->getCallReturnType(_mainVisitor->getContext()); |
| return QualType(retType->getAs<PointerType>()->getPointeeType()); |
| } |
| ); |
| } |
| |
| return true; |
| } |
| |
| // Check if type is a "Field() *" pointer type, or alternatively a pointer to |
| // any type in "alt" if provided. |
| bool CheckAllocationsInFunctionVisitor::IsFieldPointer( |
| const QualType& qtype, const char* alt) |
| { |
| if (qtype->isPointerType()) |
| { |
| auto name = qtype->getPointeeType() |
| .getDesugaredType(_mainVisitor->getContext()).getAsString(); |
| return StartsWith(name, "class Memory::WriteBarrierPtr<") |
| || StartsWith(name, "typename WriteBarrierFieldTypeTraits<") |
| || (alt && strstr(alt, name.c_str())); |
| } |
| |
| return false; |
| } |
| |
| bool CheckAllocationsInFunctionVisitor::CommonVisitCastExpr(CastExpr *cast) |
| { |
| if (IsFieldPointer(cast->getSubExpr()->getType()) && // from Field() * |
| cast->getType()->isPointerType() && // to a pointer type |
| !IsFieldPointer(cast->getType(), // not another Field() * |
| "int|float|double|unsigned char")) // not int/float/double/byte * |
| { |
| _mainVisitor->ReportIllegalBarrierCast(cast->getLocStart()); |
| |
| if (Log::GetLevel() >= Log::LogLevel::Info) |
| { |
| cast->dumpColor(); |
| cast->getSubExpr()->getType()->getPointeeType() |
| .getDesugaredType(_mainVisitor->getContext()).dump("CAST_FROM"); |
| cast->getType()->getPointeeType() |
| .getDesugaredType(_mainVisitor->getContext()).dump("CAST_TO"); |
| } |
| } |
| |
| return true; |
| } |
| |
| void RecyclerCheckerConsumer::HandleTranslationUnit(ASTContext& context) |
| { |
| MainVisitor mainVisitor(_compilerInstance, context, _fix); |
| mainVisitor.TraverseDecl(context.getTranslationUnitDecl()); |
| |
| mainVisitor.Inspect(); |
| mainVisitor.ApplyFix(); |
| } |
| |
| std::unique_ptr<ASTConsumer> RecyclerCheckerAction::CreateASTConsumer( |
| CompilerInstance& compilerInstance, llvm::StringRef) |
| { |
| return llvm::make_unique<RecyclerCheckerConsumer>(compilerInstance, _fix); |
| } |
| |
| bool RecyclerCheckerAction::ParseArgs( |
| const CompilerInstance& compilerInstance, const std::vector<std::string>& args) |
| { |
| for (auto i = args.begin(); i != args.end(); i++) |
| { |
| if (*i == "-fix") |
| { |
| this->_fix = true; |
| } |
| else if (*i == "-info") |
| { |
| Log::SetLevel(Log::LogLevel::Info); |
| } |
| else if (*i == "-verbose") |
| { |
| Log::SetLevel(Log::LogLevel::Verbose); |
| } |
| else |
| { |
| Log::errs() |
| << "ERROR: Unrecognized check-recycler option: " << *i << "\n" |
| << "Supported options:\n" |
| << " -fix Fix missing write barrier annotations" |
| << " -info Log info messages\n" |
| << " -verbose Log verbose messages\n"; |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| static FrontendPluginRegistry::Add<RecyclerCheckerAction> recyclerPlugin( |
| "check-recycler", "Checks the recycler allocations"); |