Sessions: Condition de course dans FilesystemStore

Créé le 11 juin 2013  ·  23Commentaires  ·  Source: gorilla/sessions

Il y a une condition de concurrence dans FilesystemStore que j'ai l'intention de corriger mais j'aimerais avoir votre avis avant de continuer et de le faire. Fondamentalement, le problème est que si vous avez des demandes simultanées du même utilisateur (même session), ce qui suit est possible:

  1. La requête 1 ouvre la session pour effectuer une opération semi-longue
  2. La requête 2 ouvre la session
  3. Request 2 Supprime les données de session pour effectuer une "déconnexion" ou similaire
  4. Demander 2 sauvegardes
  5. La demande 1 enregistre, ce qui donne l'impression que la session n'a jamais été déconnectée

J'ai ajouté un cas de test pour cette faille à cless / sessions @ f84abeda17de0b4fcd72d277412f3d3192f206f2

Le moyen le plus simple de résoudre ce problème serait d'introduire des verrous au niveau du système de fichiers. Cependant, golang n'a aucun moyen multiplateforme de verrouiller les fichiers. Il expose flock dans syscall mais cela ne fonctionne que si le système d'exploitation le prend en charge. Je pense que le comportement de flock peut également être différent sur différents unix bien que je ne sois pas sûr que ce soit le cas. Un autre problème avec flock est qu'il peut ne pas fonctionner sur NFS.

Une solution entièrement différente serait de conserver une carte de verrous dans l'objet FilesystemStore lui-même. Cela présente un autre ensemble d'inconvénients: vous ne pouvez pas laisser plusieurs processus accéder aux mêmes sessions de système de fichiers et vous ne pouvez pas créer plusieurs magasins pour la même session de système de fichiers dans une seule application. Cependant, ces deux choses sont déjà impossibles à faire sans causer de problèmes.

En fin de compte, je pense que la meilleure solution est de conserver une carte des verrous dans l'objet du magasin, car tous les inconvénients de ce scénario peuvent être correctement documentés et vous pouvez répondre que le comportement est le même dans différents systèmes.

D'autres backends de stockage basés sur FilesystemStore peuvent copier cette faille (j'ai remarqué ce problème lors de la révision du code Redistore pour un de mes projets boj / redistore # 2)

bug stale

Tous les 23 commentaires

Au lieu de verrouiller, qu'en est-il d'un système basé sur les transactions? L'objet Session peut porter un champ lastModified. Lorsque vous tentez de réenregistrer la session dans le magasin de sessions, une erreur indique qu'elle a été modifiée depuis sa dernière lecture. Le programmeur pourrait alors choisir de réessayer en récupérant à nouveau la session.

Cela pourrait fonctionner, cela a l'avantage qu'il n'y a aucun verrou à nettoyer et qu'il n'y a aucune chance de blocage. Je ne suis pas sûr qu'il soit toujours possible pour les développeurs de faire une nouvelle tentative significative en fonction de ce que la demande a déjà fait. Je pense que le verrouillage est certainement l'option la plus sûre, mais les transactions pourraient certainement fonctionner si l'API documente clairement le risque d'échec et si les développeurs gèrent avec diligence ces erreurs.

Cela dit, même si FilesystemStore utilise des transactions, je pense que l'API devrait être préparée pour les backends de stockage qui _do_ utilisent le verrouillage, mais cela signifierait soit l'interdiction d'appeler session.Save() plus d'une fois _ou_ l'introduction d'un nouveau session.Release() . Lequel préférez-vous ou préférez-vous qu'aucun backend de stockage n'utilise le verrouillage?

Suite à ce problème car il affecte RediStore. Merci pour les informations @cless

Je n'ai pas vraiment de préférence pour le moment à part faire ce qu'il faut sur le long terme :) Bien sûr, j'aimerais aussi conserver l'API existante. L'ajout de verrous partout introduit également beaucoup de complexité et de surcharge supplémentaires, ce qui serait également bien d'éviter.

Avez-vous des exemples d'autres cadres de session qui résolvent ce problème? Je serais curieux de voir ce qu'ils font.

Je sais que le gestionnaire de session php par défaut utilise le verrouillage basé sur le système de fichiers (dans l'archive tar php 5.4, je viens de vérifier que c'est dans le fichier ext/session/mod_files.c . Il utilise flock qui est fourni par ext/standard/flock_compat.c ).

Je ne sais pas si php est un bon exemple étant donné sa réputation, mais j'ai bien peur que ce soit le seul dont je connaisse pour le moment. Je vais essayer de jeter un œil autour de moi pour voir si je peux trouver d'autres cadres et voir comment ils résolvent le problème.

Je serais curieux de voir comment Flask, Pyramid ou Django gèrent ces choses. Je vais y jeter un œil si j'ai le temps. Les rails seraient également intéressants si quelqu'un connaît cette base de code.

Pyramid et Flask semblent utiliser un cookie pour stocker les données, je soupçonne que ni l'un ni l'autre ne gère les conditions de concurrence. Je n'ai lu que brièvement la documentation, donc je pourrais très bien me tromper.

Django prend en charge les données de session côté serveur, je vais donc jeter un coup d'œil à ce code dans un instant.

Le backend de session Django par défaut est la base de données et cela utilise la couche d'abstraction de la base de données Django avec laquelle je ne suis pas familier, c'est pourquoi il m'est difficile de l'interpréter. Cependant, j'ai trouvé ce commentaire dans le backend du système de fichiers:

        # Write the session file without interfering with other threads
        # or processes.  By writing to an atomically generated temporary
        # file and then using the atomic os.rename() to make the complete
        # file visible, we avoid having to lock the session file, while
        # still maintaining its integrity.
        #
        # Note: Locking the session file was explored, but rejected in part
        # because in order to be atomic and cross-platform, it required a
        # long-lived lock file for each session, doubling the number of
        # files in the session storage directory at any given time.  This
        # rename solution is cleaner and avoids any additional overhead
        # when reading the session data, which is the more common case
        # unless SESSION_SAVE_EVERY_REQUEST = True.
        #
        # See ticket #8616.

Cela semble suggérer qu'ils s'assurent que le contenu du fichier de session n'est jamais mutilé mais pas qu'aucune condition de concurrence ne peut se produire. Si quelqu'un a de l'expérience Django, ce serait bien de voir un cas de test comme cless / sessions @ f84abed
Stackoverflow semble également indiquer que Django pourrait avoir des conditions de concurrence dans les sessions: http://stackoverflow.com/search?q=django+session+race+condition

Très bien, FileSystemStore a déjà un mutex à grain grossier pour empêcher la corruption du magasin de session. Je ne sais pas combien de protection supplémentaire vaut la peine de mettre dans la bibliothèque car cela ajouterait beaucoup de frais généraux pour chaque demande, juste pour rendre les choses plus cohérentes pour certaines demandes. Certes, le scénario ici est réalisable, mais je pense qu'il est probablement trop compliqué à traiter dans un sens général. Je peux voir qu'il y a de nombreux cas où ce qui se passe maintenant est tout à fait correct, alors que dans certaines applications, cela pourrait être un problème. Je suppose que la plupart du temps, vous n'auriez de toute façon qu'une seule demande en vol pour une session donnée.

C'est vraiment un problème qui est présent dans n'importe quelle ressource Web, la même chose se produirait si vous aviez un GET suivi d'un PUT basé sur les informations, les choses peuvent avoir changé entre-temps.

Quoi qu'il en soit, ce sont mes pensées pour le moment, mais je suis heureux de discuter davantage.

Il semble qu'à long terme, il s'agisse davantage d'un problème de domaine d'application.

Compte tenu du scénario affiché, si la requête 1 est censée s'exécuter suffisamment longtemps pour que la requête 2 puisse se produire en parallèle (par exemple, une application à page unique écrite en Angular.js ou une application asynchrone Node.js touchant une API), il semble que le la demande de longue durée doit être découplée de la session et considérée comme un processus de travail indépendant à ce stade.

Rien de tout cela ne résout directement le problème, bien sûr, mais comme Kisielk l'a mentionné, faire n'importe quel type de verrouillage ajoute beaucoup de frais généraux pour ce qui semble être un cas marginal.

voici quelques exemples concrets de l'impact des conditions de course de session. Les bogues sont difficiles à déboguer, apparaissent apparemment au hasard et sont une gêne pour les développeurs et les utilisateurs. Étant donné une application suffisamment large qui utilise largement l'ajax, ces conditions de course apparaîtront tôt ou tard.
http://www.hiretheworld.com/blog/tech-blog/codeigniter-session-race-conditions
http://www.chipmunkninja.com/Troubles-with-Asynchronous-Ajax-Requests-g@

J'ai lu tout le fil de discussion sur EllisLab / CodeIgniter # 1746 qui traite d'un problème similaire, mais leur problème est compliqué par le fait que CodeIgniter régénère de temps en temps l'identifiant de session et la majeure partie de la discussion tourne autour de ce fait .

Je comprends votre réticence à modifier les sessions de manière incompatible ou à introduire des différences subtiles dans l'API qui pourraient interrompre les applications existantes. Cependant, je pense toujours qu'il devrait y avoir un verrouillage optionnel impliqué. Qu'en est-il d'ajouter deux fonctions à l'API du magasin comme ceci:

type Store interface {
    Get(r *http.Request, name string) (*Session, error)
    New(r *http.Request, name string) (*Session, error)
    Save(r *http.Request, w http.ResponseWriter, s *Session) error
    Lock(r *http.Request, name string) error
    Release(r *http.Request, name string) error
}

L'utilisation de ces fonctions serait entièrement facultative (bien que j'encourage personnellement à verrouiller chaque requête qui va écrire dans la session) et n'aura aucun impact sur les applications existantes.

Il y a un problème avec l'interface de verrouillage proposée: si le verrou est conservé dans une base de données telle que MySQL et que votre connexion à la base de données tombe après avoir acquis le verrou, vous ne pouvez jamais le libérer et votre session est effectivement bloquée. Les verrous devraient expirer d'une manière ou d'une autre, mais je ne suis pas tout à fait sûr de savoir comment cela devrait être exposé au développeur.

Désolé, j'ai manqué la réponse de Boj quand j'ai posté la mienne:
Il est important de garder à l'esprit que les demandes de longue durée ne sont pas une exigence pour déclencher une condition de concurrence. Le sommeil de 500 ms dans mon cas de test n'est là que pour s'assurer que la condition de concurrence est déclenchée. Dans le monde réel, ces conditions de concurrence se produiront également pour les requêtes "courtes", un peu moins fréquemment, et c'est exactement ce qui les rend difficiles à déboguer.

Vous devez également garder à l'esprit que sous une charge élevée, même les demandes courtes peuvent prendre un certain temps à s'exécuter.

@cless Bons points.

J'aime vos modifications d'interface proposées.

Votre expiration de verrouillage serait relativement facile à implémenter pour RediStore, Redis a une commande EXPIRE qui permet de définir un TTL pour une clé.

Ce type de verrouillage à grain fin ne serait-il pas encore assez inefficace pour de nombreux types de magasins de session? Et l'expiration est également un problème.

Il semble qu'une partie du problème est que Save peut réussir même si la session existe plus longtemps, ne contribuerait-il pas à résoudre le problème si ce n'était pas le cas?

@kisielk , pouvez-vous donner des exemples de magasins qui seraient inefficaces? La plupart des bases de données clé-valeur ont une forme d'opérations atomiques qui vous permettent de créer des verrous. Les bases de données SQL ont généralement accès au verrouillage basé sur les lignes (même si je dois dire que je ne suis pas familier avec les détails ici).
La méthode de verrouillage des magasins de valeurs clés comme redis nécessite cependant plusieurs allers-retours vers la base de données, c'est peut-être ce que vous vouliez dire?
Il y a un problème avec les magasins de système de fichiers car go ne nous fournit pas un moyen multiplateforme pour accéder aux verrous de fichiers. Je suppose que cgo vous permettrait d'en créer une qui prend en charge les principales plates-formes, mais c'est probablement une solution que j'éviterais.

Je ne pense pas que sauvegarder lorsque la session n'existe plus est un vrai problème à moins que vous ne parliez de supprimer la session en cours et de la remplacer par une nouvelle session pour éviter la fixation de session, mais pour être honnête, c'est un problème entièrement différent.

@boj , le problème est que le programmeur a besoin d'un moyen de vérifier qu'un verrou n'a pas encore expiré avant d'essayer de sauvegarder. Quelle doit être l'heure d'expiration d'une serrure?

C'est une façon de procéder, mais je ne suis pas sûr que je l'aime beaucoup. Vous dépendez du fait que l'horloge ne saute pas en avant ou en arrière soudainement et ce n'est pas une hypothèse sûre à faire:

func Lock(expiration time.Duration) time.Time {
    // Block here until the lock becomes your. Set the lock to expire after
    // dur+50ms. This extra 50 ms ensures that you never assume you own an
    // expired lock
    return time.Now().Add(expiration)
}

func main() {
    end := Lock(1000 * time.Millisecond)

    // Do your thing here ...
    // time.Sleep(1100 * time.Millisecond)

    if time.Now().After(end) {
        fmt.Println("Lock expired, throw an error")
    } else {
        fmt.Println("Good to go, save the session")
    }
}

@cless Tout votre commentaire résume en quelque sorte ce qui semble être les inquiétudes de

Il pourrait très bien être simple d'implémenter la partie interface de gorilla / sessions comme vous l'avez proposé, cependant, vous créez un scénario dans lequel vous pouvez ou non pouvoir échanger facilement des backends à moins qu'ils ne mettent en œuvre Lock / Release de manière tout aussi simple. et de manière efficace. Il semble y avoir trop de et de si en ce qui concerne ce que vous proposez, le principal étant "le backend peut-il même l'implémenter sans recourir à une programmation hackish?", Suivi de "J'ai essayé de passer de FileSystemStore à FooBarStore, mais ce n'est pas le cas semblent implémenter Lock / Release et mon programme ne se comporte pas comme prévu. "

Il y a un bon article sur les nuances du verrouillage par session ici:

http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/

Comme nous l'avons déjà conclu ici, le délai d'attente des verrous en cas de problème est l'un des principaux problèmes de mise en œuvre de ce type de schéma. Cela nécessite plus de réflexion et dépend beaucoup du backend

En ce qui concerne l'implémentation de Lock / Release, nous pourrions rendre l'implémentation du verrou par session facultative en ayant une interface séparée. Le backend pourrait alors être affirmé contre cette interface et s'il ne l'implémentait pas, nous pourrions fournir une implémentation par défaut en utilisant des verrous en mémoire.

Un autre problème est bien sûr que les utilisateurs de la bibliothèque devront mettre à jour leur code pour utiliser Lock and Release, mais je suppose que sans cela, ils auraient le même comportement que maintenant.

"nous pourrions fournir une implémentation par défaut en utilisant des verrous en mémoire"

Cela n'aurait aucun sens dans un environnement multi-serveurs où les demandes sont équilibrées en charge.

J'aime l'idée de fournir une interface entièrement différente pour le verrouillage, car elle vous donne plus de liberté pour l'implémenter correctement sans modifier le comportement du code existant qui utilise des sessions.

Comme @boj l'a noté, vous ne pouvez pas vraiment fournir de verrous de mémoire sur un magasin de base de données de manière significative. Le fardeau de fournir la fonctionnalité de verrouillage devrait probablement incomber au développeur du magasin. Si l'interface des verrous est manquante, vous pouvez toujours renvoyer des erreurs lorsque la session tente de les utiliser. Cela ne devrait vraiment pas être un problème, si les magasins par défaut fournissent un verrouillage, d'autres développeurs suivront et les utilisateurs seront peu, voire pas du tout, incommodés.

Les verrous de mémoire peuvent toujours avoir du sens dans certains magasins, et je pense que le FilesystemStore est un bon candidat pour eux. En réalité, vous n'utiliserez pas le stockage du système de fichiers dans une situation de charge équilibrée (bien que théoriquement possible avec les systèmes de fichiers réseau). Les verrous de système de fichiers semblent préférables, mais ils semblent difficiles à implémenter entre plates-formes et en plus de cela, ils ne sont pas non plus garantis de fonctionner avec NFS.

@boj : d'accord, mais comme @cless le souligne, cela

Je suis totalement d'accord avec @cless et @kisielk pour avoir une nouvelle interface (avoir les fonctions «Verrouiller» et «Libérer» a du sens. Pouvons-nous l'avoir s'il vous plaît? Go devrait être le langage à utiliser pour des performances et une fiabilité élevées. désireux de contribuer à certaines implémentations (FileSystem, Redis et Memcache) par exemple. Notez que nous avons besoin de quelques nouvelles options de configuration:

  • spinLockWait: en millisecondes de veille entre les tentatives d'acquisition du verrou (par défaut: 150)
  • lockMaxWait: secondes pour attendre un verrou (par défaut: 30)
  • lockMaxAge: secondes pendant lesquelles un verrou peut être conservé avant qu'il ne soit libéré automatiquement (par défaut: lockMaxWait)

Ce problème a été automatiquement marqué comme obsolète car il n'a pas vu de mise à jour récente. Il sera automatiquement fermé dans quelques jours.

Cette page vous a été utile?
0 / 5 - 0 notes