This is an automated email from the ASF dual-hosted git repository.

Cole-Greer pushed a commit to branch 3.7-dev
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git


The following commit(s) were added to refs/heads/3.7-dev by this push:
     new bece4a34c7 TINKERPOP-3278 Disable Java serialization on Gryo IO reads 
(#3578)
bece4a34c7 is described below

commit bece4a34c7eef753e1ce5ac3b6c95ee585f016b4
Author: Guian Gumpac <[email protected]>
AuthorDate: Wed Jul 29 14:56:46 2026 -0700

    TINKERPOP-3278 Disable Java serialization on Gryo IO reads (#3578)
    
    Several Gryo type registrations, mostly `TraversalStrategy` implementations 
and `TraversalExplanation`,
    were bound to Kryo's `JavaSerializer`, which deserializes via 
`java.io.ObjectInputStream.readObject()`.
    Reading a Gryo document from an untrusted source could therefore 
reconstruct and run an arbitrary
    `Serializable` object graph during decoding, before the graph layer accepts 
or rejects anything.
    
    This change stops the graph-document IO paths from reaching that sink, 
while leaving full fidelity
    available for trusted, in-process use.
    
    Assisted-by: Kiro: Claude Opus 4.8
---
 CHANGELOG.asciidoc                                 |   1 +
 THREAT_MODEL.md                                    |  21 +-
 docs/src/upgrade/release-3.7.x.asciidoc            |  43 +++
 .../process/traversal/step/sideEffect/IoStep.java  |   6 +-
 .../gremlin/structure/io/gryo/GryoIo.java          |   4 +-
 .../gremlin/structure/io/gryo/GryoMapper.java      |  52 ++-
 .../gremlin/structure/io/gryo/GryoReader.java      |   5 +-
 .../gremlin/structure/io/gryo/GryoWriter.java      |   5 +-
 .../gremlin/structure/io/gryo/GryoMapperTest.java  | 352 +++++++++++++++++++++
 .../gremlin/hadoop/structure/io/HadoopPools.java   |   2 +
 .../hadoop/structure/io/gryo/GryoRecordReader.java |   4 +-
 .../hadoop/structure/io/gryo/GryoRecordWriter.java |   4 +-
 .../io/gryo/GryoRecordReaderWriterTest.java        |  88 ++++++
 13 files changed, 571 insertions(+), 16 deletions(-)

diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 37f9e7e4c5..8b6a157413 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -25,6 +25,7 @@ 
image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
 [[release-3-7-7]]
 === TinkerPop 3.7.7 (Release Date: NOT OFFICIALLY RELEASED YET)
 
+* Disabled unsafe Java deserialization on the Gryo IO paths and added 
`GryoMapper.Builder.javaSerializationAllowed(boolean)` to control it.
 * Fixed `subgraph()` to throw a descriptive error identifying the required 
`Edge` input instead of an internal `ClassCastException` when the traversal 
produces a non-edge value.
 * Fixed `where(P)` to throw a descriptive error identifying the required 
String scope key (and suggesting `is(P)` for value comparisons) instead of an 
internal `ClassCastException` when given a non-String predicate value.
 * Fixed `PeerPressure.property_name` in `gremlin-python` incorrectly mapping 
to the `pageRank` property name token.
diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md
index 0f1fb753c1..1b2f80bc4f 100644
--- a/THREAT_MODEL.md
+++ b/THREAT_MODEL.md
@@ -247,7 +247,7 @@ Per-surface trust table:
 | Gremlin Server — bytecode/traversal request | serialized traversal bytecode 
| **yes**, within privileges | auth; traversal-step allow-list; resource limits 
|
 | Gremlin Server — session id (`SessionOpProcessor`) | client-supplied session 
string | **yes** | keyed by the string with no owning-user check, so any client 
presenting the id shares the session (see §11b) |
 | Request deserialization (GraphSON / GraphBinary) | serialized bytes | 
**yes** (pre-auth) | robustness of the wire serializers |
-| Graph IO — Gryo/GraphSON/GraphML files (`io()` step, persistence, OLAP) | 
on-disk / cluster bytes | only if the caller loads untrusted files | GraphSON, 
locked Gryo (`registrationRequired=true`), and GraphML with the default XML 
factory owe deserializer integrity. Unlocked Gryo and a caller-supplied 
unhardened `XMLInputFactory` (XXE) are the caller's responsibility |
+| Graph IO — Gryo/GraphSON/GraphML files (`io()` step, persistence, OLAP) | 
on-disk / cluster bytes | only if the caller loads untrusted files | GraphSON, 
the hardened Gryo mappers the IO paths build (`registrationRequired=true` plus 
`javaSerializationAllowed=false`), and GraphML with the default XML factory owe 
deserializer integrity. Unlocked Gryo, a Gryo mapper that keeps Java 
serialization, and a caller-supplied unhardened `XMLInputFactory` (XXE) are the 
caller's responsibility |
 | Gremlin string parser (`gremlin-language` ANTLR) | Gremlin string | **yes** 
| parser robustness, no crash/hang/OOM on malformed input and no grammar 
breakout / step injection (distinct from execution cost, §8/Q7) |
 | Any string the grammar accepts as an argument (e.g. a `regex` pattern) | 
Gremlin string / bytecode | **yes** | a grammatically valid string must not 
enable DoS (e.g. ReDoS via a pathological pattern), the Q7 
super-linear-amplification carve-out (§8) |
 | GLV (client) — server response | serialized bytes from the server | yes if 
the server is malicious/compromised, or a MITM (TLS off / cert not validated) | 
response-deserialization robustness; TLS with cert validation |
@@ -337,14 +337,18 @@ Per-surface trust table:
   Groovy string by concatenation, which is the calling application's concern 
(§9). *Violation symptom:*
   grammar breakout / step injection from a value that should stay a literal. 
*Severity:* critical.
  
-- **Deserializer integrity.** The wire deserializers (GraphSON, GraphBinary) 
and **Gryo in its locked default
-  (`registrationRequired=true`)** reading attacker bytes do not lead to 
arbitrary object instantiation / code
-  execution beyond the registered type set. Because `inject()` and value 
arguments let a request carry any
+- **Deserializer integrity.** The wire deserializers (GraphSON, GraphBinary) 
and **the hardened Gryo mappers the
+  IO paths build** (`registrationRequired=true` plus 
`javaSerializationAllowed=false`, i.e. `io()`, `GryoReader`,
+  `GryoWriter`, `GryoIo`, and the Hadoop Gryo input/output formats) reading 
attacker bytes do not reach native Java
+  deserialization
+  (`ObjectInputStream.readObject()`). Because `inject()` and value arguments 
let a request carry any
   supported type, a bug in a **registered** type's (de)serializer that 
crashes/OOMs the reader is also
   in-model, on **both** the server (request) and the GLV (response) side. The 
GraphML reader disables
   external entities and DTDs by default (XXE-safe). *Violation symptom:* 
deserialization gadget / RCE / XXE,
   or a registered-type serializer crashing/OOMing either end. *Severity:* 
critical. Gryo is not on the wire,
-  and unlocked Gryo or a caller-supplied unhardened XML factory is 
out-of-model (user responsibility, §9).
+  and unlocked Gryo (as run by `spark-gremlin` and the Hadoop object pools), a 
directly built `GryoMapper` or
+  `GryoPool` that keeps Java serialization, or a caller-supplied unhardened 
XML factory is out-of-model
+  (user responsibility, §9).
  
 - **Resource bounds — split.** Malformed/pre-auth input that 
crashes/OOMs/hangs the server is **in-model**
   (above). Ordinary expensive traversals / large results are **operator 
capacity**, NOT in-model, unless a
@@ -394,7 +398,12 @@ Per-surface trust table:
   allow-list (`registrationRequired=true`), so it is not an 
arbitrary-instantiation sink, and a break within
   that locked config is a `VALID` bug like any deserializer. **Running Gryo 
unlocked
   (`registrationRequired=false`) is not a safe boundary against untrusted 
bytes and is the user's
-  responsibility.** (A few registered types use Java native serialization, a 
gadget caveat even when locked.)
+  responsibility.** A few registered types are also serialized with Kryo's 
`JavaSerializer`, which reads by way of
+  `ObjectInputStream.readObject()` and is a gadget caveat even when locked. 
The mappers the IO paths build
+  (`io()`, `GryoReader`, `GryoWriter`, `GryoIo`, and the Hadoop Gryo 
input/output formats) drop those registrations
+  (`GryoMapper.Builder.javaSerializationAllowed(boolean)` selects the 
behavior), so a break there is `VALID`. A
+  directly built `GryoMapper` and `GryoPool` keep them, as do the 
`spark-gremlin` and Hadoop object pools that
+  additionally run unlocked; those remain the user's responsibility.
   Gryo is not on the wire, so this is an IO/file-surface concern (`io()` step, 
persistence, OLAP).
  
 - **A `TraversalStrategy` is not an access-control boundary on its own.** A 
remote request can remove or
diff --git a/docs/src/upgrade/release-3.7.x.asciidoc 
b/docs/src/upgrade/release-3.7.x.asciidoc
index 2c8f82cade..703eccf077 100644
--- a/docs/src/upgrade/release-3.7.x.asciidoc
+++ b/docs/src/upgrade/release-3.7.x.asciidoc
@@ -33,6 +33,49 @@ complete list of all the modifications that are part of this 
release.
 
 === Upgrading for Users
 
+==== Gryo IO Disables Java Serialization
+
+A small number of Gryo type registrations, mostly `TraversalStrategy` 
implementations, were registered with Kryo's
+`JavaSerializer`, which reads by way of 
`java.io.ObjectInputStream.readObject()`. Reading a Gryo document from an
+untrusted source was therefore unsafe, as a crafted document could carry an 
arbitrary Java object graph that runs
+during decoding.
+
+The paths that consume graph documents, `io()`, `GryoReader`, `GryoWriter` and 
`GryoIo` (including `gryo` graph
+persistence) and the Hadoop Gryo input/output formats, no longer install those 
registrations. A graph document only needs graph structure, so nothing is lost
+in practice. A stream that does present one of the affected type ids now fails 
with an unregistered class id:
+
+[source,text]
+----
+org.apache.tinkerpop.shaded.kryo.KryoException: Encountered unregistered class 
ID: 187
+----
+
+The affected types are `PartitionStrategy`, `SubgraphStrategy`, 
`SeedStrategy`, `VertexProgramStrategy`,
+`ProductiveByStrategy`, `OptionsStrategy` and `TraversalExplanation` in both 
Gryo 1.0 and 3.0, plus
+`GroupStep.GroupBiOperator` and `OrderGlobalStep.OrderBiOperator` in Gryo 1.0.
+
+A mapper built directly is unchanged and keeps full fidelity for trusted, 
in-process work such as OLAP. The new
+`GryoMapper.Builder.javaSerializationAllowed(boolean)` selects the behavior:
+
+[source,java]
+----
+// full fidelity, unchanged, appropriate for trusted bytes only
+GryoMapper.build().create();
+
+// hardened, what io() and GryoReader/GryoWriter now use by default
+GryoMapper.build().javaSerializationAllowed(false).create();
+----
+
+Callers that supply their own mapper to `GryoReader` or `GryoWriter` are 
unaffected and should add
+`javaSerializationAllowed(false)` if they read bytes they do not control. 
`GryoIo` accepts an `onMapper` consumer, so
+full fidelity can be restored where the bytes are trusted:
+
+[source,java]
+----
+GryoIo.build().graph(graph).onMapper(m -> ((GryoMapper.Builder) 
m).javaSerializationAllowed(true)).create();
+----
+
+See: link:https://issues.apache.org/jira/browse/TINKERPOP-3278[TINKERPOP-3278]
+
 ==== conjoin() Step Null Handling
 
 The `conjoin()` step previously returned `null` when elements in the incoming 
list are `null`. This behavior has
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/IoStep.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/IoStep.java
index e469e65c68..d42d477f88 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/IoStep.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/IoStep.java
@@ -151,7 +151,8 @@ public class IoStep<S> extends AbstractStep<S,S> implements 
ReadWriting {
                 detectRegistries().forEach(builder::addRegistry);
                 return 
GraphSONReader.build().mapper(builder.create()).create();
             } else if (objectOrClass.equals(IO.gryo)){
-                final GryoMapper.Builder builder = GryoMapper.build();
+                // io() reads bytes the caller may not control, so refuse 
native Java serialization; a graph document only needs graph structure
+                final GryoMapper.Builder builder = 
GryoMapper.build().javaSerializationAllowed(false);
                 detectRegistries().forEach(builder::addRegistry);
                 return GryoReader.build().mapper(builder.create()).create();
             } else if (objectOrClass.equals(IO.graphml))
@@ -185,7 +186,8 @@ public class IoStep<S> extends AbstractStep<S,S> implements 
ReadWriting {
                 detectRegistries().forEach(builder::addRegistry);
                 return 
GraphSONWriter.build().mapper(builder.create()).create();
             } else if (objectOrClass.equals(IO.gryo)){
-                final GryoMapper.Builder builder = GryoMapper.build();
+                // kept symmetrical with the reader above so that io() cannot 
write a document it will not read
+                final GryoMapper.Builder builder = 
GryoMapper.build().javaSerializationAllowed(false);
                 detectRegistries().forEach(builder::addRegistry);
                 return GryoWriter.build().mapper(builder.create()).create();
             } else if (objectOrClass.equals(IO.graphml))
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoIo.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoIo.java
index 4c9fc71934..e608f6518d 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoIo.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoIo.java
@@ -70,7 +70,9 @@ public final class GryoIo implements Io<GryoReader.Builder, 
GryoWriter.Builder,
      */
     @Override
     public GryoMapper.Builder mapper() {
-        final GryoMapper.Builder builder = GryoMapper.build().version(version);
+        // as with GryoReader/GryoWriter, graph documents may be untrusted, so 
native Java serialization is
+        // disabled. An onMapper consumer can restore it for trusted, 
in-process work.
+        final GryoMapper.Builder builder = 
GryoMapper.build().version(version).javaSerializationAllowed(false);
         onMapper.ifPresent(c -> c.accept(builder));
         return builder;
     }
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoMapper.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoMapper.java
index aeaa417a04..502cc52b7d 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoMapper.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoMapper.java
@@ -29,6 +29,7 @@ import org.apache.tinkerpop.shaded.kryo.Kryo;
 import org.apache.tinkerpop.shaded.kryo.Serializer;
 import org.apache.tinkerpop.shaded.kryo.util.DefaultStreamFactory;
 import org.apache.tinkerpop.shaded.kryo.util.MapReferenceResolver;
+import org.apache.tinkerpop.shaded.kryo.serializers.JavaSerializer;
 import org.javatuples.Pair;
 
 import java.util.ArrayList;
@@ -78,10 +79,12 @@ public final class GryoMapper implements Mapper<Kryo> {
     private final List<TypeRegistration<?>> typeRegistrations;
     private final boolean registrationRequired;
     private final boolean referenceTracking;
+    private final boolean javaSerializationAllowed;
     private final Supplier<ClassResolver> classResolver;
     private final GryoVersion version;
 
     private GryoMapper(final Builder builder) {
+        this.javaSerializationAllowed = builder.javaSerializationAllowed;
         this.typeRegistrations = builder.typeRegistrations;
         this.version = builder.version;
         validate();
@@ -97,11 +100,30 @@ public final class GryoMapper implements Mapper<Kryo> {
         kryo.addDefaultSerializer(Map.Entry.class, new 
UtilSerializers.EntrySerializer());
         kryo.setRegistrationRequired(registrationRequired);
         kryo.setReferences(referenceTracking);
-        for (TypeRegistration tr : typeRegistrations)
+        for (TypeRegistration tr : typeRegistrations) {
+            // drop any registration that resolves to a JavaSerializer; the 
Function- and default-serializer
+            // forms can only be inspected once a Kryo exists, which is why 
this is done here rather than up front
+            if (!javaSerializationAllowed && resolvesToJavaSerializer(tr, 
kryo)) continue;
             tr.registerWith(kryo);
+        }
         return kryo;
     }
 
+    /**
+     * Detects a registration whose serializer is, or resolves to, Kryo's 
{@code JavaSerializer}. A directly supplied
+     * serializer is known statically, while a {@code Function} or a class 
default serializer can only be resolved
+     * from a live {@link Kryo} instance, which is why this runs at {@link 
#createMapper()} time.
+     */
+    private static boolean resolvesToJavaSerializer(final TypeRegistration<?> 
tr, final Kryo kryo) {
+        if (null != tr.getShadedSerializer())
+            return tr.getShadedSerializer() instanceof JavaSerializer;
+        if (null != tr.getFunctionOfShadedKryo())
+            return tr.getFunctionOfShadedKryo().apply(kryo) instanceof 
JavaSerializer;
+        if (!tr.hasSerializer())
+            return kryo.getDefaultSerializer(tr.getTargetClass()) instanceof 
JavaSerializer;
+        return false;
+    }
+
     public GryoVersion getVersion() {
         return version;
     }
@@ -110,6 +132,11 @@ public final class GryoMapper implements Mapper<Kryo> {
         return 
this.typeRegistrations.stream().map(TypeRegistration::getTargetClass).collect(Collectors.toList());
     }
 
+    /**
+     * Note that when {@link Builder#javaSerializationAllowed(boolean)} is 
{@code false}, registrations whose
+     * serializer is (or resolves to) a {@code JavaSerializer} are still 
listed here even though
+     * {@link #createMapper()} skips installing them.
+     */
     public List<TypeRegistration<?>> getTypeRegistrations() {
         return typeRegistrations;
     }
@@ -155,6 +182,7 @@ public final class GryoMapper implements Mapper<Kryo> {
 
         private boolean registrationRequired = true;
         private boolean referenceTracking = true;
+        private boolean javaSerializationAllowed = true;
         private Supplier<ClassResolver> classResolver;
 
         private Builder() {
@@ -248,6 +276,28 @@ public final class GryoMapper implements Mapper<Kryo> {
             return this;
         }
 
+        /**
+         * When set to {@code false}, every registration whose serializer is 
Kryo's {@code JavaSerializer} is dropped,
+         * producing a mapper suited to reading bytes from an untrusted 
source. By default this value is {@code true}.
+         * <p/>
+         * That serializer reads by way of {@code 
java.io.ObjectInputStream.readObject()}, which reconstructs and runs
+         * an arbitrary {@code Serializable} object graph while decoding, 
before the graph layer can accept or reject
+         * anything. The affected types are mostly {@code TraversalStrategy} 
implementations that a graph document does
+         * not need; a stream that carries one now fails with an unregistered 
class id. Registrations contributed
+         * through an {@link IoRegistry} or {@code addCustom(...)} are covered 
on the same terms, including those
+         * whose serializer is a {@code Function} or a class default that 
resolves to a {@code JavaSerializer}. This
+         * relies on the default {@link #registrationRequired(boolean)} of 
{@code true}. Callers that need the full
+         * fidelity for trusted, in-process work should leave this value at 
{@code true}.
+         *
+         * @param javaSerializationAllowed set to {@code false} to drop the 
{@code JavaSerializer} registrations or
+         *                                 {@code true} to keep them
+         * @see #registrationRequired(boolean)
+         */
+        public Builder javaSerializationAllowed(final boolean 
javaSerializationAllowed) {
+            this.javaSerializationAllowed = javaSerializationAllowed;
+            return this;
+        }
+
         /**
          * By default, each appearance of an object in the graph after the 
first is stored as an integer ordinal.
          * This allows multiple references to the same object and cyclic 
graphs to be serialized. This has a small
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoReader.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoReader.java
index 68f130a26e..09739f53a9 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoReader.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoReader.java
@@ -274,9 +274,10 @@ public final class GryoReader implements GraphReader {
 
         private long batchSize = 10000;
         /**
-         * Always use the most recent gryo version by default
+         * Always use the most recent gryo version by default. Graph documents 
read here may come from an untrusted
+         * source, so native Java serialization is disabled. Supply a mapper 
explicitly to opt back in.
          */
-        private Mapper<Kryo> gryoMapper = GryoMapper.build().create();
+        private Mapper<Kryo> gryoMapper = 
GryoMapper.build().javaSerializationAllowed(false).create();
 
         private Builder() {
         }
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoWriter.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoWriter.java
index 8172ba90b3..3451e0b270 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoWriter.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoWriter.java
@@ -161,9 +161,10 @@ public final class GryoWriter implements GraphWriter {
 
     public final static class Builder implements WriterBuilder<GryoWriter> {
         /**
-         * Always creates the most current version available.
+         * Always creates the most current version available. Native Java 
serialization is disabled here as it is on
+         * the {@link GryoReader} default, so that a document written here can 
always be read back.
          */
-        private Mapper<Kryo> gryoMapper = GryoMapper.build().create();
+        private Mapper<Kryo> gryoMapper = 
GryoMapper.build().javaSerializationAllowed(false).create();
 
         private Builder() {
         }
diff --git 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoMapperTest.java
 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoMapperTest.java
index 8951281053..5de7fa31ec 100644
--- 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoMapperTest.java
+++ 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoMapperTest.java
@@ -22,28 +22,36 @@ import 
org.apache.tinkerpop.gremlin.process.remote.traversal.DefaultRemoteTraver
 import org.apache.tinkerpop.gremlin.process.traversal.Bytecode;
 import org.apache.tinkerpop.gremlin.process.traversal.Merge;
 import org.apache.tinkerpop.gremlin.process.traversal.TextP;
+import 
org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.OptionsStrategy;
 import 
org.apache.tinkerpop.gremlin.process.traversal.util.TraversalExplanation;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.apache.tinkerpop.gremlin.structure.io.Io;
 import org.apache.tinkerpop.gremlin.structure.io.IoX;
 import org.apache.tinkerpop.gremlin.structure.io.IoXIoRegistry;
 import org.apache.tinkerpop.gremlin.structure.io.IoY;
 import org.apache.tinkerpop.gremlin.structure.io.IoYIoRegistry;
 import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONTokens;
 import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertex;
+import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph;
 import org.apache.tinkerpop.shaded.kryo.ClassResolver;
 import org.apache.tinkerpop.shaded.kryo.Kryo;
 import org.apache.tinkerpop.shaded.kryo.Registration;
 import org.apache.tinkerpop.shaded.kryo.Serializer;
 import org.apache.tinkerpop.shaded.kryo.io.Input;
 import org.apache.tinkerpop.shaded.kryo.io.Output;
+import org.apache.tinkerpop.shaded.kryo.serializers.JavaSerializer;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
 import java.io.OutputStream;
+import java.io.Serializable;
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
 import java.sql.Timestamp;
@@ -65,13 +73,17 @@ import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Function;
 import java.util.function.Supplier;
 
 import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.__;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThan;
 import static org.hamcrest.core.Is.is;
 import static org.hamcrest.core.IsInstanceOf.instanceOf;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.fail;
@@ -95,6 +107,23 @@ public class GryoMapperTest {
     @Parameterized.Parameter(value = 1)
     public Supplier<GryoMapper.Builder> builder;
 
+    /**
+     * The Gryo type id of {@code OptionsStrategy}, registered with the shaded 
{@code JavaSerializer} in both
+     * {@link GryoVersion#V1_0} and {@link GryoVersion#V3_0}.
+     */
+    private static final int OPTIONS_STRATEGY_GRYO_ID = 187;
+
+    /**
+     * Kryo shifts written class ids to leave room for its {@code NULL} and 
{@code NAME} markers, as
+     * {@link AbstractGryoClassResolver#readClass(Input)} shows.
+     */
+    private static final int CLASS_ID_OFFSET = 2;
+
+    /**
+     * Kryo's reference marker for an object being seen for the first time.
+     */
+    private static final int KRYO_NOT_NULL = 1;
+
     @Test
     public void shouldMakeNewInstance() {
         final GryoMapper.Builder b = GryoMapper.build();
@@ -349,6 +378,306 @@ public class GryoMapperTest {
         assertEquals(te.toString(), serializeDeserialize(te, 
TraversalExplanation.class).toString());
     }
 
+    /**
+     * Gryo registers a handful of types with the shaded Kryo {@code 
JavaSerializer}, which deserializes by way of
+     * {@code java.io.ObjectInputStream.readObject()}. A stream that presents 
one of those type ids therefore
+     * reconstructs whatever {@code Serializable} object graph follows and 
runs its {@code readObject()} methods,
+     * which is an unsafe-deserialization sink on caller-supplied bytes. The 
canary used here carries no
+     * payload at all: the mere execution of its {@code readObject()} is the 
proof.
+     */
+    @Test
+    public void shouldNotInvokeJavaDeserializationOnGryoRead() throws 
Exception {
+        final Kryo kryo = 
builder.get().javaSerializationAllowed(false).create().createMapper();
+
+        // Kryo frames an object as varint(class id + 2) followed by a 
NOT_NULL reference marker. Confirm that
+        // framing against a known registration (HashMap is id 11 in both V1_0 
and V3_0) rather than trusting a
+        // hard coded Kryo internal, so the crafted stream below cannot 
silently stop reaching the serializer.
+        final int hashMapId = kryo.getRegistration(HashMap.class).getId();
+        assertEquals(11, hashMapId);
+        final Output probe = new Output(64, -1);
+        kryo.writeClassAndObject(probe, new HashMap<String, Object>());
+        probe.flush();
+        final Input probeInput = new Input(probe.toBytes());
+        assertEquals(hashMapId + CLASS_ID_OFFSET, probeInput.readVarInt(true));
+        assertEquals(KRYO_NOT_NULL, probeInput.readVarInt(true));
+
+        final byte[] malicious = maliciousGryoBytes();
+
+        DeserializationCanary.FIRED = false;
+        try {
+            kryo.readClassAndObject(new Input(new 
ByteArrayInputStream(malicious)));
+        } catch (Exception ignored) {
+            // refusing the stream outright is the expected outcome. what 
matters is that nothing was deserialized on
+            // the way to that decision.
+        }
+
+        assertFalse("Reading Gryo " + name + " bytes must not invoke 
ObjectInputStream.readObject() on the stream, " +
+                        "since a crafted stream presenting a 
JavaSerializer-backed type id such as " +
+                        OPTIONS_STRATEGY_GRYO_ID + " (OptionsStrategy) could 
otherwise carry an arbitrary Java " +
+                        "object graph",
+                DeserializationCanary.FIRED);
+    }
+
+    /**
+     * Positive control for {@link 
#shouldNotInvokeJavaDeserializationOnGryoRead()}. Without hardening, the same 
bytes
+     * do reach {@code ObjectInputStream.readObject()}, which is what gives 
the assertion there any meaning: were the
+     * crafted framing ever to stop selecting the {@code JavaSerializer}, this 
test would fail and say so. It also
+     * documents that a directly built mapper stays full fidelity and must 
only be pointed at trusted bytes.
+     */
+    @Test
+    public void shouldInvokeJavaDeserializationOnDefaultMapperRead() throws 
Exception {
+        final Kryo kryo = builder.get().create().createMapper();
+        final byte[] malicious = maliciousGryoBytes();
+
+        DeserializationCanary.FIRED = false;
+        try {
+            kryo.readClassAndObject(new Input(new 
ByteArrayInputStream(malicious)));
+        } catch (Exception ignored) {
+            // the payload deserializes to the canary rather than to an 
OptionsStrategy, so a failure is possible
+            // here, but it would come after readObject() has already run
+        }
+
+        assertTrue("the crafted stream must reach 
ObjectInputStream.readObject() on a full fidelity mapper, " +
+                        "otherwise the hardened assertions prove nothing",
+                DeserializationCanary.FIRED);
+    }
+
+    /**
+     * The same crafted stream fed through the reader that {@code io()} and 
graph persistence use, whose default
+     * mapper is hardened.
+     */
+    @Test
+    public void shouldNotInvokeJavaDeserializationOnGryoReaderRead() throws 
Exception {
+        final GryoReader reader = GryoReader.build().create();
+
+        DeserializationCanary.FIRED = false;
+        try (final InputStream stream = new 
ByteArrayInputStream(maliciousGryoBytes())) {
+            reader.readObject(stream, Object.class);
+        } catch (Exception ignored) {
+            // as above, refusing the stream is the expected outcome
+        }
+
+        assertFalse("GryoReader must not invoke ObjectInputStream.readObject() 
on the bytes it reads",
+                DeserializationCanary.FIRED);
+    }
+
+    /**
+     * Hardening the mapper must not cost anything on the graph structure that 
a Gryo document actually carries.
+     */
+    @Test
+    public void shouldRoundTripGraphStructureWithJavaSerializationDisabled() 
throws Exception {
+        final Kryo kryo = 
builder.get().javaSerializationAllowed(false).create().createMapper();
+
+        final Map<String, Object> props = new HashMap<>();
+        final List<Map<String, Object>> propertyNames = new ArrayList<>(1);
+        final Map<String, Object> propertyName = new HashMap<>();
+        propertyName.put(GraphSONTokens.ID, "x");
+        propertyName.put(GraphSONTokens.KEY, "x");
+        propertyName.put(GraphSONTokens.VALUE, "no-way-this-will-ever-work");
+        propertyNames.add(propertyName);
+        props.put("x", propertyNames);
+
+        final Output out = new Output(1024, -1);
+        kryo.writeClassAndObject(out, new DetachedVertex(100, 
Vertex.DEFAULT_LABEL, props));
+        out.flush();
+
+        final DetachedVertex readX = (DetachedVertex) kryo.readClassAndObject(
+                new Input(new ByteArrayInputStream(out.toBytes())));
+        assertEquals("no-way-this-will-ever-work", readX.value("x"));
+    }
+
+    /**
+     * A Gryo stream that presents {@code OptionsStrategy}'s type id and then 
a raw Java-serialized payload. Crafting
+     * it needs no cooperation from the Gryo writer, which is why the sink was 
reachable from untrusted bytes.
+     */
+    private byte[] maliciousGryoBytes() throws Exception {
+        final ByteArrayOutputStream javaPayload = new ByteArrayOutputStream();
+        try (final ObjectOutputStream oos = new 
ObjectOutputStream(javaPayload)) {
+            oos.writeObject(new DeserializationCanary());
+        }
+
+        final Output malicious = new Output(javaPayload.size() + 64, -1);
+        malicious.writeVarInt(OPTIONS_STRATEGY_GRYO_ID + CLASS_ID_OFFSET, 
true);
+        malicious.writeVarInt(KRYO_NOT_NULL, true);
+        malicious.writeBytes(javaPayload.toByteArray());
+        malicious.flush();
+        return malicious.toBytes();
+    }
+
+    /**
+     * Companion to {@link #shouldNotInvokeJavaDeserializationOnGryoRead()} 
that covers the whole sink surface rather
+     * than one carrier type. The assertion is made against the {@code Kryo} 
instance that actually decodes bytes, so
+     * that it cannot pass merely by asking the same question of the same 
metadata the filter itself used.
+     */
+    @Test
+    public void shouldNotRegisterTypesWithJavaSerializerWhenDisabled() {
+        final Kryo hardened = 
builder.get().javaSerializationAllowed(false).create().createMapper();
+
+        for (final TypeRegistration<?> tr : javaSerializedRegistrations()) {
+            final Class<?> clazz = tr.getTargetClass();
+            try {
+                hardened.getRegistration(clazz);
+                fail(clazz.getSimpleName() + " must not be registered on a 
hardened mapper");
+            } catch (IllegalArgumentException expected) {
+                // Kryo refuses an unregistered class while registration is 
required
+            }
+        }
+    }
+
+    /**
+     * A custom type contributed with Kryo's {@code JavaSerializer} is 
filtered on the same terms as the defaults,
+     * since an {@link org.apache.tinkerpop.gremlin.structure.io.IoRegistry} 
is an untrusted-input path too.
+     */
+    @Test
+    public void shouldNotRegisterCustomTypesWithJavaSerializerWhenDisabled() {
+        final Kryo hardened = builder.get().addCustom(IoX.class, new 
JavaSerializer()).
+                javaSerializationAllowed(false).create().createMapper();
+
+        try {
+            hardened.getRegistration(IoX.class);
+            fail("a custom JavaSerializer registration must not survive on a 
hardened mapper");
+        } catch (IllegalArgumentException expected) {
+            // as above
+        }
+    }
+
+    /**
+     * A custom type whose serializer is supplied as a {@code Function} 
resolving to a {@code JavaSerializer} can only
+     * be recognized once a {@code Kryo} exists, so it is dropped at 
mapper-creation time rather than at build time.
+     */
+    @Test
+    public void 
shouldNotRegisterCustomFunctionTypesWithJavaSerializerWhenDisabled() {
+        final Kryo hardened = builder.get().addCustom(IoX.class, 
(Function<Kryo, Serializer>) k -> new JavaSerializer()).
+                javaSerializationAllowed(false).create().createMapper();
+
+        try {
+            hardened.getRegistration(IoX.class);
+            fail("a custom Function supplied JavaSerializer must not survive 
on a hardened mapper");
+        } catch (IllegalArgumentException expected) {
+            // Kryo refuses an unregistered class while registration is 
required
+        }
+    }
+
+    /**
+     * A type carrying {@code @DefaultSerializer(JavaSerializer.class)} and 
registered without an explicit serializer
+     * resolves to a {@code JavaSerializer} through Kryo's default, which is 
likewise dropped at mapper-creation time.
+     */
+    @Test
+    public void 
shouldNotRegisterDefaultSerializerJavaSerializerTypesWhenDisabled() {
+        final Kryo hardened = 
builder.get().addCustom(JavaSerializedByDefault.class).
+                javaSerializationAllowed(false).create().createMapper();
+
+        try {
+            hardened.getRegistration(JavaSerializedByDefault.class);
+            fail("a @DefaultSerializer(JavaSerializer) registration must not 
survive on a hardened mapper");
+        } catch (IllegalArgumentException expected) {
+            // as above
+        }
+    }
+
+    /**
+     * The full fidelity mapper is unchanged and remains available for 
trusted, in-process round-trips. This test
+     * documents which registrations that leaves on native Java serialization.
+     */
+    @Test
+    public void shouldRegisterTypesWithJavaSerializerByDefault() {
+        final List<String> found = new ArrayList<>();
+        for (final TypeRegistration<?> tr : javaSerializedRegistrations())
+            found.add(String.format("%s(%d)", 
tr.getTargetClass().getSimpleName(), tr.getId()));
+
+        final List<String> expected = name.equals("1_0") ?
+                Arrays.asList("TraversalExplanation(106)", 
"GroupBiOperator(117)", "OrderBiOperator(118)",
+                        "PartitionStrategy(140)", "SubgraphStrategy(141)", 
"SeedStrategy(192)",
+                        "VertexProgramStrategy(142)", 
"ProductiveByStrategy(195)", "OptionsStrategy(187)") :
+                Arrays.asList("PartitionStrategy(140)", 
"SubgraphStrategy(141)", "SeedStrategy(192)",
+                        "VertexProgramStrategy(142)", 
"ProductiveByStrategy(195)", "OptionsStrategy(187)",
+                        "TraversalExplanation(106)");
+        assertEquals(expected, found);
+    }
+
+    /**
+     * The inverse of {@link 
#shouldNotRegisterTypesWithJavaSerializerWhenDisabled()}. Setting the value 
explicitly
+     * keeps the affected types usable, which is what a trusted, in-process 
round-trip relies on.
+     */
+    @Test
+    public void shouldRoundTripStrategyWhenJavaSerializationAllowed() throws 
Exception {
+        final Kryo kryo = 
builder.get().javaSerializationAllowed(true).create().createMapper();
+
+        final Output out = new Output(1024, -1);
+        kryo.writeClassAndObject(out, OptionsStrategy.build().with("some-key", 
"some-value").create());
+        out.flush();
+
+        final OptionsStrategy read = (OptionsStrategy) kryo.readClassAndObject(
+                new Input(new ByteArrayInputStream(out.toBytes())));
+        assertEquals("some-value", read.getOptions().get("some-key"));
+    }
+
+    /**
+     * {@link GryoIo} hardens its mapper, and the {@code onMapper} consumer is 
the documented way to restore full
+     * fidelity where the bytes are trusted. This pins the form shown in the 
upgrade documentation, including the cast.
+     */
+    @Test
+    public void shouldRestoreJavaSerializationThroughGryoIoOnMapper() {
+        final Io.Builder<GryoIo> io = GryoIo.build(gryoVersion());
+        io.graph(EmptyGraph.instance());
+        io.onMapper(m -> ((GryoMapper.Builder) 
m).javaSerializationAllowed(true));
+
+        final Kryo restored = io.create().mapper().create().createMapper();
+        assertEquals(OPTIONS_STRATEGY_GRYO_ID, 
restored.getRegistration(OptionsStrategy.class).getId());
+    }
+
+    /**
+     * Without such a consumer, {@link GryoIo} is hardened like the reader and 
writer defaults.
+     */
+    @Test
+    public void shouldNotRegisterTypesWithJavaSerializerOnGryoIoDefault() {
+        final Io.Builder<GryoIo> io = GryoIo.build(gryoVersion());
+        io.graph(EmptyGraph.instance());
+
+        final Kryo hardened = io.create().mapper().create().createMapper();
+        try {
+            hardened.getRegistration(OptionsStrategy.class);
+            fail("GryoIo must not register OptionsStrategy by default");
+        } catch (IllegalArgumentException expected) {
+            // Kryo refuses an unregistered class while registration is 
required
+        }
+    }
+
+    /**
+     * The writer default is hardened too, so a document carrying one of the 
dropped types cannot be produced by the
+     * paths that could not read it back.
+     */
+    @Test
+    public void shouldNotWriteTypesWithJavaSerializerOnGryoWriterDefault() 
throws Exception {
+        final GryoWriter writer = GryoWriter.build().create();
+
+        try (final OutputStream stream = new ByteArrayOutputStream()) {
+            writer.writeObject(stream, 
OptionsStrategy.build().with("some-key", "some-value").create());
+            fail("the GryoWriter default must not write a JavaSerializer 
backed type");
+        } catch (IllegalArgumentException expected) {
+            // as above, Kryo refuses the unregistered class
+        }
+    }
+
+    private GryoVersion gryoVersion() {
+        return name.equals("1_0") ? GryoVersion.V1_0 : GryoVersion.V3_0;
+    }
+
+    /**
+     * The registrations that the full fidelity mapper of the version under 
test backs with Kryo's
+     * {@code JavaSerializer}, each of which is a carrier for the sink.
+     */
+    private List<TypeRegistration<?>> javaSerializedRegistrations() {
+        final List<TypeRegistration<?>> found = new ArrayList<>();
+        for (final TypeRegistration<?> tr : 
builder.get().create().getTypeRegistrations()) {
+            if (tr.getShadedSerializer() instanceof JavaSerializer) 
found.add(tr);
+        }
+
+        // if detection ever breaks, the tests that loop over this would pass 
without checking anything
+        assertThat(found.size(), greaterThan(0));
+        return found;
+    }
+
     @Test
     public void shouldHandleBytecode() throws Exception {
         final Bytecode bytecode = 
__().out().outV().outE().asAdmin().getBytecode();
@@ -456,4 +785,27 @@ public class GryoMapperTest {
         }
     }
 
+    /**
+     * A type that resolves to Kryo's {@code JavaSerializer} through the 
class-level {@code @DefaultSerializer}
+     * annotation rather than an explicit registration, exercising the 
default-serializer branch of the filter.
+     */
+    @org.apache.tinkerpop.shaded.kryo.DefaultSerializer(JavaSerializer.class)
+    public static class JavaSerializedByDefault implements Serializable {
+        private static final long serialVersionUID = 1L;
+    }
+
+    /**
+     * A deliberately inert {@code Serializable} used to detect whether native 
Java deserialization ran during a Gryo
+     * read. It touches nothing outside this class: no process execution, no 
filesystem, no reflection.
+     */
+    private static class DeserializationCanary implements Serializable {
+        private static final long serialVersionUID = 1L;
+
+        static volatile boolean FIRED = false;
+
+        private void readObject(final ObjectInputStream in) throws 
IOException, ClassNotFoundException {
+            in.defaultReadObject();
+            FIRED = true;
+        }
+    }
 }
diff --git 
a/hadoop-gremlin/src/main/java/org/apache/tinkerpop/gremlin/hadoop/structure/io/HadoopPools.java
 
b/hadoop-gremlin/src/main/java/org/apache/tinkerpop/gremlin/hadoop/structure/io/HadoopPools.java
index 2284f279f6..7201b4acd2 100644
--- 
a/hadoop-gremlin/src/main/java/org/apache/tinkerpop/gremlin/hadoop/structure/io/HadoopPools.java
+++ 
b/hadoop-gremlin/src/main/java/org/apache/tinkerpop/gremlin/hadoop/structure/io/HadoopPools.java
@@ -47,6 +47,8 @@ public final class HadoopPools {
                     
poolSize(configuration.getInt(GryoPool.CONFIG_IO_GRYO_POOL_SIZE, 256)).
                     
version(GryoVersion.valueOf(configuration.getString(GryoPool.CONFIG_IO_GRYO_VERSION,
 GryoPool.CONFIG_IO_GRYO_POOL_VERSION_DEFAULT.name()))).
                     ioRegistries(configuration.getList(IoRegistry.IO_REGISTRY, 
Collections.emptyList())).
+                    // intra-job shuffle path (VertexWritable), not untrusted 
input, so it is left unlocked and keeps the
+                    // JavaSerializer registrations. The Gryo IO formats are 
the hardened untrusted-file path.
                     initializeMapper(m -> m.registrationRequired(false)).
                     create();
         }
diff --git 
a/hadoop-gremlin/src/main/java/org/apache/tinkerpop/gremlin/hadoop/structure/io/gryo/GryoRecordReader.java
 
b/hadoop-gremlin/src/main/java/org/apache/tinkerpop/gremlin/hadoop/structure/io/gryo/GryoRecordReader.java
index 8fead07f4b..f904eac0f1 100644
--- 
a/hadoop-gremlin/src/main/java/org/apache/tinkerpop/gremlin/hadoop/structure/io/gryo/GryoRecordReader.java
+++ 
b/hadoop-gremlin/src/main/java/org/apache/tinkerpop/gremlin/hadoop/structure/io/gryo/GryoRecordReader.java
@@ -72,8 +72,10 @@ public final class GryoRecordReader extends 
RecordReader<NullWritable, VertexWri
         final Configuration configuration = context.getConfiguration();
         if (configuration.get(Constants.GREMLIN_HADOOP_GRAPH_FILTER, null) != 
null)
             this.graphFilter = 
VertexProgramHelper.deserialize(ConfUtil.makeApacheConfiguration(configuration),
 Constants.GREMLIN_HADOOP_GRAPH_FILTER);
+        // reads only graph Elements from untrusted split bytes, never the 
JavaSerializer backed types, so native
+        // Java serialization is disabled here (unlike the intra-job MapReduce 
shuffle path, left unlocked).
         this.gryoReader = GryoReader.build().mapper(
-                
GryoMapper.build().addRegistries(IoRegistryHelper.createRegistries(ConfUtil.makeApacheConfiguration(configuration))).create()).create();
+                
GryoMapper.build().javaSerializationAllowed(false).addRegistries(IoRegistryHelper.createRegistries(ConfUtil.makeApacheConfiguration(configuration))).create()).create();
         long start = split.getStart();
         final Path file = split.getPath();
         if (null != new CompressionCodecFactory(configuration).getCodec(file)) 
{
diff --git 
a/hadoop-gremlin/src/main/java/org/apache/tinkerpop/gremlin/hadoop/structure/io/gryo/GryoRecordWriter.java
 
b/hadoop-gremlin/src/main/java/org/apache/tinkerpop/gremlin/hadoop/structure/io/gryo/GryoRecordWriter.java
index 983e05c5cb..34340297ca 100644
--- 
a/hadoop-gremlin/src/main/java/org/apache/tinkerpop/gremlin/hadoop/structure/io/gryo/GryoRecordWriter.java
+++ 
b/hadoop-gremlin/src/main/java/org/apache/tinkerpop/gremlin/hadoop/structure/io/gryo/GryoRecordWriter.java
@@ -46,8 +46,10 @@ public final class GryoRecordWriter extends 
RecordWriter<NullWritable, VertexWri
     public GryoRecordWriter(final DataOutputStream outputStream, final 
Configuration configuration) {
         this.outputStream = outputStream;
         this.hasEdges = 
configuration.getBoolean(Constants.GREMLIN_HADOOP_GRAPH_WRITER_HAS_EDGES, true);
+        // as with GryoRecordReader, this writer only serializes graph 
Elements, which never include the
+        // JavaSerializer backed types, so native Java serialization is 
disabled and reads/writes stay symmetric.
         this.gryoWriter = GryoWriter.build().mapper(
-                
GryoMapper.build().addRegistries(IoRegistryHelper.createRegistries(ConfUtil.makeApacheConfiguration(configuration))).create()).create();
+                
GryoMapper.build().javaSerializationAllowed(false).addRegistries(IoRegistryHelper.createRegistries(ConfUtil.makeApacheConfiguration(configuration))).create()).create();
     }
 
     @Override
diff --git 
a/hadoop-gremlin/src/test/java/org/apache/tinkerpop/gremlin/hadoop/structure/io/gryo/GryoRecordReaderWriterTest.java
 
b/hadoop-gremlin/src/test/java/org/apache/tinkerpop/gremlin/hadoop/structure/io/gryo/GryoRecordReaderWriterTest.java
index fb6282d942..1a45f9a556 100644
--- 
a/hadoop-gremlin/src/test/java/org/apache/tinkerpop/gremlin/hadoop/structure/io/gryo/GryoRecordReaderWriterTest.java
+++ 
b/hadoop-gremlin/src/test/java/org/apache/tinkerpop/gremlin/hadoop/structure/io/gryo/GryoRecordReaderWriterTest.java
@@ -18,11 +18,30 @@
  */
 package org.apache.tinkerpop.gremlin.hadoop.structure.io.gryo;
 
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.LocalFileSystem;
+import org.apache.hadoop.fs.Path;
 import org.apache.hadoop.io.NullWritable;
 import org.apache.hadoop.mapreduce.InputFormat;
 import org.apache.hadoop.mapreduce.OutputFormat;
+import org.apache.hadoop.mapreduce.TaskAttemptContext;
+import org.apache.hadoop.mapreduce.TaskAttemptID;
+import org.apache.hadoop.mapreduce.TaskType;
+import org.apache.hadoop.mapreduce.lib.input.FileSplit;
+import org.apache.hadoop.mapreduce.task.TaskAttemptContextImpl;
+import org.apache.tinkerpop.gremlin.features.TestFiles;
 import org.apache.tinkerpop.gremlin.hadoop.structure.io.RecordReaderWriterTest;
 import org.apache.tinkerpop.gremlin.hadoop.structure.io.VertexWritable;
+import 
org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.OptionsStrategy;
+import org.apache.tinkerpop.gremlin.structure.io.gryo.GryoReader;
+import org.apache.tinkerpop.shaded.kryo.Kryo;
+import org.junit.Test;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.util.UUID;
+
+import static org.junit.Assert.fail;
 
 /**
  * @author Marko A. Rodriguez (http://markorodriguez.com)
@@ -44,4 +63,73 @@ public class GryoRecordReaderWriterTest extends 
RecordReaderWriterTest {
     protected Class<? extends OutputFormat<NullWritable, VertexWritable>> 
getOutputFormat() {
         return GryoOutputFormat.class;
     }
+
+    /**
+     * The reader builds a hardened mapper, i.e. one without the {@code 
JavaSerializer} backed registrations. The
+     * general hardening behavior is covered in gremlin-core's {@code 
GryoMapperTest}; this pins the wiring here.
+     */
+    @Test
+    public void gryoRecordReaderShouldBuildAHardenedMapper() throws Exception {
+        final GryoRecordReader reader = new GryoRecordReader();
+        final File file = new File(TestFiles.PATHS.get(getInputFilename()));
+
+        final Configuration config = new Configuration(false);
+        config.set("fs.file.impl", LocalFileSystem.class.getName());
+        config.set("fs.defaultFS", "file:///");
+        final TaskAttemptContext job = new TaskAttemptContextImpl(config,
+                new TaskAttemptID(UUID.randomUUID().toString(), 0, 
TaskType.MAP, 0, 0));
+
+        reader.initialize(new FileSplit(new Path(file.toURI()), 0, 
file.length(), null), job);
+
+        assertMapperRefusesOptionsStrategy(readerKryo(reader));
+    }
+
+    /**
+     * The writer builds a hardened mapper on the same terms, kept symmetric 
with the reader.
+     */
+    @Test
+    public void gryoRecordWriterShouldBuildAHardenedMapper() throws Exception {
+        final File outputDirectory = new 
File(System.getProperty("java.io.tmpdir"),
+                "gryo-record-writer-hardening-" + UUID.randomUUID());
+        final Configuration config = new Configuration(false);
+        config.set("fs.file.impl", LocalFileSystem.class.getName());
+        config.set("fs.defaultFS", "file:///");
+        config.set("mapreduce.output.fileoutputformat.outputdir", 
outputDirectory.toURI().toString());
+        final TaskAttemptContext job = new TaskAttemptContextImpl(config,
+                new TaskAttemptID(UUID.randomUUID().toString(), 0, 
TaskType.REDUCE, 0, 0));
+
+        final GryoRecordWriter writer = (GryoRecordWriter) new 
GryoOutputFormat().getRecordWriter(job);
+        try {
+            assertMapperRefusesOptionsStrategy(writerKryo(writer));
+        } finally {
+            writer.close(job);
+        }
+    }
+
+    private static void assertMapperRefusesOptionsStrategy(final Kryo kryo) {
+        try {
+            kryo.getRegistration(OptionsStrategy.class);
+            fail("a hardened Hadoop Gryo mapper must not register 
OptionsStrategy");
+        } catch (IllegalArgumentException expected) {
+            // Kryo refuses an unregistered class while registration is 
required
+        }
+    }
+
+    private static Kryo readerKryo(final GryoRecordReader reader) throws 
Exception {
+        final Field gryoReaderField = 
GryoRecordReader.class.getDeclaredField("gryoReader");
+        gryoReaderField.setAccessible(true);
+        final GryoReader gryoReader = (GryoReader) gryoReaderField.get(reader);
+        final Field kryoField = GryoReader.class.getDeclaredField("kryo");
+        kryoField.setAccessible(true);
+        return (Kryo) kryoField.get(gryoReader);
+    }
+
+    private static Kryo writerKryo(final GryoRecordWriter writer) throws 
Exception {
+        final Field gryoWriterField = 
GryoRecordWriter.class.getDeclaredField("gryoWriter");
+        gryoWriterField.setAccessible(true);
+        final Object gryoWriter = gryoWriterField.get(writer);
+        final Field kryoField = gryoWriter.getClass().getDeclaredField("kryo");
+        kryoField.setAccessible(true);
+        return (Kryo) kryoField.get(gryoWriter);
+    }
 }

Reply via email to