Ramin Gharib created FLINK-40917:
------------------------------------

             Summary: Deeply nested VARIANT values overflow the stack in 
toJson(), CAST to STRING and the variant builder
                 Key: FLINK-40917
                 URL: https://issues.apache.org/jira/browse/FLINK-40917
             Project: Flink
          Issue Type: Bug
          Components: API / Core
            Reporter: Ramin Gharib
            Assignee: Ramin Gharib


h3. Problem

Code that walks a \{{VARIANT}} recurses once per nesting level, so a deeply 
nested value overflows the thread stack. A \{{StackOverflowError}} is an 
\{{Error}}, so handlers that catch \{{Exception}} miss it. 
\{{ExceptionUtils.isJvmFatalError}} does not treat it as fatal either. The task 
fails over and restarts on the same record, without a message that says what 
went wrong.

The limit depends on the stack size and on the JIT state. On a 1 MiB stack, the 
default for task threads, about 2,000 levels overflow. Such a value takes only 
about 20 KB, far below the 16 MiB size limit of a \{{VARIANT}}.

h3. Affected code

||Recursive walker||Used by||
|\{{JsonVariantFormatter}}|\{{Variant#toJson()}}, \{{Variant#toString()}}, 
\{{JSON_STRING}}, \{{JSON_OBJECT}}, the \{{json}} and \{{raw}} formats|
|\{{VariantCastUtils.renderNode}}|\{{CAST(v AS STRING)}} and printing results|
|\{{BinaryVariantInternalBuilder.appendVariantImpl}}|the object and array 
builders of the public \{{VariantBuilder}}, and the cast of an \{{ARRAY}}, 
\{{MAP}} or \{{ROW}} to \{{VARIANT}}|

FLINK-40826 catches the overflow in the cast to \{{VARIANT}}, which fails with 
\{{Cannot cast a value of type ARRAY<VARIANT> to VARIANT because it is nested 
too deeply.}}, and \{{TRY_CAST}} returns \{{NULL}}. That covers one call site 
only. The other walkers still throw a bare \{{StackOverflowError}}.

h3. Reproduce

{code:java}
// [[[...[1]...]]] with 20,000 levels, built without recursion
BinaryVariantInternalBuilder builder = new BinaryVariantInternalBuilder(false);
builder.appendInt(1);
for (int i = 0; i < 20_000; i++) {
    builder.finishWritingArray(0, new ArrayList<>(List.of(0)));
}
BinaryVariant deep = builder.build();

deep.toJson();                                  // StackOverflowError
Variant.newBuilder().array().add(deep).build(); // StackOverflowError
{code}

In SQL, \{{JSON_STRING(v)}} and \{{CAST(v AS STRING)}} fail the same way for 
such a value.

h3. Where deep values come from

{\{PARSE_JSON}}, \{{TRY_PARSE_JSON}} and the JSON format parse with Jackson, 
which rejects JSON nested deeper than 1,000 levels by default. That is close to 
the overflow limit, so the margin is thin. Other sources have no limit, such as 
binary variants that a format reads as they are, and values that a user 
function builds with \{{VariantBuilder}}.

h3. Options

# Make the walkers iterative, with an explicit stack. This removes the limit 
and changes nothing for valid input. It needs the most code.
# Enforce a maximum nesting depth wherever a variant is built or read, well 
below the stack limit, so that deeper values fail with a clear error. This is 
simple, but a limit below 1,000 would reject JSON that \{{PARSE_JSON}} accepts 
today.
# Catch \{{StackOverflowError}} at every call site, as the cast to \{{VARIANT}} 
does. This is the least code, but it is fragile, and every new walker has to 
remember it.

Option 1 fits best. The three walkers sit in core and in the table runtime, and 
every query that uses \{{VARIANT}} reaches them. Once they are iterative, the 
catch in \{{ToVariantConverter#convert}} can go.



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

Reply via email to