Skip to content

Commit bb61d16

Browse files
committed
Initial version of a clang-tidy check
1 parent 8a6ace1 commit bb61d16

File tree

4 files changed

+353
-0
lines changed

4 files changed

+353
-0
lines changed

clang-tools-extra/clang-tidy/llvm/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ set(LLVM_LINK_COMPONENTS
66
add_clang_library(clangTidyLLVMModule STATIC
77
HeaderGuardCheck.cpp
88
IncludeOrderCheck.cpp
9+
IOSandboxCheck.cpp
910
LLVMTidyModule.cpp
1011
PreferIsaOrDynCastInConditionalsCheck.cpp
1112
PreferRegisterOverUnsignedCheck.cpp
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
//===--- FilesystemAccessCheck.cpp - clang-tidy --------------------------===//
2+
//
3+
// Enforces controlled filesystem access patterns
4+
//
5+
//===----------------------------------------------------------------------===//
6+
7+
#include "IOSandboxCheck.h"
8+
#include "clang/AST/ASTContext.h"
9+
#include "clang/AST/RecursiveASTVisitor.h"
10+
#include "clang/ASTMatchers/ASTMatchFinder.h"
11+
#include "clang/Lex/Lexer.h"
12+
13+
using namespace clang::ast_matchers;
14+
15+
namespace clang::tidy::llvm_check {
16+
// Low-level filesystem functions that should only be called from llvm::sys::fs
17+
static const llvm::StringSet<> &getForbiddenFilesystemFunctions() {
18+
static const llvm::StringSet<> Functions = {
19+
// POSIX file operations
20+
"open",
21+
"openat",
22+
"creat",
23+
"close",
24+
"read",
25+
"write",
26+
"pread",
27+
"pwrite",
28+
"lseek",
29+
"ftruncate",
30+
"truncate",
31+
"stat",
32+
"fstat",
33+
"lstat",
34+
"fstatat",
35+
"access",
36+
"faccessat",
37+
"chmod",
38+
"fchmod",
39+
"fchmodat",
40+
"chown",
41+
"fchown",
42+
"lchown",
43+
"fchownat",
44+
"link",
45+
"linkat",
46+
"symlink",
47+
"symlinkat",
48+
"readlink",
49+
"readlinkat",
50+
"unlink",
51+
"unlinkat",
52+
"remove",
53+
"rename",
54+
"renameat",
55+
"mkdir",
56+
"mkdirat",
57+
"rmdir",
58+
"opendir",
59+
"readdir",
60+
"closedir",
61+
"fdopendir",
62+
"chdir",
63+
"fchdir",
64+
"getcwd",
65+
"dup",
66+
"dup2",
67+
"dup3",
68+
"fcntl",
69+
"pipe",
70+
"pipe2",
71+
"mkfifo",
72+
"mkfifoat",
73+
"mknod",
74+
"mknodat",
75+
"utimes",
76+
"futimes",
77+
"utimensat",
78+
"futimens",
79+
80+
// C standard library file operations
81+
"fopen",
82+
"freopen",
83+
"fclose",
84+
"fflush",
85+
"fread",
86+
"fwrite",
87+
"fgetc",
88+
"fputc",
89+
"fgets",
90+
"fputs",
91+
"fseek",
92+
"ftell",
93+
"rewind",
94+
"fgetpos",
95+
"fsetpos",
96+
"tmpfile",
97+
"tmpnam",
98+
"tempnam",
99+
100+
// Windows file operations
101+
"CreateFileA",
102+
"CreateFileW",
103+
"CreateFile",
104+
"ReadFile",
105+
"WriteFile",
106+
"CloseHandle",
107+
"DeleteFileA",
108+
"DeleteFileW",
109+
"DeleteFile",
110+
"MoveFileA",
111+
"MoveFileW",
112+
"MoveFile",
113+
"CopyFileA",
114+
"CopyFileW",
115+
"CopyFile",
116+
"GetFileAttributesA",
117+
"GetFileAttributesW",
118+
"GetFileAttributes",
119+
"SetFileAttributesA",
120+
"SetFileAttributesW",
121+
"SetFileAttributes",
122+
"CreateDirectoryA",
123+
"CreateDirectoryW",
124+
"CreateDirectory",
125+
"RemoveDirectoryA",
126+
"RemoveDirectoryW",
127+
"RemoveDirectory",
128+
"FindFirstFileA",
129+
"FindFirstFileW",
130+
"FindFirstFile",
131+
"FindNextFileA",
132+
"FindNextFileW",
133+
"FindNextFile",
134+
"FindClose",
135+
"GetCurrentDirectoryA",
136+
"GetCurrentDirectoryW",
137+
"SetCurrentDirectoryA",
138+
"SetCurrentDirectoryW",
139+
140+
// Memory-mapped files
141+
"mmap",
142+
"munmap",
143+
"mprotect",
144+
"msync",
145+
"MapViewOfFile",
146+
"UnmapViewOfFile",
147+
};
148+
return Functions;
149+
}
150+
151+
static bool isInLLVMSysFsNamespace(const FunctionDecl *FD) {
152+
if (!FD)
153+
return false;
154+
155+
auto IsAnonymousNamespace = [](const DeclContext *DC) {
156+
if (!DC)
157+
return false;
158+
const auto *ND = dyn_cast<NamespaceDecl>(DC);
159+
if (!ND)
160+
return false;
161+
return ND->isAnonymousNamespace();
162+
};
163+
164+
auto GetNamedNamespace = [](const DeclContext *DC) -> const NamespaceDecl * {
165+
if (!DC)
166+
return nullptr;
167+
const auto *ND = dyn_cast<NamespaceDecl>(DC);
168+
if (!ND)
169+
return nullptr;
170+
if (ND->isAnonymousNamespace())
171+
return nullptr;
172+
return ND;
173+
};
174+
175+
const DeclContext *DC = FD->getDeclContext();
176+
177+
// Walk up the context chain looking for llvm::sys::fs
178+
SmallVector<StringRef> ReverseNamespaces;
179+
while (IsAnonymousNamespace(DC))
180+
DC = DC->getParent();
181+
while (const auto *ND = GetNamedNamespace(DC)) {
182+
ReverseNamespaces.push_back(ND->getName());
183+
DC = DC->getParent();
184+
}
185+
auto Namespaces = llvm::reverse(ReverseNamespaces);
186+
187+
return llvm::equal(Namespaces, SmallVector<StringRef>{"llvm", "sys", "fs"});
188+
}
189+
190+
static bool isLLVMSysFsCall(const CallExpr *CE) {
191+
if (!CE)
192+
return false;
193+
194+
const FunctionDecl *Callee = CE->getDirectCallee();
195+
if (!Callee)
196+
return false;
197+
198+
return isInLLVMSysFsNamespace(Callee) && !Callee->isOverloadedOperator();
199+
}
200+
201+
static bool isForbiddenFilesystemCall(const CallExpr *CE) {
202+
if (!CE)
203+
return false;
204+
205+
const FunctionDecl *Callee = CE->getDirectCallee();
206+
if (!Callee)
207+
return false;
208+
209+
const auto &ForbiddenFuncs = getForbiddenFilesystemFunctions();
210+
211+
return ForbiddenFuncs.contains(Callee->getQualifiedNameAsString());
212+
}
213+
214+
static bool hasSandboxBypass(const FunctionDecl *FD, SourceLocation CallLoc) {
215+
if (!FD || !FD->hasBody())
216+
return false;
217+
218+
const Stmt *Body = FD->getBody();
219+
if (!Body)
220+
return false;
221+
222+
// Look for variable declarations of the bypass type
223+
// We need to check if the bypass variable is declared before the call site
224+
class BypassFinder : public RecursiveASTVisitor<BypassFinder> {
225+
public:
226+
bool FoundBypass = false;
227+
SourceLocation CallLocation;
228+
const SourceManager *SM;
229+
230+
bool VisitVarDecl(VarDecl *VD) {
231+
if (!VD)
232+
return true;
233+
234+
// Check if this is a sandbox bypass variable
235+
const Type *T = VD->getType().getTypePtrOrNull();
236+
if (!T)
237+
return true;
238+
239+
const CXXRecordDecl *RD = T->getAsCXXRecordDecl();
240+
if (!RD)
241+
return true;
242+
243+
// Check for ScopedSandboxDisable or similar RAII types
244+
std::string TypeName = RD->getQualifiedNameAsString();
245+
if (TypeName.find("ScopedSandboxDisable") != std::string::npos ||
246+
TypeName.find("scopedDisable") != std::string::npos) {
247+
248+
// Check if this declaration comes before the call
249+
if (SM &&
250+
SM->isBeforeInTranslationUnit(VD->getLocation(), CallLocation)) {
251+
FoundBypass = true;
252+
return false; // Stop searching
253+
}
254+
}
255+
256+
return true;
257+
}
258+
};
259+
260+
BypassFinder Finder;
261+
Finder.CallLocation = CallLoc;
262+
Finder.SM = &FD->getASTContext().getSourceManager();
263+
Finder.TraverseStmt(const_cast<Stmt *>(Body));
264+
265+
return Finder.FoundBypass;
266+
}
267+
268+
void IOSandboxCheck::registerMatchers(MatchFinder *Finder) {
269+
// Match any call expression within a function.
270+
Finder->addMatcher(
271+
callExpr(hasAncestor(functionDecl().bind("parent_func"))).bind("call"),
272+
this);
273+
274+
// Also match variable declarations to find sandbox bypass objects
275+
Finder->addMatcher(
276+
varDecl(hasType(cxxRecordDecl(hasName("ScopedSandboxDisable"))),
277+
hasAncestor(functionDecl().bind("func_with_bypass")))
278+
.bind("bypass_var"),
279+
this);
280+
}
281+
282+
void IOSandboxCheck::check(const MatchFinder::MatchResult &Result) {
283+
const auto *Call = Result.Nodes.getNodeAs<CallExpr>("call");
284+
const auto *ParentFunc = Result.Nodes.getNodeAs<FunctionDecl>("parent_func");
285+
286+
if (!Call || !ParentFunc)
287+
return;
288+
289+
// Skip system headers and template instantiations
290+
if (Call->getBeginLoc().isInvalid() ||
291+
Result.Context->getSourceManager().isInSystemHeader(
292+
Call->getBeginLoc()) ||
293+
ParentFunc->isTemplateInstantiation())
294+
return;
295+
296+
// Rule 1: Check if calling llvm::sys::fs without sandbox bypass
297+
if (isLLVMSysFsCall(Call)) {
298+
if (!hasSandboxBypass(ParentFunc, Call->getBeginLoc())) {
299+
diag(Call->getBeginLoc(), "call to llvm::sys::fs function")
300+
<< Call->getSourceRange();
301+
}
302+
return; // Don't check rule 2 for llvm::sys::fs calls
303+
}
304+
305+
// Rule 2: Check if calling forbidden filesystem functions outside
306+
// llvm::sys::fs
307+
if (isForbiddenFilesystemCall(Call)) {
308+
if (!isInLLVMSysFsNamespace(ParentFunc)) {
309+
const auto *Callee = Call->getDirectCallee();
310+
std::string CalleeName = Callee ? Callee->getNameAsString() : "unknown";
311+
312+
diag(Call->getBeginLoc(),
313+
"low-level filesystem function '%0' may only be called from a "
314+
"llvm::sys::fs function")
315+
<< CalleeName << Call->getSourceRange();
316+
}
317+
}
318+
}
319+
} // namespace clang::tidy::llvm_check
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//===--- LLVMFilesystemAccessCheck.h - clang-tidy ---------------*- C++ -*-===//
2+
//
3+
// Enforces controlled filesystem access patterns
4+
//
5+
//===----------------------------------------------------------------------===//
6+
7+
#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_LLVM_IOSANDBOXCHECK_H
8+
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_LLVM_IOSANDBOXCHECK_H
9+
10+
#include "../ClangTidyCheck.h"
11+
12+
namespace clang::tidy::llvm_check {
13+
14+
/// Enforces two rules for filesystem access:
15+
/// 1. Functions calling llvm::sys::fs must have a sandbox bypass RAII object
16+
/// 2. Only llvm::sys::fs functions may call low-level filesystem functions
17+
///
18+
/// For the user-facing documentation see:
19+
/// https://clang.llvm.org/extra/clang-tidy/checks/llvm/io-sandbox.html
20+
class IOSandboxCheck : public ClangTidyCheck {
21+
public:
22+
IOSandboxCheck(StringRef Name, ClangTidyContext *Context)
23+
: ClangTidyCheck(Name, Context) {}
24+
25+
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
26+
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
27+
};
28+
29+
} // namespace clang::tidy::llvm_check
30+
31+
#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_LLVM_IOSANDBOXCHECK_H

clang-tools-extra/clang-tidy/llvm/LLVMTidyModule.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "../readability/NamespaceCommentCheck.h"
1414
#include "../readability/QualifiedAutoCheck.h"
1515
#include "HeaderGuardCheck.h"
16+
#include "IOSandboxCheck.h"
1617
#include "IncludeOrderCheck.h"
1718
#include "PreferIsaOrDynCastInConditionalsCheck.h"
1819
#include "PreferRegisterOverUnsignedCheck.h"
@@ -31,6 +32,7 @@ class LLVMModule : public ClangTidyModule {
3132
"llvm-else-after-return");
3233
CheckFactories.registerCheck<LLVMHeaderGuardCheck>("llvm-header-guard");
3334
CheckFactories.registerCheck<IncludeOrderCheck>("llvm-include-order");
35+
CheckFactories.registerCheck<IOSandboxCheck>("llvm-io-sandbox");
3436
CheckFactories.registerCheck<readability::NamespaceCommentCheck>(
3537
"llvm-namespace-comment");
3638
CheckFactories.registerCheck<PreferIsaOrDynCastInConditionalsCheck>(

0 commit comments

Comments
 (0)