dongjoon-hyun commented on PR #58598:
URL: https://github.com/apache/spark/pull/58598#issuecomment-5581098500

   Thank you for working on this. The underlying problem is real -- I 
reproduced the `StackOverflowError` escaping `CatalystSqlParser.parseDataType`. 
However, I believe **the depth check as implemented can be trivially 
bypassed**, so the patch does not actually provide the protection it describes.
   
   ### The check is bypassable
   
   `parseCatalystType` scans raw characters rather than tokens. But a struct 
field name may be a backquoted identifier (`complexColType: 
colName=errorCapturingIdentifier dataType ...`), and a backquoted identifier 
can contain `>`. So inserting one fake `>` per nesting level keeps the counter 
flat.
   
   I built `sql-api` and ran the real `DataTypeParser` against a replica of 
this PR's counting logic:
   
   ```
   sample      = struct<`>`:struct<`>`:struct<`>`:int>>>
   counted max = 1 (real nesting = 3)
   parses to   = STRUCT<`>`: STRUCT<`>`: STRUCT<`>`: INT>>>
   
   n=500   len=6003    counted max depth = 1  ->  *** StackOverflowError 
escaped parseDataType ***
   n=2000  len=24003   counted max depth = 1  ->  *** StackOverflowError 
escaped parseDataType ***
   n=10000 len=120003  counted max depth = 1  ->  *** StackOverflowError 
escaped parseDataType ***
   ```
   
   A type nested 10,000 levels deep passes the check with a counted depth of 
**1**, and blows the stack on a 1MB-stack thread. No value of 
`catalystTypeParsingMaxDepth` stops it.
   
   ### The check also has false positives
   
   The same token-vs-character mismatch rejects legitimate types:
   
   ```
   struct<a:int COMMENT 'a<<<<<<<<<<b'>   ->  counted=11, actual nesting=1
   struct<`<<<<<<<<<<`:int>               ->  counted=11, actual nesting=1
   array<decimal(10,2)>                   ->  counted=2,  actual nesting=1   
('(' counts as a level)
   ```
   
   Counting `(` means `decimal(p,s)` / `varchar(n)` inflate the measured depth, 
so the number the config takes does not match the "nesting depth" the doc 
describes.
   
   ### Other notes
   
   - **Default `-1` plus `internal()`**: as written this ships no protection by 
default and is undocumented. Worth noting that the values Spark itself stamps 
into `spark.sql.catalyst.type` are all shallow (`char(n)`, `varchar(n)`, 
`time(6)`, `timestamp_ltz_ns(9)`), so a correct check could be enabled by 
default at a low limit with essentially no compatibility risk.
   - **No `checkValue`**: `0` and any other negative value silently mean 
"disabled" as well. Something like `.checkValue(v => v > 0 || v == -1, ...)` 
would make the contract explicit.
   - **Doc/comment scope**: both say "during schema inference" and "driver 
stack", but `parseCatalystType` also runs on executors via `AvroDeserializer` 
(`SchemaConverters.toSqlType` at `AvroDeserializer.scala:145`).
   - **Test coverage**: the new test only exercises a depth-8 `array<...>`. It 
does not assert that an input which actually overflows the stack is rejected, 
does not cover the backquote bypass, and does not cover the 
`parseStampedStringType` / map-key-type paths. Adding the `struct<`>`:...>` 
string above to the test makes the current implementation fail immediately.
   
   ### Suggestion
   
   Character counting effectively re-implements the grammar, and this class of 
bypass tends to keep reappearing. Simpler and more robust options:
   
   1. **Catch `StackOverflowError` and convert it.** Three lines, no config, no 
heuristic, no bypass, no false positives. There is precedent in the codebase 
already -- `AbstractSqlParser.withErrorHandling` and `SqlStatementSplitter` 
both do this. Note `AbstractParser.parse` only catches `ParseException` and 
friends, and the SOE-catching `withErrorHandling` is not on the `parseDataType` 
path, which is why the error escapes today.
   2. **Put the depth guard in the parser itself.** The same exposure exists 
wherever untrusted text reaches `parseDataType` -- `from_json` DDL schema 
strings, JDBC/metastore schema strings, and so on. Fixing it only for Avro is 
quite narrow.
   3. If a pre-check is still preferred, **bound the string length** rather 
than the depth. Length is a hard upper bound on nesting and cannot be gamed by 
the content.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to