Contenidos
Una condición de carrera ocurre cuando dos operaciones deben ocurrir en un orden específico, pero se pueden realizar en el orden opuesto.
Por ejemplo, en una aplicación de subprocesos múltiples, dos subprocesos separados podrían acceder a una variable común. Como resultado, si un subproceso cambia el valor de la variable, el otro aún puede usar la versión anterior, ignorando el valor más nuevo. Esto provocará resultados no deseados.
Para comprender mejor este modelo, sería bueno echar un vistazo más de cerca al proceso de cambio de proceso del procesador.
Cómo un procesador cambia los procesos
Los sistemas operativos modernos pueden ejecutar más de un proceso al mismo tiempo, lo que se denomina multitarea. Cuando observa este proceso en términos del ciclo de ejecución de la CPU, puede encontrar que la multitarea realmente no existe.
En cambio, los procesadores cambian constantemente entre procesos para ejecutarlos simultáneamente o al menos actúan como si lo estuvieran haciendo. La CPU puede abortar un proceso antes de que se haya completado y reanudar un proceso diferente. El sistema operativo controla la gestión de estos procesos.
Por ejemplo, el algoritmo Round Robin, uno de los algoritmos de conmutación más simples, funciona de la siguiente manera:
Por lo general, este algoritmo permite que cada proceso se ejecute durante períodos de tiempo muy cortos, según lo determine el sistema operativo. Por ejemplo, esto podría ser un período de dos microsegundos.
La CPU toma cada proceso por turno y ejecuta comandos que se ejecutarán durante dos microsegundos. Luego continúe con el siguiente proceso, independientemente de si el actual está terminado o no. Por lo tanto, desde la perspectiva del usuario final, parece que se está ejecutando más de un proceso al mismo tiempo. Sin embargo, cuando miras detrás de escena, la CPU todavía está haciendo las cosas en orden.
Por cierto, como muestra el diagrama anterior, el algoritmo Round Robin no tiene noción de optimización o prioridad de procesamiento. En consecuencia, es un método bastante rudimentario que rara vez se usa en sistemas reales.
Ahora, para comprender mejor todo esto, imagine que se están ejecutando dos subprocesos. Si los hilos acceden a una variable común, puede ocurrir una condición de carrera.
Una aplicación web de muestra y una condición de carrera
Consulte la sencilla aplicación Flask a continuación para reflexionar sobre un ejemplo concreto de todo lo que ha leído hasta ahora. El objetivo de esta aplicación es gestionar las transacciones de dinero que se van a realizar en la web. Guarde lo siguiente en un archivo llamado dinero.py:
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemyapp = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(app)
class Account(db.Model):
id = db.Column(db.Integer, primary_key = True)
amount = db.Column(db.String(80), unique = True)
def __init__(self, count):
self.amount = amount
def __repr__(self):
return '' % self.amount
@app.route("/")
def hi():
account = Account.query.get(1)
return "Total Money = {}".format(account.amount)
@app.route("/send/")
def send(amount):
account = Account.query.get(1)
if int(account.amount) < amount:
return "Insufficient balance. Reset money with /reset!)"
account.amount = int(account.amount) - amount
db.session.commit()
return "Amount sent = {}".format(amount)
@app.route("/reset")
def reset():
account = Account.query.get(1)
account.amount = 5000
db.session.commit()
return "Money reset."
if __name__ == "__main__":
app.secret_key = 'heLLoTHisIsSeCReTKey!'
app.run()
Para ejecutar este código, deberá crear un registro en la tabla de cuentas y continuar las transacciones en este registro. Como puede ver en el código, este es un entorno de prueba, por lo que realiza transacciones con el primer registro de la tabla.
from money import db
db.create_all()
from money import Account
account = Account(5000)
db.session.add(account)
db.session.commit()
Ahora ha creado una cuenta con un saldo de $ 5,000. Finalmente, ejecute el código fuente anterior con el siguiente comando, siempre que tenga instalados los paquetes Flask y Flask-SQLAlchemy:
python money.py
Luego, tiene la aplicación web Flask ejecutando un proceso de extracción simple. Esta aplicación puede realizar las siguientes operaciones con enlaces de solicitud GET. Dado que Flask se ejecuta en el puerto 5000 de forma predeterminada, la dirección a la que se accede es 127.0.0.1:5000/. La aplicación proporciona los siguientes puntos finales:
- 127.0.0.1:5000/ muestra el saldo actual.
- 127.0.0.1:5000/invio/parmiimporto} resta la cantidad de la cuenta.
- 127.0.0.1:5000/ restablecer restablecer la cuenta a $ 5,000.
Ahora, en esta etapa, puede examinar cómo se produce la vulnerabilidad de la condición de carrera.
Probabilidad de una vulnerabilidad vinculada a una condición de carrera
La aplicación web anterior contiene una posible vulnerabilidad de condición de carrera.
Imagine que tiene $ 5,000 para comenzar y cree dos solicitudes HTTP diferentes que enviarán $ 1. Para esto, puede enviar dos solicitudes HTTP diferentes al enlace 127.0.0.1:5000/enviar/1. Suponga que tan pronto como el servidor web procesa la primera solicitud, la CPU detiene este proceso y procesa la segunda solicitud. Por ejemplo, el primer proceso puede fallar después de ejecutar la siguiente línea de código:
account.amount = int(account.amount) - amount
Este código ha calculado un nuevo total pero aún no ha guardado el registro en la base de datos. Cuando comience la segunda solicitud, realizará el mismo cálculo, restando $ 1 del valor en la base de datos – $ 5,000 – y almacenando el resultado. Cuando se reanude el primer proceso, almacenará su valor, $ 4999, que no reflejará el saldo de cuenta más reciente.
Luego, se completaron dos solicitudes y cada una tendría que restar $ 1 del saldo de la cuenta, lo que resultó en un nuevo saldo de $ 4,998. Pero, dependiendo del orden en que el servidor web los procese, el saldo final de la cuenta puede ser de $4.999.
Imagine enviar 128 solicitudes para realizar una transferencia de $ 1 al sistema de destino en un marco de tiempo de cinco segundos. Como resultado de esta transacción, el extracto bancario esperado será de $ 5000 – $ 128 = $ 4875. Sin embargo, debido a las condiciones de la carrera, el saldo final podría oscilar entre $ 4.875 y $ 4.999.
Los programadores son uno de los componentes más importantes de la seguridad.
En un proyecto de software, como programador, tienes algunas responsabilidades. El ejemplo anterior fue para una aplicación simple de transferencia de dinero. Imagine que está trabajando en un proyecto de software que administra una cuenta bancaria o el backend de un gran sitio de comercio electrónico.
Debe estar familiarizado con dichas vulnerabilidades para que el programa que escribió para protegerlas esté libre de vulnerabilidades. Esto requiere una fuerte responsabilidad.
Una vulnerabilidad de condición de carrera es solo una de ellas. Independientemente de la tecnología que utilice, debe conocer las vulnerabilidades en el código que escribe. Una de las habilidades más importantes que puede adquirir como programador es la familiaridad con la seguridad del software.