We're updating the issue view to help you get more done. 

StackOverflowException raised on certain LINQ queries using First(), FirstOrDefault(), Single(), or SingleOrDefault()

Description

Executing the following query results in a StackOverflowException:

var query = from acl in QueryFactory.CreateLinqQuery<AccessControlList> ()
where acl.AccessControlEntries.FirstOrDefault().GroupCondition == GroupCondition.None
select acl;

The concrete types are taken from re-motion SecurityManager, but are not important, only the query structure is.
The issue is critical, because in projects where users are allowed to formulate LINQ queries this bug can be used to take the system down.

Notes:

  • It appears to only affect the use of enum-values and EntityExpressions.

  • Downgraded priority to "Normal" since there are alternative ways of formulating the query that are also less complex in regards to the SQL output. To guard against malicous users, it would also be possible to first execute the statement preparation in a separate AppDomain.

Generic sample with "EnumType" enum, stringified epressions from subsequent invocations SqlSubStatementExpression, the innermost results in the stack overflow:

{(SELECT TOP (1) TABLE-REF(SqlJoinedTable(Cook)) AS value FROM INNER JOIN Cook.Assistants WHERE CONDITION(INNER JOIN Cook.Assistants))}

{(SELECT TOP (1) TABLE-REF(SqlJoinedTable(Cook)).EnumType AS value FROM INNER JOIN Cook.Assistants WHERE CONDITION(INNER JOIN Cook.Assistants))}

{(SELECT TOP (1) [t1].[EnumType] AS value FROM [CookTable] [t1] WHERE ([t0].[ID] == [t1].[AssistedID]))}

{(SELECT TOP (1) [t1].[EnumType] AS value FROM [CookTable] [t1] WHERE ([t0].[ID] == [t1].[AssistedID]))}

 
Generic sample with "GroupCondition" enum, stringified epressions from subsequent invocations SqlSubStatementExpression, the innermost results in the stack overflow:

{(SELECT TOP (1) TABLE-REF(SqlJoinedTable(ACE)) AS value FROM INNER JOIN ACL.ACEs WHERE CONDITION(INNER JOIN ACL.ACEs))}

{(SELECT TOP (1) TABLE-REF(SqlJoinedTable(ACE)).GroupCondition AS value FROM INNER JOIN ACL.ACEs WHERE CONDITION(INNER JOIN ACL.ACEs))}

{(SELECT TOP (1) [t1].[GroupCondition] AS value FROM [ACEView] [t1] WHERE ([t0].[ID] == [t1].[ACLID]))}

{(SELECT TOP (1) [t1].[GroupCondition] AS value FROM [ACEView] [t1] WHERE ([t0].[ID] == [t1].[ACLID]))}

Debug-Analysis:

ResolvingExpressionVisitor.VisitBinary() is called with the following BinaryExpression:

{(Convert((SELECT TOP (1) [t1].[EnumType] AS value FROM [CookTable] [t1] WHERE ([t0].[ID] == [t1].[AssistedID]))) == Convert(0))}

The problem stems from the Left-expression having the Convert-Expression first stripped off and then re-applied. This results in an equivalent Expression with a different reference identity and thus a missed abort condition in the VisitBinary() method.

Drilling down further, VisitBinary() calls EntityIdentityResolver.ResolvePotentialEntityComparison(), which is responsible for first removing the Convert-Expression from the Left-Expression and then immediately re-applying new (equivalent) Convert-Expression.

The removal happens in EntityIdentityResolver.ResolvePotentialEntity(left) which strips the Conversion-Expression in StripConversions and then does its thing on the inner expression. However, ResolvePotentialEntity() does return the original Convert-Expression if no changes are needed. This check is missing for the nested calls, e.g. CheckAndSimplifyEntityWithinSubStatement(). We likely should add this guard to all return-paths in ResolvePotentialEntity, regardless if the result value's likelyhood of getting returned in its original form.

 

Assignee

Michael Ketting

Reporter

Michael Walk

Labels

None

Components

Fix versions

Affects versions

Priority

Normal
Configure