Race condition - Dart

Race condition - Dart

Need

Prevention of race conditions in the system

Context

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

Description

Non compliant code

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

Future 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

  • Introduce a locking mechanism to ensure that the 'startProcess' and 'endProcess' functions are not executed concurrently.
  • 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.
  • Ensure that the 'endProcess' function can only be called after the 'startProcess' function has completed.
  • Consider using a queue to manage the order of function calls if the order of requests is important.

Compliant code

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

Future 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