You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 4 Next »

Application-independent code includes:

  • Code shipped with the compiler or operating system
  • Code from a third-party library
  • Code developed in-house

When application-specific code detects an error, it can respond on the spot with a specific action, as in:

if (something_really_bad_happened) {
  take_me_some_place_safe();
}

This is because the application must both detect errors and provide a mechanism for handling errors.

Since application-independent code is not associated with any application, it cannot handle errors. But it must still detect errors, and report them to an application, so the application may still handle them.

Error detection and reporting can take several forms:

  • a return value (especially of type errno_t)
  • an argument passed by address
  • a global object (e.g., errno)
  • longjmp()
  • some combination of the above

Non-Compliant Code Example

Suppose we have the following application-independent code, which is called externally by invoking the f() function.

void g(void) {
  /* ... */
  if (something_really_bad_happened) {
    fprintf( stderr, "Something really bad happened!\n");
  }
  /* ... */
}

void f(void) {
  g()
  /* ... do the rest of f ... */
}

In this code, a serious error is detected in g() which prints the error, but does nothing to alter subsequent program behavior. The application receives no indication the error even occurred.

Compliant Solution (Return Value)

One way to indicate errors is to return a value indicating success or errors. The following example changes each function to return a value of type errno_t, where 0 indicates no error occurred.

const errno_t ESOMETHINGREALLYBAD = 1;

errno_t g(void) {
  /* ... */
  if (something_really_bad_happened) {
    return ESOMETHINGREALLYBAD;
  }
  /* ... */
  return 0;
}

errno_t f(void) {
  errno_t status;
  if ((status = g()) != 0)
    return status;
  /* ... do the rest of f ... */
  return 0;
}

As you can see, calling f() will return a status indicator which is zero upon success, and a nonzero value indicating what went wrong.

A return type of errno_t is encouraged by DCL09-A. Declare functions that return an errno error code with a return type of errno_t.

While this solution is secure, it has the following drawbacks:

Compliant Solution (Address Argument)

Instead of encoding status indicators in the return value, one can provide each function with a pointer, which the function is expected to use to indicate errors. In the following example, each function uses a errno_t* argument to report errors.

const errno_t ESOMETHINGREALLYBAD = 1;

void g(errno_t* err) {
  if (err == NULL) {
    /* handle null pointer */
  }
  /* ... */
  if (something_really_bad_happened) {
    *err = ESOMETHINGREALLYBAD;
  } else {
    /* ... */
    *err = 0;
  }
}

void f(errno_t* err) {
  if (err == NULL) {
    /* handle null pointer */
  }
  g(err);
  if (*err == 0) {
    /* ... do the rest of f ... */
  }
  return 0;
}

As you can see, calling f() will provide a status indicator which is zero upon success, and a nonzero value indicating what went wrong.

While this solution is secure, it has the following drawbacks:

  • Source code becomes even larger, due to the possibilities of receiving a null pointer.
  • All error indicators must be checked after calling functions.
  • A function that allocates resources must still free them in spite of the error. This is a little easier than with return values, because one has more control over control flow.
  • Unlike return values, static analyais tools generally won't notify you if you fail to check error indicators passed as argument pointers.

Compliant Solution (Global Error Indicator)

Instead of encoding error indicators in the return value or arguments, one can provide a global variable, which functions are expected to use to indicate errors. In the following example, each function uses a static indicator called my_errno.

The original errno variable was the Standard C library's implementation of error handling using this approach.

errno_t my_errno; /* also declared in a .h file */
const errno_t ESOMETHINGREALLYBAD = 1;

void g(void) {
  /* ... */
  if (something_really_bad_happened) {
    my_errno = ESOMETHINGREALLYBAD;
    return;
  }
  /* ... */
}

void f(void) {
  g();
  if (my_errno != 0) {
    return;
  }
  /* ... do the rest of f ... */
}

As you can see, calling f() will provide a status indicator which is zero upon success, and a nonzero value indicating what went wrong.

This solution has many of the same properties as those observed with errno, including advantages and drawbacks.

  • Source code still becomes larger, though smaller than the previous examples.
  • All error indicators must be checked after calling functions.
  • A function that allocates resources must still free them in spite of the error.
  • In general, combining registries of different sets of errors is difficult. For example, changing the above code to use errno is difficult and bug-prone; as one must be precisely aware of when C library functions set and clear errno, and one must be aware of all valid errno values before adding new ones.

Compliant Solution ( setjmp() and longjmp() )

C provides two functions setjmp() and longjmp(), that can be used to alter control flow. This might enable one to ignore error values, and entrust that control flow will be correctly diverted in the event of error.

The following example uses setjmp() and longjmp() to ensure that control flow is dirupted in the event of error, along with the my_errno indicator from the previous example.

#include <setjmp.h>

const errno_t ESOMETHINGREALLYBAD = 1;

jmp_buf exception_env;

void g(void) {
  /* ... */
  if (something_really_bad_happened) {
    longjmp(exception_env, ESOMETHINGREALLYBAD);
  }
  /* ... */
}

void f(void) {
  g();
  /* ... do the rest of f ... */
}

/* ... */
errno_t err;
if ((err = setjmp(exception_env)) != 0) {
  /* if we get here, an error occurred
     and err indicates what went wrong */
}
/* ... */
f();
/* if we get here, no errors occurred */
/* ... */

As you can see, calling f() will either succeed, or divert control into an if clause designed to catch the error.

  • Source code will not become significantly larger, because function signatures do not change, and neither do functions that neither detect nor handle the error.
  • Allocated resources must still be freed despite the error.
  • Requires application to call setjmp() before invoking application-independent code.
  • Signals are not necessarily preserved through longjmp() calls.

Summary

Method

Code Increase

Manages Allocated Resources

Automatically Enforceable

Return Value

Big (30-40%)

no

yes

Address Argument

Bigger

no

no

Global Indicator

Medium

no

yes

longjmp()

Small

no

n/a

Risk Analysis

A lack of error detection mechanism prevents applications from knowing when an error has disrupted normal program behavior.

Recommendation

Severity

Likelihood

Remediation Cost

Priority

Level

ERR05-A

high

likely

high

P9

L2

Related Vulnerabilities

Search for vulnerabilities resulting from the violation of this rule on the CERT website.

References


ERR04-A. Choose an appropriate termination strategy      12. Error Handling (ERR)       ERR30-C. Set errno to zero before calling a function, and use it only after the function returns a value indicating failure

  • No labels