Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Recursive constant initialization was not checked if in constructor #2862

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0b34697
Fix: Recursive constant initialization was not checked if in constructor
MikaelMayer Oct 7, 2022
dd2ec2e
Update RELEASE_NOTES.md
MikaelMayer Oct 7, 2022
75344fb
Added review comment use case and fixed the code
MikaelMayer Oct 7, 2022
b03a40a
Merge branch 'fix-2727-soundness-constant' of https://github.com/dafn…
MikaelMayer Oct 7, 2022
4f7300f
Merge branch 'master' into fix-2727-soundness-constant
MikaelMayer Oct 7, 2022
f7e90a2
Better handling of alternatives
MikaelMayer Oct 11, 2022
04eeb43
Refactoring
MikaelMayer Oct 11, 2022
a4d3a27
Merge branch 'fix-2727-soundness-constant' of https://github.com/dafn…
MikaelMayer Oct 11, 2022
56dbf3b
Merge branch 'master' into fix-2727-soundness-constant
MikaelMayer Oct 11, 2022
5568664
Fix CI
MikaelMayer Oct 11, 2022
bac8625
Merge branch 'master' into fix-2727-soundness-constant
MikaelMayer Oct 12, 2022
bd717d1
Create 2862.fix
MikaelMayer Oct 12, 2022
e956e87
Merge branch 'fix-2727-soundness-constant' of https://github.com/dafn…
MikaelMayer Oct 12, 2022
0c20291
Merge branch 'master' into fix-2727-soundness-constant
MikaelMayer Oct 14, 2022
baa398f
Merge branch 'master' into fix-2727-soundness-constant
MikaelMayer Oct 14, 2022
2e98a55
Prevent constants to be assigned twice
MikaelMayer Oct 14, 2022
f18254b
Merge branch 'master' into fix-2727-soundness-constant
MikaelMayer Oct 21, 2022
d7fd892
Merge branch 'master' into fix-2727-soundness-constant
MikaelMayer Oct 24, 2022
b53829c
Merge branch 'master' into fix-2727-soundness-constant
MikaelMayer Oct 24, 2022
3ac8137
One more soundness fix
MikaelMayer Oct 26, 2022
5555cde
One more soundness fix
MikaelMayer Oct 26, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 188 additions & 21 deletions Source/DafnyCore/Resolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2846,7 +2846,7 @@ public class ModuleBindings {
}

// ---------------------------------- Pass 1 ----------------------------------
// This pass:
// This pass or phase:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I keep forgetting that these are called "pass", I'm always looking for "phase". Next time, if I forget again, I will be able to find them.

// * checks that type inference was able to determine all types
// * check that shared destructors in datatypes are in agreement
// * fills in the .ResolvedOp field of binary expressions
Expand Down Expand Up @@ -9049,8 +9049,27 @@ public CheckDividedConstructorInit_Visitor(Resolver resolver)
: base(resolver) {
Contract.Requires(resolver != null);
}

// Constants that were surely assigned at the point of being read
// Forms a non-empty stack so that we can figure out if constants are fully assigned
// Every pushed element of the stack constains all the elements on the stack.
// So there is no need to look further in the stack for already assigned constants
public Stack<HashSet<ConstantField>> constantsAlreadyAssigned = new Stack<HashSet<ConstantField>>();
// Constants for which there was a code path in which there were assigned
// but it cannot be guaranteed (gives a better error message)
public HashSet<ConstantField> constantsPartiallyAssigned = new HashSet<ConstantField>();
// Constants for which an error was already reported, to avoid duplicates
public HashSet<ConstantField> constantsWithErrors = new HashSet<ConstantField>();

[CanBeNull] public Stack<Statement> ContextIfCannotAssignConstant = new();

public void CheckInit(List<Statement> initStmts) {
Contract.Requires(initStmts != null);
constantsAlreadyAssigned.Clear();
constantsAlreadyAssigned.Push(new HashSet<ConstantField>());
constantsPartiallyAssigned = new();
constantsWithErrors = new();
ContextIfCannotAssignConstant.Clear();
initStmts.Iter(CheckInit);
}
/// <summary>
Expand All @@ -9074,24 +9093,92 @@ public CheckDividedConstructorInit_Visitor(Resolver resolver)
Attributes.SubExpressions(s.Attributes).Iter(VisitExpr); // (+)
var mse = s.Lhs as MemberSelectExpr;
if (mse != null && Expression.AsThis(mse.Obj) != null) {
if (s.Rhs is ExprRhs) {
if (mse.Member is ConstantField constantField && ContextIfCannotAssignConstant.Count > 0) {
var why = ContextIfCannotAssignConstant.Peek() switch {
LoopStmt => " in a loop statement.",
AlternativeStmt => " in an alternative if-case statement.",
ForallStmt => " in a forall statement.",
_ => " here."
};
resolver.reporter.Error(MessageSource.Resolver,
mse.tok, "Cannot assign constant field" + why);
}
if (s.Rhs is ExprRhs or TypeRhs) {
// This is a special case we allow. Omit s.Lhs in the recursive visits. That is, we omit (++).
// Furthermore, because the assignment goes to a field of "this" and won't be available until after
// the "new;", we can allow certain specific (and useful) uses of "this" in the RHS.
s.Rhs.SubExpressions.Iter(LiberalRHSVisit); // (+++)
} else {
s.Rhs.SubExpressions.Iter(VisitExpr); // (+++)
}

if (mse.Member is ConstantField constantField2) {
if (constantsAlreadyAssigned.Peek().Contains(constantField2)) {
resolver.reporter.Error(MessageSource.Resolver, mse.tok,
$"The constant {constantField2.Name} cannot be assigned twice.");
} else {
constantsAlreadyAssigned.Peek().Add(constantField2);
}
}
} else {
VisitExpr(s.Lhs); // (++)
s.Rhs.SubExpressions.Iter(VisitExpr); // (+++)
}
} else {
stmt.SubExpressions.Iter(VisitExpr); // (*)
}
stmt.SubStatements.Iter(CheckInit); // (**)

// If true, only constants assigned in every branch can be considered 'assigned'.
// This is because match and if then-else are guaranteed to be exhaustive.
var alternative = stmt is IfStmt or MatchStmt;
var cannotAssignConstant = stmt is LoopStmt or AlternativeStmt or ForallStmt;

if (cannotAssignConstant) {
ContextIfCannotAssignConstant.Push(stmt);
}

var previouslyAssigned = constantsAlreadyAssigned.Peek();
var locallyNewlyAssigneds = new List<HashSet<ConstantField>>(); // used if alternative
var newlyAssignedAtLeastOnce = new HashSet<ConstantField>();
if (alternative) {
constantsAlreadyAssigned.Push(new HashSet<ConstantField>(previouslyAssigned));
}

foreach (var subStmt in stmt.SubStatements) {
CheckInit(subStmt); // (**)
if (alternative) {
var locallyNewlyAssigned = constantsAlreadyAssigned.Pop();
locallyNewlyAssigned.RemoveWhere(constantField => previouslyAssigned.Contains(constantField));
newlyAssignedAtLeastOnce.UnionWith(locallyNewlyAssigned);
locallyNewlyAssigneds.Add(locallyNewlyAssigned);
constantsAlreadyAssigned.Push(new HashSet<ConstantField>(previouslyAssigned));
}
}

if (alternative) {
constantsAlreadyAssigned.Pop();
if (stmt is IfStmt ifStmt && ifStmt.Els == null) {
locallyNewlyAssigneds.Add(new HashSet<ConstantField>());
}

foreach (var locallyNewlyAssigned in locallyNewlyAssigneds) {
foreach (var constantField in newlyAssignedAtLeastOnce) {
if (!locallyNewlyAssigned.Contains(constantField)) {
constantsPartiallyAssigned.Add(constantField);
}
}
}

newlyAssignedAtLeastOnce.RemoveWhere(constantField => constantsPartiallyAssigned.Contains(constantField));
constantsAlreadyAssigned.Peek().UnionWith(newlyAssignedAtLeastOnce);
}

int dummy = 0;
VisitOneStmt(stmt, ref dummy); // (***)

if (cannotAssignConstant) {
ContextIfCannotAssignConstant.Pop();
}
}
void VisitExpr(Expression expr) {
Contract.Requires(expr != null);
Expand All @@ -9100,8 +9187,15 @@ public CheckDividedConstructorInit_Visitor(Resolver resolver)
protected override bool VisitOneExpr(Expression expr, ref int unused) {
if (expr is MemberSelectExpr) {
var e = (MemberSelectExpr)expr;
if (e.Member.IsInstanceIndependentConstant && Expression.AsThis(e.Obj) != null) {
return false; // don't continue the recursion
if (Expression.AsThis(e.Obj) != null) {
if (e.Member is ConstantField) {
var errorField = GetErrorIfConstantFieldNotInitialized(expr, new List<ConstantField>());
if (errorField == null) {
return false;
}
resolver.reporter.Error(MessageSource.Resolver, expr.tok, errorField.Value.message);
return false;
}
}
} else if (expr is ThisExpr && !(expr is ImplicitThisExpr_ConstructorCall)) {
resolver.reporter.Error(MessageSource.Resolver, expr.tok, "in the first division of the constructor body (before 'new;'), 'this' can only be used to assign to its fields");
Expand All @@ -9121,26 +9215,98 @@ public CheckDividedConstructorInit_Visitor(Resolver resolver)
// a field of "this", we must apply the same rules to uses of the values of fields of "this".
if (expr is ConcreteSyntaxExpression) {
} else if (expr is ThisExpr) {
} else if (expr is MemberSelectExpr && IsThisDotField((MemberSelectExpr)expr)) {
} else if (expr is SetDisplayExpr) {
} else if (expr is MultiSetDisplayExpr) {
} else if (expr is SeqDisplayExpr) {
} else if (expr is MapDisplayExpr) {
} else if (expr is BinaryExpr && IsCollectionOperator(((BinaryExpr)expr).ResolvedOp)) {
} else if (expr is DatatypeValue) {
} else if (expr is ITEExpr) {
var e = (ITEExpr)expr;
VisitExpr(e.Test);
LiberalRHSVisit(e.Thn);
LiberalRHSVisit(e.Els);
return;
} else {
// defer to the usual Visit
VisitExpr(expr);
return;
// We ensure that we access only constants that were surely previously assigned
if (expr is MemberSelectExpr memberSelectExpr
&& memberSelectExpr.Member is Field f
&& (Expression.AsThis(memberSelectExpr.Obj) != null)) {
if (f is ConstantField constantField) {
var errorField = GetErrorIfConstantFieldNotInitialized(expr, new List<ConstantField>());
if (errorField != null) {
if (!constantsWithErrors.Contains(errorField.Value.constantField)) {
resolver.reporter.Error(MessageSource.Resolver, expr.tok, errorField.Value.message);
constantsWithErrors.Add(errorField.Value.constantField);
}
}

return;
}
} else if (expr is SetDisplayExpr) {
} else if (expr is MultiSetDisplayExpr) {
} else if (expr is SeqDisplayExpr) {
} else if (expr is MapDisplayExpr) {
} else if (expr is BinaryExpr && IsCollectionOperator(((BinaryExpr)expr).ResolvedOp)) {
} else if (expr is DatatypeValue) {
} else if (expr is ITEExpr) {
var e = (ITEExpr)expr;
VisitExpr(e.Test);
LiberalRHSVisit(e.Thn);
LiberalRHSVisit(e.Els);
return;
} else {
// defer to the usual Visit
VisitExpr(expr);
return;
}
}

expr.SubExpressions.Iter(LiberalRHSVisit);
}

// Returns an error if the expression cannot be fully determined
(string message, ConstantField constantField)? GetErrorIfConstantFieldNotInitialized(Expression expr, List<ConstantField> visited) {
if (expr is MemberSelectExpr { Member: ConstantField field } memberSelectExpr && Expression.AsThis(memberSelectExpr.Obj) != null) {
if (visited.IndexOf(field) is var index && index >= 0) {
var msg = "Please break this constant initialization cycle: " + visited[index].Name;
for (var i = index + 1; i < visited.Count; i++) {
msg += " -> " + visited[i].Name;
}
msg += " -> " + field.Name;
return (msg, field);
}
if (field.Rhs == null && !constantsAlreadyAssigned.Peek().Contains(field)) {
var msg = constantsPartiallyAssigned.Contains(field) ?
"Not all paths might have initialized the constant field " :
"Missing initialization of constant field ";
for (var i = 0; i < visited.Count; i++) {
msg += (i == 0 ? "through the dependency " : "") + visited[i].Name + " -> ";
}
msg += field.Name + ", which needs to be initialized at this point.";
return (msg, field);
}
if (field.Rhs != null) {
foreach (var subExpr in field.Rhs.SubExpressions) {
if (GetErrorIfConstantFieldNotInitialized(subExpr, visited.Append(field).ToList()) is var msgField && msgField != null) {
return msgField;
}
}
}

return null; // No problem to declare for this field
} else if (expr is FunctionCallExpr { tok: var tok, Function: { IsStatic: false } function } && visited.Count > 0) {
var msg = "Constant field '" + visited[0].Name + "' cannot be accessed before 'new;'";
for (var i = 1; i < visited.Count; i++) {
msg += (i == 1 ? " because of the dependency " + visited[0].Name : "") + " -> " + visited[i].Name;
}
msg += ", " + (visited.Count > 1 ? "and" : "because") + " '" + visited[visited.Count - 1].Name + "' depends on the non-static function '" + function.Name + "' that can potentially read other uninitialized constants.";
return (msg, visited[0]);
} else if (expr is ThisExpr && visited.Count > 0) {
var msg = "Constant field '" + visited[0].Name + "' cannot be accessed before 'new;'";
for (var i = 1; i < visited.Count; i++) {
msg += (i == 1 ? " because of the dependency " + visited[0].Name : "") + " -> " + visited[i].Name;
}
msg += ", " + (visited.Count > 1 ? "and" : "because") + " '" + visited[visited.Count - 1].Name + "' depends on the object 'this' itself, that can potentially read other uninitialized constants.";
return (msg, visited[0]);
}
foreach (var subExpr in expr.SubExpressions) {
if (GetErrorIfConstantFieldNotInitialized(subExpr, visited) is var msgField && msgField != null) {
return msgField;
}
}

return null;
}

static bool IsThisDotField(MemberSelectExpr expr) {
Contract.Requires(expr != null);
return Expression.AsThis(expr.Obj) != null && expr.Member is Field;
Expand Down Expand Up @@ -13784,6 +13950,7 @@ private class ClonerButIVariablesAreKeptOnce : Cloner {
var div = (DividedBlockStmt)blockStmt;
Contract.Assert(currentMethod is Constructor); // divided bodies occur only in class constructors
Contract.Assert(!resolutionContext.InFirstPhaseConstructor); // divided bodies are never nested

foreach (Statement ss in div.BodyInit) {
ResolveStatementWithLabels(ss, resolutionContext with { InFirstPhaseConstructor = true });
}
Expand Down
Loading