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.



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


Ask the JVM where the class came from (Spark, spark-shell or a Scala job)
// 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"))

PySpark equivalent, run against the live session
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.


Find every parquet jar on a cluster node and read its declared version
# 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 runtimeWhere the Parquet library comes fromWhat to check
Apache Spark (open source)Bundled in the distribution under jars/ls $SPARK_HOME/jars/parquet-* on every node, driver included
Managed Spark runtimesPinned by the platform, sometimes with vendor patches appliedThe runtime release notes, plus the classpath check above on a live cluster
Apache Iceberg (Java)Transitive dependency, often overridden by the query engineResolve the dependency tree, then confirm against the running JVM
Apache FlinkBundled in the format connector jarThe connector jar on the task managers, not the job jar
Trino and PrestoOwn reader implementation for most pathsConfirm which reader path your table format and column types use
DuckDB, Polars, pyarrowNot affected by this bug: these do not use parquet-javaNo 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.



Column-level comparison of the two reads
-- 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.



Pinning the version in the common build tools
<!-- 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 and sbt
// 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.



A short action list



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.


Pranay Vatsal, Founder & CEO

Pranay Vatsal is the Founder & CEO of CelestInfo with deep expertise in Snowflake, data architecture, and building production-grade data systems for global enterprises.

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.