Skip to content

Commit ec3bf4c

Browse files
Constrain the most common constructs in non-writable files to not change. (#391)
This PR addresses function and variable declarations (because they are the most obvious case and reasonably straightforward) and checked regions (because they came up in some existing regression tests). We'll leave #387 open for the tail of unhandled cases. Also: - When 3C tries to change a non-writable file, issue an error diagnostic (not an assertion failure because there are known unhandled cases) rather than silently discarding the change. - Add a `-dump-unwritable-changes` flag to the `3c` tool to dump the new version of the file when that diagnostic appears. - Add an error diagnostic when 3C tries to change a file under the base dir other than the main file in stdout mode. This is a separate feature (part of #328) but ended up being easy to implement along with the diagnostic for a non-writable file. - Add tests for all the fixes (but not `-dump-unwritable-changes`). - Fix a bug where `-warn-all-root-cause` didn't work without `-warn-root-cause`, because this affected one of the new tests. The use of `-warn-all-root-cause` without `-warn-root-cause` in the affected test will serve as a regression test for this fix as well. Fixes part of #387 and a few unrelated minor issues.
1 parent 128c8b8 commit ec3bf4c

14 files changed

+252
-76
lines changed

clang/include/clang/3C/3C.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ struct _3COptions {
7070
// that generates diagnostics, except for the declaration merging diagnostics
7171
// that are currently fatal) and uses the default "expected" prefix.
7272
bool VerifyDiagnosticOutput;
73+
74+
bool DumpUnwritableChanges;
7375
};
7476

7577
// The main interface exposed by the 3C to interact with the tool.
@@ -92,9 +94,8 @@ class _3CInterface {
9294
// Build initial constraints.
9395
bool buildInitialConstraints();
9496

95-
// Constraint Solving. The flag: ComputeInterimState requests to compute
96-
// interim constraint solver state.
97-
bool solveConstraints(bool ComputeInterimState = false);
97+
// Constraint Solving.
98+
bool solveConstraints();
9899

99100
// Interactivity.
100101

clang/include/clang/3C/3CGlobalOptions.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ extern std::vector<std::string> AllocatorFunctions;
2828
extern bool AddCheckedRegions;
2929
extern bool WarnRootCause;
3030
extern bool WarnAllRootCause;
31+
extern bool DumpUnwritableChanges;
3132

3233
#ifdef FIVE_C
3334
extern bool RemoveItypes;

clang/include/clang/3C/CheckedRegions.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ class CheckedRegionAdder
4242
findParentCompound(const clang::ast_type_traits::DynTypedNode &N, int);
4343
bool isParentChecked(const clang::ast_type_traits::DynTypedNode &N);
4444
bool isWrittenChecked(const clang::CompoundStmt *);
45-
bool isFunctionBody(clang::CompoundStmt *S);
4645
clang::ASTContext *Context;
4746
clang::Rewriter &Writer;
4847
std::map<llvm::FoldingSetNodeID, AnnotationNeeded> &Map;

clang/lib/3C/3C.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ bool WarnRootCause;
5555
bool WarnAllRootCause;
5656
std::set<std::string> FilePaths;
5757
bool VerifyDiagnosticOutput;
58+
bool DumpUnwritableChanges;
5859

5960
#ifdef FIVE_C
6061
bool RemoveItypes;
@@ -214,6 +215,7 @@ _3CInterface::_3CInterface(const struct _3COptions &CCopt,
214215
WarnRootCause = CCopt.WarnRootCause || CCopt.WarnAllRootCause;
215216
WarnAllRootCause = CCopt.WarnAllRootCause;
216217
VerifyDiagnosticOutput = CCopt.VerifyDiagnosticOutput;
218+
DumpUnwritableChanges = CCopt.DumpUnwritableChanges;
217219

218220
#ifdef FIVE_C
219221
RemoveItypes = CCopt.RemoveItypes;
@@ -285,7 +287,7 @@ bool _3CInterface::buildInitialConstraints() {
285287
return true;
286288
}
287289

288-
bool _3CInterface::solveConstraints(bool ComputeInterimState) {
290+
bool _3CInterface::solveConstraints() {
289291
std::lock_guard<std::mutex> Lock(InterfaceMutex);
290292
assert(ConstraintsBuilt && "Constraints not yet built. We need to call "
291293
"build constraint before trying to solve them.");
@@ -301,7 +303,7 @@ bool _3CInterface::solveConstraints(bool ComputeInterimState) {
301303
if (Verbose)
302304
outs() << "Constraints solved\n";
303305

304-
if (ComputeInterimState)
306+
if (WarnRootCause)
305307
GlobalProgramInfo.computeInterimConstraintState(FilePaths);
306308

307309
if (DumpIntermediate)

clang/lib/3C/CheckedRegions.cpp

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,14 @@ using namespace llvm;
2727
using namespace clang;
2828

2929

30-
// Check if the compound statement is a function body
30+
// If S is a function body, then return the FunctionDecl, otherwise return null.
3131
// Used in both visitors so abstracted to a function
32-
bool isTopLevel(ASTContext *Context, CompoundStmt *S) {
32+
FunctionDecl *getFunctionDeclOfBody(ASTContext *Context, CompoundStmt *S) {
3333
const auto &Parents = Context->getParents(*S);
3434
if (Parents.empty()) {
35-
return false;
35+
return nullptr;
3636
}
37-
// Ensure that our parent is a functiondecl
38-
return Parents[0].get<FunctionDecl>() != nullptr;
37+
return const_cast<FunctionDecl *>(Parents[0].get<FunctionDecl>());
3938
}
4039

4140
// CheckedRegionAdder
@@ -47,7 +46,7 @@ bool CheckedRegionAdder::VisitCompoundStmt(CompoundStmt *S) {
4746
S->Profile(Id, *Context, true);
4847
switch (Map[Id]) {
4948
case IS_UNCHECKED:
50-
if (isParentChecked(DTN) && !isFunctionBody(S)) {
49+
if (isParentChecked(DTN) && getFunctionDeclOfBody(Context, S) == nullptr) {
5150
auto Loc = S->getBeginLoc();
5251
Writer.InsertTextBefore(Loc, "_Unchecked ");
5352
}
@@ -106,10 +105,6 @@ CheckedRegionAdder::findParentCompound(const ast_type_traits::DynTypedNode &N,
106105

107106

108107

109-
bool CheckedRegionAdder::isFunctionBody(CompoundStmt *S) {
110-
return isTopLevel(Context, S);
111-
}
112-
113108
bool CheckedRegionAdder::isParentChecked(
114109
const ast_type_traits::DynTypedNode &DTN) {
115110
if (const auto *Parent = findParentCompound(DTN).first) {
@@ -163,27 +158,41 @@ bool CheckedRegionFinder::VisitDoStmt(DoStmt *S) {
163158
}
164159

165160
bool CheckedRegionFinder::VisitCompoundStmt(CompoundStmt *S) {
166-
// Visit all subblocks, find all unchecked types
167161
bool Localwild = false;
162+
163+
// Is this compound statement the body of a function?
164+
FunctionDecl *FD = getFunctionDeclOfBody(Context, S);
165+
if (FD != nullptr) {
166+
auto PSL = PersistentSourceLoc::mkPSL(FD, *Context);
167+
if (!canWrite(PSL.getFileName())) {
168+
// The "location" of the function is in an unwritable file. Processing it
169+
// might result in modifying an unwritable file, so skip it completely.
170+
// This check could have both false positives and false negatives if the
171+
// code uses `#include` to assemble a function definition from multiple
172+
// files, some writable and some not, but that would be "unusual c code -
173+
// low priority".
174+
//
175+
// Currently, it's OK to perform this check only at the function level
176+
// because a function is normally in a single file and 3C doesn't add
177+
// checked annotations at higher levels (e.g., `#pragma CHECKED_SCOPE`)
178+
return false;
179+
}
180+
181+
// Need to check return type
182+
auto retType = FD->getReturnType().getTypePtr();
183+
if (retType->isPointerType()) {
184+
CVarOption CV = Info.getVariable(FD, Context);
185+
Localwild |= isWild(CV) || containsUncheckedPtr(FD->getReturnType());
186+
}
187+
}
188+
189+
// Visit all subblocks, find all unchecked types
168190
for (const auto &SubStmt : S->children()) {
169191
CheckedRegionFinder Sub(Context, Writer, Info, Seen, Map, EmitWarnings);
170192
Sub.TraverseStmt(SubStmt);
171193
Localwild |= Sub.Wild;
172194
}
173195

174-
// If we are a function def, need to check return type
175-
if (isTopLevel(Context, S)) {
176-
const auto &Parents = Context->getParents(*S);
177-
assert(!Parents.empty());
178-
FunctionDecl* Parent = const_cast<FunctionDecl*>(Parents[0].get<FunctionDecl>());
179-
assert(Parent != nullptr);
180-
auto retType = Parent->getReturnType().getTypePtr();
181-
if (retType->isPointerType()) {
182-
CVarOption CV = Info.getVariable(Parent, Context);
183-
Localwild |= isWild(CV) || containsUncheckedPtr(Parent->getReturnType());
184-
}
185-
}
186-
187196
markChecked(S, Localwild);
188197

189198
Wild = false;

clang/lib/3C/ProgramInfo.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,9 @@ void ProgramInfo::addVariable(clang::DeclaratorDecl *D,
634634
llvm_unreachable("unknown decl type");
635635

636636
assert("We shouldn't be adding a null CV to Variables map." && NewCV);
637+
if (!canWrite(PLoc.getFileName())) {
638+
NewCV->constrainToWild(CS, "Declaration in non-writable file", &PLoc);
639+
}
637640
constrainWildIfMacro(NewCV, D->getLocation());
638641
Variables[PLoc] = NewCV;
639642
}
@@ -843,8 +846,7 @@ bool ProgramInfo::computeInterimConstraintState(
843846
CAtoms Tmp;
844847
getVarsFromConstraint(C, Tmp);
845848
AllValidVars.insert(Tmp.begin(), Tmp.end());
846-
if (FilePaths.count(FileName) ||
847-
FileName.find(BaseDir) != std::string::npos)
849+
if (canWrite(FileName))
848850
ValidVarsVec.insert(ValidVarsVec.begin(), Tmp.begin(), Tmp.end());
849851
}
850852
}

clang/lib/3C/RewriteUtils.cpp

Lines changed: 84 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -136,52 +136,93 @@ static void emit(Rewriter &R, ASTContext &C, std::string &OutputPostfix) {
136136
if (Verbose)
137137
errs() << "Writing files out\n";
138138

139-
// Check if we are outputing to stdout or not, if we are, just output the
140-
// main file ID to stdout.
141139
SourceManager &SM = C.getSourceManager();
142-
if (OutputPostfix == "-") {
143-
if (const RewriteBuffer *B = R.getRewriteBufferFor(SM.getMainFileID()))
144-
B->write(outs());
145-
} else {
146-
// Iterate over each modified rewrite buffer
147-
for (auto Buffer = R.buffer_begin(); Buffer != R.buffer_end(); ++Buffer) {
148-
if (const FileEntry *FE = SM.getFileEntryForID(Buffer->first)) {
149-
assert(FE->isValid());
150-
151-
// Produce a path/file name for the rewritten source file.
152-
// That path should be the same as the old one, with a
153-
// suffix added between the file name and the extension.
154-
// For example \foo\bar\a.c should become \foo\bar\a.checked.c
155-
// if the OutputPostfix parameter is "checked" .
156-
std::string PfName = sys::path::filename(FE->getName()).str();
157-
std::string DirName = sys::path::parent_path(FE->getName()).str();
158-
std::string FileName = sys::path::remove_leading_dotslash(PfName).str();
159-
std::string Ext = sys::path::extension(FileName).str();
160-
std::string Stem = sys::path::stem(FileName).str();
161-
std::string NFile = Stem + "." + OutputPostfix + Ext;
162-
if (!DirName.empty())
163-
NFile = DirName + sys::path::get_separator().str() + NFile;
164-
165-
// Write this file if it was specified as a file on the command line.
166-
std::string FeAbsS = "";
167-
if (getAbsoluteFilePath(FE->getName(), FeAbsS))
168-
FeAbsS = sys::path::remove_leading_dotslash(FeAbsS);
169-
170-
if (canWrite(FeAbsS)) {
171-
std::error_code EC;
172-
raw_fd_ostream Out(NFile, EC, sys::fs::F_None);
173-
174-
if (!EC) {
175-
if (Verbose)
176-
outs() << "writing out " << NFile << "\n";
177-
Buffer->second.write(Out);
178-
} else
179-
errs() << "could not open file " << NFile << "\n";
180-
// This is awkward. What to do? Since we're iterating, we could have
181-
// created other files successfully. Do we go back and erase them? Is
182-
// that surprising? For now, let's just keep going.
140+
// Iterate over each modified rewrite buffer
141+
for (auto Buffer = R.buffer_begin(); Buffer != R.buffer_end(); ++Buffer) {
142+
if (const FileEntry *FE = SM.getFileEntryForID(Buffer->first)) {
143+
assert(FE->isValid());
144+
145+
auto MaybeDumpUnwritableChange = [&]() {
146+
if (DumpUnwritableChanges) {
147+
errs() << "=== Beginning of new version of " << FE->getName() << " ===\n";
148+
Buffer->second.write(errs());
149+
errs() << "=== End of new version of " << FE->getName() << " ===\n";
150+
} else {
151+
DiagnosticsEngine &DE = C.getDiagnostics();
152+
unsigned ID = DE.getCustomDiagID(
153+
DiagnosticsEngine::Note,
154+
"use the -dump-unwritable-changes option to see the new version "
155+
"of the file");
156+
DE.Report(SM.translateFileLineCol(FE, 1, 1), ID);
183157
}
158+
};
159+
160+
// Check whether we are allowed to write this file.
161+
std::string FeAbsS = "";
162+
if (getAbsoluteFilePath(FE->getName(), FeAbsS))
163+
FeAbsS = sys::path::remove_leading_dotslash(FeAbsS);
164+
if (!canWrite(FeAbsS)) {
165+
DiagnosticsEngine &DE = C.getDiagnostics();
166+
unsigned ID = DE.getCustomDiagID(
167+
DiagnosticsEngine::Error,
168+
"3C internal error: 3C generated changes to this file even though it "
169+
"is not allowed to write to the file "
170+
"(https://github.com/correctcomputation/checkedc-clang/issues/387)");
171+
DE.Report(SM.translateFileLineCol(FE, 1, 1), ID);
172+
MaybeDumpUnwritableChange();
173+
continue;
184174
}
175+
176+
if (OutputPostfix == "-") {
177+
// Stdout mode
178+
// TODO: If we don't have a rewrite buffer for the main file, that means
179+
// we generated no changes to the file and we should print its original
180+
// content
181+
// (https://github.com/correctcomputation/checkedc-clang/issues/328#issuecomment-760243604).
182+
if (Buffer->first == SM.getMainFileID()) {
183+
// This is the new version of the main file. Print it to stdout.
184+
Buffer->second.write(outs());
185+
} else {
186+
DiagnosticsEngine &DE = C.getDiagnostics();
187+
unsigned ID = DE.getCustomDiagID(
188+
DiagnosticsEngine::Error,
189+
"3C generated changes to this file, which is under the base dir "
190+
"but is not the main file and thus cannot be written in stdout "
191+
"mode");
192+
DE.Report(SM.translateFileLineCol(FE, 1, 1), ID);
193+
MaybeDumpUnwritableChange();
194+
}
195+
continue;
196+
}
197+
198+
// -output-postfix mode
199+
200+
// Produce a path/file name for the rewritten source file.
201+
// That path should be the same as the old one, with a
202+
// suffix added between the file name and the extension.
203+
// For example \foo\bar\a.c should become \foo\bar\a.checked.c
204+
// if the OutputPostfix parameter is "checked" .
205+
std::string PfName = sys::path::filename(FE->getName()).str();
206+
std::string DirName = sys::path::parent_path(FE->getName()).str();
207+
std::string FileName = sys::path::remove_leading_dotslash(PfName).str();
208+
std::string Ext = sys::path::extension(FileName).str();
209+
std::string Stem = sys::path::stem(FileName).str();
210+
std::string NFile = Stem + "." + OutputPostfix + Ext;
211+
if (!DirName.empty())
212+
NFile = DirName + sys::path::get_separator().str() + NFile;
213+
214+
std::error_code EC;
215+
raw_fd_ostream Out(NFile, EC, sys::fs::F_None);
216+
217+
if (!EC) {
218+
if (Verbose)
219+
outs() << "writing out " << NFile << "\n";
220+
Buffer->second.write(Out);
221+
} else
222+
errs() << "could not open file " << NFile << "\n";
223+
// This is awkward. What to do? Since we're iterating, we could have
224+
// created other files successfully. Do we go back and erase them? Is
225+
// that surprising? For now, let's just keep going.
185226
}
186227
}
187228
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Test that non-canWrite files are constrained not to change so that the final
2+
// annotations of other files are consistent with the original annotations of
3+
// the non-canWrite files. The currently supported cases are function and
4+
// variable declarations and checked regions.
5+
// (https://github.com/correctcomputation/checkedc-clang/issues/387)
6+
7+
// TODO: When https://github.com/correctcomputation/checkedc-clang/issues/327 is
8+
// fixed, replace the absolute -I option with a .. in the #include directive.
9+
//
10+
// TODO: Windows compatibility?
11+
12+
// "Lower" case: -base-dir should default to the working directory, so we should
13+
// not allow canwrite_constraints_function_and_variable.h to change, and the
14+
// internal types of q and the return should remain wild.
15+
//
16+
// RUN: cd %S && 3c -addcr -extra-arg=-I${PWD%/*} -output-postfix=checked -warn-all-root-cause -verify %s
17+
// RUN: FileCheck -match-full-lines -check-prefixes=CHECK_LOWER --input-file %S/canwrite_constraints_function_and_variable.checked.c %s
18+
// RUN: test ! -f %S/../canwrite_constraints_function_and_variable.checked.h
19+
// RUN: rm %S/canwrite_constraints_function_and_variable.checked.c
20+
21+
// "Higher" case: When -base-dir is set to the parent directory, we can change
22+
// canwrite_constraints_function_and_variable.h, so both q and the return should
23+
// become checked.
24+
//
25+
// RUN: cd %S && 3c -addcr -extra-arg=-I${PWD%/*} -base-dir=${PWD%/*} -output-postfix=checked %s
26+
// RUN: FileCheck -match-full-lines -check-prefixes=CHECK_HIGHER --input-file %S/canwrite_constraints_function_and_variable.checked.c %s
27+
// RUN: FileCheck -match-full-lines -check-prefixes=CHECK_HIGHER --input-file %S/../canwrite_constraints_function_and_variable.checked.h %S/../canwrite_constraints_function_and_variable.h
28+
// RUN: rm %S/canwrite_constraints_function_and_variable.checked.c %S/../canwrite_constraints_function_and_variable.checked.h
29+
30+
#include "canwrite_constraints_function_and_variable.h"
31+
32+
int *bar(int *q) {
33+
// CHECK_LOWER: int *bar(int *q : itype(_Ptr<int>)) : itype(_Ptr<int>) {
34+
// CHECK_HIGHER: _Ptr<int> bar(_Ptr<int> q) {
35+
foo(q);
36+
return foo_var;
37+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// An example of a case (typedefs) that is not yet handled by the canWrite
2+
// constraints code and causes 3C to generate a change to an unwritable file.
3+
// Test that 3C generates an error diagnostic.
4+
// (https://github.com/correctcomputation/checkedc-clang/issues/387)
5+
6+
// TODO: Ditto the TODO comments from
7+
// canwrite_constraints_function_and_variable.c re the RUN commands.
8+
// RUN: cd %S && 3c -addcr -extra-arg=-I${PWD%/*} -verify %s | FileCheck -match-full-lines %s
9+
10+
// expected-error@unwritable_typedef.h:1 {{3C internal error: 3C generated changes to this file even though it is not allowed to write to the file}}
11+
// expected-note@unwritable_typedef.h:1 {{-dump-unwritable-changes}}
12+
13+
#include "unwritable_typedef.h"
14+
15+
foo_typedef p = ((void *)0);
16+
17+
// To make sure we are testing what we want to test, make sure bar is rewritten
18+
// as if foo_typedef is unconstrained. If foo_typedef were constrained, we'd
19+
// expect bar to be rewritten differently.
20+
int *bar(void) {
21+
// CHECK: _Ptr<int> bar(void) _Checked {
22+
return p;
23+
// Make sure 3C isn't inserting a cast or something clever like that.
24+
// CHECK: return p;
25+
}

clang/test/3C/base_subdir/readme

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
This subdirectory is used as the -base-dir by tests that want to have files
2+
outside the -base-dir.

0 commit comments

Comments
 (0)