When forking a child process, file descriptors are copied to the child process, which can result in concurrent operations on the file. Concurrent operations on the same file can cause data to be read or written in a nondeterministic order, creating race conditions and unpredictable behavior.

Noncompliant Code Example

In this example, the programmer wishes to open a file, read a character, fork, and then have both parent and child process read the second character of the file independently. However, because both processes share a file descriptor, one process might get the second character, and one might get the third. Furthermore, there is no guarantee the reads are atomic—the processes might get unpredictable results. Regardless of what the programmer is trying to accomplish with this code, this code is incorrect because it contains a race condition.

char c;
pid_t pid;

int fd = open(filename, O_RDWR);
if (fd == -1) {
  /* Handle error */
}
read(fd, &c, 1);
printf("root process:%c\n",c);

pid = fork();
if (pid == -1) {
  /* Handle error */
}

if (pid == 0) { /*child*/
  read(fd, &c, 1);
  printf("child:%c\n",c);
}
else { /*parent*/
  read(fd, &c, 1);
  printf("parent:%c\n",c);
}

If the file accessed has contents "abc", the output of this program could be either

root process:a
parent: b
child: c

or

root process: a
child: b
parent: c

This code's output cannot reliably be determined and should not be used.

Compliant Solution

In this compliant solution, the programmer closes the file descriptor in the child after forking and then reopens it, ensuring that the file has not been modified in the meantime. See POS01-C. Check for the existence of links when dealing with files for details.

char c;

pid_t pid;

/* Open file and remember file status  */
struct stat orig_st;
if (lstat( filename, &orig_st) != 0) {
  /* handle error */
}
int fd = open(filename, O_RDWR);
if (fd == -1) {
  /* Handle error */
}
struct stat new_st;
if (fstat(fd, &new_st) != 0) {
  /* handle error */
}
if (orig_st.st_dev != new_st.st_dev ||
    orig_st.st_ino != new_st.st_ino) {
  /* file was tampered with while opening */
}

/* file is good, operate on fd */

read(fd,&c,1);
printf("root process:%c\n",c);

pid = fork();
if (pid == -1) {
  /* Handle error */
}

if (pid == 0){ /*child*/
  close(fd);

  /* Reopen file, creating new file descriptor */
  fd = open(filename, O_RDONLY);
  if (fd == -1) {
    /* Handle error */
  }
  if (fstat(fd, &new_st) != 0) {
    /* handle error */
  }
  if (orig_st.st_dev != new_st.st_dev ||
      orig_st.st_ino != new_st.st_ino) {
    /* file was tampered with between opens */
  }

  read(fd, &c, 1);
  read(fd, &c, 1);
  printf("child:%c\n", c);
  close(fd);
}

else { /*parent*/
  read(fd, &c, 1);
  printf("parent:%c\n", c);
  close(fd);
}

The output of this code is

root process:a
child:b
parent:b

Risk Assessment

Because race conditions in code are extremely hard to find, this problem might not appear during standard debugging stages of development. However, depending on what file is being read and how important the order of read operations is, this problem can be particular dangerous.

Rule

Severity

Likelihood

Remediation Cost

Priority

Level

POS38-C

medium

unlikely

medium

P4

L3

Automated Detection

Tool

Version

Checker

Description

CodeSonar

BADFUNC.FORK

Use of fork

Helix QAC

DF4951, DF4952
Parasoft C/C++test

CERT_C-POS38-a

Avoid race conditions when using fork and file descriptors

Polyspace Bug Finder

CERT C: Rule POS38-CChecks for file descriptor exposure to child process (rule fully covered)

Bibliography

TODO