Возможность явного удаления поддержки сериализации для лямбды
Как уже известно, легко добавить поддержку сериализации в лямбда-выражение, когда целевой интерфейс еще не наследуется Serializable
, как (TargetInterface&Serializable)()->{/*code*/}
,
То, что я прошу, это способ сделать обратное, явно удалить поддержку сериализации, когда целевой интерфейс наследует 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
отзовет
Решение о поддержке сериализации принимается во время компиляции. Требуется синтетический фабричный метод, добавленный в CreationSite
и флаг, переданный метафорическому методу. Без флага сгенерированная лямбда не будет поддерживать сериализацию, даже если интерфейс наследует Serializable
, У класса лямбды даже будет writeObject
метод, как в NotSupportingSerialization
пример выше. А без синтетического фабричного метода десериализация невозможна.
Это приводит к единственному решению, которое я нашел. Вы можете создать копию интерфейса и изменить ее, чтобы не наследовать 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 $, которая необходима в классе захвата, даже не будет там.