You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by us...@apache.org on 2026/05/30 12:41:06 UTC

(lucene) branch branch_10x updated: Reject non-SerializableObject classes in SerializableObject.readClass (#16138)

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

asf-gitbox-commits pushed a commit to branch branch_10x
in repository https://gitbox.apache.org/repos/asf/lucene.git


The following commit(s) were added to refs/heads/branch_10x by this push:
     new f5d93f7ab57 Reject non-SerializableObject classes in SerializableObject.readClass (#16138)
f5d93f7ab57 is described below

commit f5d93f7ab575911dc98d470654e201351edec206
Author: metsw24-max <me...@gmail.com>
AuthorDate: Sat May 30 18:05:18 2026 +0530

    Reject non-SerializableObject classes in SerializableObject.readClass (#16138)
    
    Co-authored-by: Uwe Schindler <us...@apache.org>
    # Conflicts:
    #       lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/SerializableObject.java
---
 lucene/CHANGES.txt                                 |  4 ++
 .../lucene/spatial3d/geom/SerializableObject.java  | 75 ++++++++++----------
 .../lucene/spatial3d/geom/StandardObjects.java     |  8 ++-
 .../geom/TestSerializableObjectClassFilter.java    | 82 ++++++++++++++++++++++
 4 files changed, 128 insertions(+), 41 deletions(-)

diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index 87f76d8c5f7..ca236533bd3 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -253,6 +253,10 @@ Bug Fixes
   reporting. Also added missing dimensionSums array accounting in quantized writers.
   (Prithvi S)
 
+* GITHUB#16138: Reject non-SerializableObject classes in SerializableObject.readClass before they are
+  instantiated, so a crafted spatial3d shape stream cannot load arbitrary classes. Also adapt
+  experimental SerializedObject API to be more type safe. (metsw24-max, Uwe Schindler)
+
 Other
 ---------------------
 * GITHUB#15586: Document that scoring and ranking may change across major Lucene versions, and that applications requiring stable ranking should explicitly configure Similarity. (Parveen Saini)
diff --git a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/SerializableObject.java b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/SerializableObject.java
index 9c532b561ec..78ad1ca5757 100644
--- a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/SerializableObject.java
+++ b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/SerializableObject.java
@@ -20,6 +20,7 @@ package org.apache.lucene.spatial3d.geom;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.lang.invoke.MethodHandles;
 import java.lang.reflect.Array;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
@@ -47,7 +48,7 @@ public interface SerializableObject {
    * @param outputStream is the output stream.
    * @param object is the object to write.
    */
-  public static void writePlanetObject(final OutputStream outputStream, final PlanetObject object)
+  static void writePlanetObject(final OutputStream outputStream, final PlanetObject object)
       throws IOException {
     object.getPlanetModel().write(outputStream);
     writeObject(outputStream, object);
@@ -59,7 +60,7 @@ public interface SerializableObject {
    * @param inputStream is the input stream.
    * @return the PlanetObject.
    */
-  public static PlanetObject readPlanetObject(final InputStream inputStream) throws IOException {
+  static PlanetObject readPlanetObject(final InputStream inputStream) throws IOException {
     final PlanetModel pm = new PlanetModel(inputStream);
     final SerializableObject so = readObject(pm, inputStream);
     if (!(so instanceof PlanetObject)) {
@@ -75,7 +76,7 @@ public interface SerializableObject {
    * @param outputStream is the output stream.
    * @param object is the object to write.
    */
-  public static void writeObject(final OutputStream outputStream, final SerializableObject object)
+  static void writeObject(final OutputStream outputStream, final SerializableObject object)
       throws IOException {
     writeClass(outputStream, object.getClass());
     object.write(outputStream);
@@ -88,16 +89,12 @@ public interface SerializableObject {
    * @param inputStream is the input stream.
    * @return the deserialized object.
    */
-  public static SerializableObject readObject(
-      final PlanetModel planetModel, final InputStream inputStream) throws IOException {
-    try {
+  static SerializableObject readObject(final PlanetModel planetModel, final InputStream inputStream)
+      throws IOException {
       // Read the class
-      final Class<?> clazz = readClass(inputStream);
+    final Class<? extends SerializableObject> clazz = readClass(inputStream);
       return readObject(planetModel, inputStream, clazz);
-    } catch (ClassNotFoundException e) {
-      throw new IOException("Can't find class for deserialization: " + e.getMessage(), e);
     }
-  }
 
   /**
    * Read an object from a stream (for objects that do not need a PlanetModel).
@@ -105,15 +102,11 @@ public interface SerializableObject {
    * @param inputStream is the input stream.
    * @return the deserialized object.
    */
-  public static SerializableObject readObject(final InputStream inputStream) throws IOException {
-    try {
+  static SerializableObject readObject(final InputStream inputStream) throws IOException {
       // read the class
-      final Class<?> clazz = readClass(inputStream);
+    final Class<? extends SerializableObject> clazz = readClass(inputStream);
       return readObject(inputStream, clazz);
-    } catch (ClassNotFoundException e) {
-      throw new IOException("Can't find class for deserialization: " + e.getMessage(), e);
     }
-  }
 
   /**
    * Instantiate a serializable object from a stream.
@@ -123,19 +116,16 @@ public interface SerializableObject {
    * @param clazz is the class to instantiate.
    */
   static SerializableObject readObject(
-      final PlanetModel planetModel, final InputStream inputStream, final Class<?> clazz)
+      final PlanetModel planetModel,
+      final InputStream inputStream,
+      final Class<? extends SerializableObject> clazz)
       throws IOException {
     try {
       // Look for the right constructor
-      final Constructor<?> c = clazz.getDeclaredConstructor(PlanetModel.class, InputStream.class);
+      final Constructor<? extends SerializableObject> c =
+          clazz.getDeclaredConstructor(PlanetModel.class, InputStream.class);
       // Invoke it
-      final Object object = c.newInstance(planetModel, inputStream);
-      // check whether caste will work
-      if (!(object instanceof SerializableObject)) {
-        throw new IOException(
-            "Object " + clazz.getName() + " does not implement SerializableObject");
-      }
-      return (SerializableObject) object;
+      return c.newInstance(planetModel, inputStream);
     } catch (InstantiationException e) {
       throw new IOException(
           "Instantiation exception for class " + clazz.getName() + ": " + e.getMessage(), e);
@@ -157,19 +147,15 @@ public interface SerializableObject {
    * @param inputStream is the input stream.
    * @param clazz is the class to instantiate.
    */
-  static SerializableObject readObject(final InputStream inputStream, final Class<?> clazz)
+  static SerializableObject readObject(
+      final InputStream inputStream, final Class<? extends SerializableObject> clazz)
       throws IOException {
     try {
       // Look for the right constructor
-      final Constructor<?> c = clazz.getDeclaredConstructor(InputStream.class);
+      final Constructor<? extends SerializableObject> c =
+          clazz.getDeclaredConstructor(InputStream.class);
       // Invoke it
-      final Object object = c.newInstance(inputStream);
-      // check whether caste will work
-      if (!(object instanceof SerializableObject)) {
-        throw new IOException(
-            "Object " + clazz.getName() + " does not implement SerializableObject");
-      }
-      return (SerializableObject) object;
+      return c.newInstance(inputStream);
     } catch (InstantiationException e) {
       throw new IOException(
           "Instantiation exception for class " + clazz.getName() + ": " + e.getMessage(), e);
@@ -191,7 +177,9 @@ public interface SerializableObject {
    * @param outputStream is the output stream.
    * @param clazz is the class to write.
    */
-  static void writeClass(final OutputStream outputStream, final Class<?> clazz) throws IOException {
+  static void writeClass(
+      final OutputStream outputStream, final Class<? extends SerializableObject> clazz)
+      throws IOException {
     Integer index = StandardObjects.CLASS_REGISTRY.get(clazz);
     if (index == null) {
       writeBoolean(outputStream, false);
@@ -208,17 +196,28 @@ public interface SerializableObject {
    * @param inputStream is the stream to read from.
    * @return is the class read
    */
-  static Class<?> readClass(final InputStream inputStream)
-      throws IOException, ClassNotFoundException {
+  private static Class<? extends SerializableObject> readClass(final InputStream inputStream)
+      throws IOException {
     boolean standard = readBoolean(inputStream);
     if (standard) {
       int index = inputStream.read();
+      if (StandardObjects.CODE_REGISTRY.containsKey(index)) {
       return StandardObjects.CODE_REGISTRY.get(index);
+    } else {
+        throw new IOException("No standard object found for index: " + index);
+      }
     } else {
       String className = readString(inputStream);
-      return Class.forName(className);
+      // Load without initializing and confirm the named class is actually a SerializableObject
+      // before it can be instantiated, so a crafted stream cannot load arbitrary classes.
+      try {
+        return MethodHandles.lookup().findClass(className).asSubclass(SerializableObject.class);
+      } catch (ClassNotFoundException | IllegalAccessException | ClassCastException e) {
+        throw new IOException(
+            "Can't find or access class of correct type for deserialization: " + e.getMessage(), e);
     }
   }
+  }
 
   /**
    * Write a string to a stream.
diff --git a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/StandardObjects.java b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/StandardObjects.java
index 911959c781b..f5b1a15e032 100644
--- a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/StandardObjects.java
+++ b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/StandardObjects.java
@@ -29,10 +29,11 @@ import org.apache.lucene.internal.hppc.IntObjectHashMap;
 class StandardObjects {
 
   /** Registry of standard classes to corresponding code */
-  static final Map<Class<?>, Integer> CLASS_REGISTRY = new HashMap<>();
+  static final Map<Class<? extends SerializableObject>, Integer> CLASS_REGISTRY = new HashMap<>();
 
   /** Registry of codes to corresponding classes */
-  static final IntObjectHashMap<Class<?>> CODE_REGISTRY = new IntObjectHashMap<>();
+  static final IntObjectHashMap<Class<? extends SerializableObject>> CODE_REGISTRY =
+      new IntObjectHashMap<>();
 
   static {
     CLASS_REGISTRY.put(GeoPoint.class, 0);
@@ -75,7 +76,8 @@ class StandardObjects {
     CLASS_REGISTRY.put(GeoExactCircle.class, 37);
     CLASS_REGISTRY.put(GeoS2Shape.class, 38);
 
-    for (Map.Entry<Class<?>, Integer> entry : CLASS_REGISTRY.entrySet()) {
+    for (Map.Entry<Class<? extends SerializableObject>, Integer> entry :
+        CLASS_REGISTRY.entrySet()) {
       CODE_REGISTRY.put(entry.getValue(), entry.getKey());
     }
   }
diff --git a/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestSerializableObjectClassFilter.java b/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestSerializableObjectClassFilter.java
new file mode 100644
index 00000000000..261d7cf6378
--- /dev/null
+++ b/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/TestSerializableObjectClassFilter.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.lucene.spatial3d.geom;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.apache.lucene.tests.util.LuceneTestCase;
+
+public class TestSerializableObjectClassFilter extends LuceneTestCase {
+
+  static volatile boolean nonSerializableConstructed = false,
+      nonSerializableClassInitialized = false;
+
+  /** A SerializableObject that is not registered in {@link StandardObjects}. */
+  public static class CustomShape implements SerializableObject {
+    final int value;
+
+    public CustomShape(int value) {
+      this.value = value;
+    }
+
+    public CustomShape(InputStream inputStream) throws IOException {
+      this.value = SerializableObject.readInt(inputStream);
+    }
+
+    @Override
+    public void write(OutputStream outputStream) throws IOException {
+      SerializableObject.writeInt(outputStream, value);
+    }
+  }
+
+  /** Not a SerializableObject, but instantiable from an InputStream. */
+  public static class NotASerializableObject {
+    static {
+      nonSerializableClassInitialized = true;
+    }
+
+    public NotASerializableObject(InputStream inputStream) {
+      nonSerializableConstructed = true;
+    }
+  }
+
+  public void testCustomClassRoundTrips() throws IOException {
+    CustomShape shape = new CustomShape(42);
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    SerializableObject.writeObject(out, shape);
+    ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+    SerializableObject copy = SerializableObject.readObject(in);
+    assertTrue(copy instanceof CustomShape);
+    assertEquals(42, ((CustomShape) copy).value);
+  }
+
+  public void testRejectsNonSerializableObjectClass() throws IOException {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    SerializableObject.writeBoolean(out, false);
+    SerializableObject.writeString(out, NotASerializableObject.class.getName());
+    ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+    var e = expectThrows(IOException.class, () -> SerializableObject.readObject(in));
+    assertTrue(e.getCause() instanceof ClassCastException);
+    assertTrue(e.getMessage().contains(NotASerializableObject.class.getName()));
+    assertFalse(nonSerializableClassInitialized);
+    assertFalse(nonSerializableConstructed);
+  }
+}