Main Content

CWE Rule 667

Improper Locking

Since R2024a

Description

Rule Description

The product does not properly acquire or release a lock on a resource, leading to unexpected resource state changes and behaviors.

Polyspace Implementation

The rule checker checks for these issues:

  • Blocking operation while holding lock

  • Destruction of locked mutex

  • Missing unlock

Examples

expand all

Issue

This checker is deactivated in a default Polyspace® as You Code analysis. See Checkers Deactivated in Polyspace as You Code Analysis (Polyspace Access).

This issue occurs when a task (thread) performs a potentially lengthy operation while holding a lock.

The checker considers calls to these functions as potentially lengthy:

  • Functions that access a network such as recv

  • System call functions such as fork, pipe and system

  • Functions for I/O operations such as getchar and scanf

  • File handling functions such as fopen, remove and lstat

  • Directory manipulation functions such as mkdir and rmdir

The checker automatically detects certain primitives that hold and release a lock, for instance, pthread_mutex_lock and pthread_mutex_unlock. For the full list of primitives that are automatically detected, see Auto-Detection of Thread Creation and Critical Section in Polyspace.

Risk

If a thread performs a lengthy operation when holding a lock, other threads that use the lock have to wait for the lock to be available. As a result, system performance can slow down or deadlocks can occur.

Fix

Perform the blocking operation before holding the lock or after releasing the lock.

Some functions detected by this checker can be called in a way that does not make them potentially lengthy. For instance, the function recv can be called with the parameter O_NONBLOCK which causes the call to fail if no message is available. When called with this parameter, recv does not wait for a message to become available.

Example — Network I/O Operations with recv While Holding Lock
#include <pthread.h>
#include <sys/socket.h>

pthread_mutexattr_t attr;
pthread_mutex_t mutex;
 
void thread_foo(void *ptr) {
  unsigned int num;
  int result;
  int sock;
 
  /* sock is a connected TCP socket */
 
  if ((result = pthread_mutex_lock(&mutex)) != 0) {
    /* Handle Error */
  }
 
  if ((result = recv(sock, (void *)&num, sizeof(unsigned int), 0)) < 0) { //Noncompliant
    /* Handle Error */
  }
 
  /* ... */
 
  if ((result = pthread_mutex_unlock(&mutex)) != 0) {
    /* Handle Error */
  }
}
 
int main() {
  pthread_t thread;
  int result;
 
  if ((result = pthread_mutexattr_settype(
      &attr, PTHREAD_MUTEX_ERRORCHECK)) != 0) {
    /* Handle Error */
  }
 
  if ((result = pthread_mutex_init(&mutex, &attr)) != 0) {
    /* Handle Error */
  }
 
  if (pthread_create(&thread, NULL,(void*(*)(void*))& thread_foo, NULL) != 0) {
    /* Handle Error */
  }
 
  /* ... */
 
  pthread_join(thread, NULL);
 
  if ((result = pthread_mutex_destroy(&mutex)) != 0) {
    /* Handle Error */
  }
 
  return 0;
}

In this example, in each thread created with pthread_create, the function thread_foo performs a network I/O operation with recv after acquiring a lock with pthread_mutex_lock. Other threads using the same lock variable mutex have to wait for the operation to complete and the lock to become available.

Correction — Perform Blocking Operation Before Acquiring Lock

One possible correction is to call recv before acquiring the lock.

#include <pthread.h>
#include <sys/socket.h>

pthread_mutexattr_t attr;
pthread_mutex_t mutex;
 
void thread_foo(void *ptr) {
  unsigned int num;
  int result;
  int sock;
 
  /* sock is a connected TCP socket */
  if ((result = recv(sock, (void *)&num, sizeof(unsigned int), 0)) < 0) {
    /* Handle Error */
  }
  
  if ((result = pthread_mutex_lock(&mutex)) != 0) {
    /* Handle Error */
  }
 
    /* ... */
 
  if ((result = pthread_mutex_unlock(&mutex)) != 0) {
    /* Handle Error */
  }
}
 
int main() {
  pthread_t thread;
  int result;
 
  if ((result = pthread_mutexattr_settype(
      &attr, PTHREAD_MUTEX_ERRORCHECK)) != 0) {
    /* Handle Error */
  }
 
  if ((result = pthread_mutex_init(&mutex, &attr)) != 0) {
    /* Handle Error */
  }
 
  if (pthread_create(&thread, NULL,(void*(*)(void*))& thread_foo, NULL) != 0) {
    /* Handle Error */
  }
 
  /* ... */
 
  pthread_join(thread, NULL);
 
  if ((result = pthread_mutex_destroy(&mutex)) != 0) {
    /* Handle Error */
  }
 
  return 0;
}
Issue

This checker is deactivated in a default Polyspace as You Code analysis. See Checkers Deactivated in Polyspace as You Code Analysis (Polyspace Access).

This issue occurs when a task destroys a mutex after it is locked (and before it is unlocked). The locking and destruction can happen in the same task or different tasks.

Risk

A mutex is locked to protect shared variables from concurrent access. If a mutex is destroyed in the locked state, the protection does not apply.

Fix

To fix this defect, destroy the mutex only after you unlock it. It is a good design practice to:

  • Initialize a mutex before creating the threads where you use the mutex.

  • Destroy a mutex after joining the threads that you created.

On the Result Details pane, you see two events, the locking and destruction of the mutex, and the tasks that initiated the events. To navigate to the corresponding line in your source code, click the event.

Example — Locking and Destruction in Different Tasks

#include <pthread.h>

pthread_mutex_t lock1;
pthread_mutex_t lock2;
pthread_mutex_t lock3;

void t0 (void) {
  pthread_mutex_lock (&lock1);
  pthread_mutex_lock (&lock2);
  pthread_mutex_lock (&lock3);
  pthread_mutex_unlock (&lock2);
  pthread_mutex_unlock (&lock1);
  pthread_mutex_unlock (&lock3);
}

void t1 (void) {
  pthread_mutex_lock (&lock1);
  pthread_mutex_lock (&lock2);
  pthread_mutex_destroy (&lock3); //Noncompliant
  pthread_mutex_unlock (&lock2);
  pthread_mutex_unlock (&lock1);
}

In this example, after task t0 locks the mutex lock3, task t1 can destroy it. The destruction occurs if the following events happen in sequence:

  1. t0 acquires lock3.

  2. t0 releases lock2.

  3. t0 releases lock1.

  4. t1 acquires the lock lock1 released by t0.

  5. t1 acquires the lock lock2 released by t0.

  6. t1 destroys lock3.

For simplicity, this example uses a mix of automatic and manual concurrency detection. The tasks t0 and t1 are manually specified as entry points by using the option Tasks (-entry-points). The critical sections are implemented through primitives pthread_mutex_lock and pthread_mutex_unlock that the software detects automatically. In practice, for entry point specification (thread creation), you will use primitives such as pthread_create. The next example shows how the defect can appear when you use pthread_create.

Correction — Place Lock-Unlock Pair Together in Same Critical Section as Destruction

The locking and destruction of lock3 occurs inside the critical section imposed by lock1 and lock2, but the unlocking occurs outside. One possible correction is to place the lock-unlock pair in the same critical section as the destruction of the mutex. Use one of these critical sections:

  • Critical section imposed by lock1 alone.

  • Critical section imposed by lock1 and lock2.

In this corrected code, the lock-unlock pair and the destruction is placed in the critical section imposed by lock1 and lock2. When t0 acquires lock1 and lock2, t1 has to wait for their release before it executes the instruction pthread_mutex_destroy (&lock3);. Therefore, t1 cannot destroy mutex lock3 in the locked state.


#include <pthread.h>

pthread_mutex_t lock1;
pthread_mutex_t lock2;
pthread_mutex_t lock3;

void t0 (void) {
  pthread_mutex_lock (&lock1);
  pthread_mutex_lock (&lock2);

  pthread_mutex_lock (&lock3);
  pthread_mutex_unlock (&lock3);

  pthread_mutex_unlock (&lock2);
  pthread_mutex_unlock (&lock1);
}

void t1 (void) {
  pthread_mutex_lock (&lock1);
  pthread_mutex_lock (&lock2);

  pthread_mutex_destroy (&lock3);

  pthread_mutex_unlock (&lock2);
  pthread_mutex_unlock (&lock1);
}

Example — Locking and Destruction in Start Routine of Thread
#include <pthread.h>

/* Define globally accessible variables and a mutex */
#define NUMTHREADS 4
pthread_t callThd[NUMTHREADS];
pthread_mutex_t lock;
void atomic_operation(void);

void *do_create(void *arg) {
    /* Creation thread */
    pthread_mutex_init(&lock, NULL);
    pthread_exit((void*) 0);
}

void *do_work(void *arg) {
    /* Worker thread */
    pthread_mutex_lock (&lock);
    atomic_operation();
    pthread_mutex_unlock (&lock);
    pthread_exit((void*) 0);
}

void *do_destroy(void *arg) {
    /* Destruction thread */
    pthread_mutex_destroy(&lock); //Noncompliant
    pthread_exit((void*) 0);
}

int main (int argc, char *argv[]) {  
   int i;
   void *status;
   pthread_attr_t attr;

           
   /* Create threads */
   pthread_attr_init(&attr);
   pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);

   /* Thread that initializes mutex */
   pthread_create(&callThd[0], &attr, do_create, NULL);

   /* Threads that use mutex for atomic operation*/
   for(i=0; i<NUMTHREADS-1; i++) {
      pthread_create(&callThd[i], &attr, do_work, (void *)i);
   }

   /* Thread that destroys mutex */
   pthread_create(&callThd[NUMTHREADS -1], &attr, do_destroy, NULL);

   pthread_attr_destroy(&attr);

   /* Join threads */
   for(i=0; i<NUMTHREADS; i++) {
      pthread_join(callThd[i], &status);
   }

   pthread_exit(NULL);
}

In this example, four threads are created. The threads are assigned different actions.

  • The first thread callThd[0] initializes the mutex lock.

  • The second and third threads, callThd[1] and callThd[2], perform an atomic operation protected by the mutex lock.

  • The fourth thread callThd[3] destroys the mutex lock.

The threads can interrupt each other. Therefore, immediately after the second or third thread locks the mutex, the fourth thread can destroy it.

Correction — Initialize and Destroy Mutex Outside Start Routine

One possible correction is to initialize and destroy the mutex in the main function outside the start routine of the threads. The threads perform only the atomic operation. You need two fewer threads because the mutex initialization and destruction threads are not required.

#include <pthread.h>

/* Define globally accessible variables and a mutex */
#define NUMTHREADS 2
pthread_t callThd[NUMTHREADS];
pthread_mutex_t lock;
void atomic_operation(void);

void *do_work(void *arg) {
   pthread_mutex_lock (&lock);
   atomic_operation();
   pthread_mutex_unlock (&lock);
   pthread_exit((void*) 0);
}

int main (int argc, char *argv[]) {  
   int i;
   void *status;
   pthread_attr_t attr;

           
   /* Create threads */
   pthread_attr_init(&attr);
   pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);

   /* Initialize mutex */
   pthread_mutex_init(&lock, NULL);

   for(i=0; i<NUMTHREADS; i++) {
      pthread_create(&callThd[i], &attr, do_work, (void *)i);
   }

   pthread_attr_destroy(&attr);

   /* Join threads */
   for(i=0; i<NUMTHREADS; i++) {
      pthread_join(callThd[i], &status);
   }

   /* Destroy mutex */
   pthread_mutex_destroy(&lock); 
  
   pthread_exit(NULL);
}

Correction — Use A Second Mutex To Protect Lock-Unlock Pair and Destruction

Another possible correction is to use a second mutex and protect the lock-unlock pair from the destruction. This corrected code uses the mutex lock2 to achieve this protection. The second mutex is initialized in the main function outside the start routine of the threads.

#include <pthread.h>

/* Define globally accessible variables and a mutex */
#define NUMTHREADS 4
pthread_t callThd[NUMTHREADS];
pthread_mutex_t lock;
pthread_mutex_t lock2;
void atomic_operation(void);

void *do_create(void *arg) {
    /* Creation thread */
    pthread_mutex_init(&lock, NULL);
    pthread_exit((void*) 0);
}

void *do_work(void *arg) {
    /* Worker thread */
    pthread_mutex_lock (&lock2);
    pthread_mutex_lock (&lock);
    atomic_operation();
    pthread_mutex_unlock (&lock);
    pthread_mutex_unlock (&lock2);
    pthread_exit((void*) 0);
}

void *do_destroy(void *arg) {
    /* Destruction thread */
    pthread_mutex_lock (&lock2);
    pthread_mutex_destroy(&lock);
    pthread_mutex_unlock (&lock2);
    pthread_exit((void*) 0);
}


int main (int argc, char *argv[]) {  
   int i;
   void *status;
   pthread_attr_t attr;

           
   /* Create threads */
   pthread_attr_init(&attr);
   pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);

   /* Initialize second mutex */
   pthread_mutex_init(&lock2, NULL);

   /* Thread that initializes first mutex */
   pthread_create(&callThd[0], &attr, do_create, NULL);

   /* Threads that use first mutex for atomic operation */
   /* The threads use second mutex to protect first from destruction in locked state*/
   for(i=0; i<NUMTHREADS-1; i++) {
      pthread_create(&callThd[i], &attr, do_work, (void *)i);
   }

   /* Thread that destroys first mutex */
   /* The thread uses the second mutex to prevent destruction of locked mutex */
   pthread_create(&callThd[NUMTHREADS -1], &attr, do_destroy, NULL);


   pthread_attr_destroy(&attr);

   /* Join threads */
   for(i=0; i<NUMTHREADS; i++) {
      pthread_join(callThd[i], &status);
   }

   /* Destroy second mutex */
   pthread_mutex_destroy(&lock2);

   pthread_exit(NULL);
}
Issue

This checker is deactivated in a default Polyspace as You Code analysis. See Checkers Deactivated in Polyspace as You Code Analysis (Polyspace Access).

This issue occurs when:

  • A task calls a lock function.

  • The task ends without a call to an unlock function.

In multitasking code, a lock function begins a critical section of code and an unlock function ends it. When a task, my_task, calls a lock function, my_lock, other tasks calling my_lock must wait until my_task calls the corresponding unlock function.

To find this defect, specify your lock and unlock functions using one of these methods:

Risk

An unlock function ends a critical section so that other waiting tasks can enter the critical section. A missing unlock function can result in tasks blocked for an unnecessary length of time.

Fix

Identify the critical section of code, that is, the section that you want to be executed as an atomic block. At the end of this section, call the unlock function that corresponds to the lock function used at the beginning of the section.

There can be other reasons and corresponding fixes for the defect. Perhaps you called the incorrect unlock function. Check the lock-unlock function pair in your Polyspace analysis configuration and fix the mismatch.

See examples of fixes below. To avoid the issue, you can follow the practice of calling the lock and unlock functions in the same module at the same level of abstraction. For instance, in this example, func calls the lock and unlock function at the same level but func2 does not.

void func() {
  my_lock();
  {
    ...
  }
  my_unlock();
}

void func2() {
  {
   my_lock();
   ...
  }
  my_unlock();
}

If you do not want to fix the issue, add comments to your result or code to avoid another review. See:

Extend Checker

You might be using locking and unlocking functions that are not supported by Polyspace. Extend this checker by mapping these functions to their known POSIX® equivalent. See Extend Concurrency Defect Checkers to Unsupported Multithreading Environments.

Example — Missing Unlock


void begin_critical_section(void);
void end_critical_section(void);

int global_var;

void reset() 
{
    begin_critical_section();
    global_var = 0;
    end_critical_section();
}

void my_task(void)
{
    begin_critical_section();  //Noncompliant
    global_var += 1;
}

In this example, to emulate multitasking behavior, specify the following options:

OptionValue
Configure multitasking manually
Tasks (-entry-points)

my_task, reset

Critical section details (-critical-section-begin -critical-section-end)Starting routineEnding routine
begin_critical_sectionend_critical_section

On the command-line, you can use the following:

polyspace-bug-finder
   -entry-points my_task,reset
   -critical-section-begin begin_critical_section:cs1
   -critical-section-end end_critical_section:cs1

The example has two entry points, my_task and reset. my_task enters a critical section through the call begin_critical_section();. my_task ends without calling end_critical_section.

Correction — Provide Unlock

One possible correction is to call the unlock function end_critical_section after the instructions in the critical section.



void begin_critical_section(void);
void end_critical_section(void);

int global_var;

void reset(void)
{
    begin_critical_section(); 
    global_var = 0;
    end_critical_section();
}

void my_task(void)
{
    begin_critical_section(); 
    global_var += 1;
    end_critical_section();
}
Example — Unlock in Condition


void begin_critical_section(void);
void end_critical_section(void);

int global_var;

void reset() {
    begin_critical_section();
    global_var=0;
    end_critical_section();
}

void my_task(void) {
    int index=0;
    volatile int numCycles;

    while(numCycles) {
      begin_critical_section(); //Noncompliant
      global_var ++;
      if(index%10==0) {
        global_var = 0;
        end_critical_section();
      }
      index++;
    }
}

In this example, to emulate multitasking behavior, specify the following options.

OptionSpecification
Configure multitasking manually
Tasks (-entry-points)

my_task, reset

Critical section details (-critical-section-begin -critical-section-end)Starting routineEnding routine
begin_critical_sectionend_critical_section

On the command-line, you can use the following:

polyspace-bug-finder
   -entry-points my_task,reset
   -critical-section-begin begin_critical_section:cs1
   -critical-section-end end_critical_section:cs1

The example has two entry points, my_task and reset.

In the while loop, my_task enters a critical section through the call begin_critical_section();. In an iteration of the while loop:

  • If my_task enters the if condition branch, the critical section ends through a call to end_critical_section.

  • If my_task does not enter the if condition branch and leaves the while loop, the critical section does not end. Therefore, a Missing unlock defect occurs.

  • If my_task does not enter the if condition branch and continues to the next iteration of the while loop, the lock function begin_critical_section is called again. A Double lock defect occurs.

Because numCycles is a volatile variable, it can take any value. Any of the cases above is possible. Therefore, a Missing unlock defect and a Double lock defect appear on the call begin_critical_section.

Correction — Place Unlock Outside Condition

One possible correction is to call the unlock function end_critical_section outside the if condition.



void begin_critical_section(void);
void end_critical_section(void);

int global_var;

void reset() {
    begin_critical_section();
    global_var=0;
    end_critical_section();
}

void my_task(void) {
    int index=0;
    volatile int numCycles;

    while(numCycles) {
      begin_critical_section();
      global_var ++;
      if(index%10==0) {
        global_var=0;
      }
      end_critical_section();
      index++;
    }
}
Correction — Place Unlock in Every Conditional Branch

Another possible correction is to call the unlock function end_critical_section in every branches of the if condition.



void begin_critical_section(void);
void end_critical_section(void);

int global_var;

void reset() {
    begin_critical_section();
    global_var=0;
    end_critical_section();
}

void my_task(void) {
    int index=0;
    volatile int numCycles;

    while(numCycles) {
      begin_critical_section();
      global_var ++;
      if(index%10==0) {
        global_var=0;
        end_critical_section();
      }
      else
        end_critical_section();
      index++;
    }
}

Check Information

Category: Others

Version History

Introduced in R2024a