Ramin Gharib created FLINK-40217:
------------------------------------

             Summary: Variant.toJson() can emit invalid JSON for numbers that 
overflow to non-finite doubles
                 Key: FLINK-40217
                 URL: https://issues.apache.org/jira/browse/FLINK-40217
             Project: Flink
          Issue Type: Bug
          Components: API / Type Serialization System, Table SQL / API
    Affects Versions: 2.1.3, 2.2.1, 2.3.0
            Reporter: Ramin Gharib
            Assignee: Ramin Gharib


 

{{PARSE_JSON}} accepts a JSON number whose magnitude exceeds the {{double}} 
range and silently stores it as a non-finite {{{}double{}}}. 
{{Variant.toJson()}} then serializes that value as the bare token {{Infinity}} 
/ {{{}-Infinity{}}}, which is not valid JSON. The result cannot be read back by 
{{{}PARSE_JSON{}}}, so the round-trip {{PARSE_JSON(x).toJson()}} is not closed 
over valid JSON.

*Reproduction (SQL)*

 
{code:java}
-- Accepted. Overflows to a non-finite double:
SELECT PARSE_JSON('1e400');
-- +----------+
-- |   EXPR$0 |
-- +----------+
-- | Infinity |   <-- invalid JSON emitted by toJson()
-- +----------+

-- '-1e400' likewise yields -Infinity.

-- The reverse direction is already (correctly) rejected:
SELECT PARSE_JSON('Infinity');

-- Caused by: com.fasterxml.jackson.core.JsonParseException:
--   Non-standard token 'Infinity': enable 
`JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS` to allow 
{code}
**

 

So {{PARSE_JSON}} produces output ({{{}Infinity{}}}) that {{PARSE_JSON}} itself 
refuses to accept.

*Root cause*

Two spots in {{{}flink-core{}}}, package {{{}org.apache.flink.types.variant{}}}:
 # {{BinaryVariantInternalBuilder.parseFloatingPoint}} — {{1e400}} uses 
scientific notation, so {{tryParseDecimal}} returns false and the value goes 
through {{{}appendDouble(parser.getDoubleValue()){}}}. Jackson's 
{{getDoubleValue()}} coerces the out-of-range number to 
{{{}Double.POSITIVE_INFINITY{}}}. The overflow is silent.

{code:java}
private void parseFloatingPoint(JsonParser parser) throws IOException {
    if (!tryParseDecimal(parser.getText())) {
        appendDouble(parser.getDoubleValue()); // 1e400 -> +Infinity, no error
    }
} {code}

 # {{BinaryVariant.toJsonImpl}} — the {{DOUBLE}} and {{FLOAT}} cases append via 
{{{}StringBuilder.append(double/float){}}}, i.e. {{{}Double.toString(...){}}}, 
which yields {{Infinity}} / {{-Infinity}} / {{{}NaN{}}}. None are valid JSON.

{code:java}
case DOUBLE:
    sb.append(BinaryVariantUtil.getDouble(value, pos)); // "Infinity" etc.
    break;
...
case FLOAT:
    sb.append(BinaryVariantUtil.getFloat(value, pos));
    break; {code}

The builder API ({{{}Variant.newBuilder().of(Double.NaN){}}}) can also place a 
non-finite value into a Variant, which hits the same {{toJson()}} defect. 
{{PARSE_JSON}} can only reach {{±Infinity}} (via overflow); {{NaN}} is only 
reachable through the builder.



*Expected behavior*

{{toJson()}} must always produce valid JSON, and it should be readable back by 
{{{}PARSE_JSON{}}}. A Variant sourced from JSON should never hold a value that 
JSON cannot represent.

*Proposed fix*
 * *Preferred (parse side):* reject the lossy coercion in 
{{{}parseFloatingPoint{}}}. If {{getDoubleValue()}} is non-finite while the 
source token was finite text, throw a clear parse error. This makes 
{{PARSE_JSON('1e400')}} fail the same way {{PARSE_JSON('Infinity')}} already 
does, and keeps non-finite values out of JSON-sourced Variants entirely.
 * *Defense-in-depth (serialize side):* in {{{}toJsonImpl{}}}, handle 
non-finite {{{}double{}}}/{{{}float{}}} explicitly so a Variant built via the 
builder API cannot emit invalid JSON. Options: emit {{null}} (matches 
{{JSON.stringify}} semantics) or throw. Emitting {{null}} is lossy; throwing is 
safer for a serializer that advertises valid-JSON output. Maintainer's call.

Recommendation: do the parse-side rejection as the root-cause fix, plus a guard 
in {{toJson()}} so the builder path is also safe.

*Verifying the change*

Add unit tests in {{BinaryVariantTest}} / the {{PARSE_JSON}} function tests: 
assert {{PARSE_JSON('1e400')}} and {{'-1e400'}} fail with a clear message, and 
that {{toJson()}} never returns a string containing {{Infinity}} / {{{}NaN{}}}. 
Add a round-trip test asserting {{PARSE_JSON(v.toJson())}} succeeds for any 
Variant produced by {{{}PARSE_JSON{}}}.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to