Возможность явного удаления поддержки Serialization для лямбда
Как уже известно, легко добавить поддержку Serialization в выражение лямбда, когда целевой интерфейс еще не наследует Serializable
, как и (TargetInterface&Serializable)()->{/*code*/}
.
То, что я прошу, - это способ сделать обратное, явно удалить поддержку Serialization, когда целевой интерфейс наследует Serializable
.
Поскольку вы не можете удалить интерфейс из типа, языковое решение может выглядеть как (@NotSerializable TargetInterface)()->{/* code */}
. Но, насколько я знаю, такого решения нет. (Исправьте меня, если я ошибаюсь, это будет прекрасный ответ)
Отказ от сериализации, даже если класс реализует Serializable
, был законным поведением в прошлом и с классами под контролем программистов, шаблон выглядел бы следующим образом:
public class NotSupportingSerialization extends SerializableBaseClass {
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
throw new NotSerializableException();
}
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException {
throw new NotSerializableException();
}
private void readObjectNoData() throws ObjectStreamException {
throw new NotSerializableException();
}
}
Но для выражения лямбда программист не имеет этого контроля над классом лямбда.
Зачем кому-то беспокоиться об удалении поддержки? Ну, помимо большого кода, созданного для поддержки поддержки Serialization
, он создает угрозу безопасности. Рассмотрим следующий код:
public class CreationSite {
public static void main(String... arg) {
TargetInterface f=CreationSite::privateMethod;
}
private static void privateMethod() {
System.out.println("should be private");
}
}
Здесь доступ к закрытому методу не отображается, даже если TargetInterface
- public
(методы интерфейса всегда public
), если программист берет на себя заботу, а не передает экземпляр f
в ненадежный код.
Однако вещи меняются, если TargetInterface
наследует Serializable
. Затем, даже если CreationSite
никогда не раздаст экземпляр, злоумышленник может создать эквивалентный экземпляр путем де-сериализации потока, созданного вручную. Если интерфейс для приведенного выше примера выглядит как
public interface TargetInterface extends Runnable, Serializable {}
его так же просто, как:
SerializedLambda l=new SerializedLambda(CreationSite.class,
TargetInterface.class.getName().replace('.', '/'), "run", "()V",
MethodHandleInfo.REF_invokeStatic,
CreationSite.class.getName().replace('.', '/'), "privateMethod",
"()V", "()V", new Object[0]);
ByteArrayOutputStream os=new ByteArrayOutputStream();
try(ObjectOutputStream oos=new ObjectOutputStream(os)) { oos.writeObject(l);}
TargetInterface f;
try(ByteArrayInputStream is=new ByteArrayInputStream(os.toByteArray());
ObjectInputStream ois=new ObjectInputStream(is)) {
f=(TargetInterface) ois.readObject();
}
f.run();// invokes privateMethod
Обратите внимание, что атакующий код не содержит никаких действий, которые отменил бы SecurityManager
.
Решение о поддержке сериализации производится во время компиляции. Для этого требуется синтетический метод factory, добавленный в CreationSite
, а flag передан metafactory. Без флага генерируемая лямбда не поддерживает сериализацию, даже если интерфейс имеет наследование Serializable
. Класс лямбда даже будет иметь метод writeObject
, как в приведенном выше примере NotSupportingSerialization
. И без синтетического метода factory де-сериализация невозможна.
Это приводит к одному решению, я нашел. Вы можете создать копию интерфейса и изменить его, чтобы не наследовать Serializable
, а затем скомпилировать эту модифицированную версию. Поэтому, когда реальная версия во время выполнения наследует Serializable
, сериализация все равно будет отменена.
Ну, еще одно решение - никогда не использовать лямбда-выражения/ссылки на методы в соответствующем коде безопасности, по крайней мере, если целевой интерфейс наследует Serializable
, который всегда должен быть повторно проверен при компиляции с более новой версией интерфейса.
Но я думаю, что должны быть лучшие, предпочтительно, языковые решения.
Ответы
Ответ 1
Как справиться с сериализацией является одной из самых больших проблем для EG; достаточно сказать, что отличных решений не было, только компромиссы между различными минусами. Некоторые стороны настаивали на том, чтобы все лямбды были автоматически сериализуемыми (!); другие настаивали на том, что лямбды никогда не могут быть сериализуемыми (что иногда казалось привлекательной идеей, но, к сожалению, плохо нарушало ожидания пользователей).
Вы отмечаете:
Ну, еще одно решение - никогда не использовать лямбда-выражения/ссылки на методы в соответствующем коде безопасности,
Фактически, спецификация сериализации теперь точно говорит об этом.
Но есть довольно простой трюк, чтобы делать то, что вы хотите здесь. Предположим, у вас есть библиотека, которая хочет сериализуемые экземпляры:
public interface SomeLibType extends Runnable, Serializable { }
с методами, которые ожидают этого типа:
public void gimmeLambda(SomeLibType r)
и вы хотите передать лямбда в него, но не иметь их сериализуемыми (и принять последствия этого). Итак, напишите себе этот вспомогательный метод:
public static SomeLibType launder(Runnable r) {
return new SomeLibType() {
public void run() { r.run(); }
}
}
Теперь вы можете вызвать метод библиотеки:
gimmeLambda(launder(() -> myPrivateMethod()));
Компилятор преобразует вашу лямбду в несериализуемый Runnable, а обертка для отмывания завершает ее экземпляром, который удовлетворяет системе типов. Когда вы пытаетесь его сериализовать, это не сработает, поскольку r
не является сериализуемым. Что еще более важно, вы не можете подделать доступ к частному методу, потому что поддержка $deserializeLambda $, которая нужна в классе захвата, даже не будет там.