Race condition - Go

Race condition - Go

Need

Prevention of race conditions in the system

Context

  • Usage of Go 1.16 for developing scalable and efficient applications
  • Usage of Gin framework for building web applications in Go

Description

Non compliant code

        package main

import (
	"sync"
	"time"
	"github.com/gin-gonic/gin"
)

var counter int

func main() {
	r := gin.Default()
	var mu sync.Mutex

	r.GET("/increment", func(c *gin.Context) {
		go func() {
			mu.Lock()
			counter++
			mu.Unlock()
		}()
		c.JSON(200, gin.H{
			"message": "Incremented!",
		})
	})

	r.GET("/get_counter", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"counter": counter,
		})
	})

	r.Run() 
}
        
        

In the code above, we have a simple server with two routes: /increment and /get_counter.

The /increment route starts a new goroutine to increment a global variable counter. It uses a mutex mu to lock the critical section where the increment operation is performed. However, the mutex lock and unlock are both within the goroutine.

This introduces a race condition. If the /increment route is hit multiple times in quick succession, multiple goroutines may be spawned to perform the increment operation. Since the mutex lock and unlock are both within the goroutine, the lock may not be effective in preventing other goroutines from entering the critical section.

This can lead to the counter variable not being incremented correctly. For example, if two goroutines enter the critical section at the same time, they might both read the counter as 0, increment it to 1, and write back 1. This results in the counter being 1 instead of 2, even though the /increment route was hit twice.

The /get_counter route simply returns the current value of the counter. This value may not be accurate due to the race condition described above.

Steps

  • Identify the critical section of code where the race condition is occurring.
  • Use a synchronization mechanism to protect the critical section from concurrent access.
  • In this case, the vulnerable code is using a global counter variable, so we can use a mutex to synchronize access to it.
  • Create a mutex object to control access to the counter variable.
  • Before incrementing the counter, lock the mutex to ensure exclusive access.
  • After incrementing the counter, unlock the mutex to allow other goroutines to access it.
  • Repeat steps 5 and 6 for any other code that accesses or modifies the counter variable.
  • Test the application to ensure that the race condition has been resolved.

Compliant code

        package main

import (
	"sync"
	"github.com/gin-gonic/gin"
)

var counter int
var mu sync.Mutex

func main() {
	r := gin.Default()

	r.GET("/increment", func(c *gin.Context) {
		mu.Lock()
		defer mu.Unlock()
		counter++
		c.JSON(200, gin.H{
			"message": "Incremented!",
		})
	})

	r.GET("/get_counter", func(c *gin.Context) {
		mu.Lock()
		defer mu.Unlock()
		c.JSON(200, gin.H{
			"counter": counter,
		})
	})

	r.Run() 
}
        
        

The original code had a race condition vulnerability due to the concurrent access of the global variable counter. This could lead to unexpected behavior if the /increment and /get_counter endpoints were accessed simultaneously from different goroutines.

The fixed code uses a mutex, mu, to synchronize access to the counter variable. This ensures that only one goroutine can access the counter at a time, preventing the race condition.

In the /increment endpoint, the mutex is locked before the counter is incremented and unlocked immediately after. The defer keyword is used to ensure that the mutex is unlocked even if an error occurs. This prevents a potential deadlock situation where the mutex remains locked indefinitely.

Similarly, in the /get_counter endpoint, the mutex is locked before the counter is accessed and unlocked immediately after. This ensures that the value of counter cannot be changed by another goroutine while it is being read.

By using a mutex to control access to the counter variable, the race condition vulnerability is resolved. The application should be tested thoroughly to ensure that the fix is effective and does not introduce new issues.

References