logo

Race condition - Dart


Need

Prevention of race conditions in the system


Context

  1. Usage of Dart 2.0 for building scalable and performant applications
  2. Usage of dart:async for asynchronous programming in Dart
  3. Usage of Shelf for managing data storage and retrieval
  4. Usage of shelf_io for managing and organizing data in a Node.js application

Description

Insecure Code Example

import 'dart:async';
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as io;

shelf.Response handleRequest(shelf.Request request) {
  var path = request.url.path;
  if (path == 'start') {
    startProcess();
  } else if (path == 'end') {
    endProcess();
  }
  return shelf.Response.ok('Request handled');
}

Future<void> startProcess() async {
  // some time-consuming process
  await Future.delayed(Duration(seconds: 2));
  print('Process started');
}

Future<void> endProcess() async {
  // some time-consuming process
  await Future.delayed(Duration(seconds: 2));
  print('Process ended');
}

void main() {
  var handler = const shelf.Pipeline().addMiddleware(shelf.logRequests()).addHandler(handleRequest);

  io.serve(handler, 'localhost', 8080).then((server) {
    print('Serving at http://${server.address.host}:${server.port}');
  });
}

In the above code snippet, we have a server application that uses the Shelf framework. This application has two endpoints: `/start` and `/end`. When a request is received at the `/start` endpoint, it initiates a process (simulated by a delay of 2 seconds), and when a request is received at the `/end` endpoint, it ends the process (also simulated by a delay of 2 seconds). The vulnerability lies in the fact that these two processes are independent and don't check the state of each other. This leads to a race condition where if the `/end` request arrives before the `/start` request has completed, the system will try to end a process that hasn't started yet, leading to unexpected behavior. For example, if two requests are made almost simultaneously, one to `/start` and one to `/end`, there's no guarantee that the start process will complete before the end process begins. If the end process tries to end a process that hasn't started, it could lead to errors or unexpected behavior. This is a classic example of a race condition, where the order and timing of events can affect the system's behavior. In this case, the system assumes that the `/start` request will always complete before the `/end` request is received, which may not always be the case.

Steps

  1. Introduce a locking mechanism to ensure that the 'startProcess' and 'endProcess' functions are not executed concurrently.
  2. Use a boolean flag to track if a process is currently running. If a process is running, the 'startProcess' function should not be able to start another one.
  3. Ensure that the 'endProcess' function can only be called after the 'startProcess' function has completed.
  4. Consider using a queue to manage the order of function calls if the order of requests is important.

Secure Code Example

import 'dart:async';
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as io;

bool isProcessRunning = false;

shelf.Response handleRequest(shelf.Request request) {
  var path = request.url.path;
  if (path == 'start') {
    if (!isProcessRunning) {
      startProcess();
    }
  } else if (path == 'end') {
    if (isProcessRunning) {
      endProcess();
    }
  }
  return shelf.Response.ok('Request handled');
}

Future<void> startProcess() async {
  isProcessRunning = true;
  // some time-consuming process
  await Future.delayed(Duration(seconds: 2));
  print('Process started');
}

Future<void> endProcess() async {
  // some time-consuming process
  await Future.delayed(Duration(seconds: 2));
  print('Process ended');
  isProcessRunning = false;
}

void main() {
  var handler = const shelf.Pipeline().addMiddleware(shelf.logRequests()).addHandler(handleRequest);

  io.serve(handler, 'localhost', 8080).then((server) {
    print('Serving at http://${server.address.host}:${server.port}');
  });
}

The code above introduces a locking mechanism to prevent race conditions. A boolean flag `isProcessRunning` is used to track whether a process is currently running. In the `handleRequest` function, before starting a process with `startProcess`, it checks if a process is already running by checking the `isProcessRunning` flag. If a process is running, it will not start another one, preventing multiple processes from running concurrently. Similarly, before ending a process with `endProcess`, it checks if a process is currently running. If no process is running, it will not attempt to end a process, ensuring that the `endProcess` function can only be called after the `startProcess` function has completed. In the `startProcess` function, the `isProcessRunning` flag is set to true at the beginning, indicating that a process is running. In the `endProcess` function, the `isProcessRunning` flag is set to false at the end, indicating that no process is running. This solution ensures that the `startProcess` and `endProcess` functions are not executed concurrently, preventing race conditions. If the order of requests is important, consider using a queue to manage the order of function calls.


References

  • 124 - Race condition

  • Last updated

    2023/09/18