Comment puis-je copier des collections en toute sécurité?

9

Dans le passé, j'ai dit que pour copier une collection en toute sécurité, faites quelque chose comme:

public static void doThing(List<String> strs) {
    List<String> newStrs = new ArrayList<>(strs);

ou

public static void doThing(NavigableSet<String> strs) {
    NavigableSet<String> newStrs = new TreeSet<>(strs);

Mais ces constructeurs de «copie», des méthodes de création statique similaires et des flux, sont-ils vraiment sûrs et où sont les règles spécifiées? Par sûr, j'entends les garanties d' intégrité sémantique de base offertes par le langage Java et les collections appliquées contre un appelant malveillant, en supposant SecurityManagerqu'elles soient sauvegardées par un raisonnable et qu'il n'y a pas de défauts.

Je suis heureux avec le lancement de la méthode ConcurrentModificationException, NullPointerException, IllegalArgumentException, ClassCastException, etc., ou peut - être même accroché.

J'ai choisi Stringcomme exemple d'argument de type immuable. Pour cette question, je ne suis pas intéressé par les copies complètes pour les collections de types mutables qui ont leurs propres pièges.

(Pour être clair, j'ai regardé le code source d'OpenJDK et j'ai une sorte de réponse pour ArrayListet TreeSet.)

Tom Hawtin - sellerie
la source
2
Qu'entendez-vous par coffre - fort ? De manière générale, les classes dans le cadre des collections ont tendance à fonctionner de manière similaire, avec des exceptions spécifiées dans les javadocs. Les constructeurs de copie sont tout aussi "sûrs" que tous les autres constructeurs. Y a-t-il une chose que vous avez en tête, parce que demander si un constructeur de copie de collection est sûr semble très spécifique?
Kayaman
1
Eh bien, NavigableSetet d'autres Comparablecollections basées peuvent parfois détecter si une classe ne s'implémente pas compareTo()correctement et lever une exception. On ne sait pas trop ce que vous entendez par arguments non fiables. Vous voulez dire qu'un malfaiteur crée une collection de mauvaises cordes et lorsque vous les copiez dans votre collection, quelque chose de mauvais se produit? Non, le cadre des collections est assez solide, il existe depuis la 1.2.
Kayaman
1
@JesseWilson, vous pouvez compromettre de nombreuses collections standard sans pirater leurs internes, HashSet(et toutes les autres collections de hachage en général) repose sur l'exactitude / l'intégrité de la hashCodemise en œuvre des éléments, TreeSetet PriorityQueuedépend de la Comparator(et vous ne pouvez même pas créer une copie équivalente sans accepter le comparateur personnalisé s'il en existe un), EnumSetfait confiance à l'intégrité du enumtype particulier qui n'est jamais vérifié après la compilation, de sorte qu'un fichier de classe, non généré javacou fabriqué à la main, peut le subvertir.
Holger
1
Dans vos exemples, vous avez new TreeSet<>(strs)strsest un NavigableSet. Ce n'est pas une copie en bloc, car le résultat TreeSetutilisera le comparateur de la source, ce qui est même nécessaire pour conserver la sémantique. Si vous êtes prêt à simplement traiter les éléments contenus, toArray()c'est la voie à suivre; il conservera même l'ordre d'itération. Lorsque vous êtes d'accord avec "prendre élément, valider élément, utiliser élément", vous n'avez même pas besoin d'en faire une copie. Les problèmes commencent lorsque vous souhaitez vérifier tous les éléments, puis en utilisant tous les éléments. Ensuite, vous ne pouvez pas faire confiance à une TreeSetcopie avec un comparateur personnalisé
Holger
1
La seule opération de copie en bloc ayant pour effet un checkcastpour chaque élément, concerne toArrayun type spécifique. Nous y terminons toujours. Les collections génériques ne connaissent même pas leur type d'élément réel, de sorte que leurs constructeurs de copie ne peuvent pas fournir une fonctionnalité similaire. Bien sûr, vous pouvez reporter tout contrôle à une bonne utilisation antérieure, mais je ne sais pas à quoi vos questions visent. Vous n'avez pas besoin d '«intégrité sémantique», lorsque vous êtes prêt à vérifier et à échouer immédiatement avant d'utiliser des éléments.
Holger

Réponses:

12

Il n'y a pas de véritable protection contre le code intentionnellement malveillant exécuté dans la même JVM dans les API ordinaires, comme l'API Collection.

Comme on peut facilement le démontrer:

public static void main(String[] args) throws InterruptedException {
    Object[] array = { "foo", "bar", "baz", "and", "another", "string" };
    array[array.length - 1] = new Object() {
        @Override
        public String toString() {
            Collections.shuffle(Arrays.asList(array));
            return "string";
        }
    };
    doThing(new ArrayList<String>() {
        @Override public Object[] toArray() {
            return array;
        }
    });
}

public static void doThing(List<String> strs) {
    List<String> newStrs = new ArrayList<>(strs);

    System.out.println("made a safe copy " + newStrs);
    for(int i = 0; i < 10; i++) {
        System.out.println(newStrs);
    }
}
made a safe copy [foo, bar, baz, and, another, string]
[bar, and, string, string, another, foo]
[and, baz, bar, string, string, string]
[another, baz, and, foo, bar, string]
[another, bar, and, foo, string, and]
[another, baz, string, another, and, foo]
[string, and, another, foo, string, foo]
[baz, string, foo, and, baz, string]
[bar, another, string, and, another, baz]
[bar, string, foo, string, baz, and]
[bar, string, bar, another, and, foo]

Comme vous pouvez le voir, s'attendre à ce qu'une List<String>garantie n'obtienne pas réellement une liste d' Stringinstances. En raison de l'effacement des types et des types bruts, il n'y a même pas de correctif possible du côté de l'implémentation de la liste.

L'autre chose dont vous pouvez blâmer ArrayListle constructeur est la confiance dans l' toArrayimplémentation de la collection entrante . TreeMapn'est pas affecté de la même manière, mais uniquement parce qu'il n'y a pas un tel gain de performances en passant le tableau, comme dans la construction d'un ArrayList. Aucune des deux classes ne garantit une protection dans le constructeur.

Normalement, il est inutile d'essayer d'écrire du code en supposant du code intentionnellement malveillant à chaque coin de rue. Il y a trop à faire pour se protéger de tout. Une telle protection n'est utile que pour le code qui encapsule vraiment une action qui pourrait donner à un appelant malveillant l'accès à quelque chose, il ne pourrait pas déjà y accéder sans ce code.

Si vous avez besoin de sécurité pour un code particulier, utilisez

public static void doThing(List<String> strs) {
    String[] content = strs.toArray(new String[0]);
    List<String> newStrs = new ArrayList<>(Arrays.asList(content));

    System.out.println("made a safe copy " + newStrs);
    for(int i = 0; i < 10; i++) {
        System.out.println(newStrs);
    }
}

Ensuite, vous pouvez être sûr qu'il newStrsne contient que des chaînes et ne peut pas être modifié par un autre code après sa construction.

Ou utilisez List<String> newStrs = List.of(strs.toArray(new String[0]));avec Java 9 ou une version plus récente.
Notez que Java 10 List.copyOf(strs)fait de même, mais sa documentation n'indique pas qu'il est garanti de ne pas faire confiance à la toArrayméthode de la collection entrante . Donc, appeler List.of(…), qui fera certainement une copie au cas où il retournerait une liste basée sur un tableau, est plus sûr.

Étant donné qu'aucun appelant ne peut modifier la façon dont les tableaux fonctionnent, le vidage de la collection entrante dans un tableau, suivi du remplissage de la nouvelle collection avec elle, rendra toujours la copie sûre. Étant donné que la collection peut contenir une référence au tableau renvoyé, comme illustré ci-dessus, elle peut la modifier pendant la phase de copie, mais elle ne peut pas affecter la copie dans la collection.

Par conséquent, tout contrôle de cohérence doit être effectué après que l'élément particulier a été récupéré du tableau ou sur la collection résultante dans son ensemble.

Holger
la source
2
Le modèle de sécurité de Java fonctionne en accordant au code l'intersection des jeux d'autorisations de tout le code de la pile, donc lorsque l'appelant de votre code fait faire à votre code des choses involontaires, il ne reçoit toujours pas plus d'autorisations qu'il n'en avait initialement. Ainsi, votre code ne fait que des choses que le code malveillant aurait pu faire sans votre code. Il vous suffit de durcir le code que vous avez l'intention d'exécuter avec des privilèges élevés via AccessController.doPrivileged(…)etc. Mais la longue liste de bogues liés à la sécurité des applets nous donne une idée de la raison pour laquelle cette technologie a été abandonnée…
Holger
1
Mais j'aurais dû insérer «dans des API ordinaires comme l'API Collection», car c'est ce sur quoi je me concentrais dans la réponse.
Holger
2
Pourquoi devriez-vous durcir votre code, qui n'est apparemment pas pertinent pour la sécurité, contre du code privilégié qui permet à une implémentation de collection malveillante de se glisser? Cet appelant hypothétique serait toujours sujet au comportement malveillant avant et après avoir appelé votre code. Il ne remarquerait même pas que votre code est le seul à se comporter correctement. L'utilisation en new ArrayList<>(…)tant que constructeur de copie est acceptable en supposant des implémentations de collection correctes. Ce n'est pas votre devoir de résoudre les problèmes de sécurité quand il est déjà trop tard. Qu'en est-il du matériel compromis? Le système d'exploitation? Que diriez-vous du multi-threading?
Holger
2
Je ne préconise pas «pas de sécurité», mais la sécurité aux bons endroits, au lieu d'essayer de réparer un environnement cassé après coup. C'est une affirmation intéressante qu '« il existe de nombreuses collections qui n'implémentent pas correctement leurs supertypes », mais cela est déjà allé trop loin, pour demander des preuves, étendant cela encore plus loin. La question d'origine a reçu une réponse complète; les points que vous soulevez maintenant n'en faisaient jamais partie. Comme dit, List.copyOf(strs)ne repose pas sur l'exactitude de la collecte entrante à cet égard, au prix évident. ArrayListest un compromis raisonnable pour tous les jours.
Holger
4
Il indique clairement qu'il n'existe aucune spécification de ce type pour toutes les «méthodes et flux de création statiques similaires». Donc, si vous voulez être absolument sûr, vous devez vous appeler toArray(), car les tableaux ne peuvent pas avoir de comportement remplacé, puis créer une copie de collection du tableau, comme new ArrayList<>(Arrays.asList( strs.toArray(new String[0])))ou List.of(strs.toArray(new String[0])). Les deux ont également pour effet secondaire d'imposer le type d'élément. Personnellement, je ne pense pas qu'ils permettront jamais copyOfde compromettre les collections immuables, mais les alternatives sont là, dans la réponse.
Holger
1

Je préfère laisser ces informations en commentaire, mais je n'ai pas assez de réputation, désolé :) Je vais essayer de l'expliquer le plus verbeux possible.

Au lieu de quelque chose comme un constmodificateur utilisé en C ++ pour marquer les fonctions membres qui ne sont pas censées modifier le contenu des objets, en Java était à l'origine utilisé le concept d '"immuabilité". L'encapsulation (ou OCP, Open-Closed Principle) était censée protéger contre toute mutation (modification) inattendue d'un objet. Bien sûr, l'API de réflexion marche autour de cela; l'accès direct à la mémoire fait de même; c'est plus de tirer sur sa propre jambe :)

java.util.Collectionlui-même est une interface mutable: il a une addméthode qui est censée modifier la collection. Bien sûr, le programmeur peut envelopper la collection dans quelque chose qui déclenchera ... et toutes les exceptions d'exécution se produiront car un autre programmeur n'a pas pu lire javadoc qui indique clairement que la collection est immuable.

J'ai décidé d'utiliser le java.util.Iterabletype pour exposer la collection immuable dans mes interfaces. Sémantiquement Iterablen'a pas une telle caractéristique de collection que la "mutabilité". Vous pourrez toujours (très probablement) modifier les collections sous-jacentes via des flux.


JIC, pour exposer des cartes de manière immuable java.util.Function<K,V>peut être utilisé (la getméthode de la carte correspond à cette définition)

Alexandre
la source
Les concepts d'interfaces en lecture seule et d'immuabilité sont orthogonaux. Le point de C ++ et C est qu'ils ne prennent pas en charge l'intégrité sémantique . Les arguments également copier objet / struct - const & est une optimisation douteuse pour cela. Si vous deviez passer un Iteratoralors cela force pratiquement une copie élément par élément, mais ce n'est pas bien. L'utilisation de forEachRemaining/ forEachva évidemment être un désastre complet. (Je dois également mentionner que cela Iteratora une removeméthode.)
Tom Hawtin - tackline
Si vous regardez la bibliothèque des collections Scala, il y a une distinction stricte entre les interfaces mutables et immuables. Bien que (je suppose) cela ait été fait pour des raisons complètement différentes, mais c'est toujours une démonstration de la façon dont la sécurité peut être atteinte. L'interface en lecture seule suppose sémantiquement l' immuabilité, c'est ce que j'essaie de dire. (Je suis d'accord sur Iterablele fait que ce n'est pas réellement immuable, mais je ne vois aucun problème avec forEach*)
Alexander