markj-db opened a new pull request, #58014:
URL: https://github.com/apache/spark/pull/58014

   ### What changes were proposed in this pull request?
   
   This hardens the shuffle order-independent (row-based) checksum
   (`spark.sql.shuffle.orderIndependentChecksum.enabled`, on by default) 
against a class of JVM
   crashes.
   
   `UnsafeRowChecksum` computes each row's checksum by reading the row's raw 
backing memory via
   `XXH64.hashUnsafeBytes` -> `Platform.getLong`, with no validation. When a 
row's `UnsafeRow` carries
   an invalid pointer -- a null `baseObject` with a near-zero `baseOffset`, or 
a negative size -- that
   unchecked read dereferences bad memory and the JVM crashes with a SIGSEGV 
(`si_addr=0x0`). Because
   the checksum is enabled by default, a single latent bad-row bug surfaces as 
a process crash that
   takes the whole executor down.
   
   This PR adds a validation step that runs in `RowBasedChecksum.update` 
*before* the row is ever
   dereferenced:
   
   - `RowBasedChecksum` gains an overridable `validateRow` hook, a row-ordinal 
counter, and an
     overridable `failOnInvalidRow` member (default true). A flagged row is 
logged (via the
     structured-logging framework) with full context (stage / partition / task 
/ row-ordinal and the
     row's `baseObject`, `baseOffset`, `sizeInBytes`) and then, by default, 
fails the task with a
     descriptive error.
   - `UnsafeRowChecksum` implements the check for `UnsafeRow` backing memory 
and overrides
     `failOnInvalidRow` via its constructor. It flags only what a 
validly-constructed `UnsafeRow` can
     never be -- a negative size, a null off-heap `baseObject` pointing into 
the first memory page (the
     `si_addr=0x0` shape), or an out-of-bounds `byte[]` offset. Any other 
non-null on-heap base (e.g.
     the common `long[]` buffer) is accepted, and a corrupt-but-large size is 
deliberately not flagged,
     so a well-formed row is never flagged.
   - A new internal conf 
`spark.sql.shuffle.orderIndependentChecksum.failOnInvalidRow` (default `true`)
     exists only as a safety valve: set it to `false` to log and disable that 
partition's checksum
     instead of failing, should a validator false positive ever be observed. 
`ShuffleExchangeExec`
     reads it when it constructs the per-partition checksums.
   
   Failing is strictly better than the status quo: today a bad row crashes the 
JVM with a SIGSEGV,
   which fails the task anyway (via retry exhaustion) and is more disruptive 
because it takes the whole
   executor down. Failing the task with a clear error never increases query 
failures for a genuinely
   bad row and makes it debuggable. The invalid pointer is never dereferenced 
in either mode. A stale
   off-heap pointer at a *plausible* (high) address cannot be distinguished 
from a live one here and is
   not caught -- that would need the separate "checksum the serialized bytes" 
change.
   
   ### Why are the changes needed?
   
   The order-independent shuffle checksum is enabled by default, so any bad 
`UnsafeRow` reaching it
   crashes the executor with a SIGSEGV instead of failing the task with an 
actionable error. Guarding
   the read turns an opaque process crash into a loggable, attributable task 
failure without changing
   behavior for well-formed rows.
   
   ### Does this PR introduce _any_ user-facing change?
   
   No. For well-formed rows behavior is unchanged. A genuinely invalid row that 
would previously have
   crashed the JVM now fails the task with a descriptive `SparkException` (or, 
with the new
   safety-valve conf disabled, is logged and the partition's checksum is 
skipped).
   
   ### How was this patch tested?
   
   Unit tests added to the existing `UnsafeRowChecksumSuite` (sql/core):
   
   - `validate` accepts well-formed on-heap rows (both `byte[]`- and 
`long[]`-backed) and empty rows,
     and flags a null-`baseObject` first-page pointer (the `si_addr=0x0` shape) 
and a negative size; a
     large-but-positive size is deliberately not flagged.
   - Fail mode (the default, including the no-arg constructor) raises a 
descriptive `SparkException` on
     an invalid row.
   - Recover mode (`failOnInvalidRow = false`) disables the checksum on an 
invalid row without throwing
     or dereferencing, and stays disabled for subsequent rows.
   - `createUnsafeRowChecksums` threads the `failOnInvalidRow` flag through to 
the checksum objects.
   
   Ran locally (JDK 17, SBT): `UnsafeRowChecksumSuite` (9 tests) and 
`LogKeysSuite` pass.
   
   ### Was this patch authored or co-authored using generative AI tooling?
   
   Yes -- authored using Claude Code (Opus 4.8), reviewed by the author.
   


-- 
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