Sessions: Condición de carrera en FilesystemStore

Creado en 11 jun. 2013  ·  23Comentarios  ·  Fuente: gorilla/sessions

Hay una condición de carrera en FilesystemStore que tengo la intención de corregir, pero me gustaría recibir su opinión antes de seguir adelante y hacerlo. Básicamente, el problema es que si tiene solicitudes simultáneas del mismo usuario (misma sesión), es posible lo siguiente:

  1. La solicitud 1 abre la sesión para realizar una operación semi larga
  2. La solicitud 2 abre la sesión
  3. Solicitud 2 Elimina los datos de la sesión para realizar el "cierre de sesión" o similar
  4. Solicitar 2 guardados
  5. La solicitud 1 se guarda, lo que hace que sea como si la sesión nunca se hubiera cerrado

He agregado un caso de prueba para esta falla en cless / sessions @ f84abeda17de0b4fcd72d277412f3d3192f206f2

La forma más sencilla de solucionar este problema sería mediante la introducción de bloqueos en el nivel del sistema de archivos. Sin embargo, golang no tiene una forma multiplataforma de bloquear archivos. Expone flock en syscall pero eso solo funciona si el sistema operativo lo admite. Creo que el comportamiento de flock también puede ser diferente en diferentes Unix, aunque no estoy seguro de que este sea el caso. Otro problema con flock es que podría no funcionar en NFS.

Una solución completamente diferente sería mantener un mapa de bloqueos en el propio objeto FilesystemStore . Esto tiene otro conjunto de desventajas: no puede hacer que varios procesos accedan a las mismas sesiones del sistema de archivos y no puede crear varias tiendas para la misma sesión del sistema de archivos dentro de una sola aplicación. Sin embargo, ambas cosas ya son imposibles de hacer sin causar problemas.

Al final, creo que la mejor solución es mantener un mapa de bloqueos en el objeto de la tienda porque todas las desventajas en ese escenario pueden documentarse adecuadamente y puede responder que el comportamiento es el mismo en diferentes sistemas.

Otros backends de almacenamiento que se basan en FilesystemStore pueden copiar esta falla (noté este problema al revisar el código de Redistore para un proyecto mío boj / redistore # 2)

bug stale

Todos 23 comentarios

En lugar de bloquear, ¿qué pasa con un sistema basado en transacciones? El objeto Session podría contener un campo lastModified. Cuando intenta volver a guardar la sesión en el almacén de sesiones, devolverá un error que indica que se ha modificado desde la última lectura. El programador podría optar por reintentar obteniendo la sesión nuevamente.

Eso podría funcionar, tiene la ventaja de que no hay bloqueos que deban limpiarse y no hay posibilidad de interbloqueos. No estoy seguro de que siempre sea posible que los desarrolladores realicen un reintento significativo dependiendo de lo que ya haya hecho la solicitud. Creo que el bloqueo es definitivamente la opción más segura, pero las transacciones ciertamente podrían funcionar si la API documenta claramente la posibilidad de falla y si los desarrolladores manejan esos errores con diligencia.

Dicho esto, incluso si FilesystemStore usa transacciones, creo que la API debería estar preparada para backends de almacenamiento que _hacen_ usar bloqueo, pero eso significaría rechazar llamar a session.Save() más de una vez _o_ introducir un nuevo session.Release() . ¿Cuál preferiría o preferiría que ningún backend de almacenamiento use el bloqueo?

Siguiendo este problema, ya que afecta a RediStore. Gracias por la información @cless

Realmente no tengo ninguna preferencia en este momento más que hacer lo correcto a largo plazo :) Por supuesto, también me gustaría mantener la API existente. Además, agregar cerraduras por todas partes introduce mucha complejidad y gastos generales adicionales, que también sería bueno evitar.

¿Tiene algún ejemplo de otros marcos de sesión que resuelvan este problema? Tendría curiosidad por ver qué hacen.

Sé que el controlador de sesión php predeterminado usa el bloqueo basado en el sistema de archivos (en el tarball de php 5.4 acabo de verificar que esto está en el archivo ext/session/mod_files.c . Usa flock que es proporcionado por ext/standard/flock_compat.c ).

No estoy seguro de si php es un buen ejemplo dada su reputación, pero me temo que es el único que conozco en este momento. Intentaré echar un vistazo para ver si puedo encontrar otros marcos y ver cómo resuelven el problema.

Tendría curiosidad por ver cómo Flask, Pyramid o Django manejan estas cosas. Los echaré un vistazo si tengo tiempo. Rails también sería interesante si alguien está familiarizado con ese código base.

Tanto Pyramid como Flask parecen estar usando una cookie para almacenar los datos, sospecho que ninguno maneja las condiciones de carrera. Solo he leído brevemente la documentación, por lo que podría estar equivocado.

Django admite datos de sesión del lado del servidor, así que echaré un vistazo a ese código en un momento.

El backend predeterminado de la sesión de Django es la base de datos y utiliza la capa de abstracción de la base de datos de Django con la que no estoy familiarizado, por lo que es difícil de interpretar. Sin embargo, encontré este comentario en el backend del sistema de archivos:

        # 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.

Esto parece sugerir que se aseguren de que el contenido del archivo de sesión nunca se altere, pero no que no puedan ocurrir condiciones de carrera. Si alguien tiene experiencia con Django, sería bueno ver un caso de prueba como cless / sessions @ f84abed
Stackoverflow también parece indicar que Django podría tener condiciones de carrera en las sesiones: http://stackoverflow.com/search?q=django+session+race+condition

Bien, bien FileSystemStore ya tiene un mutex de grano grueso para evitar la corrupción del almacén de sesiones. No estoy seguro de cuánta protección adicional vale la pena poner en la biblioteca porque agregaría mucha sobrecarga para cada solicitud, solo para hacer las cosas más consistentes para algunas solicitudes. Ciertamente, el escenario aquí es factible, pero creo que probablemente sea demasiado complicado de manejar en un sentido general. Puedo ver que hay muchos casos en los que lo que sucede ahora está totalmente bien, mientras que en algunas aplicaciones podría ser un problema. Sospecho que la mayoría de las veces solo tendría una solicitud en vuelo para una sesión determinada de todos modos.

Realmente es un problema que está presente en cualquier recurso web, lo mismo sucedería si tuviera un GET seguido de un PUT basado en la información, las cosas pueden haber cambiado mientras tanto.

De todos modos, esos son mis pensamientos en este momento, pero me alegra discutir más.

Parece que a largo plazo esto es más un problema de dominio de aplicación.

Dado el escenario cless publicado, si se espera que la Solicitud 1 se ejecute el tiempo suficiente para que la Solicitud 2 pueda suceder en paralelo (digamos, una Aplicación de Página Única escrita en Angular.js, o una aplicación Node.js asíncrona que golpea alguna API), parece que La solicitud de larga duración debe desacoplarse de la sesión y considerarse un proceso de trabajo independiente en ese momento.

Nada de esto aborda directamente el problema, por supuesto, pero como mencionó kisielk, hacer cualquier tipo de bloqueo agrega mucha sobrecarga para lo que parece ser un caso marginal.

aquí hay algunos ejemplos del mundo real del impacto de las condiciones de la carrera de sesión. Los errores son difíciles de depurar, aparecen aparentemente al azar y son una molestia tanto para los desarrolladores como para los usuarios. Dada una aplicación lo suficientemente grande que hace un uso extensivo de ajax, estas condiciones de carrera aparecerán tarde o temprano.
http://www.hiretheworld.com/blog/tech-blog/codeigniter-session-race-conditions
http://www.chipmunkninja.com/Troubles-with-Asynchronous-Ajax-Requests-g@

He leído todo el hilo en EllisLab / CodeIgniter # 1746 que trata con un problema similar, pero su problema se complica por el hecho de que CodeIgniter regenera a la fuerza la identificación de la sesión de vez en cuando y la mayor parte de la discusión gira en torno a este hecho. .

Entiendo su renuencia a cambiar las sesiones de una manera incompatible o introducir diferencias sutiles en la API que podrían romper las aplicaciones existentes. Sin embargo, sigo pensando que debería haber algún bloqueo opcional involucrado. ¿Qué hay de agregar dos funciones a la API de la tienda como esta?

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
}

El uso de estas funciones sería completamente opcional (aunque personalmente recomendaría bloquear cada solicitud que se escribirá en la sesión) y no tendrá ningún impacto en las aplicaciones existentes.

Hay un problema con la interfaz de bloqueo propuesta: si el bloqueo se mantiene en una base de datos como MySQL y la conexión de su base de datos se interrumpe después de adquirir el bloqueo, nunca podrá liberarlo y su sesión estará efectivamente bloqueada. Los bloqueos deberían caducar de alguna manera, pero no estoy del todo seguro de cómo debería exponerse al desarrollador.

Siento haber perdido la respuesta de Boj cuando publiqué la mía:
Es importante tener en cuenta que las solicitudes de larga duración no son un requisito para activar una condición de carrera. El sueño de 500 ms en mi caso de prueba está ahí solo para garantizar que se active la condición de carrera. En el mundo real, estas condiciones de carrera ocurrirán también para solicitudes "cortas", pero con menos frecuencia, y eso es exactamente lo que las hace difíciles de depurar.

También debe tener en cuenta que con una carga alta, incluso las solicitudes cortas pueden tardar un tiempo en ejecutarse.

@cless Buenos puntos.

Me gustan los cambios de interfaz propuestos.

El vencimiento de su bloqueo sería relativamente fácil de implementar para RediStore, Redis tiene un comando EXPIRE que permite configurar un TTL para una clave.

¿No sería este tipo de bloqueo de grano fino todavía bastante ineficaz para muchos tipos de tiendas de sesión? Y la caducidad también es un problema.

Parece que parte del problema es que Save puede tener éxito incluso si la sesión ya existe, ¿no sería de gran ayuda para resolver el problema si ese no fuera el caso?

@kisielk , ¿puedes dar ejemplos de tiendas que serían ineficientes? La mayoría de las bases de datos de valores clave tienen algún tipo de operaciones atómicas que le permiten crear bloqueos. Las bases de datos SQL generalmente tienen acceso al bloqueo basado en filas (aunque debo decir que no estoy familiarizado con los detalles aquí).
Sin embargo, el método de bloqueo de las tiendas de valores clave como redis requiere varios viajes de ida y vuelta a la base de datos, ¿quizás eso es lo que quería decir?
Existe un problema con las tiendas del sistema de archivos porque go no nos proporciona una forma multiplataforma para acceder a los bloqueos de archivos. Supongo que cgo le permitiría crear uno que admita las plataformas principales, pero esa es probablemente una solución que evitaría.

No creo que guardar cuando la sesión ya no existe sea un problema real a menos que esté hablando de eliminar la sesión actual y reemplazarla con una nueva sesión para evitar la fijación de la sesión, pero para ser honesto, ese es un problema completamente diferente.

@boj , el problema es que el programador necesita alguna forma de verificar que un bloqueo aún no haya expirado antes de intentar guardar. ¿Cuál debería ser el tiempo de vencimiento de una cerradura?

Esta es una forma posible de hacerlo, pero no estoy seguro de que me guste mucho. Depende de que el reloj no salte hacia adelante o hacia atrás de repente y esa no es una suposición segura:

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 Todo tu comentario resume lo que parecen ser las preocupaciones de @kisielk . X hace esto, Y hace aquello, Z podría no ser posible a menos que estén involucrados algunos extremos. Si bien las condiciones de carrera no son deseables dados los raros casos en que pueden ocurrir, al menos el diseño actual de gorila / sesiones es muy simple y sigue el elemento de la filosofía de la menor sorpresa.

Podría ser muy sencillo implementar la parte de la interfaz de gorilla / sessions como propusiste, sin embargo, creas un escenario en el que puedes o no puedes intercambiar fácilmente backends a menos que implementen Lock / Release de una manera igualmente fácil y de manera eficiente. Parece que hay demasiados y si con respecto a lo que propones, el más importante es "¿puede el backend incluso implementar esto sin recurrir a la programación pirateada?", Seguido de "Intenté cambiar de FileSystemStore a FooBarStore, pero no parece implementar Bloquear / Liberar y mi programa no se comporta como se esperaba ".

Hay un buen artículo sobre los matices del bloqueo por sesión aquí:

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

Como ya hemos concluido aquí, el tiempo de espera para los bloqueos si algo sale mal es uno de los principales problemas al implementar este tipo de esquema. Esto necesita más reflexión y depende mucho del backend

En cuanto a la implementación de bloqueo / liberación, podríamos hacer que la implementación de bloqueo por sesión sea opcional al tener una interfaz separada. El backend podría afirmarse contra esa interfaz y, si no lo implementa, podríamos proporcionar una implementación predeterminada utilizando bloqueos en memoria.

Otro problema, por supuesto, es que los usuarios de la biblioteca necesitarán actualizar su código para usar Lock and Release, pero supongo que sin eso tendrían el mismo comportamiento que ahora.

"Podríamos proporcionar una implementación predeterminada utilizando bloqueos en memoria"

Esto no tendría sentido en un entorno de varios servidores donde las solicitudes tienen un equilibrio de carga.

Me gusta la idea de proporcionar una interfaz de bloqueo completamente diferente porque te permite más libertad para implementarla correctamente sin alterar el comportamiento del código existente que usa sesiones.

Como señaló @boj , realmente no puede proporcionar bloqueos de memoria en un almacén de base de datos de una manera significativa. La carga de proporcionar la funcionalidad de bloqueo probablemente debería recaer en el desarrollador de la tienda. Si falta la interfaz de bloqueos, aún puede devolver errores cuando la sesión intente usarlos. Eso realmente no debería ser un problema, si las tiendas predeterminadas proporcionan bloqueo, entonces otros desarrolladores seguirán su ejemplo y los usuarios serán mínimamente, si acaso, molestos.

Los bloqueos de memoria todavía pueden tener sentido en algunas tiendas, y creo que FilesystemStore es un buen candidato para ellos. De manera realista, no usará el almacén del sistema de archivos en una situación de equilibrio de carga (aunque teóricamente es posible con sistemas de archivos de red). Los bloqueos del sistema de archivos parecen preferibles, pero parecen ser difíciles de implementar en plataformas cruzadas y, además, tampoco se garantiza que funcionen con NFS.

@boj : de acuerdo, pero como señala @cless , eso se basa en el tipo de tienda que tienes. FilesystemStore ya se basa en un RWMutex para evitar modificaciones simultáneas del almacén de sesiones. Espero que la mayoría de los backends realmente implementen la interfaz de bloqueo al menos eventualmente, pero tener una configuración predeterminada permitiría a un desarrollador lanzar una versión adecuada para un entorno de servidor único mientras tanto. FilesystemStore sería uno de esos hasta que mejore la situación de bloqueo multiplataforma en las bibliotecas de Go.

Estoy totalmente de acuerdo con @cless y quiero enfatizar la importancia del bloqueo de sesión y las plataformas maduras (como PHP) lo implementan en sus tiendas de sesión. En los archivos, se usa el syscall 'flock' y en Memcache el valor de retorno de la operación 'agregar' y una clave adicional con un sufijo '.lock' y un conjunto de expiración. También estoy de acuerdo con el enfoque de @kisielk de tener una nueva interfaz (tener las funciones 'Bloquear' y 'Liberar' tiene sentido. ¿Podemos tener esto, por favor? Go debería ser el idioma al que acudir para obtener un alto rendimiento y confiabilidad. dispuesto a contribuir con algunas implementaciones (FileSystem, Redis y Memcache) por ejemplo. Tenga en cuenta que necesitamos algunas opciones de configuración nuevas:

  • spinLockWait: milisegundos para dormir entre intentos de adquirir el bloqueo (predeterminado: 150)
  • lockMaxWait: segundos para esperar un bloqueo (predeterminado: 30)
  • lockMaxAge: segundos en que se puede mantener un bloqueo antes de que se libere automáticamente (predeterminado: lockMaxWait)

Este problema se ha marcado automáticamente como obsoleto porque no se ha actualizado recientemente. Se cerrará automáticamente en unos días.

¿Fue útil esta página
0 / 5 - 0 calificaciones