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