parquet-java 1.18.0 Can Silently Corrupt Reads: How to Tell If You Are Exposed
Quick answer: parquet-java 1.18.0, released on 11 August 2026, contains a data corruption bug in Binary.ByteBufferBackedBinary.getBytes(). It affects reads of repeated FIXED_LEN_BYTE_ARRAY columns when the reader is handed a non-array-backed buffer. The read returns wrong bytes rather than throwing, so nothing in your pipeline fails. The fix was merged on 25 August 2026 and as of 26 August 2026 there is no 1.18.1 release, so the practical answer is to establish which parquet-java version each engine actually loads and pin anything on 1.18.0 back to 1.17.1.
Last updated: August 2026
A read bug that throws an exception is an inconvenience. A read bug that returns the wrong bytes and keeps going is a different category of problem, because the pipeline stays green, the row counts match, and the numbers are wrong somewhere downstream where nobody is looking for a library version.
That is what landed in parquet-java 1.18.0, which was published on 11 August 2026. Under specific conditions the reader returns corrupted values for repeated FIXED_LEN_BYTE_ARRAY columns. The fix was merged upstream on 25 August 2026. At the time of writing there is no 1.18.1 on the release page, so anyone already running 1.18.0 has to act on the version they have rather than wait for a patch.
This article covers what the bug actually is, the conditions that trigger it, how to work out which parquet-java version each of your engines really loads (which is rarely the one in your dependency file), and what to do while there is no patched release.
What the bug does
The defect sits in Binary.ByteBufferBackedBinary.getBytes() and toStringUsingUTF8(), on the branch that handles buffers which are not backed by a Java array. Direct and memory-mapped buffers take that branch. Heap buffers do not.
The root cause is shared mutable state. FixedLenByteArrayPlainValuesReader hands out Binary values that all point at a single page-wide ByteBuffer, and that buffer's live position advances on every readBytes() call. The affected methods called value.limit(offset + length) directly on the shared buffer. ByteBuffer.limit() clamps the position whenever the position is greater than the new limit, so reading an earlier value after later values had moved the cursor permanently rewound it, corrupting every read that followed.
The upstream fix duplicates the buffer before adjusting position and limit, so the shared buffer is never mutated. That change was merged on 25 August 2026.
- Affected versions: 1.18.0 and any build from the branch before the 25 August 2026 fix.
- Not affected: 1.17.1 (12 May 2026) and earlier.
- Failure mode: wrong bytes returned, no exception raised.
- Fixed release: none published as of 26 August 2026.
The conditions that actually trigger it
Three things have to line up, which is why this did not surface immediately and why most workloads are fine.
- A repeated
FIXED_LEN_BYTE_ARRAYcolumn. In practice that means a list or array whose elements are fixed-width binary. Fixed-length decimals, UUIDs and fixed-size hashes are all stored this way. - More than one element per row, across more than one row. Record assembly holds each
Binaryand only materialises it once the full row or group is built, which is what creates the out-of-order access the bug depends on. - A non-array-backed buffer on the read path. Off-heap and memory-mapped reads take that branch. A plain heap buffer does not.
A scalar decimal column is not enough on its own. Neither is a list of strings, because variable-length values use a different reader. The combination is narrow, and that is exactly why it is worth checking rather than assuming: teams with a schema that hits all three will not get a warning.
Which parquet-java version does your engine actually load?
This is the part that catches people out. The version in your pom.xml, build.sbt or requirements.txt is frequently not the version on the classpath at runtime. Engines bundle their own copy, shade it under a renamed package, or resolve a newer transitive version than the one you declared.
Ask the running JVM rather than the build file. The reliable method is to find the jar that the loaded class came from.
// Prints the actual jar backing the Parquet reader classes.
val cls = Class.forName("org.apache.parquet.io.api.Binary")
println(cls.getProtectionDomain.getCodeSource.getLocation)
// And the version the jar declares about itself.
println(Option(cls.getPackage.getImplementationVersion).getOrElse("not set in manifest"))
jvm = spark._jvm
cls = jvm.java.lang.Class.forName("org.apache.parquet.io.api.Binary")
print(cls.getProtectionDomain().getCodeSource().getLocation().toString())
print(cls.getPackage().getImplementationVersion())
If the printed path points at a shaded or assembly jar, the manifest version may describe the bundling engine rather than Parquet. In that case inspect the jar directly.
# Locate candidate jars
find / -name 'parquet-*.jar' 2>/dev/null
# Read the version each one declares
for j in $(find / -name 'parquet-hadoop*.jar' 2>/dev/null); do
echo "== $j"
unzip -p "$j" META-INF/MANIFEST.MF | grep -i -E 'Implementation-Version|Bundle-Version'
done
# Shaded builds hide the classes under a renamed package.
# This finds them wherever they were relocated to.
for j in $(find / -name '*.jar' 2>/dev/null); do
if unzip -l "$j" 2>/dev/null | grep -q 'parquet/io/api/Binary.class'; then
echo "contains Parquet reader classes: $j"
fi
done
Where each engine gets its copy
The table below is a starting point for the audit, not a substitute for it. Bundled versions move between engine releases, and a platform runtime can carry a patched build whose version string still reads 1.18.0. Check the running system.
| Engine or runtime | Where the Parquet library comes from | What to check |
|---|---|---|
| Apache Spark (open source) | Bundled in the distribution under jars/ | ls $SPARK_HOME/jars/parquet-* on every node, driver included |
| Managed Spark runtimes | Pinned by the platform, sometimes with vendor patches applied | The runtime release notes, plus the classpath check above on a live cluster |
| Apache Iceberg (Java) | Transitive dependency, often overridden by the query engine | Resolve the dependency tree, then confirm against the running JVM |
| Apache Flink | Bundled in the format connector jar | The connector jar on the task managers, not the job jar |
| Trino and Presto | Own reader implementation for most paths | Confirm which reader path your table format and column types use |
| DuckDB, Polars, pyarrow | Not affected by this bug: these do not use parquet-java | No action needed for these engines |
That last row matters for scoping. This is a bug in the Java implementation. A pipeline that reads the same files through Arrow or DuckDB is reading them with different code and is not exposed to this particular defect.
How to tell whether you have already read bad data
Because the corruption is silent, absence of errors proves nothing. The practical test is to read the same files twice with different library versions and compare, which is the same technique used for a migration reconciliation.
- Identify tables with a repeated fixed-width binary column. In a Hive or Iceberg catalog that means array or list columns whose element type is a fixed-length decimal, UUID or fixed binary.
- Read a representative sample through an engine on 1.18.0 and through one on 1.17.1, writing both results to staging tables.
- Compare at column level. A row-count check passes even when every value in the column is wrong.
- Where the two disagree, treat the 1.17.1 result as the reference and work out which downstream outputs consumed the bad read.
-- Hash each row's affected column under both readers, then diff. -- Cast the array to string first so the hash is stable and comparable. WITH a AS ( SELECT id, MD5(CAST(fixed_binary_list AS VARCHAR)) AS h FROM staging.read_with_1_18_0 ), b AS ( SELECT id, MD5(CAST(fixed_binary_list AS VARCHAR)) AS h FROM staging.read_with_1_17_1 ) SELECT COUNT(*) AS rows_compared, COUNT_IF(a.h <> b.h) AS rows_differing, COUNT_IF(a.h IS NULL OR b.h IS NULL) AS rows_missing_one_side FROM a FULL OUTER JOIN b ON a.id = b.id;
If rows_differing comes back as zero across a fair sample, your schema and read path do not hit the bug and you can stop. If it comes back non-zero, you have both a version problem and a data problem, and the data problem is the one with a deadline.
What to do while there is no patched release
There are three options, and they are not equally good. We recommend the first for almost everyone.
- Pin back to 1.17.1. It is the last release without the defect, it was published on 12 May 2026, and reverting a minor version is a change most teams can make and reason about in an afternoon.
- Build from the fixed branch. Viable if you already build and host internal artifacts, and if you are comfortable running a library that has no released version number behind it. Most teams should not take this on for a single fix.
- Wait for 1.18.1. Reasonable only if you have confirmed your schemas cannot trigger the bug. Waiting without that confirmation means accepting silent corruption for an unknown period.
<!-- Maven: force the resolved version across the whole tree -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.parquet</groupId>
<artifactId>parquet-hadoop</artifactId>
<version>1.17.1</version>
</dependency>
</dependencies>
</dependencyManagement>
// Gradle: a resolution strategy beats a plain dependency declaration,
// because it also catches versions pulled in transitively.
configurations.all {
resolutionStrategy.force 'org.apache.parquet:parquet-hadoop:1.17.1'
}
// sbt
dependencyOverrides += "org.apache.parquet" % "parquet-hadoop" % "1.17.1"
Pinning in the build file only helps for jobs you build. For a managed runtime that bundles its own copy, the lever is the runtime version rather than your dependency file, and the check above on a live cluster is the only way to confirm which one won.
The reader library needs a pinned version too
Most teams have a documented format version for the data they write, and no documented version for the libraries that read it. Readers get upgraded silently as a side effect of a runtime bump, and nobody records which version produced which output.
Two habits make the next incident of this kind cheaper to handle. Record the reader library version alongside the load metadata you already write, so that a bad read can be traced to a specific library rather than a date range. And keep one non-Java reader available for the same files, because being able to read a table two ways turns a suspicion into an answer in an hour.
- Add the resolved Parquet version to your job's run metadata table, next to the load timestamp and source file list.
- Include a library-version check in the same pre-flight step that checks connectivity and credentials.
- Keep an independent reader available for spot checks. Reading through Arrow or DuckDB gives you a second opinion that does not share the Java code path.
A short action list
- Run the classpath check on every engine that reads Parquet, on a live cluster rather than in the build file.
- List tables with repeated fixed-width binary columns. That is the exposure set.
- For anything on 1.18.0 with those column types, run the two-version comparison before doing anything else.
- Pin to 1.17.1 where you control the dependency, and move the runtime where you do not.
- Record the resolved Parquet version in your run metadata from now on.
If your estate does not contain a repeated fixed-width binary column anywhere, the check costs an hour and the answer is that you are not exposed. That is still an hour well spent, because the alternative is finding out from a reconciliation six months from now.
Related Articles
Frequently Asked Questions
Q: Which parquet-java versions are affected by the 1.18.0 corruption bug?
1.18.0, released on 11 August 2026, and any build taken from that branch before the fix was merged on 25 August 2026. Version 1.17.1, published on 12 May 2026, and earlier releases do not contain the defect.
Q: Has parquet-java 1.18.1 been released?
Not as of 26 August 2026. The fix for the corruption was merged upstream on 25 August 2026, but the Apache Parquet releases page still lists 1.18.0 as the latest version. Anyone running 1.18.0 has to pin back or build from the branch rather than wait for a patched release.
Q: Does the bug throw an error, or does it corrupt data silently?
It corrupts silently. The reader returns wrong bytes and the job completes normally, so row counts reconcile and no alert fires. That is why a version audit is worth doing even when nothing appears to be wrong.
Q: What column types are actually at risk?
Repeated FIXED_LEN_BYTE_ARRAY columns, meaning arrays or lists whose elements are fixed-width binary. Fixed-length decimals, UUIDs and fixed-size hashes are stored that way. A scalar decimal column on its own does not trigger it, and neither does a list of variable-length strings.
Q: Are DuckDB, Polars and pyarrow affected?
No. This is a defect in the Java implementation of Parquet. Engines with their own reader implementations are reading the same files with different code and are not exposed to this particular bug.
Q: How do I find which Parquet version my Spark cluster is really using?
Ask the running JVM rather than the build file, because bundled and shaded copies frequently win over declared dependencies. Load org.apache.parquet.io.api.Binary and print its code source location and implementation version from a live session, then confirm against the jars present on the worker nodes.
