1//===-- llvm-cfi-verify.cpp - CFI Verification tool for LLVM --------------===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8//
9// This tool verifies Control Flow Integrity (CFI) instrumentation by static
10// binary analysis. See the design document in /docs/CFIVerify.rst for more
11// information.
12//
13// This tool is currently incomplete. It currently only does disassembly for
14// object files, and searches through the code for indirect control flow
15// instructions, printing them once found.
16//
17//===----------------------------------------------------------------------===//
18
19#include "lib/FileAnalysis.h"
20#include "lib/GraphBuilder.h"
21
22#include "llvm/BinaryFormat/ELF.h"
23#include "llvm/DebugInfo/Symbolize/SymbolizableModule.h"
24#include "llvm/Support/CommandLine.h"
25#include "llvm/Support/Error.h"
26#include "llvm/Support/FormatVariadic.h"
27#include "llvm/Support/SpecialCaseList.h"
28#include "llvm/Support/VirtualFileSystem.h"
29
30#include <cstdlib>
31
32using namespace llvm;
33using namespace llvm::object;
34using namespace llvm::cfi_verify;
35
36static cl::OptionCategory CFIVerifyCategory("CFI Verify Options");
37
38static cl::opt<std::string> InputFilename(cl::Positional,
39 cl::desc("<input file>"),
40 cl::Required,
41 cl::cat(CFIVerifyCategory));
42static cl::opt<std::string> IgnorelistFilename(cl::Positional,
43 cl::desc("[ignorelist file]"),
44 cl::init(Val: "-"),
45 cl::cat(CFIVerifyCategory));
46static cl::opt<bool> PrintGraphs(
47 "print-graphs",
48 cl::desc("Print graphs around indirect CF instructions in DOT format."),
49 cl::init(Val: false), cl::cat(CFIVerifyCategory));
50static cl::opt<unsigned> PrintBlameContext(
51 "blame-context",
52 cl::desc("Print the blame context (if possible) for BAD instructions. This "
53 "specifies the number of lines of context to include, where zero "
54 "disables this feature."),
55 cl::init(Val: 0), cl::cat(CFIVerifyCategory));
56static cl::opt<unsigned> PrintBlameContextAll(
57 "blame-context-all",
58 cl::desc("Prints the blame context (if possible) for ALL instructions. "
59 "This specifies the number of lines of context for non-BAD "
60 "instructions (see --blame-context). If --blame-context is "
61 "unspecified, it prints this number of contextual lines for BAD "
62 "instructions as well."),
63 cl::init(Val: 0), cl::cat(CFIVerifyCategory));
64static cl::opt<bool> Summarize("summarize", cl::desc("Print the summary only."),
65 cl::init(Val: false), cl::cat(CFIVerifyCategory));
66
67ExitOnError ExitOnErr;
68
69static void printBlameContext(const DILineInfo &LineInfo, unsigned Context) {
70 auto FileOrErr = MemoryBuffer::getFile(Filename: LineInfo.FileName);
71 if (!FileOrErr) {
72 errs() << "Could not open file: " << LineInfo.FileName << "\n";
73 return;
74 }
75
76 std::unique_ptr<MemoryBuffer> File = std::move(FileOrErr.get());
77 SmallVector<StringRef, 100> Lines;
78 File->getBuffer().split(A&: Lines, Separator: '\n');
79
80 for (unsigned i = std::max<size_t>(a: 1, b: LineInfo.Line - Context);
81 i < std::min<size_t>(a: Lines.size() + 1, b: LineInfo.Line + Context + 1);
82 ++i) {
83 if (i == LineInfo.Line)
84 outs() << ">";
85 else
86 outs() << " ";
87
88 outs() << i << ": " << Lines[i - 1] << "\n";
89 }
90}
91
92static void printInstructionInformation(const FileAnalysis &Analysis,
93 const Instr &InstrMeta,
94 const GraphResult &Graph,
95 CFIProtectionStatus ProtectionStatus) {
96 outs() << "Instruction: " << format_hex(N: InstrMeta.VMAddress, Width: 2) << " ("
97 << stringCFIProtectionStatus(Status: ProtectionStatus) << "): ";
98 Analysis.printInstruction(InstrMeta, OS&: outs());
99 outs() << " \n";
100
101 if (PrintGraphs)
102 Graph.printToDOT(Analysis, OS&: outs());
103}
104
105static void printInstructionStatus(unsigned BlameLine, bool CFIProtected,
106 const DILineInfo &LineInfo) {
107 if (BlameLine) {
108 outs() << "Ignorelist Match: " << IgnorelistFilename << ":" << BlameLine
109 << "\n";
110 if (CFIProtected)
111 outs() << "====> Unexpected Protected\n";
112 else
113 outs() << "====> Expected Unprotected\n";
114
115 if (PrintBlameContextAll)
116 printBlameContext(LineInfo, Context: PrintBlameContextAll);
117 } else {
118 if (CFIProtected) {
119 outs() << "====> Expected Protected\n";
120 if (PrintBlameContextAll)
121 printBlameContext(LineInfo, Context: PrintBlameContextAll);
122 } else {
123 outs() << "====> Unexpected Unprotected (BAD)\n";
124 if (PrintBlameContext)
125 printBlameContext(LineInfo, Context: PrintBlameContext);
126 }
127 }
128}
129
130static void
131printIndirectCFInstructions(FileAnalysis &Analysis,
132 const SpecialCaseList *SpecialCaseList) {
133 uint64_t ExpectedProtected = 0;
134 uint64_t UnexpectedProtected = 0;
135 uint64_t ExpectedUnprotected = 0;
136 uint64_t UnexpectedUnprotected = 0;
137
138 std::map<unsigned, uint64_t> BlameCounter;
139
140 for (object::SectionedAddress Address : Analysis.getIndirectInstructions()) {
141 const auto &InstrMeta = Analysis.getInstructionOrDie(Address: Address.Address);
142 GraphResult Graph = GraphBuilder::buildFlowGraph(Analysis, Address);
143
144 CFIProtectionStatus ProtectionStatus =
145 Analysis.validateCFIProtection(Graph);
146 bool CFIProtected = (ProtectionStatus == CFIProtectionStatus::PROTECTED);
147
148 if (!Summarize) {
149 outs() << "-----------------------------------------------------\n";
150 printInstructionInformation(Analysis, InstrMeta, Graph, ProtectionStatus);
151 }
152
153 if (IgnoreDWARFFlag) {
154 if (CFIProtected)
155 ExpectedProtected++;
156 else
157 UnexpectedUnprotected++;
158 continue;
159 }
160
161 auto InliningInfo = Analysis.symbolizeInlinedCode(Address);
162 if (!InliningInfo || InliningInfo->getNumberOfFrames() == 0) {
163 errs() << "Failed to symbolise " << format_hex(N: Address.Address, Width: 2)
164 << " with line tables from " << InputFilename << "\n";
165 exit(EXIT_FAILURE);
166 }
167
168 const auto &LineInfo = InliningInfo->getFrame(Index: 0);
169
170 // Print the inlining symbolisation of this instruction.
171 if (!Summarize) {
172 for (uint32_t i = 0; i < InliningInfo->getNumberOfFrames(); ++i) {
173 const auto &Line = InliningInfo->getFrame(Index: i);
174 outs() << " " << format_hex(N: Address.Address, Width: 2) << " = "
175 << Line.FileName << ":" << Line.Line << ":" << Line.Column
176 << " (" << Line.FunctionName << ")\n";
177 }
178 }
179
180 if (!SpecialCaseList) {
181 if (CFIProtected) {
182 if (PrintBlameContextAll && !Summarize)
183 printBlameContext(LineInfo, Context: PrintBlameContextAll);
184 ExpectedProtected++;
185 } else {
186 if (PrintBlameContext && !Summarize)
187 printBlameContext(LineInfo, Context: PrintBlameContext);
188 UnexpectedUnprotected++;
189 }
190 continue;
191 }
192
193 unsigned BlameLine = 0;
194 for (auto &K : {"cfi-icall", "cfi-vcall"}) {
195 if (!BlameLine) {
196 auto [FileIdx, Line] =
197 SpecialCaseList->inSectionBlame(Section: K, Prefix: "src", Query: LineInfo.FileName);
198 BlameLine = Line;
199 }
200 if (!BlameLine) {
201 auto [FileIdx, Line] =
202 SpecialCaseList->inSectionBlame(Section: K, Prefix: "fun", Query: LineInfo.FunctionName);
203 BlameLine = Line;
204 }
205 }
206
207 if (BlameLine) {
208 BlameCounter[BlameLine]++;
209 if (CFIProtected)
210 UnexpectedProtected++;
211 else
212 ExpectedUnprotected++;
213 } else {
214 if (CFIProtected)
215 ExpectedProtected++;
216 else
217 UnexpectedUnprotected++;
218 }
219
220 if (!Summarize)
221 printInstructionStatus(BlameLine, CFIProtected, LineInfo);
222 }
223
224 uint64_t IndirectCFInstructions = ExpectedProtected + UnexpectedProtected +
225 ExpectedUnprotected + UnexpectedUnprotected;
226
227 if (IndirectCFInstructions == 0) {
228 outs() << "No indirect CF instructions found.\n";
229 return;
230 }
231
232 outs() << formatv(Fmt: "\nTotal Indirect CF Instructions: {0}\n"
233 "Expected Protected: {1} ({2:P})\n"
234 "Unexpected Protected: {3} ({4:P})\n"
235 "Expected Unprotected: {5} ({6:P})\n"
236 "Unexpected Unprotected (BAD): {7} ({8:P})\n",
237 Vals&: IndirectCFInstructions, Vals&: ExpectedProtected,
238 Vals: ((double)ExpectedProtected) / IndirectCFInstructions,
239 Vals&: UnexpectedProtected,
240 Vals: ((double)UnexpectedProtected) / IndirectCFInstructions,
241 Vals&: ExpectedUnprotected,
242 Vals: ((double)ExpectedUnprotected) / IndirectCFInstructions,
243 Vals&: UnexpectedUnprotected,
244 Vals: ((double)UnexpectedUnprotected) / IndirectCFInstructions);
245
246 if (!SpecialCaseList)
247 return;
248
249 outs() << "\nIgnorelist Results:\n";
250 for (const auto &KV : BlameCounter) {
251 outs() << " " << IgnorelistFilename << ":" << KV.first << " affects "
252 << KV.second << " indirect CF instructions.\n";
253 }
254}
255
256int main(int argc, char **argv) {
257 cl::HideUnrelatedOptions(Categories: {&CFIVerifyCategory, &getColorCategory()});
258 cl::ParseCommandLineOptions(
259 argc, argv,
260 Overview: "Identifies whether Control Flow Integrity protects all indirect control "
261 "flow instructions in the provided object file, DSO or binary.\nNote: "
262 "Anything statically linked into the provided file *must* be compiled "
263 "with '-g'. This can be relaxed through the '--ignore-dwarf' flag.");
264
265 InitializeAllTargetInfos();
266 InitializeAllTargetMCs();
267 InitializeAllAsmParsers();
268 InitializeAllDisassemblers();
269
270 if (PrintBlameContextAll && !PrintBlameContext)
271 PrintBlameContext.setValue(V: PrintBlameContextAll);
272
273 std::unique_ptr<SpecialCaseList> SpecialCaseList;
274 if (IgnorelistFilename != "-") {
275 std::string Error;
276 SpecialCaseList = SpecialCaseList::create(Paths: {IgnorelistFilename},
277 FS&: *vfs::getRealFileSystem(), Error);
278 if (!SpecialCaseList) {
279 errs() << "Failed to get ignorelist: " << Error << "\n";
280 exit(EXIT_FAILURE);
281 }
282 }
283
284 FileAnalysis Analysis = ExitOnErr(FileAnalysis::Create(Filename: InputFilename));
285 printIndirectCFInstructions(Analysis, SpecialCaseList: SpecialCaseList.get());
286
287 return EXIT_SUCCESS;
288}
289