xazax.hun created this revision. xazax.hun added reviewers: NoQ, haowei. xazax.hun added a project: clang. Herald added subscribers: Charusso, gamesh411, dkrupp, donat.nagy, Szelethus, mikhail.ramalho, a.sidorin, rnkovacs, szepet, baloghadamsoftware, whisperity, mgorny.
This check is based on https://reviews.llvm.org/D36022 but it takes a bit different approach. It does less state splitting and tries to avoid the evalCall callback. The state machine is also a bit different, now the escaped and untracked states are merged. There were some problems in the original patch with non-pointer escapes. I did not really see those problems with my current model (which is slightly different) but there might be some skeletons waiting to fall out. Disclaimer: this patch will not apply cleanly on top of tree just yet. There are some dependencies that I plan to upload soon, but in the meantime I wanted this to be available for review. Repository: rG LLVM Github Monorepo https://reviews.llvm.org/D70470 Files: clang/include/clang/StaticAnalyzer/Checkers/Checkers.td clang/include/clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt clang/lib/StaticAnalyzer/Checkers/FuchsiaHandleChecker.cpp clang/test/Analysis/fuchsia_handle.c
Index: clang/test/Analysis/fuchsia_handle.c =================================================================== --- /dev/null +++ clang/test/Analysis/fuchsia_handle.c @@ -0,0 +1,114 @@ +// RUN: %clang_analyze_cc1 -analyzer-checker=fuchsia.FuchsiaHandleChecker -verify %s + +typedef __typeof__(sizeof(int)) size_t; +typedef int zx_status_t; +typedef __typeof__(sizeof(int)) zx_handle_t; +typedef unsigned int uint32_t; +#define NULL ((void *)0) + +#if defined(__clang__) +#define XZ_HANDLE_ACQUIRE __attribute__((acquire_handle)) +#define XZ_HANDLE_RELEASE __attribute__((release_handle)) +#define XZ_HANDLE_USE __attribute__((use_handle)) +#else +#define XZ_HANDLE_ACQUIRE +#define XZ_HANDLE_RELEASE +#define XZ_HANDLE_USE +#endif + +zx_status_t zx_channel_create( +uint32_t options, + XZ_HANDLE_ACQUIRE zx_handle_t* out0, + zx_handle_t* out1 XZ_HANDLE_ACQUIRE); + +zx_status_t zx_handle_close( + zx_handle_t handle XZ_HANDLE_RELEASE); + +void escape1(zx_handle_t *in); +void escape2(zx_handle_t in); + +void use1(const zx_handle_t *in XZ_HANDLE_USE); +void use2(zx_handle_t in XZ_HANDLE_USE); + +void checkNoLeak01() { + zx_handle_t sa, sb; + zx_channel_create(0, &sa, &sb); + zx_handle_close(sa); + zx_handle_close(sb); +} + +void checkNoLeak02() { + zx_handle_t ay[2]; + zx_channel_create(0, &ay[0], &ay[1]); + zx_handle_close(ay[0]); + zx_handle_close(ay[1]); +} + +void checkNoLeak03() { + zx_handle_t ay[2]; + zx_channel_create(0, &ay[0], &ay[1]); + for (int i = 0; i < 2; i++) + zx_handle_close(ay[i]); +} + +zx_handle_t checkNoLeak04() { + zx_handle_t sa, sb; + zx_channel_create(0, &sa, &sb); + zx_handle_close(sa); + return sb; // no warning +} + +zx_handle_t checkNoLeak05(zx_handle_t *out1) { + zx_handle_t sa, sb; + zx_channel_create(0, &sa, &sb); + *out1 = sa; + return sb; // no warning +} + +void checkNoLeak06() { + zx_handle_t sa, sb; + if (zx_channel_create(0, &sa, &sb)) + return; + zx_handle_close(sa); + zx_handle_close(sb); +} + +void checkNoLeak07(int tag) { + zx_handle_t sa, sb; + if (zx_channel_create(0, &sa, &sb)) + return; + if (tag) { + escape1(&sa); + escape2(sb); + } + zx_handle_close(sa); + zx_handle_close(sb); +} + +void checkNoLeak08(int tag) { + zx_handle_t sa, sb; + zx_channel_create(0, &sa, &sb); + use1(&sa); + if (tag) + zx_handle_close(sa); + use2(sb); // expected-warning {{Potential leak of handle}} + zx_handle_close(sb); +} + +void checkDoubleRelease01(int tag) { + zx_handle_t sa, sb; + zx_channel_create(0, &sa, &sb); + if (tag) + zx_handle_close(sa); + zx_handle_close(sa); // expected-warning {{Releasing a previously released handle}} + zx_handle_close(sb); +} + +void checkUseAfterFree01(int tag) { + zx_handle_t sa, sb; + zx_channel_create(0, &sa, &sb); + zx_handle_close(sa); + use1(&sa); // expected-warning {{Using a previously released handle}} + zx_handle_close(sb); + use2(sb); // expected-warning {{Using a previously released handle}} +} Index: clang/lib/StaticAnalyzer/Checkers/FuchsiaHandleChecker.cpp =================================================================== --- /dev/null +++ clang/lib/StaticAnalyzer/Checkers/FuchsiaHandleChecker.cpp @@ -0,0 +1,453 @@ +//=== FuchsiaHandleChecker.cpp - Find handle leaks/double closes -*- C++ -*--=// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// This checker checks if the handle of Fuchsia is properly used according to +// following rules. +// - If a handle is acquired, it should be released before execution +// ends. +// - If a handle is released, it should not be released again. +// - If a handle is released, it should not be used for other purposes +// such as I/O. +// +// In this checker, each tracked handle is associated with a state. When the +// handle variable is passed to different function calls or syscalls, its state +// changes. The state changes can be generally represented by following ASCII +// Art: +// +// +-----------------------------------------------------------+ +// | | +// | release_func failed | +// | +---------+ | +// | | | | +// | | | | +// | +-+---------v-+ | +// | acquire_func succeeded | | Escape | +// | +-----------------> Allocated +-------------------+ +// | | | | +// | | +-----+------++ +// | | | | +// | | release_func | | +// | | succeeded | +--+ +// | | | | handle +--------+ +// | | +----v-----+ | dies | | +// | | Escape | | +----------> Leaked | +// +--v-------+--+ +------------+ Released | |(REPORT)| +// | | | | | +--------+ +// | Not tracked <--+ +----+---+-+ +// | | | | | As argument by value +// +------+------+ | release_func | +------+ in function call +// | | | | or by reference in +// | | | | use_func call +// | | +----v-----+ | +-----------+ +// +---------+ | | | | | +// acquire_func failed | Double | +-----> Use after | +// | released | | released | +// | (REPORT) | | (REPORT) | +// +----------+ +-----------+ +// +// acquire_func represents the functions or syscalls that may acquire a handle. +// release_func represents the functions or syscalls that may release a handle. +// use_func represents the functions or syscall that requires an open handle. +// +// If a tracked handle dies in "Released" or "Escaped" state, we assume it +// is properly used. Otherwise a bug and will be reported. +// +// Due to the fact that the number of handle related syscalls in Fuchsia +// is large, we adopt the annotation attributes to descript syscalls' +// operations(acquire/release/use) on handles instead of hardcoding +// everything in the checker. +// +// We use following annotation attributes for handle related syscalls or +// functions: +// 1. __attribute__((clang::acquire_handle)) |handle will be acquired +// 2. __attribute__((clang::release_handle)) |handle will be released +// 3. __attribute__((clang::use_handle)) |handle will not transit to +// escaped state, it also needs to be open. +// +// For example, an annotated syscall: +// zx_status_t zx_channel_create( +// uint32_t options, +// zx_handle_t* out0 __attribute__((annotate("clang::acquire_handle"))) , +// zx_handle_t* out1 __attribute__((annotate("clang::acquire_handle")))); +// denotes a syscall which will acquire two handles and save them to 'out0' and +// 'out1' when succeeded. +// +//===----------------------------------------------------------------------===// + +#include "clang/AST/Decl.h" +#include "clang/AST/Type.h" +#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h" +#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h" +#include "clang/StaticAnalyzer/Core/Checker.h" +#include "clang/StaticAnalyzer/Core/CheckerManager.h" +#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h" +#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h" +#include "clang/StaticAnalyzer/Core/PathSensitive/ConstraintManager.h" +#include "clang/StaticAnalyzer/Core/PathSensitive/ExplodedGraph.h" +#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h" + +using namespace clang; +using namespace ento; + +namespace { + +static const StringRef HandleTypeName = "zx_handle_t"; +static const StringRef ErrorTypeName = "zx_status_t"; + +class HandleState { +private: + enum class Kind { Allocated, Released } K; + SymbolRef ErrorSym; + HandleState(Kind k, SymbolRef ErrorSym) : K(k), ErrorSym(ErrorSym) {} + +public: + bool operator==(const HandleState &Other) const { + return K == Other.K && ErrorSym == Other.ErrorSym; + } + bool isAllocated() const { return K == Kind::Allocated; } + bool isReleased() const { return K == Kind::Released; } + + static HandleState getAllocated(SymbolRef ErrorSym) { + return HandleState(Kind::Allocated, ErrorSym); + } + + static HandleState getReleased(SymbolRef ErrorSym) { + return HandleState(Kind::Released, ErrorSym); + } + + // Only use this in checkDeadSymbols! + bool hasError(ProgramStateRef State) const { + if (!ErrorSym) + return false; + ConditionTruthVal IsNull = + State->getConstraintManager().isNull(State, ErrorSym); + return IsNull.isConstrainedFalse(); + } + + SymbolRef getErrorSym() const { return ErrorSym; } + + void Profile(llvm::FoldingSetNodeID &ID) const { + ID.AddInteger(static_cast<int>(K)); + ID.AddPointer(ErrorSym); + } + + LLVM_DUMP_METHOD void dump(raw_ostream &OS) const { + switch (K) { +#define CASE(ID) \ + case ID: \ + OS << #ID; \ + break; + CASE(Kind::Allocated) + CASE(Kind::Released) + } + } + + LLVM_DUMP_METHOD void dump() const { dump(llvm::errs()); } +}; + +class FuchsiaHandleChecker + : public Checker<check::PostCall, check::PreCall, check::DeadSymbols, + check::PointerEscape, eval::Assume> { + std::unique_ptr<BugType> LeakBugType; + std::unique_ptr<BugType> DoubleReleaseBugType; + std::unique_ptr<BugType> UseAfterFreeBugType; + +public: + FuchsiaHandleChecker() + : LeakBugType(new BugType(this, "Fuchsia handle leak", + "Fuchsia Handle Error", + /*SuppressOnSink=*/true)), + DoubleReleaseBugType(new BugType(this, "Fuchsia handle double release", + "Fuchsia Handle Error")), + UseAfterFreeBugType(new BugType( + this, "Fuchsia handle use after release", "Fuchsia Handle Error")) { + } + + void checkPreCall(const CallEvent &Call, CheckerContext &C) const; + void checkPostCall(const CallEvent &Call, CheckerContext &C) const; + void checkDeadSymbols(SymbolReaper &SymReaper, CheckerContext &C) const; + ProgramStateRef evalAssume(ProgramStateRef State, SVal Cond, + bool Assumption) const; + ProgramStateRef checkPointerEscape(ProgramStateRef State, + const InvalidatedSymbols &Escaped, + const CallEvent *Call, + PointerEscapeKind Kind) const; + + ExplodedNode *reportLeaks(ArrayRef<SymbolRef> LeakedHandles, + CheckerContext &C, ExplodedNode *Pred) const; + + ExplodedNode *reportDoubleRelease(SymbolRef HandleSym, + const SourceRange &Range, CheckerContext &C, + ExplodedNode *Pred) const; + + ExplodedNode *reportUseAfterFree(SymbolRef HandleSym, + const SourceRange &Range, CheckerContext &C, + ExplodedNode *Pred) const; + + void reportBug(SymbolRef Sym, ExplodedNode *ErrorNode, CheckerContext &C, + const SourceRange *Range, const std::unique_ptr<BugType> &Type, + StringRef Msg) const; + + void printState(raw_ostream &Out, ProgramStateRef State, const char *NL, + const char *Sep) const override; +}; +} // end anonymous namespace + +REGISTER_MAP_WITH_PROGRAMSTATE(HStateMap, SymbolRef, HandleState) + +/// Returns the symbols extracted from the argument or null if it cannot be +/// found. +SymbolRef getHandleSymbol(QualType QT, SVal Arg, ProgramStateRef State) { + int PtrToHandleLevel = 0; + const Type *T = QT.getTypePtr(); + while (T->isAnyPointerType() || T->isReferenceType() || T->isArrayType()) { + ++PtrToHandleLevel; + T = T->getPointeeOrArrayElementType(); + } + if (const auto *HandleType = dyn_cast<TypedefType>(T)) { + if (HandleType->getDecl()->getName() != HandleTypeName) + return nullptr; + if (PtrToHandleLevel > 1) { + // Not supported yet. + // TODO: escaping. + return nullptr; + } + + if (PtrToHandleLevel == 0) + return Arg.getAsSymbol(); + else { + assert(PtrToHandleLevel == 1); + if (Optional<Loc> ArgLoc = Arg.getAs<Loc>()) + return State->getSVal(*ArgLoc).getAsSymbol(); + } + } + return nullptr; +} + +void FuchsiaHandleChecker::checkPreCall(const CallEvent &Call, + CheckerContext &C) const { + const FunctionDecl *FuncDecl = dyn_cast_or_null<FunctionDecl>(Call.getDecl()); + if (!FuncDecl) + return; + + ExplodedNode *Pred = C.getPredecessor(); + ProgramStateRef State = C.getState(); + + for (unsigned Arg = 0; Arg < Call.getNumArgs(); ++Arg) { + Optional<unsigned> ParamIdx = Call.getAdjustedParameterIndex(Arg); + if (!ParamIdx) + continue; + + const ParmVarDecl *PVD = FuncDecl->getParamDecl(*ParamIdx); + SymbolRef HandleSym = + getHandleSymbol(PVD->getType(), Call.getArgSVal(Arg), State); + if (!HandleSym) + continue; + + const HandleState *HState = State->get<HStateMap>(HandleSym); + if (PVD->hasAttr<UseHandleAttr>()) { + if (HState && HState->isReleased()) { + Pred = + reportUseAfterFree(HandleSym, Call.getArgSourceRange(Arg), C, Pred); + // Avoid further reports on this symbol. + State = State->remove<HStateMap>(HandleSym); + } + } + } + C.addTransition(State, Pred); +} + +void FuchsiaHandleChecker::checkPostCall(const CallEvent &Call, + CheckerContext &C) const { + const FunctionDecl *FuncDecl = dyn_cast_or_null<FunctionDecl>(Call.getDecl()); + if (!FuncDecl) + return; + + ProgramStateRef State = C.getState(); + ExplodedNode *Pred = C.getPredecessor(); + + SymbolRef ResultSymbol = nullptr; + if (const auto *TypeDefTy = FuncDecl->getReturnType()->getAs<TypedefType>()) + if (TypeDefTy->getDecl()->getName() == ErrorTypeName) + ResultSymbol = Call.getReturnValue().getAsSymbol(); + + // Function returns an open handle. + if (FuncDecl->hasAttr<AcquireHandleAttr>()) { + SymbolRef RetSym = Call.getReturnValue().getAsSymbol(); + State = State->set<HStateMap>(RetSym, HandleState::getAllocated(nullptr)); + } + + for (unsigned Arg = 0; Arg < Call.getNumArgs(); ++Arg) { + Optional<unsigned> ParamIdx = Call.getAdjustedParameterIndex(Arg); + if (!ParamIdx) + continue; + + const ParmVarDecl *PVD = FuncDecl->getParamDecl(*ParamIdx); + SymbolRef HandleSym = + getHandleSymbol(PVD->getType(), Call.getArgSVal(Arg), State); + if (!HandleSym) + continue; + + const HandleState *HState = State->get<HStateMap>(HandleSym); + if (PVD->hasAttr<ReleaseHandleAttr>()) { + // Release + if (HState && HState->isReleased()) { + Pred = reportDoubleRelease(HandleSym, Call.getArgSourceRange(Arg), C, + Pred); + // Avoid further reports on this symbol. + State = State->remove<HStateMap>(HandleSym); + } else + State = State->set<HStateMap>(HandleSym, + HandleState::getReleased(ResultSymbol)); + } else if (PVD->hasAttr<AcquireHandleAttr>()) { + State = State->set<HStateMap>(HandleSym, + HandleState::getAllocated(ResultSymbol)); + } + } + // TODO: we should add the transition before generating error nodes!? + C.addTransition(State, Pred); +} + +void FuchsiaHandleChecker::checkDeadSymbols(SymbolReaper &SymReaper, + CheckerContext &C) const { + ProgramStateRef State = C.getState(); + SmallVector<SymbolRef, 2> LeakedSyms; + HStateMapTy TrackedHandles = State->get<HStateMap>(); + for (auto &CurItem : TrackedHandles) { + if (!SymReaper.isDead(CurItem.first)) + continue; + if (CurItem.second.isAllocated()) + LeakedSyms.push_back(CurItem.first); + State = State->remove<HStateMap>(CurItem.first); + } + + ExplodedNode *N = C.getPredecessor(); + if (!LeakedSyms.empty()) + N = reportLeaks(LeakedSyms, C, N); + + C.addTransition(State, N); +} + +ProgramStateRef FuchsiaHandleChecker::evalAssume(ProgramStateRef State, + SVal Cond, + bool Assumption) const { + ConstraintManager &Cmr = State->getConstraintManager(); + HStateMapTy TrackedHandles = State->get<HStateMap>(); + for (auto &CurItem : TrackedHandles) { + SymbolRef ErrorSym = CurItem.second.getErrorSym(); + if (Cmr.isNull(State, ErrorSym).isConstrainedFalse()) { + if (CurItem.second.isReleased()) + State = State->set<HStateMap>(CurItem.first, + HandleState::getAllocated(nullptr)); + else + State = State->remove<HStateMap>(CurItem.first); + } + } + return State; +} + +ProgramStateRef FuchsiaHandleChecker::checkPointerEscape( + ProgramStateRef State, const InvalidatedSymbols &Escaped, + const CallEvent *Call, PointerEscapeKind Kind) const { + const FunctionDecl *FuncDecl = + Call ? dyn_cast_or_null<FunctionDecl>(Call->getDecl()) : nullptr; + for (const SymbolRef &EscapedSymbol : Escaped) { + if (State->contains<HStateMap>(EscapedSymbol)) { + bool IsUnescapedUse = false; + // Not all calls should escape our symbols. + if (FuncDecl && (Kind == PSK_DirectEscapeOnCall || + Kind == PSK_IndirectEscapeOnCall)) { + for (unsigned Arg = 0; Arg < Call->getNumArgs(); ++Arg) { + Optional<unsigned> ParamIdx = Call->getAdjustedParameterIndex(Arg); + if (!ParamIdx) + continue; + + const ParmVarDecl *PVD = FuncDecl->getParamDecl(*ParamIdx); + SymbolRef HandleSym = + getHandleSymbol(PVD->getType(), Call->getArgSVal(Arg), State); + if (!HandleSym) + continue; + if (HandleSym == EscapedSymbol && PVD->hasAttr<UseHandleAttr>()) + IsUnescapedUse = true; + } + } + if (!IsUnescapedUse) + State = State->remove<HStateMap>(EscapedSymbol); + } + } + return State; +} + +ExplodedNode * +FuchsiaHandleChecker::reportLeaks(ArrayRef<SymbolRef> LeakedHandles, + CheckerContext &C, ExplodedNode *Pred) const { + ExplodedNode *ErrNode = C.generateNonFatalErrorNode(C.getState(), Pred); + for (SymbolRef LeakedHandle : LeakedHandles) { + reportBug(LeakedHandle, ErrNode, C, nullptr, LeakBugType, + "Potential leak of handle"); + } + return ErrNode; +} + +ExplodedNode *FuchsiaHandleChecker::reportDoubleRelease( + SymbolRef HandleSym, const SourceRange &Range, CheckerContext &C, + ExplodedNode *Pred) const { + ExplodedNode *ErrNode = C.generateNonFatalErrorNode(C.getState(), Pred); + reportBug(HandleSym, ErrNode, C, &Range, DoubleReleaseBugType, + "Releasing a previously released handle"); + return ErrNode; +} + +ExplodedNode *FuchsiaHandleChecker::reportUseAfterFree( + SymbolRef HandleSym, const SourceRange &Range, CheckerContext &C, + ExplodedNode *Pred) const { + ExplodedNode *ErrNode = C.generateNonFatalErrorNode(C.getState(), Pred); + reportBug(HandleSym, ErrNode, C, &Range, UseAfterFreeBugType, + "Using a previously released handle"); + return ErrNode; +} + +void FuchsiaHandleChecker::reportBug(SymbolRef Sym, ExplodedNode *ErrorNode, + CheckerContext &C, + const SourceRange *Range, + const std::unique_ptr<BugType> &Type, + StringRef Msg) const { + if (!ErrorNode) + return; + + auto R = std::make_unique<PathSensitiveBugReport>(*Type, Msg, ErrorNode); + if (Range) + R->addRange(*Range); + R->markInteresting(Sym); + C.emitReport(std::move(R)); +} + +void ento::registerFuchsiaHandleChecker(CheckerManager &mgr) { + mgr.registerChecker<FuchsiaHandleChecker>(); +} + +bool ento::shouldRegisterFuchsiaHandleChecker(const LangOptions &LO) { + return true; +} + +void FuchsiaHandleChecker::printState(raw_ostream &Out, ProgramStateRef State, + const char *NL, const char *Sep) const { + + HStateMapTy StateMap = State->get<HStateMap>(); + + if (!StateMap.isEmpty()) { + Out << Sep << "FuchsiaHandleChecker :" << NL; + for (HStateMapTy::iterator I = StateMap.begin(), E = StateMap.end(); I != E; + ++I) { + I.getKey()->dumpToStream(Out); + Out << " : "; + I.getData().dump(Out); + Out << NL; + } + } +} \ No newline at end of file Index: clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt =================================================================== --- clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt +++ clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt @@ -37,6 +37,7 @@ EnumCastOutOfRangeChecker.cpp ExprInspectionChecker.cpp FixedAddressChecker.cpp + FuchsiaHandleChecker.cpp GCDAntipatternChecker.cpp GenericTaintChecker.cpp GTestChecker.cpp Index: clang/include/clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h =================================================================== --- clang/include/clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h +++ clang/include/clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h @@ -213,6 +213,22 @@ return addTransition(State, (Tag ? Tag : Location.getTag())); } + /// Generate a transition to a node that will be used to report + /// an error. This node will not be a sink. That is, exploration will + /// continue along this path. + /// + /// @param State The state of the generated node. + /// @param Pred The transition will be generated from the specified Pred node + /// to the newly generated node. + /// @param Tag The tag to uniquely identify the creation site. If null, + /// the default tag for the checker will be used. + ExplodedNode * + generateNonFatalErrorNode(ProgramStateRef State, + ExplodedNode *Pred, + const ProgramPointTag *Tag = nullptr) { + return addTransition(State, Pred, (Tag ? Tag : Location.getTag())); + } + /// Emit the diagnostics report. void emitReport(std::unique_ptr<BugReport> R) { Changed = true; Index: clang/include/clang/StaticAnalyzer/Checkers/Checkers.td =================================================================== --- clang/include/clang/StaticAnalyzer/Checkers/Checkers.td +++ clang/include/clang/StaticAnalyzer/Checkers/Checkers.td @@ -1431,4 +1431,8 @@ Dependencies<[PthreadLockBase]>, Documentation<HasDocumentation>; +def FuchsiaHandleChecker : Checker<"FuchsiaHandleChecker">, + HelpText<"A Checker that detect leaks related to Fuchsia handles">, + Documentation<HasDocumentation>; + } // end fuchsia
_______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits