Возможность явного удаления поддержки сериализации для лямбды

Как уже известно, легко добавить поддержку сериализации в лямбда-выражение, когда целевой интерфейс еще не наследуется 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 $, которая необходима в классе захвата, даже не будет там.

Другие вопросы по тегам