[ 
https://issues.apache.org/jira/browse/CALCITE-7893?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

ASF GitHub Bot updated CALCITE-7893:
------------------------------------
    Labels: pull-request-available  (was: )

> Type mapping rules can be corrupted when identity hash codes of SqlTypeName 
> values collide
> ------------------------------------------------------------------------------------------
>
>                 Key: CALCITE-7893
>                 URL: https://issues.apache.org/jira/browse/CALCITE-7893
>             Project: Calcite
>          Issue Type: Bug
>          Components: core
>            Reporter: Vladimir Sitnikov
>            Priority: Major
>              Labels: pull-request-available
>
> {{SqlTypeAssignmentRule}} and {{SqlTypeCoercionRule}} build their type tables 
> once per JVM. If two different sets of {{SqlTypeName}} passed to the builder 
> have the same hash code, the type added later receives the source set of the 
> type added earlier. {{SqlTypeName}} is an enum, so its hash code is the 
> identity hash code, and a set's hash code is the sum of its elements' hash 
> codes. Identity hash codes change from one JVM run to the next, so the 
> corruption is probabilistic: a query can pass in one run and fail in the 
> next, and the wrong table stays in effect until the JVM exits.
> With a corrupted table, the validator accepts casts it should reject, rejects 
> valid calls, and resolves user-defined functions to the wrong overload. I 
> have not observed a collision with default JVM settings.
> h3. How to reproduce
> The JVM option {{-XX:+UnlockExperimentalVMOptions -XX:hashCode=2}} makes 
> every identity hash code equal to 1, so every collision that can happen does 
> happen. It is a way to make the defect deterministic, not a setting anyone 
> runs in production.
> Gradle init script {{hash.gradle}}:
> {code:groovy}
> allprojects {
>   tasks.withType(Test).configureEach {
>     jvmArgs '-XX:+UnlockExperimentalVMOptions', '-XX:hashCode=2'
>   }
> }
> {code}
> {code:bash}
> ./gradlew -I hash.gradle :core:test --tests 
> org.apache.calcite.test.SqlValidatorTest --tests 
> org.apache.calcite.test.UdfTest
> {code}
> With the option, the validator accepts this query:
> {code:sql}
> values (cast(DATE '1243-12-01' as TIME))
> {code}
> Expected: validation fails with {{Cast function cannot convert value of type 
> DATE NOT NULL to type TIME}}, as {{SqlValidatorTest#testCastFails}} asserts. 
> Actual:
> {noformat}
> java.lang.AssertionError: Expected query to throw exception, but it did not; 
> query [values (cast(DATE '1243-12-01' as TIME))]; expected [(?s).*Cast 
> function cannot convert value of type DATE NOT NULL to type TIME.*]
>       at org.apache.calcite.sql.test.SqlTests.checkEx(SqlTests.java:251)
>       at 
> org.apache.calcite.test.SqlValidatorTest.testCastFails(SqlValidatorTest.java:1626)
> {noformat}
> The table itself is wrong: 
> {{SqlTypeMappingRules.instance(false).getTypeMapping().get(SqlTypeName.TIME)}}
>  returns {{[DATE]}}, while {{SqlTypeAssignmentRule}} declares TIME assignable 
> from {{[TIME]}} only. The same corruption breaks UDF lookup, for example in 
> {{UdfTest#testUserDefinedFunctionWithMethodName}}:
> {noformat}
> Suppressed: org.apache.calcite.util.TestUtil$ExtraInformation: With 
> materializationsEnabled=false, limit=-1, sql=values "adhoc"."fun1"('Foo Bar')
> Caused by: java.lang.NumberFormatException: For input string: "Foo Bar"
>       at 
> org.apache.calcite.linq4j.tree.Primitive.checkNumericString(Primitive.java:463)
>       at org.apache.calcite.runtime.SqlFunctions.toInt(SqlFunctions.java:5519)
> {noformat}
> and {{UdfTest#testDate}} fails with {{No match found for function signature 
> timeFun(<TIME>)}}.
> Locally on JDK 21.0.9 this fails 11 tests: {{SqlValidatorTest#testCastFails}} 
> and 10 tests in {{UdfTest}}. In CI ([run 
> 40321987597|https://github.com/vlsi/calcite/actions/runs/40321987597], JDK 
> 11, 21, and 24 on Linux, macOS, and Windows) the four jobs with the option 
> that reached {{:core:test}} each failed the same 41 tests there, among them 
> {{JdbcTest}} table macro and table function tests, {{SqlAdvisorJdbcTest}}, 
> {{testCastDateTime}} in the operator tests, {{InterpreterTest}}, 
> {{TableFunctionTest}}, and {{sql/spatial.iq}}. Jobs without the option passed.
> h3. Cause
> {{SqlTypeMappingRules.Builder#add}} 
> ([SqlTypeMappingRules.java:77|https://github.com/apache/calcite/blob/2df193e496/core/src/main/java/org/apache/calcite/sql/type/SqlTypeMappingRules.java#L77])
>  uses the caller's set as the key of a Guava {{LoadingCache}}. 
> {{SqlTypeAssignmentRule}} and {{SqlTypeCoercionRule}} pass one {{HashSet}} 
> named {{rule}}, which they clear and refill between calls, so every cache 
> entry is keyed by the same mutable object. The cache matches an entry by its 
> stored hash and then by {{equals}}, and the key always equals itself. Any 
> lookup whose set has the hash code of an earlier set therefore returns that 
> earlier set. {{Set#hashCode}} is the sum of the element hash codes, so under 
> the option every one-element set hashes to 1, and {{\{TIME\}}} receives the 
> entry stored for an earlier one-element set such as {{\{DATE\}}}.
> h3. Proposed fix
> Make {{SqlTypeMappingRules.Builder#add}} copy its argument into an immutable 
> set before it uses the set as a key:
> {code:java}
> void add(SqlTypeName toType, Set<SqlTypeName> fromTypes) {
>   map.put(toType, sets.computeIfAbsent(Sets.immutableEnumSet(fromTypes), k -> 
> k));
> }
> {code}
> Here {{sets}} becomes a plain {{HashMap<ImmutableSet<SqlTypeName>, 
> ImmutableSet<SqlTypeName>>}}: {{add}} is package-private, and Calcite fills 
> its rule tables once, in static initializers, so the builder needs neither 
> eviction nor thread safety. Equal sets still share one instance, and the 
> values stored in the table are {{ImmutableEnumSet}}s as before. The callers 
> can keep reusing their {{rule}} set.
> A regression test in {{SqlTypeUtilTest}} can force the collision without JVM 
> options: pass a {{HashSet}} subclass whose {{hashCode()}} returns a constant, 
> fill it with DATE, add it for DATE, refill it with TIME, add it for TIME, and 
> assert that TIME maps to {{[TIME]}}.
> A local patch that passes {{ImmutableSet.copyOf(fromTypes)}} as the cache key 
> makes all 17088 tests of {{:core:test}} pass under the option (0 failures, 
> 146 skipped).



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to