Skip to main content
GitHub

DCL03-F. Avoid undefined behavior due to uninitialized variables

Accessing an uninitialized variable can lead to undefined behavior due to its indeterminate value.

Always initialize variables before using them, helping ensure deterministic behavior and prevent bugs in the code.

Some programming languages automatically initialize variables to default values, but Fortran, C, and C++ often do not. Consequently, variables left uninitialized contain unpredictable data until they are explicitly set by the programmer. In Fortran, reading any variable that has not been explicitly initialized results in undefined behavior.

Since uninitialized variables may contain any arbitrary values, reading and using them can lead to undefined behavior, potentially causing incorrect results, crashes, or other unintended outcomes. Compilers are not required to warn about these issues and, even if they do, they typically still allow the code to compile and run.

Some compilers may appear to "help" by zero-initializing variables under certain conditions (e.g., specific compilation flags). While this can make technically incorrect code run as originally intended, relying on such incidental behavior creates a false sense of security and masks underlying logical errors. Ultimately, this can vanish under different compilation settings and can vary between compilers.

Lastly, while some compilers provide options to automatically initialize certain data types (e.g., gfortran's -finit-integer=<value> or gcc's -ftrivial-auto-var-init=<value>), these features reduce portability to other development environments and hide problems in the code rather than addressing them.

Noncompliant Code Example

Consider the following code, which aims to sum the elements of an array:

Non-compliant code
! example_array.f90
program main
  use iso_fortran_env, only: real32
  implicit none

  real(kind=real32) :: array(5)
  array = [0.24, 0.33, 0.17, 0.89, 0.05]

  print *, "Sum is:", sum_array(array)

contains

  pure real(kind=real32) function sum_array(array)
    implicit none
    real(kind=real32), intent(in) :: array(:)
    real(kind=real32) :: sum
    integer :: i

    do i = 1, size(array, 1)
      sum = sum + array(i)
    end do

    sum_array = sum
  end function sum_array

end program main

Note how sum is never explicitly initialized. Although it might seem like it "should" logically start at 0, the Fortran standard does not guarantee this. Thus, the initial value of sum is indeterminate, leading to different outcomes depending on the compiler settings.

Implementation Details (Unix)

Consider the following compiler settings that produce different results. For instance, gfortran -O2 appears to start sum at 0, allowing the program to work as intended:

$ gfortran --version
GNU Fortran (Debian 14.2.0-8) 14.2.0
$ gfortran -O2 example_array.f90 -o example_array_gfortran
$ ./example_array_gfortran 
 Sum is:   1.67999995

However, with flang -O2, sum appears to contain arbitrary data, leading to incorrect results:

$ flang-new --version
Debian flang-new version 19.1.5 (1)
$ flang-new -O2 example_array.f90 -o example_array_flang
$ ./example_array_flang 
 Sum is: NaN

Compliant Solution

The solution is straightforward, always initialize variables before using them:

Compliant code
real(kind=real32) :: sum

sum = 0

This principle applies to all variable types, including other elemental types like integer, derived types, and arrays.

Noncompliant Code Example

Arrays are a critical part of simulation codes. For managing dynamic, n-dimensional arrays in Fortran, both pointer and allocatable variables are available. The latter, introduced in Fortran 2003, are generally safer and more robust. Unlike pointer, variables with the allocatable attribute automatically free their memory and are always set by default to the unallocated state.

For example, the following code technically leads to undefined behavior because a pointer is used without explicit initialization:

Non-compliant code
program main
  implicit none
  integer, pointer :: array(:)

  if (.not. associated(array)) then
    print *, "Undefined behavior"
  end if
end program main

Compliant Solution

In contrast, an allocatable array can always be safely checked using the allocated function, even when not explicitly initialized:

Compliant code
program main
  implicit none
  integer, allocatable :: array(:)

  if (.not. allocated(array)) then
    print *, "Defined behavior"
  end if
end program main

While these examples may seem trivial, these types of issues can arise in large, complex codebases where variables traverse multiple procedures and are subject to intricate conditional logic.

If, for any reason, you still need to use pointer variables, it's a good practice to nullify them as early as possible for additional safety. For module variables and derived type initializations, you can nullify right at the point of declaration:

Compliant code
type :: t
  integer, pointer :: array(:) => null()
end type

But within procedures, it's best to declare the pointer variable first and then nullify it. This avoids inadvertently introducing an implicit save behavior:

Compliant code
integer, pointer :: array(:)

nullify(array)

Risk Assessment

Undefined behavior can produce incorrect results, silent data corruption, crashes, or nondeterministic behavior that varies across compilers or platforms. Programmers should ensure that the code avoids undefined behavior in all cases.

RecommendationSeverityLikelihoodDetectableRepairablePriorityLevel
DCL03-FHighLikelyYesYesP27L1

Attachments: