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);
+ }
+}