はじめに
- 1 概要・論文編
- 2-1 LLVM 関数計装編
- 2-2 LLVM モジュール計装編 (本記事)
基礎技術研究部の末吉です。
第 1 回と第 2 回の記事では、ASan の概要を説明した後に、ASan の関数に対する計装で、どのような命令が追加されるか見てきました。 今回は、以前の記事で紹介した ASan の手法がどのように実装されているのか知りたい方や、これから ASan の実装を読んでいきたい方を対象に、モジュールに対する計装処理や、ASan ランタイムの初期化処理について解説します。
本記事で使用している LLVM のバージョンは前回同様 18.1.5 です。
モジュールの計装
モジュールの計装は instrumentModule 関数で行われます。
この関数では、ASan ランタイムの初期化処理およびグローバル変数のサニタイズ処理を計装します。
ASan ランタイムの初期化処理の計装
まずは、ASan ランタイムの初期化処理の計装を見てみましょう。
bool ModuleAddressSanitizer::instrumentModule(Module &M) { initializeCallbacks(M); // 各種必要なコールバック関数の初期化 // Create a module constructor. A destructor is created lazily because not all // platforms, and not all modules need it. // asan.module_ctor の作成 if (ConstructorKind == AsanCtorKind::Global) { if (CompileKernel) { // (...省略...) } else { std::string AsanVersion = std::to_string(GetAsanVersion(M)); // ASan のバージョン文字列の取得 std::string VersionCheckName = InsertVersionCheck ? (kAsanVersionCheckNamePrefix + AsanVersion) : ""; std::tie(AsanCtorFunction, std::ignore) = createSanitizerCtorAndInitFunctions(M, kAsanModuleCtorName, // 初期化関数の計装 kAsanInitName, /*InitArgTypes=*/{}, /*InitArgs=*/{}, VersionCheckName); } } // (...省略...) }
初期化に必要な処理は上記箇所で計装されます。
createSanitizerCtorAndInitFunction 関数を見てみましょう。
この関数は引数が 7 個ありますが、上では 6 個だけ指定されています。
第 7 引数の Weak はデフォルト引数であり、デフォルト値は false です。
// 宣言 std::pair<Function *, FunctionCallee> createSanitizerCtorAndInitFunctions( Module &M, StringRef CtorName, StringRef InitName, ArrayRef<Type *> InitArgTypes, ArrayRef<Value *> InitArgs, StringRef VersionCheckName = StringRef(), bool Weak = false); std::pair<Function *, FunctionCallee> llvm::createSanitizerCtorAndInitFunctions( Module &M, StringRef CtorName, StringRef InitName, ArrayRef<Type *> InitArgTypes, ArrayRef<Value *> InitArgs, StringRef VersionCheckName, bool Weak) { assert(!InitName.empty() && "Expected init function name"); assert(InitArgs.size() == InitArgTypes.size() && "Sanitizer's init function expects different number of arguments"); FunctionCallee InitFunction = declareSanitizerInitFunction(M, InitName, InitArgTypes, Weak); // __asan_init 関数を宣言 Function *Ctor = createSanitizerCtor(M, CtorName); // asan.module_ctor の作成 IRBuilder<> IRB(M.getContext()); BasicBlock *RetBB = &Ctor->getEntryBlock(); if (Weak) { // (...省略...) } else { IRB.SetInsertPoint(RetBB->getTerminator()); } IRB.CreateCall(InitFunction, InitArgs); // __asan_init(); if (!VersionCheckName.empty()) { FunctionCallee VersionCheckFunction = M.getOrInsertFunction( VersionCheckName, FunctionType::get(IRB.getVoidTy(), {}, false), AttributeList()); IRB.CreateCall(VersionCheckFunction, {}); // __asan_version_mismatch_check_v8(); } if (Weak) IRB.CreateBr(RetBB); return std::make_pair(Ctor, InitFunction); }
ランタイムでは、最初に kAsanModuleCtorName (asan.module_ctor *1) が実行され、その中で, ASan の初期化を担う kAsanInitName (__asan_init) が実行されます。
declareSanitizerInitFunction で __asan_init 関数の型を宣言しています。
FunctionCallee llvm::declareSanitizerInitFunction(Module &M, StringRef InitName, ArrayRef<Type *> InitArgTypes, bool Weak) { assert(!InitName.empty() && "Expected init function name"); auto *VoidTy = Type::getVoidTy(M.getContext()); auto *FnTy = FunctionType::get(VoidTy, InitArgTypes, false); auto FnCallee = M.getOrInsertFunction(InitName, FnTy); // void __asan_init() 関数型の作成 auto *Fn = cast<Function>(FnCallee.getCallee()); if (Weak && Fn->isDeclaration()) Fn->setLinkage(Function::ExternalWeakLinkage); return FnCallee; }
Weak = false の時は簡単で、asan.module_ctor に以下を計装しているだけです。
__asan_init(); __asan_version_mismatch_check_v8();
__asan_init が ASan を初期化する関数で、__asan_version_mismatch_check_v8 が ASan のバージョン差異を検出するための関数です。
__asan_version_mismatch_check_v8 関数は、ASan のバージョンによって末尾の数値が変化します。
18.1.5 における ASan のバージョンは 8 であるため、関数名は常に __asan_version_mismatch_check_v8 になります (32 ビット版 Android を除く)。
なお、この関数の実体は常に何もせずリターンするだけです。
ASan はバージョン間の互換性を気にしていないため、コンパイル時の ASan と実行時の ASan バージョンが異なる場合、すなわち libasan.so を動的リンクしている場合、コンパイル時と実行時でバージョンが異なると ASan が正しく機能しません。
よって、バージョンが異なる場合は動的リンク時に関数を発見できず失敗するようにして、すぐ気付けるようになっています。*2
(★先にランタイム側の処理を見たい方用) ランタイム側の処理は ASan ランタイムの初期化処理に書いてあります。
グローバル変数に対する計装
さて、instrumentModule 関数に戻って、今度はグローバル変数の計装処理を見てみましょう。
bool ModuleAddressSanitizer::instrumentModule(Module &M) { // asan.module_ctor の計装 // (...省略...) bool CtorComdat = true; if (ClGlobals) { assert(AsanCtorFunction || ConstructorKind == AsanCtorKind::None); if (AsanCtorFunction) { IRBuilder<> IRB(AsanCtorFunction->getEntryBlock().getTerminator()); instrumentGlobals(IRB, M, &CtorComdat); // ASan の初期化後にグローバル変数の計装を入れる } else { IRBuilder<> IRB(*C); instrumentGlobals(IRB, M, &CtorComdat); } } // (...省略...) }
instrumentGlobals 関数を見てみます。
void ModuleAddressSanitizer::instrumentGlobals(IRBuilder<> &IRB, Module &M, bool *CtorComdat) { // Build set of globals that are aliased by some GA, where // getExcludedAliasedGlobal(GA) returns the relevant GlobalVariable. SmallPtrSet<const GlobalVariable *, 16> AliasedGlobalExclusions; if (CompileKernel) { for (auto &GA : M.aliases()) { if (const GlobalVariable *GV = getExcludedAliasedGlobal(GA)) AliasedGlobalExclusions.insert(GV); } } SmallVector<GlobalVariable *, 16> GlobalsToChange; for (auto &G : M.globals()) { if (!AliasedGlobalExclusions.count(&G) && shouldInstrumentGlobal(&G)) GlobalsToChange.push_back(&G); } // (...省略...) }
この関数は、グローバル変数に計装が必要か確認し、必要であれば計装する関数です。
shouldInstrumentGlobal 関数がグローバル変数を計装すべきか判定し、以下の全ての条件を満たしたグローバル変数が計装されます。
- コンパイル時にサイズを確定できる
- イニシャライザーを持つ
- スレッドローカル変数ではない
- レッドゾーンよりアラインメントが大きくない
- 定義が一意に定まる
- サニタイズしないと明示された関数ではない
- LLVM や ASan が挿入したセクションに存在しない
- LLVM や ASan が挿入した変数ではない
bool ModuleAddressSanitizer::shouldInstrumentGlobal(GlobalVariable *G) const { Type *Ty = G->getValueType(); LLVM_DEBUG(dbgs() << "GLOBAL: " << *G << "\n"); if (G->hasSanitizerMetadata() && G->getSanitizerMetadata().NoAddress) // __no_sanitize__ などでサニタイズしないと明示されているグローバル変数の場合、計装しない return false; if (!Ty->isSized()) return false; // サイズをコンパイル時に決定できない if (!G->hasInitializer()) return false; // イニシャライザーを持たない // Globals in address space 1 and 4 are supported for AMDGPU. if (G->getAddressSpace() && !(TargetTriple.isAMDGPU() && !isUnsupportedAMDGPUAddrspace(G))) return false; // LLVM が生成したグローバル変数や ASan の関数は計装しない if (GlobalWasGeneratedByCompiler(G)) return false; // Our own globals. // Two problems with thread-locals: // - The address of the main thread's copy can't be computed at link-time. // - Need to poison all copies, not just the main thread's one. // スレッドローカル変数は計装しない if (G->isThreadLocal()) return false; // For now, just ignore this Global if the alignment is large. // レッドゾーンよりアラインメントが大きい変数は計装しない if (G->getAlign() && *G->getAlign() > getMinRedzoneSizeForGlobal()) return false; // For non-COFF targets, only instrument globals known to be defined by this // TU. // FIXME: We can instrument comdat globals on ELF if we are using the // GC-friendly metadata scheme. // 定義が一意に定まらない変数は計装しない if (!TargetTriple.isOSBinFormatCOFF()) { if (!G->hasExactDefinition() || G->hasComdat()) return false; } else { // On COFF, don't instrument non-ODR linkages. if (G->isInterposable()) return false; } // If a comdat is present, it must have a selection kind that implies ODR // semantics: no duplicates, any, or exact match. if (Comdat *C = G->getComdat()) { switch (C->getSelectionKind()) { case Comdat::Any: case Comdat::ExactMatch: case Comdat::NoDeduplicate: break; case Comdat::Largest: case Comdat::SameSize: return false; } } if (G->hasSection()) { // The kernel uses explicit sections for mostly special global variables // that we should not instrument. E.g. the kernel may rely on their layout // without redzones, or remove them at link time ("discard.*"), etc. if (CompileKernel) return false; StringRef Section = G->getSection(); // Globals from llvm.metadata aren't emitted, do not instrument them. // LLVMによって挿入されるメタデータ変数やセクションにある場合は計装しない if (Section == "llvm.metadata") return false; // Do not instrument globals from special LLVM sections. if (Section.contains("__llvm") || Section.contains("__LLVM")) return false; // Do not instrument function pointers to initialization and termination // routines: dynamic linker will not properly handle redzones. // 初期化と終了時に呼ばれるセクションにある場合は計装しない if (Section.starts_with(".preinit_array") || Section.starts_with(".init_array") || Section.starts_with(".fini_array")) { return false; } // Do not instrument user-defined sections (with names resembling // valid C identifiers) // ユーザーが定義したセクションにある場合は計装しない if (TargetTriple.isOSBinFormatELF()) { if (llvm::all_of(Section, [](char c) { return llvm::isAlnum(c) || c == '_'; })) return false; } // On COFF, if the section name contains '$', it is highly likely that the // user is using section sorting to create an array of globals similar to // the way initialization callbacks are registered in .init_array and // .CRT$XCU. The ATL also registers things in .ATL$__[azm]. Adding redzones // to such globals is counterproductive, because the intent is that they // will form an array, and out-of-bounds accesses are expected. // See https://github.com/google/sanitizers/issues/305 // and http://msdn.microsoft.com/en-US/en-en/library/bb918180(v=vs.120).aspx // COFF形式で `$` を含むセクションにある場合は計装しない if (TargetTriple.isOSBinFormatCOFF() && Section.contains('$')) { LLVM_DEBUG(dbgs() << "Ignoring global in sorted section (contains '$'): " << *G << "\n"); return false; } if (TargetTriple.isOSBinFormatMachO()) { // (...省略...) } } // (...省略...) return true; }
では、改めて instrumentGlobals 関数を見てみましょう。
void ModuleAddressSanitizer::instrumentGlobals(IRBuilder<> &IRB, Module &M, bool *CtorComdat) { // Build set of globals that are aliased by some GA, where // getExcludedAliasedGlobal(GA) returns the relevant GlobalVariable. SmallPtrSet<const GlobalVariable *, 16> AliasedGlobalExclusions; // (...省略...) SmallVector<GlobalVariable *, 16> GlobalsToChange; for (auto &G : M.globals()) { if (!AliasedGlobalExclusions.count(&G) && shouldInstrumentGlobal(&G)) GlobalsToChange.push_back(&G); } size_t n = GlobalsToChange.size(); auto &DL = M.getDataLayout(); // A global is described by a structure // size_t beg; // size_t size; // size_t size_with_redzone; // const char *name; // const char *module_name; // size_t has_dynamic_init; // size_t padding_for_windows_msvc_incremental_link; // size_t odr_indicator; // We initialize an array of such structures and pass it to a run-time call. StructType *GlobalStructTy = StructType::get(IntptrTy, IntptrTy, IntptrTy, IntptrTy, IntptrTy, IntptrTy, IntptrTy, IntptrTy); SmallVector<GlobalVariable *, 16> NewGlobals(n); SmallVector<Constant *, 16> Initializers(n); bool HasDynamicallyInitializedGlobals = false; // We shouldn't merge same module names, as this string serves as unique // module ID in runtime. GlobalVariable *ModuleName = n != 0 ? createPrivateGlobalForString(M, M.getModuleIdentifier(), /*AllowMerging*/ false, kAsanGenPrefix) : nullptr; for (size_t i = 0; i < n; i++) { GlobalVariable *G = GlobalsToChange[i]; GlobalValue::SanitizerMetadata MD; if (G->hasSanitizerMetadata()) MD = G->getSanitizerMetadata(); // The runtime library tries demangling symbol names in the descriptor but // functionality like __cxa_demangle may be unavailable (e.g. // -static-libstdc++). So we demangle the symbol names here. std::string NameForGlobal = G->getName().str(); GlobalVariable *Name = createPrivateGlobalForString(M, llvm::demangle(NameForGlobal), /*AllowMerging*/ true, kAsanGenPrefix); Type *Ty = G->getValueType(); // 型 const uint64_t SizeInBytes = DL.getTypeAllocSize(Ty); // サイズ const uint64_t RightRedzoneSize = getRedzoneSizeForGlobal(SizeInBytes); // レッドゾーンのサイズを取得 Type *RightRedZoneTy = ArrayType::get(IRB.getInt8Ty(), RightRedzoneSize); // char[RightRedzoneSize] StructType *NewTy = StructType::get(Ty, RightRedZoneTy); Constant *NewInitializer = ConstantStruct::get( NewTy, G->getInitializer(), Constant::getNullValue(RightRedZoneTy)); // レッドゾーンはゼロで初期化 // Create a new global variable with enough space for a redzone. GlobalValue::LinkageTypes Linkage = G->getLinkage(); if (G->isConstant() && Linkage == GlobalValue::PrivateLinkage) Linkage = GlobalValue::InternalLinkage; // 置き換えるグローバル変数を作成 GlobalVariable *NewGlobal = new GlobalVariable( M, NewTy, G->isConstant(), Linkage, NewInitializer, "", G, G->getThreadLocalMode(), G->getAddressSpace()); NewGlobal->copyAttributesFrom(G); NewGlobal->setComdat(G->getComdat()); NewGlobal->setAlignment(Align(getMinRedzoneSizeForGlobal())); // レッドゾーンサイズに合わせてアラインメント // Don't fold globals with redzones. ODR violation detector and redzone // poisoning implicitly creates a dependence on the global's address, so it // is no longer valid for it to be marked unnamed_addr. NewGlobal->setUnnamedAddr(GlobalValue::UnnamedAddr::None); // Move null-terminated C strings to "__asan_cstring" section on Darwin. if (TargetTriple.isOSBinFormatMachO() && !G->hasSection() && G->isConstant()) { auto Seq = dyn_cast<ConstantDataSequential>(G->getInitializer()); if (Seq && Seq->isCString()) NewGlobal->setSection("__TEXT,__asan_cstring,regular"); } // Transfer the debug info and type metadata. The payload starts at offset // zero so we can copy the metadata over as is. NewGlobal->copyMetadata(G, 0); Value *Indices2[2]; Indices2[0] = IRB.getInt32(0); Indices2[1] = IRB.getInt32(0); G->replaceAllUsesWith( ConstantExpr::getGetElementPtr(NewTy, NewGlobal, Indices2, true)); NewGlobal->takeName(G); G->eraseFromParent(); NewGlobals[i] = NewGlobal; Constant *ODRIndicator = ConstantPointerNull::get(PtrTy); GlobalValue *InstrumentedGlobal = NewGlobal; bool CanUsePrivateAliases = TargetTriple.isOSBinFormatELF() || TargetTriple.isOSBinFormatMachO() || TargetTriple.isOSBinFormatWasm(); if (CanUsePrivateAliases && UsePrivateAlias) { // Create local alias for NewGlobal to avoid crash on ODR between // instrumented and non-instrumented libraries. InstrumentedGlobal = GlobalAlias::create(GlobalValue::PrivateLinkage, "", NewGlobal); } // ODR should not happen for local linkage. if (NewGlobal->hasLocalLinkage()) { ODRIndicator = ConstantExpr::getIntToPtr(ConstantInt::get(IntptrTy, -1), PtrTy); } else if (UseOdrIndicator) { // With local aliases, we need to provide another externally visible // symbol __odr_asan_XXX to detect ODR violation. auto *ODRIndicatorSym = new GlobalVariable(M, IRB.getInt8Ty(), false, Linkage, Constant::getNullValue(IRB.getInt8Ty()), kODRGenPrefix + NameForGlobal, nullptr, NewGlobal->getThreadLocalMode()); // Set meaningful attributes for indicator symbol. ODRIndicatorSym->setVisibility(NewGlobal->getVisibility()); ODRIndicatorSym->setDLLStorageClass(NewGlobal->getDLLStorageClass()); ODRIndicatorSym->setAlignment(Align(1)); ODRIndicator = ODRIndicatorSym; } // 計装済みグローバル変数のイニシャライザ Constant *Initializer = ConstantStruct::get( GlobalStructTy, ConstantExpr::getPointerCast(InstrumentedGlobal, IntptrTy), ConstantInt::get(IntptrTy, SizeInBytes), ConstantInt::get(IntptrTy, SizeInBytes + RightRedzoneSize), ConstantExpr::getPointerCast(Name, IntptrTy), ConstantExpr::getPointerCast(ModuleName, IntptrTy), ConstantInt::get(IntptrTy, MD.IsDynInit), Constant::getNullValue(IntptrTy), ConstantExpr::getPointerCast(ODRIndicator, IntptrTy)); if (ClInitializers && MD.IsDynInit) HasDynamicallyInitializedGlobals = true; LLVM_DEBUG(dbgs() << "NEW GLOBAL: " << *NewGlobal << "\n"); Initializers[i] = Initializer; } // Add instrumented globals to llvm.compiler.used list to avoid LTO from // ConstantMerge'ing them. SmallVector<GlobalValue *, 16> GlobalsToAddToUsedList; for (size_t i = 0; i < n; i++) { GlobalVariable *G = NewGlobals[i]; if (G->getName().empty()) continue; GlobalsToAddToUsedList.push_back(G); } appendToCompilerUsed(M, ArrayRef<GlobalValue *>(GlobalsToAddToUsedList)); if (UseGlobalsGC && TargetTriple.isOSBinFormatELF()) { // Use COMDAT and register globals even if n == 0 to ensure that (a) the // linkage unit will only have one module constructor, and (b) the register // function will be called. The module destructor is not created when n == // 0. *CtorComdat = true; instrumentGlobalsELF(IRB, M, NewGlobals, Initializers, getUniqueModuleId(&M)); } else if (n == 0) { // When UseGlobalsGC is false, COMDAT can still be used if n == 0, because // all compile units will have identical module constructor/destructor. *CtorComdat = TargetTriple.isOSBinFormatELF(); } else { *CtorComdat = false; if (UseGlobalsGC && TargetTriple.isOSBinFormatCOFF()) { InstrumentGlobalsCOFF(IRB, M, NewGlobals, Initializers); } else if (UseGlobalsGC && ShouldUseMachOGlobalsSection()) { InstrumentGlobalsMachO(IRB, M, NewGlobals, Initializers); } else { InstrumentGlobalsWithMetadataArray(IRB, M, NewGlobals, Initializers); } } // Create calls for poisoning before initializers run and unpoisoning after. // 動的に初期化されるグローバル変数のポイズン if (HasDynamicallyInitializedGlobals) createInitializerPoisonCalls(M, ModuleName); LLVM_DEBUG(dbgs() << M); }
グローバル変数は、グローバル変数および周囲のレッドゾーンの情報を持つ GlobalStructTy 構造体に置換されます。
GlobalStructTy の構造は上記でもコメントで書かれていますが、__asan_global 構造体が、ランタイムで使用される際のデータ構造です。
// This structure is used to describe the source location of a place where // global was defined. struct __asan_global_source_location { const char *filename; int line_no; int column_no; }; // This structure describes an instrumented global variable. struct __asan_global { uptr beg; // The address of the global. uptr size; // The original size of the global. uptr size_with_redzone; // The size with the redzone. const char *name; // Name as a C string. const char *module_name; // Module name as a C string. This pointer is a // unique identifier of a module. uptr has_dynamic_init; // Non-zero if the global has dynamic initializer. __asan_global_source_location *gcc_location; // Source location of a global, // used by GCC compiler. LLVM uses // llvm-symbolizer that relies // on DWARF debugging info. uptr odr_indicator; // The address of the ODR indicator symbol. };
グローバル変数は実行バイナリフォーマットによって異なる方法で計装されます。今回は ELF 形式と仮定して instrumentGlobalsELF 関数を読みます。
void ModuleAddressSanitizer::instrumentGlobalsELF( IRBuilder<> &IRB, Module &M, ArrayRef<GlobalVariable *> ExtendedGlobals, ArrayRef<Constant *> MetadataInitializers, const std::string &UniqueModuleId) { assert(ExtendedGlobals.size() == MetadataInitializers.size()); // Putting globals in a comdat changes the semantic and potentially cause // false negative odr violations at link time. If odr indicators are used, we // keep the comdat sections, as link time odr violations will be dectected on // the odr indicator symbols. bool UseComdatForGlobalsGC = UseOdrIndicator && !UniqueModuleId.empty(); SmallVector<GlobalValue *, 16> MetadataGlobals(ExtendedGlobals.size()); for (size_t i = 0; i < ExtendedGlobals.size(); i++) { GlobalVariable *G = ExtendedGlobals[i]; GlobalVariable *Metadata = CreateMetadataGlobal(M, MetadataInitializers[i], G->getName()); MDNode *MD = MDNode::get(M.getContext(), ValueAsMetadata::get(G)); Metadata->setMetadata(LLVMContext::MD_associated, MD); MetadataGlobals[i] = Metadata; if (UseComdatForGlobalsGC) SetComdatForGlobalMetadata(G, Metadata, UniqueModuleId); } // Update llvm.compiler.used, adding the new metadata globals. This is // needed so that during LTO these variables stay alive. if (!MetadataGlobals.empty()) appendToCompilerUsed(M, MetadataGlobals); // RegisteredFlag serves two purposes. First, we can pass it to dladdr() // to look up the loaded image that contains it. Second, we can store in it // whether registration has already occurred, to prevent duplicate // registration. // // Common linkage ensures that there is only one global per shared library. GlobalVariable *RegisteredFlag = new GlobalVariable( // __asan_globals_registered グローバル変数の作成 M, IntptrTy, false, GlobalVariable::CommonLinkage, ConstantInt::get(IntptrTy, 0), kAsanGlobalsRegisteredFlagName); RegisteredFlag->setVisibility(GlobalVariable::HiddenVisibility); // Create start and stop symbols. GlobalVariable *StartELFMetadata = new GlobalVariable( // __start_asan_globals グローバル変数の作成 M, IntptrTy, false, GlobalVariable::ExternalWeakLinkage, nullptr, "__start_" + getGlobalMetadataSection()); StartELFMetadata->setVisibility(GlobalVariable::HiddenVisibility); GlobalVariable *StopELFMetadata = new GlobalVariable( // __stop_asan_globals グローバル変数の作成 M, IntptrTy, false, GlobalVariable::ExternalWeakLinkage, nullptr, "__stop_" + getGlobalMetadataSection()); StopELFMetadata->setVisibility(GlobalVariable::HiddenVisibility); // Create a call to register the globals with the runtime. if (ConstructorKind == AsanCtorKind::Global) IRB.CreateCall(AsanRegisterElfGlobals, // __asan_register_elf_globals((void*)RegisteredFlag, (void*)StartELFMetadata, (void*)StopELFMetadata) {IRB.CreatePointerCast(RegisteredFlag, IntptrTy), IRB.CreatePointerCast(StartELFMetadata, IntptrTy), IRB.CreatePointerCast(StopELFMetadata, IntptrTy)}); // We also need to unregister globals at the end, e.g., when a shared library // gets closed. if (DestructorKind != AsanDtorKind::None && !MetadataGlobals.empty()) { IRBuilder<> IrbDtor(CreateAsanModuleDtor(M)); IrbDtor.CreateCall(AsanUnregisterElfGlobals, // __asan_unregister_elf_globals((void*)RegisteredFlag, (void*)StartELFMetadata, (void*)StopELFMetadata) {IRB.CreatePointerCast(RegisteredFlag, IntptrTy), IRB.CreatePointerCast(StartELFMetadata, IntptrTy), IRB.CreatePointerCast(StopELFMetadata, IntptrTy)}); } }
この関数のポイントは、RegisteredFlag (__asan_globals_registered), StartELFMetadata (__start_asan_globals) と StopELFMetadata (__stop_asan_globals) 変数の作成および AsanRegisterElfGlobals (__asan_register_elf_globals) 関数の呼び出しです。
上記の関数は、ASan ランタイムの初期化処理に以下を追加計装します。
__asan_register_elf_globals(&__asan_globals_registered, &__start_asan_globals, &__stop_asan_globals);
(★先にランタイム側の処理を見たい方用) __asan_register_elf_globals の処理はグローバル変数の登録とポイズニングに書いてあります。
ASan ランタイムの初期化処理
ASan を計装したアプリケーションを実行すると、ASan 関連の処理はまず asan.module_ctor が実行されます。
この関数は main 関数よりも先に実行されます*3。
__asan_init(); __asan_version_mismatch_check_v8();
その中で __asan_init 関数が呼び出されます。これは、名前の通り ASan の変数などを初期化します。
void __asan_init() { AsanActivate(); AsanInitFromRtl(); }
AsanActivate 関数を見てみましょう。
void AsanActivate() { if (!asan_is_deactivated) return; VReport(1, "Activating ASan\n"); UpdateProcessName(); // 各種設定値の初期化 asan_deactivated_flags.OverrideFromActivationFlags(); SetCanPoisonMemory(asan_deactivated_flags.poison_heap); SetMallocContextSize(asan_deactivated_flags.malloc_context_size); ReInitializeAllocator(asan_deactivated_flags.allocator_options); asan_is_deactivated = false; if (Verbosity()) { Report("Activated with flags:\n"); asan_deactivated_flags.Print(); } }
次に AsanInitFromRtl 関数を見てみましょう。
といってもこの関数はロックを獲得するだけで、本体は AsanInitInternal 関数です。
// Initialize as requested from some part of ASan runtime library (interceptors, // allocator, etc). void AsanInitFromRtl() { if (LIKELY(AsanInited())) // ASan が既に初期化されている場合は何もせずリターン return; SpinMutexLock lock(&asan_inited_mutex); AsanInitInternal(); }
static bool AsanInitInternal() { if (LIKELY(AsanInited())) // ASan が既に初期化されていれば何もせずリターン return true; SanitizerToolName = "AddressSanitizer"; CacheBinaryName(); // Initialize flags. This must be done early, because most of the // initialization steps look at flags(). InitializeFlags(); WaitForDebugger(flags()->sleep_before_init, "before init"); // (...省略...) InitializeHighMemEnd(); // (...省略...) InitializeAsanInterceptors(); // (...省略...) InitializeShadowMemory(); // (...省略...) VReport(1, "AddressSanitizer Init done\n"); WaitForDebugger(flags()->sleep_after_init, "after init"); return true; }
初期化処理では、設定の初期化、互換性チェックなどを行います。
シャドウメモリの初期化
シャドウメモリの仕組みについては その 1 で説明しました。 LLVM ではシャドウメモリをどう実装し、初期化しているか見ていきましょう。
InitializeShadowMemory 関数を見ていきます。
この関数は名前の通りシャドウメモリを初期化する関数です。
void InitializeShadowMemory() { // Set the shadow memory address to uninitialized. __asan_shadow_memory_dynamic_address = kDefaultShadowSentinel; uptr shadow_start = kLowShadowBeg; // Detect if a dynamic shadow address must used and find a available location // when necessary. When dynamic address is used, the macro |kLowShadowBeg| // expands to |__asan_shadow_memory_dynamic_address| which is // |kDefaultShadowSentinel|. bool full_shadow_is_available = false; if (shadow_start == kDefaultShadowSentinel) { // Ubuntu の場合、ここは常に通らない shadow_start = FindDynamicShadowStart(); if (SANITIZER_LINUX) full_shadow_is_available = true; } // Update the shadow memory address (potentially) used by instrumentation. __asan_shadow_memory_dynamic_address = shadow_start; if (kLowShadowBeg) shadow_start -= GetMmapGranularity(); // mmap の粒度 (ページサイズ == 0x1000) 分シャドウメモリの開始アドレスを減算 if (!full_shadow_is_available) // ローシャドウの始端からハイシャドウの終端まで全領域を利用できる場合、フルシャドウとする full_shadow_is_available = MemoryRangeIsAvailable(shadow_start, kHighShadowEnd); #if SANITIZER_LINUX && defined(__x86_64__) && defined(_LP64) && \ !ASAN_FIXED_MAPPING // Linux かつ x86_64 かつ 64 ビットポインタかつ固定アドレスでシャドウメモリをマップしない場合 if (!full_shadow_is_available) { // フルシャドウでない場合、ミッドシャドウを作成する kMidMemBeg = kLowMemEnd < 0x3000000000ULL ? 0x3000000000ULL : 0; kMidMemEnd = kLowMemEnd < 0x3000000000ULL ? 0x4fffffffffULL : 0; } #endif if (Verbosity()) PrintAddressSpaceLayout(); if (full_shadow_is_available) { // mmap the low shadow plus at least one page at the left. // フルシャドウの場合、ローシャドウとハイシャドウを予約する if (kLowShadowBeg) ReserveShadowMemoryRange(shadow_start, kLowShadowEnd, "low shadow"); // ローシャドウ領域の予約 // mmap the high shadow. ReserveShadowMemoryRange(kHighShadowBeg, kHighShadowEnd, "high shadow"); // ハイシャドウ領域の予約 // protect the gap. ProtectGap(kShadowGapBeg, kShadowGapEnd - kShadowGapBeg + 1); // シャドウギャップをアクセス不可領域に指定 CHECK_EQ(kShadowGapEnd, kHighShadowBeg - 1); } else if (kMidMemBeg && MemoryRangeIsAvailable(shadow_start, kMidMemBeg - 1) && MemoryRangeIsAvailable(kMidMemEnd + 1, kHighShadowEnd)) { // ミッドシャドウも予約する CHECK(kLowShadowBeg != kLowShadowEnd); // mmap the low shadow plus at least one page at the left. ReserveShadowMemoryRange(shadow_start, kLowShadowEnd, "low shadow"); // ローシャドウの予約 // mmap the mid shadow. ReserveShadowMemoryRange(kMidShadowBeg, kMidShadowEnd, "mid shadow"); // ミッドシャドウの予約 // mmap the high shadow. ReserveShadowMemoryRange(kHighShadowBeg, kHighShadowEnd, "high shadow"); // ハイシャドウの予約 // protect the gaps. ProtectGap(kShadowGapBeg, kShadowGapEnd - kShadowGapBeg + 1); // ローとミッド間のシャドウギャップ ProtectGap(kShadowGap2Beg, kShadowGap2End - kShadowGap2Beg + 1); // ミッドとミッドメモリ領域間のシャドウギャップ ProtectGap(kShadowGap3Beg, kShadowGap3End - kShadowGap3Beg + 1); // ミッドメモリ領域とハイ間のシャドウギャップ } else { Report( "Shadow memory range interleaves with an existing memory mapping. " "ASan cannot proceed correctly. ABORTING.\n"); Report("ASan shadow was supposed to be located in the [%p-%p] range.\n", (void*)shadow_start, (void*)kHighShadowEnd); MaybeReportLinuxPIEBug(); DumpProcessMap(); Die(); } }
シャドウメモリの始端アドレス __asan_shadow_memory_dynamic_address は固定値 kLowShadowBeg です。これは OS やアーキテクチャによってコンパイル時に定まります。
ちなみに Linux x64 の場合は 0x000000007fff8000 ですが、そこから mmap の粒度 (ページサイズ) 分減算されるため、マップされる始端アドレスはこれより 0x1000 小さい値になります。
シャドウメモリの終端アドレス kHighShadowEnd は (((kHighMemEnd) >> 3) + (kConstShadowOffset)) に展開されます。
メモリ空間の終端アドレス kHighMemEnd は実行中に (AsanInitInternal の中で) 定まります。
static void InitializeHighMemEnd() { #if !ASAN_FIXED_MAPPING kHighMemEnd = GetMaxUserVirtualAddress(); // Increase kHighMemEnd to make sure it's properly // aligned together with kHighMemBeg: kHighMemEnd |= (GetMmapGranularity() << ASAN_SHADOW_SCALE) - 1; #endif // !ASAN_FIXED_MAPPING CHECK_EQ((kHighMemBeg % GetMmapGranularity()), 0); }
GetMaxUserVirtualAddress 関数はユーザー空間の仮想メモリの上限 (Linux x64 の場合、0x00007fffffffffff) を返します。
kConstShadowOffset は Linux x64 の場合、kLowShadowBeg と同じ値 0x000000007fff8000 です。
というわけで、その 1 で紹介した計算方法より、kHighShadowEnd = (0x7fffffffffff >> 3) + 0x7fff8000 = 0x10007fff7fff になります。
そして、シャドウメモリ領域を mmap で予約します。
なお、Linux x64 の場合は約 13TB の領域 (kLowShadowBeg - 0x1000 から kHighShadowEnd まで) を mmap で予約しますが、仮想メモリを予約するだけであり、これだけで物理メモリを割り当てるわけではありません。
必要に応じて動的に物理メモリが確保されます。
ただし、条件によっては動的シャドウメモリ (dynamic shadow) と呼ばれる方法を使用します。 これは mmap を使用してシャドウメモリを確保するものです。 動的シャドウメモリの詳細は割愛します。
シャドウメモリアドレスに対応するシャドウメモリである禁止領域 (bad region) ことシャドウギャップ (shadow gap) によって、シャドウメモリがローシャドウとハイシャドウに分割されることは、その 1 で説明した通りです。 ですが、LLVM の実装にはミッドシャドウ (mid shadow) と呼ばれる謎のシャドウメモリ領域も存在します。 ミッドシャドウはシャドウメモリのアドレス全体をシャドウメモリとして予約できる場合 (full shadow is available)、作られません。 論文に書いてあるのは、full shadow is available な時のシャドウメモリレイアウトです。
LLVM ASan のシャドウメモリレイアウトは asan_mapping.h に書いてあります。
この中にミッドシャドウが必要になる理由も書いてあります。
// Special case when something is already mapped between // 0x003000000000 and 0x005000000000 (e.g. when prelink is installed): // || `[0x10007fff8000, 0x7fffffffffff]` || HighMem || // || `[0x02008fff7000, 0x10007fff7fff]` || HighShadow || // || `[0x005000000000, 0x02008fff6fff]` || ShadowGap3 || // || `[0x003000000000, 0x004fffffffff]` || MidMem || // prelink が占領する領域 // || `[0x000a7fff8000, 0x002fffffffff]` || ShadowGap2 || // || `[0x00067fff8000, 0x000a7fff7fff]` || MidShadow || // || `[0x00008fff7000, 0x00067fff7fff]` || ShadowGap || // || `[0x00007fff8000, 0x00008fff6fff]` || LowShadow || // || `[0x000000000000, 0x00007fff7fff]` || LowMem ||
prelink がインストールされている場合、0x003000000000 〜 0x005000000000 のアドレスがシャドウメモリと競合してしまうため、その領域を避けるためにミッドシャドウが必要になります。
ミッドシャドウを配置する分、シャドウギャップも 3 つに増えます。
関数割り込み (interceptor)
サニタイザーの弱点の 1 つとして、あくまでも計装したバイナリ内でしか検証できないという点があります。 例えば、バイナリ A に共有オブジェクト B を動的リンクして実行する場合、A を計装していても B を計装していない場合、B の中でバッファオーバーフローしてもその瞬間を検知できません。
Linux では多くのバイナリが libc.so を動的リンクして実行します。
その中にある strcpy などは使用を誤ってバッファオーバーフローを発生させやすいというのは有名な話です。
しかし、ユーザーの手元にある A をビルド時に計装しても、libc.so は自分でソースコードからビルドしない限り計装できないため、A から呼び出される strcpy で発生したバグを即座に検知できません。
サニタイザーではその対策として、割り込み (interceptor) を使用します。
interceptor は、割り込み対象のオリジナル関数を ASan の関数に置換します。
strcpy を例に割り込みの概略を説明すると、以下のようになります。
strcpyを呼び出すと代わりに__intercept_strcpyが呼び出される__intercept_strcpyを呼び出すと___intercept_strcpyへジャンプ___intercept_strcpyでメモリアクセスを検証し、問題が無ければreal_strcpy(元のstrcpy) を呼び出す
これがどのように実装されているか見ていきましょう。
InitializeAsanInterceptors 関数では、strcpy や memset などメモリ操作系の主要な関数に割り込み、ASan の関数に置き換えて検証処理やシャドウメモリ操作などを行います。
void InitializeAsanInterceptors() { static bool was_called_once; CHECK(!was_called_once); was_called_once = true; InitializePlatformInterceptors(); // プラットフォーム固有の割り込み InitializeCommonInterceptors(); // サニタイザー共通の割り込み InitializeSignalInterceptors(); // signal 系関数の割り込み // Intercept str* functions. ASAN_INTERCEPT_FUNC(strcat); ASAN_INTERCEPT_FUNC(strcpy); ASAN_INTERCEPT_FUNC(strncat); ASAN_INTERCEPT_FUNC(strncpy); ASAN_INTERCEPT_FUNC(strdup); #if ASAN_INTERCEPT___STRDUP ASAN_INTERCEPT_FUNC(__strdup); #endif #if ASAN_INTERCEPT_INDEX && ASAN_USE_ALIAS_ATTRIBUTE_FOR_INDEX ASAN_INTERCEPT_FUNC(index); #endif // (...省略...) VReport(1, "AddressSanitizer: libc interceptors initialized\n"); }
上記では strcat などが置き換えられています。
また、InitializeCommonInterceptors 関数の中で mmap や strcmp などが置き換えられています。
InitializeCommonInterceptors は MSan など、ASan 以外のサニタイザーでも使用される割り込み計装関数です。
ASan 固有の割り込み計装は InitializeAsanInterceptors で行われています。
static void InitializeCommonInterceptors() { #if SI_POSIX static u64 metadata_mem[sizeof(MetadataHashMap) / sizeof(u64) + 1]; interceptor_metadata_map = new ((void *)&metadata_mem) MetadataHashMap(); #endif __sanitizer::InitializeMemintrinsicInterceptors(); INIT_MMAP; INIT_MMAP64; INIT_TEXTDOMAIN; INIT_STRLEN; INIT_STRNLEN; // (...省略...) }
InitializeCommonInterceptors では INIT_* マクロで関数を置換しています。
例えば INIT_STRLEN は以下のようになっています。
#define INIT_STRLEN COMMON_INTERCEPT_FUNCTION(strlen)
マクロが何重にもなっていて面倒ですが、元をたどっていくと ASan の場合は ASAN_INTERCEPT_FUNC にたどり着きます。
#define COMMON_INTERCEPT_FUNCTION(name) ASAN_INTERCEPT_FUNC(name)
INIT_* は最終的に ASAN_INTERCEPT_FUNC(*) と等価です。
各々のサニタイザーに応じた計装方法を取るため、ASan の場合はこのマクロになりますが、MSan などの場合はまた別のマクロになります。ただ、その場合でも最終的な計装内容はほぼ同じです。
# if !SANITIZER_APPLE # define ASAN_INTERCEPT_FUNC(name) \ do { \ if (!INTERCEPT_FUNCTION(name)) \ VReport(1, "AddressSanitizer: failed to intercept '%s'\n", #name); \ } while (0) # define ASAN_INTERCEPT_FUNC_VER(name, ver) \ do { \ if (!INTERCEPT_FUNCTION_VER(name, ver)) \ VReport(1, "AddressSanitizer: failed to intercept '%s@@%s'\n", \ #name, ver); \ } while (0) # define ASAN_INTERCEPT_FUNC_VER_UNVERSIONED_FALLBACK(name, ver) \ do { \ if (!INTERCEPT_FUNCTION_VER(name, ver) && !INTERCEPT_FUNCTION(name)) \ VReport(1, \ "AddressSanitizer: failed to intercept '%s@@%s' or '%s'\n", \ #name, ver, #name); \ } while (0) # else // OS X interceptors don't need to be initialized with INTERCEPT_FUNCTION. # define ASAN_INTERCEPT_FUNC(name) # endif // SANITIZER_APPLE #define ASAN_INTERCEPTOR_ENTER(ctx, func) \ AsanInterceptorContext _ctx = {#func}; \ ctx = (void *)&_ctx; \ (void) ctx;
# define INTERCEPT_FUNCTION(func) INTERCEPT_FUNCTION_LINUX_OR_FREEBSD(func)
#define INTERCEPT_FUNCTION_LINUX_OR_FREEBSD(func) \ ::__interception::InterceptFunction( \ #func, \ (::__interception::uptr *)&REAL(func), \ (::__interception::uptr)&(func), \ (::__interception::uptr)&TRAMPOLINE(func))
// Weak aliases of weak aliases do not work, therefore we need to set up a // trampoline function. The function "func" is a weak alias to the trampoline // (so that we may check if "func" was overridden), which calls the weak // function __interceptor_func, which in turn aliases the actual interceptor // implementation ___interceptor_func: // // [wrapper "func": weak] --(alias)--> [TRAMPOLINE(func)] // | // +--------(tail call)-------+ // | // v // [__interceptor_func: weak] --(alias)--> [WRAP(func)] // // We use inline assembly to define most of this, because not all compilers // support functions with the "naked" attribute with every architecture. # define WRAP(x) ___interceptor_ ## x # define TRAMPOLINE(x) __interceptor_trampoline_ ## x // (...省略...) # define PTR_TO_REAL(x) real_##x # define REAL(x) __interception::PTR_TO_REAL(x)
という風になっています。
上記を元に ASAN_INTERCEPT_FUNC を展開すると、以下のようになります。
do { if ( !::__interception::InterceptFunction( #name, // 置換する関数の名前 (::__interception::uptr *)&(real_ ## name), // real_置換関数のアドレスがあるポインタ (::__interception::uptr)&(name), // オリジナル関数のアドレスがあるポインタ (::__interception::uptr)&(__interceptor_trampoline_ ## name) // __interceptor_trampoline__置換関数のアドレスがあるポインタ ) ) VReport(1, "AddressSanitizer: failed to intercept '%s'\n", #name); } while (0)
InterceptFunction および中で使用される GetFuncAddr は以下の関数です。
InterceptFunction は、オリジナル関数アドレスを取得して real_* 変数に代入します。
static void *GetFuncAddr(const char *name, uptr trampoline) { #if SANITIZER_NETBSD // FIXME: Find a better way to handle renames if (StrCmp(name, "sigaction")) name = "__sigaction14"; #endif void *addr = dlsym(RTLD_NEXT, name); if (!addr) { // If the lookup using RTLD_NEXT failed, the sanitizer runtime library is // later in the library search order than the DSO that we are trying to // intercept, which means that we cannot intercept this function. We still // want the address of the real definition, though, so look it up using // RTLD_DEFAULT. addr = dlsym(RTLD_DEFAULT, name); // In case `name' is not loaded, dlsym ends up finding the actual wrapper. // We don't want to intercept the wrapper and have it point to itself. if ((uptr)addr == trampoline) addr = nullptr; } return addr; } bool InterceptFunction(const char *name, uptr *ptr_to_real, uptr func, uptr trampoline) { void *addr = GetFuncAddr(name, trampoline); // オリジナル関数のアドレスを取得 *ptr_to_real = (uptr)addr; // real_func のアドレスをオリジナルに書き換え return addr && (func == trampoline); // 成否を返す }
具体例として、ASAN_INTERCEPT_FUNC(strcpy) マクロを展開すると、以下のようになります。
do { if ( !::__interception::InterceptFunction( "strcpy", (::__interception::uptr *)&real_strcpy, (::__interception::uptr)&strcpy, (::__interception::uptr)&__interceptor_trampoline_strcpy ) ) VReport(1, "AddressSanitizer: failed to intercept '%s'\n", "strcpy"); } while (0)
展開してみると、real_strcpy や __interceptor_trampoline_strcpy という変数が出現しました。
これらの変数は、asan_interceptors.cpp の中で定義されます。
uptr internal_strlen(const char *s) { uptr i = 0; while (s[i]) i++; return i; } INTERCEPTOR(char *, strcpy, char *to, const char *from) { void *ctx; ASAN_INTERCEPTOR_ENTER(ctx, strcpy); if constexpr (SANITIZER_APPLE) { // strcpy is called from malloc_default_purgeable_zone() // in __asan::ReplaceSystemAlloc() on Mac. if (UNLIKELY(!AsanInited())) return REAL(strcpy)(to, from); } else { if (!TryAsanInitFromRtl()) return REAL(strcpy)(to, from); // まだ ASan の初期化がされていない場合は元の strcpy を呼び出すだけ } if (flags()->replace_str) { // デフォルト設定ならこのパスを通る uptr from_size = internal_strlen(from) + 1; // 文字列長を調べる CHECK_RANGES_OVERLAP("strcpy", to, from_size, from, from_size); // 読み書きする範囲が重複していないか検証 ASAN_READ_RANGE(ctx, from, from_size); // 読込可能か検証 ASAN_WRITE_RANGE(ctx, to, from_size); // 書込可能か検証 } return REAL(strcpy)(to, from); // 元の strcpy を呼び出す }
INTERCEPTOR マクロを見てみましょう。
#define INTERCEPTOR(ret_type, func, ...) \ DEFINE_REAL(ret_type, func, __VA_ARGS__) \ DECLARE_WRAPPER(ret_type, func, __VA_ARGS__) \ extern "C" INTERCEPTOR_ATTRIBUTE ret_type WRAP(func)(__VA_ARGS__)
# define INTERCEPTOR_ATTRIBUTE __attribute__((visibility("default")))
# define DEFINE_REAL(ret_type, func, ...) \ typedef ret_type (*FUNC_TYPE(func))(__VA_ARGS__); \ namespace __interception { \ FUNC_TYPE(func) PTR_TO_REAL(func); \ } # define DECLARE_WRAPPER(ret_type, func, ...) \ extern "C" ret_type func(__VA_ARGS__); \ // 置換対象の関数 func extern "C" ret_type TRAMPOLINE(func)(__VA_ARGS__); \ // トランポリン __interceptor_trampoline_func extern "C" ret_type __interceptor_##func(__VA_ARGS__) \ // 置換後の関数 __interceptor__func INTERCEPTOR_ATTRIBUTE __attribute__((weak)) ALIAS(WRAP(func)); \ asm( \ // __interceptor_trampoline_func の中身 ".text\n" \ __ASM_WEAK_WRAPPER(func) \ ".set " #func ", " SANITIZER_STRINGIFY(TRAMPOLINE(func)) "\n" \ ".globl " SANITIZER_STRINGIFY(TRAMPOLINE(func)) "\n" \ ".type " SANITIZER_STRINGIFY(TRAMPOLINE(func)) ", " \ ASM_TYPE_FUNCTION_STR "\n" \ SANITIZER_STRINGIFY(TRAMPOLINE(func)) ":\n" \ SANITIZER_STRINGIFY(CFI_STARTPROC) "\n" \ C_ASM_TAIL_CALL(SANITIZER_STRINGIFY(TRAMPOLINE(func)), \ "__interceptor_" \ SANITIZER_STRINGIFY(ASM_PREEMPTIBLE_SYM(func))) "\n" \ SANITIZER_STRINGIFY(CFI_ENDPROC) "\n" \ ".size " SANITIZER_STRINGIFY(TRAMPOLINE(func)) ", " \ ".-" SANITIZER_STRINGIFY(TRAMPOLINE(func)) "\n" \ );
strcpy の具体例で説明します。
strcpy の DEFINE_REAL を展開すると、以下の通りです。
DEFINE_REAL では __iterception::real_strcpy と strcpy_type を定義します。
real_strcpy はオリジナルの strcpy 関数アドレスを入れるための変数、strcpy_type は strcpy の関数型です。
real_strcpy に入る値は ASan 初期化時の InterceptFunction 関数で設定されます。
typedef char* (*strcpy_type)(char* to, const char* from); namespace __interception { strcpy_type real_strcpy; }
DECLARE_WRAPPER では strcpy と __interceptor_trampoline_strcpy と __interceptor_strcpy (エイリアスで ___interceptor_strcpy *4)を extern 宣言し、アセンブリで __interceptor_trampoline_strcpy を実装しています。
extern "C" char * strcpy(char *to, const char *from); extern "C" char * __interceptor_trampoline_strcpy(char *to, const char *from); extern "C" char * __interceptor_strcpy(char *to, const char *from) \ __attribute__((visibility("default"))) \ __attribute__((weak)) \ __attribute__((alias("___interceptor_strcpy")))
strcpy のインラインアセンブリを展開すると、以下のようになります。
オリジナルの strcpy を __interceptor_trampoline_strcpy にし、__interceptor_trampoline_strcpy の中身として単に __interceptor_strcpy へジャンプするようにしています。
.text .weak strcpy .set strcpy, __interceptor_trampoline_strcpy .globl __interceptor_trampoline_strcpy .type __interceptor_trampoline_strcpy, @function __interceptor_trampoline_strcpy: .cfi_startproc jmp __interceptor_strcpy@plt .cfi_endproc .size __interceptor_trampoline_strcpy, .-__interceptor_trampoline_strcpy
REAL(strcpy) は real_strcpy に置換されます。これはオリジナルの strcpy が呼ばれます。
INTERCEPTOR マクロに戻ります。
#define INTERCEPTOR(ret_type, func, ...) \ DEFINE_REAL(ret_type, func, __VA_ARGS__) \ DECLARE_WRAPPER(ret_type, func, __VA_ARGS__) \ extern "C" INTERCEPTOR_ATTRIBUTE ret_type WRAP(func)(__VA_ARGS__)
最後の行は、続くブロックの関数型です。展開すると、以下のようになります。
extern "C" __attribute__((visibility("default"))) char * ___interceptor_strcpy(char *to, const char *from)
よって、INTERCEPTOR の関数は ___interceptor_strcpy という関数名で定義されます。すなわち、
INTERCEPTOR(char *, strcpy, char *to, const char *from) {
void *ctx;
ASAN_INTERCEPTOR_ENTER(ctx, strcpy);
// (...省略...)
}
は
extern "C" __attribute__((visibility("default"))) char * ___interceptor_strcpy(char *to, const char *from) {
void *ctx;
ASAN_INTERCEPTOR_ENTER(ctx, strcpy);
// (...省略...)
}
というように展開されます。
試しに以下のプログラムをビルドして、アセンブリを確認してみましょう。
#include <stdio.h> #include <string.h> int main(int argc, char** argv) { char buf[16]; if (argc < 2) { printf("%s <string>\n", argv[0]); return 1; } strcpy(buf, argv[1]); printf("buf = %s\n", buf); return 0; }
clang -fsanitize=address -g test-strcpy.c
main 関数内で strcpy が呼ばれます。
strcpy は __interceptor_trampoline_strcpy になっていて、さらに JMP 命令で ___interceptor_strcpy に飛びます。
__interceptor_strcpy と ___interceptor_strcpy はエイリアスの関係であるため、同じアドレスを指します。
Ghidra で解釈すると _ が 3 つの方を優先して表示するようです。
__interceptor_trampoline_strcpy
___interceptor_strcpy では、最初に ASan が初期化済みか調べて、初期化されていれば ___interceptor_strcpy が呼ばれます。
なお、この ___interceptor_strcpy と、トランポリンから飛んだ ___interceptor_strcpy は同シンボル名なだけで別のアドレスを指します。
グローバル変数の登録とポイズニング
ランタイムにおけるグローバル変数の登録とポイズニングについて見ていきます。
グローバル変数の登録は、asan.module_ctor 内の __asan_register_elf_globals で行われます。
ここでいう「登録 (register)」は、検知もしくはデバッグ時のレポートへ出力するために、グローバル変数のメタデータを保持しておくことを指します。
__asan_register_elf_globals(&__asan_globals_registered, &__start_asan_globals, &__stop_asan_globals);
__asan_globals_registered は、グローバル変数を管理する ASan メタデータを登録済みか示すフラグです。
__start_asan_globals および __stop_asan_globals は、メタデータ領域の始点と終点を指す変数です。
グローバル変数を扱うランタイムの関数は asan_globals.cpp にまとめられています。
__asan_register_elf_globals 関数は以下の通りです。
void __asan_register_elf_globals(uptr *flag, void *start, void *stop) { if (*flag) return; // 既に登録済みの場合、何もしない if (!start) return; CHECK_EQ(0, ((uptr)stop - (uptr)start) % sizeof(__asan_global)); __asan_global *globals_start = (__asan_global*)start; // StartELFMetadata __asan_global *globals_stop = (__asan_global*)stop; //StopELFMetadata __asan_register_globals(globals_start, globals_stop - globals_start); *flag = 1; // 登録済みフラグを立てる }
この関数でグローバル変数を登録します。
本体は __asan_register_globals 関数です。
void __asan_register_globals(__asan_global *globals, uptr n) { if (!flags()->report_globals) return; GET_STACK_TRACE_MALLOC; u32 stack_id = StackDepotPut(stack); // stack depot (デバッグ用にスタック状態を退避・保存しておく場所) に今のスタック状態を保存 Lock lock(&mu_for_globals); if (!global_registration_site_vector) { global_registration_site_vector = new (GetGlobalLowLevelAllocator()) GlobalRegistrationSiteVector; global_registration_site_vector->reserve(128); } GlobalRegistrationSite site = {stack_id, &globals[0], &globals[n - 1]}; global_registration_site_vector->push_back(site); // これから登録するグローバル変数をどこで登録したか、スタック状態とともに記録 if (flags()->report_globals >= 2) { PRINT_CURRENT_STACK(); Printf("=== ID %d; %p %p\n", stack_id, (void *)&globals[0], (void *)&globals[n - 1]); } for (uptr i = 0; i < n; i++) { if (SANITIZER_WINDOWS && globals[i].beg == 0) { // Windows 用の処理 // (...省略...) continue; } RegisterGlobal(&globals[i]); } // Poison the metadata. It should not be accessible to user code. // 普通ならアクセスされないため、グローバル変数のメタデータ領域をポイズンしておく PoisonShadow(reinterpret_cast<uptr>(globals), n * sizeof(__asan_global), kAsanGlobalRedzoneMagic); // kAsanGlobalRedzoneMagic = 0xf9 }
上記では global_registration_site_vector に、今登録するグローバル変数がどこで登録されたか、スタック状態とともに保持しておきます。
この情報はレポートで使用されます。
その後、RegisterGlobal でグローバル変数を登録します。
// Register a global variable. // This function may be called more than once for every global // so we store the globals in a map. static void RegisterGlobal(const Global *g) { // 諸々の assertion // (...省略...) if (CanPoisonMemory()) PoisonRedZones(*g); // グローバル変数の領域をポイズンする ListOfGlobals *l = new (GetGlobalLowLevelAllocator()) ListOfGlobals; l->g = g; l->next = list_of_all_globals; list_of_all_globals = l; // グローバル変数リストに登録した変数を追加 if (g->has_dynamic_init) { // 動的に初期化するグローバル変数の場合、`dynamic_init_globals` にも挿入 if (!dynamic_init_globals) { dynamic_init_globals = new (GetGlobalLowLevelAllocator()) VectorOfGlobals; dynamic_init_globals->reserve(kDynamicInitGlobalsInitialCapacity); } DynInitGlobal dyn_global = { *g, false }; dynamic_init_globals->push_back(dyn_global); } }
RegisterGlobal は諸々の assertion の後、PoisonRedZones でグローバル変数領域をポイズンします。
その後、単方向リストである list_of_all_globals にグローバル変数を追加します。
また、動的に初期化されるグローバル変数の場合は dynamic_init_globals にも追加されます。
なお、静的な初期化とは、int a = 4649; のように、コンパイル時もしくは OS によるプログラム読み込み時 (.bss) に決定される初期化を指します。
このタイプはコンパイル時に値が決まっているため、ランタイムにおけるポイズンも、ASan の初期化時に行えばいいだけです。
対して、動的な初期化とは、int a = foo(); のように、実行時に決定される初期化です。
動的な初期化のグローバル変数を別に扱っているのは、Static Initialization Order Fiasco を検知するためです。
PoisonRedZones 関数は、グローバル変数の範囲内をアラインメントを考慮しながらポイズンするものです。
ALWAYS_INLINE void PoisonRedZones(const Global &g) { uptr aligned_size = RoundUpTo(g.size, ASAN_SHADOW_GRANULARITY); // アラインメント FastPoisonShadow(g.beg + aligned_size, g.size_with_redzone - aligned_size, // グローバル変数の範囲をポイズンする kAsanGlobalRedzoneMagic); if (g.size != aligned_size) { FastPoisonShadowPartialRightRedzone( // アラインメントからずれた分をポイズン g.beg + RoundDownTo(g.size, ASAN_SHADOW_GRANULARITY), g.size % ASAN_SHADOW_GRANULARITY, ASAN_SHADOW_GRANULARITY, kAsanGlobalRedzoneMagic); } }
Static Initialization Order Fiasco の検知
Static Initialization Order Fiasco については、末尾のおまけで簡単に説明しています。 ご存知でない方は先にそちらをご覧ください。
ASan は Static Initialization Order Fiasco もオプションをつければ検知してくれます。
check_initialization_order オプションを設定して実行してみましょう。
$ clang++ -g -fsanitize=address a.cc b.cc
$ ASAN_OPTIONS=check_initialization_order=true ./a.out
=================================================================
==41970==ERROR: AddressSanitizer: initialization-order-fiasco on address 0x609336306080 at pc 0x6093358ef9ba bp 0x7ffe6a4bbde0 sp 0x7ffe6a4bbdd8
READ of size 4 at 0x609336306080 thread T0
#0 0x6093358ef9b9 in read_extern_global() /home/user/a.cc:4:10
#1 0x6093357b539c in __cxx_global_var_init /home/user/a.cc:6:9
#2 0x6093357b5414 in _GLOBAL__sub_I_a.cc /home/user/a.cc
#3 0x7675aba29eba in call_init csu/../csu/libc-start.c:145:3
#4 0x7675aba29eba in __libc_start_main csu/../csu/libc-start.c:379:5
#5 0x6093357b54d4 in _start (/home/user/a.out+0x2b4d4)
0x609336306080 is located 0 bytes inside of global variable 'extern_global' defined in 'b.cc' (0x609336306080) of size 4
registered at:
#0 0x6093357d0d07 in __asan_register_globals.part.0 /home/user/llvm-project/compiler-rt/lib/asan/asan_globals.cpp:369:3
#1 0x6093357d13db in __asan_register_globals /home/user/llvm-project/compiler-rt/lib/asan/asan_globals.cpp:352:55
#2 0x6093357d13db in __asan_register_elf_globals /home/user/llvm-project/compiler-rt/lib/asan/asan_globals.cpp:352:26
#3 0x7675aba29eba in call_init csu/../csu/libc-start.c:145:3
#4 0x7675aba29eba in __libc_start_main csu/../csu/libc-start.c:379:5
SUMMARY: AddressSanitizer: initialization-order-fiasco /home/user/a.cc:4:10 in read_extern_global()
(...省略...)
なお、この場合 a.cc b.cc の順番では検知してくれますが、b.cc a.cc の順番では検知してくれません。
その順番でも検知してほしい場合は strict_init_order も併せてセットします。
$ clang++ -g -fsanitize=address b.cc a.cc $ ASAN_OPTIONS=check_initialization_order=true ./a.out # 検知されない 43 $ ASAN_OPTIONS=check_initialization_order=true:strict_init_order=true ./a.out # 検知される ================================================================= ==33516==ERROR: AddressSanitizer: initialization-order-fiasco on address 0x5a64ecee3560 at pc 0x5a64ec55904a bp 0x7ffe655bb910 sp 0x7ffe655bb908 READ of size 4 at 0x5a64ecee3560 thread T0 (...省略...)
では、どうやって検知しているのか実装を見てみましょう。
instrumentGlobals 関数に戻ります。
void ModuleAddressSanitizer::instrumentGlobals(IRBuilder<> &IRB, Module &M, bool *CtorComdat) { // (...省略...) GlobalVariable *ModuleName = // グローバル変数に対応するモジュール名 n != 0 ? createPrivateGlobalForString(M, M.getModuleIdentifier(), /*AllowMerging*/ false, kAsanGenPrefix) : nullptr; for (size_t i = 0; i < n; i++) { // (...省略...) if (ClInitializers && MD.IsDynInit) HasDynamicallyInitializedGlobals = true; // 動的初期化を持つグローバル変数の場合 } // (...省略...) // Create calls for poisoning before initializers run and unpoisoning after. if (HasDynamicallyInitializedGlobals) createInitializerPoisonCalls(M, ModuleName); }
動的初期化されるグローバル変数は、createInitializerPoisonCalls で追加の計装が行われます。
void ModuleAddressSanitizer::createInitializerPoisonCalls( Module &M, GlobalValue *ModuleName) { GlobalVariable *GV = M.getGlobalVariable("llvm.global_ctors"); if (!GV) return; ConstantArray *CA = dyn_cast<ConstantArray>(GV->getInitializer()); if (!CA) return; for (Use &OP : CA->operands()) { if (isa<ConstantAggregateZero>(OP)) continue; ConstantStruct *CS = cast<ConstantStruct>(OP); // Must have a function or null ptr. if (Function *F = dyn_cast<Function>(CS->getOperand(1))) { if (F->getName() == kAsanModuleCtorName) continue; // asan.module_ctor は計装しない auto *Priority = cast<ConstantInt>(CS->getOperand(0)); // Don't instrument CTORs that will run before asan.module_ctor. // asan.module_ctor より前に実行されるコンストラクタは計装しない if (Priority->getLimitedValue() <= GetCtorAndDtorPriority(TargetTriple)) continue; poisonOneInitializer(*F, ModuleName); // 計装する } } }
この関数は動的初期化の関数を計装します。
計装の本体は poisonOneInitializer です。
void ModuleAddressSanitizer::poisonOneInitializer(Function &GlobalInit, GlobalValue *ModuleName) { // Set up the arguments to our poison/unpoison functions. IRBuilder<> IRB(&GlobalInit.front(), GlobalInit.front().getFirstInsertionPt()); // Add a call to poison all external globals before the given function starts. Value *ModuleNameAddr = ConstantExpr::getPointerCast(ModuleName, IntptrTy); IRB.CreateCall(AsanPoisonGlobals, ModuleNameAddr); // __asan_before_dynamic_init(ModuleName); // Add calls to unpoison all globals before each return instruction. for (auto &BB : GlobalInit) if (ReturnInst *RI = dyn_cast<ReturnInst>(BB.getTerminator())) CallInst::Create(AsanUnpoisonGlobals, "", RI); // __asan_after_dynamic_init(); }
この関数はイニシャライザー関数の直前と直後にそれぞれ AsanPoisonGlobals (__asan_before_dynamic_init) と AsanUnpoisonGlobals (__asan_after_dynamic_init) を計装します。
モジュール名 は、グローバル変数が定義されたモジュール名 (ファイル名)、イニシャライザーはグローバル変数の動的初期化を行う関数です。
a.cc の例でいうと、x のモジュール名は a.cc という文字列で、イニシャライザーは __cxx_global_var_init です。
__asan_before_dynamic_init(モジュール名); イニシャライザー(); __asan_after_dynamic_init();
では、ランタイムのコードを見ていきましょう。
まずは __asan_before_dynamic_init を見てみます。
// This method runs immediately prior to dynamic initialization in each TU, // when all dynamically initialized globals are unpoisoned. This method // poisons all global variables not defined in this TU, so that a dynamic // initializer can only touch global variables in the same TU. void __asan_before_dynamic_init(const char *module_name) { if (!flags()->check_initialization_order || !CanPoisonMemory() || !dynamic_init_globals) return; bool strict_init_order = flags()->strict_init_order; CHECK(module_name); CHECK(AsanInited()); Lock lock(&mu_for_globals); if (flags()->report_globals >= 3) Printf("DynInitPoison module: %s\n", module_name); for (uptr i = 0, n = dynamic_init_globals->size(); i < n; ++i) { DynInitGlobal &dyn_g = (*dynamic_init_globals)[i]; const Global *g = &dyn_g.g; if (dyn_g.initialized) // 初期化済みなら無視 continue; if (g->module_name != module_name) // グローバル変数が定義されたモジュールと異なる場合、ポイズンする PoisonShadowForGlobal(g, kAsanInitializationOrderMagic); else if (!strict_init_order) dyn_g.initialized = true; // strict_init_order が無効で同じモジュール名の場合、初期化済みフラグを立てる } }
行っていることは、今初期化しようとしているグローバル変数のモジュール名と異なるモジュール名であるグローバル変数の領域へのポイズンです。
まず、グローバル変数が初期化済みか確認します。 グローバル変数が初期化済みということは、この後の初期化処理で値が変わることはないと言えるため、気兼ねなく使用できます。 よって、このときはポイズンしません。
次に、モジュール名がグローバル変数を定義したモジュール名と一緒か確認し、異なればポイズンします。
この関数の引数である module_name には、__cxx_global_var_init を呼び出したモジュール名が入ります。
具体的には、x の定義は a.cc でされているため、そこには a.cc, 同様に extern_global は b.cc といった文字列が入ります。
初期化しようとしている変数のモジュール名 module_name と、動的初期化される全てのグローバル変数のモジュール名 g->module_name を比較し、一致する場合はポイズンしません。
これは、変数の定義場所が同じモジュール名ということは同じ翻訳単位であるため、バグが発生しないためです。
そして、変数に初期化済みフラグを立てて終了します。
さて、strict_init_order が無効の場合、a.cc b.cc が検知されて b.cc a.cc が検知されませんでした。
a.cc b.cc の時は x -> extern_global の順で初期化されるため、x の初期化時に extern_global を参照します。
しかし、__asan_before_dynamic_init によって b.cc にある extern_global の領域はポイズンされているため、このときに検知されます。
b.cc a.cc の時は extern_global -> x の順で初期化されます。
先に extern_global を初期化してから x の初期化で extern_global が参照されるため、その領域はポイズンされず、検知されません。
strict_init_order が有効の場合、グローバル変数が初期化されても initialized が true になりません。
そのため、extern_global が先に初期化されていてもポイズンされ、x で extern_global を参照したタイミングで検知されます。
つまり、strict_init_order は異なるモジュール間での初期化時にグローバル変数の相互参照を禁止するものと言えます。
これは、異なる翻訳単位でグローバル変数を参照したくないときには有効ですが、何らかの事情で参照する必要がある時は誤検知が発生します。
__asan_before_dynamic_init は前述の通り、グローバル変数領域をポイズンします。
これを放置すると後々グローバル変数を使用するタイミングで誤検知されてしまうため、初期化後は __asan_after_dynamic_init 関数で元に戻しておきます。
// This method runs immediately after dynamic initialization in each TU, when // all dynamically initialized globals except for those defined in the current // TU are poisoned. It simply unpoisons all dynamically initialized globals. void __asan_after_dynamic_init() { if (!flags()->check_initialization_order || !CanPoisonMemory() || !dynamic_init_globals) return; CHECK(AsanInited()); Lock lock(&mu_for_globals); // FIXME: Optionally report that we're unpoisoning globals from a module. for (uptr i = 0, n = dynamic_init_globals->size(); i < n; ++i) { DynInitGlobal &dyn_g = (*dynamic_init_globals)[i]; const Global *g = &dyn_g.g; if (!dyn_g.initialized) { // 未初期化のグローバル変数をアンポイズンする // Unpoison the whole global. PoisonShadowForGlobal(g, 0); // Poison redzones back. PoisonRedZones(*g); } } }
おまけ: Static Initialization Order Fiasco
Static Initialization Order Fiasco について、簡単に説明します。 詳細に知りたい方は What's the "static initialization order fiasco"? などをご覧ください。
sanitizers Wiki に載っている例を引用します。
a.cc:
#include <stdio.h> extern int extern_global; int __attribute__((noinline)) read_extern_global() { return extern_global; } int x = read_extern_global() + 1; int main() { printf("%d\n", x); return 0; }
b.cc:
int foo() { return 42; } int extern_global = foo();
上記の a.cc は、グローバル変数 x の値を出力するだけのプログラムです。
x の値は read_extern_global() + 1 と定義されていて、動的に初期化されるようになっています。
read_extern_global 関数は単にグローバル変数の extern_global の中身を返すだけのものです。
さて、extern_global は extern 修飾子がついているため、外部の extern_global 変数の定義を取り込みます。
その定義は b.cc に書いてあります。
b.cc は int extern_global = foo() で、foo 関数を読んで動的に初期化します。
foo 関数は単に 42 を返すだけです。
さて、このプログラムをビルドして実行すると、何が表示されるでしょうか。
$ clang++ a.cc b.cc && ./a.out 1 $ clang++ b.cc a.cc && ./a.out 43
実際に動かしてみると、引数の a.cc と b.cc の順番を入れ替えただけでなぜか実行結果が変化します。
Clang ではコマンドライン引数の順番で、ソースコードファイルを翻訳する順番が変化します。 この翻訳順序の違いによって挙動が変化し、バグを生じさせる問題が Static Initialization Order Fiasco です。
Clang でビルドすると、グローバル変数の動的な初期化は __cxx_global_var_init という関数内で、main の実行前に行われます。
a.cc b.cc の順番にした場合、a.cc がまず翻訳されます。この時、a.cc で定義されている x を動的に初期化する __cxx_global_var_init が作成されます。
その次に b.cc が翻訳され、extern_global を動的に初期化するそれが作成されます。
動的に初期化されるグローバル変数は x と extern_global があるため、それぞれの __cxx_global_var_init 関数が作成されます。
a.cc b.ccという順番の場合、先にxの定義が来るため、xの動的初期化が実行され、次にextern_globalの動的初期化が実行されます。b.cc a.ccという順番の場合、先にextern_globalが、次にxが動的初期化されます。
実際にデバッガで __cxx_global_var_init にブレークポイントを置いてデバッグしてみると、呼ばれる順序が変化することがわかります。
$ clang++ -g a.cc b.cc # a.cc b.cc の順 $ gdb -q ./a.out Reading symbols from a.out... (gdb) b __cxx_global_var_init Breakpoint 1 at 0x1054: __cxx_global_var_init. (2 locations) (gdb) r Starting program: /home/user/a.out [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Breakpoint 1, __cxx_global_var_init () at a.cc:6 // 1 回目に呼ばれる __cxx_global_var_init は x のもの 6 int x = read_extern_global() + 1; (gdb) c Continuing. Breakpoint 1, __cxx_global_var_init () at b.cc:2 // 2 回目に呼ばれる __cxx_global_var_init は extern_global のもの 2 int extern_global = foo(); (gdb) Continuing. 1 [Inferior 1 (process 51905) exited normally]
$ clang++ -g b.cc a.cc # b.cc a.cc の順 $ gdb -q a.out Reading symbols from ./a.out... (gdb) b __cxx_global_var_init Breakpoint 1 at 0x1054: __cxx_global_var_init. (2 locations) (gdb) r Starting program: /home/user/a.out [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Breakpoint 1, __cxx_global_var_init () at b.cc:2 // 1 回目に呼ばれる __cxx_global_var_init は extern_global のもの 2 int extern_global = foo(); (gdb) c Continuing. Breakpoint 1, __cxx_global_var_init () at a.cc:6 // 2 回目に呼ばれる __cxx_global_var_init は x のもの 6 int x = read_extern_global() + 1; (gdb) Continuing. 43 [Inferior 1 (process 51402) exited normally]
おわりに
今回はモジュール計装や ASan ランタイムの初期化処理について解説しました。 こうしてコードを読んでいくと、ローカル変数とグローバル変数で扱いを変える必要があったり、ミッドシャドウでツールの競合を回避したりなど、論文では考慮されていなかった実用上の課題と対策、そして苦労が垣間見えます。
なお、ここでは取り上げませんでしたが、ODR*5 違反の検知といったものもモジュール計装で行われています。興味のある方はコードを読んでみてください。



