I am interested in seeing what people would think of adding an option within Clang to collect coverage information of compiler-provided exploit mitigations like stack protectors, stack clash protection, control flow integrity, and automatic variable initialization. I’ve outlined my approach below and I’d be happy to share more if people are interested.
Motivation
Measuring mitigation coverage is difficult to do in a large codebase, programs like checksec can be used to infer which mitigations exist in a binary, but they lack fine grained granularity that a compiler pass would be able to give.
Alternatively, one can keep track of which build flags are used to compile files with, but this too falls short because now the denominator to track coverage is overly wide - not every file will have functions that are candidates for every mitigation applied to it.
Proposal
We can emit metadata for each function where exploit mitigations are inserted likeso:
CGBuiltin.cpp
static void initializeAlloca(CodeGenFunction &CGF, AllocaInst *AI, Value *Size,
Align AlignmentInBytes) {
ConstantInt *Byte;
switch (CGF.getLangOpts().getTrivialAutoVarInit()) {
case LangOptions::TrivialAutoVarInitKind::Uninitialized:
// Nothing to initialize.
attachMetadataToFunction(*CGF.CurFn, "auto-var-init", false);
return;
case LangOptions::TrivialAutoVarInitKind::Zero:
Byte = CGF.Builder.getInt8(0x00);
break;
case LangOptions::TrivialAutoVarInitKind::Pattern: {
llvm::Type *Int8 = llvm::IntegerType::getInt8Ty(CGF.CGM.getLLVMContext());
Byte = llvm::dyn_cast<llvm::ConstantInt>(
initializationPatternFor(CGF.CGM, Int8));
break;
}
}
attachMetadataToFunction(*CGF.CurFn, "auto-var-init", true);
if (CGF.CGM.stopAutoInit())
return;
auto *I = CGF.Builder.CreateMemSet(AI, Byte, Size, AlignmentInBytes);
I->addAnnotationMetadata("auto-init");
}
Later, the metadata can be amalgamated centrally and a coverage mapping can be created. In my own prototype, I created an LTO pass which emitted JSON containing coverage for each function. When functions don’t have entries for a given mitigation, we know them to be ineligible and as such they don’t count for or against our total coverage % for a mitigation.
The exploit mitigations (or prevention measures) I have been able to track this way are:
- -ftrivial-auto-var-init
- -fstack-protector{-strong,-all}
- -fstack-clash-protection
- -fsanitize=cfi{-icall,-vcall,-nvcall}
Caveats
Unfortunately, there are limits to what a compiler-based approach to tracking mitigation coverage can do. The chief concern is that preprocessor-level mitigations like -D_FORTIFY_SOURCE and LibCPP Hardening Mode are excluded from this analysis. There are ways to come up with heuristics or hack them in but it wouldn’t be as straightforward as emitting metadata during codegen.
I would greatly appreciate any feedback on this, whether its overkill or useful or if theres any other suggestions to make it better. Thanks ![]()