LLVM IR Undefined Behavior (UB) Manual

Abstract

This document describes the undefined behavior (UB) in LLVM’s IR, including undef and poison values, as well as the freeze instruction. We also provide guidelines on when to use each form of UB.

Introduction

Undefined behavior (UB) is used to specify the behavior of corner cases for which we don’t wish to specify the concrete results. UB is also used to provide additional constraints to the optimizers (e.g., assumptions that the frontend guarantees through the language type system or the runtime). For example, we could specify the result of division by zero as zero, but since we are not really interested in the result, we say it is UB.

There exist two forms of undefined behavior in LLVM: immediate UB and deferred UB. The latter comes in two flavors: undef and poison values. There is also a freeze instruction to tame the propagation of deferred UB. The lattice of values in LLVM is: immediate UB > poison > undef > freeze(poison) > concrete value.

We explain each of the concepts in detail below.

Immediate UB

Immediate UB is the most severe form of UB. It should be avoided whenever possible. Immediate UB should be used only for operations that trap in most CPUs supported by LLVM. Examples include division by zero, dereferencing a null pointer, etc.

The reason that immediate UB should be avoided is that it makes optimizations such as hoisting a lot harder. Consider the following example:

define i32 @f(i1 %c, i32 %v) {
  br i1 %c, label %then, label %else

then:
  %div = udiv i32 3, %v
  br label %ret

else:
  br label %ret

ret:
  %r = phi i32 [ %div, %then ], [ 0, %else ]
  ret i32 %r
}

We might be tempted to simplify this function by removing the branching and executing the division speculatively because %c is true most of times. We would obtain the following IR:

define i32 @f(i1 %c, i32 %v) {
  %div = udiv i32 3, %v
  %r = select i1 %c, i32 %div, i32 0
  ret i32 %r
}

However, this transformation is not correct! Since division triggers UB when the divisor is zero, we can only execute speculatively if we are sure we don’t hit that condition. The function above, when called as f(false, 0), would return 0 before the optimization, and triggers UB after being optimized.

This example highlights why we minimize the cases that trigger immediate UB as much as possible. As a rule of thumb, use immediate UB only for the cases that trap the CPU for most of the supported architectures.

Time Travel

Immediate UB in LLVM IR allows the so-called time travelling. What this means is that if a program triggers UB, then we are not required to preserve any of its observable behavior, including I/O. For example, the following function triggers UB after calling printf:

define void @fn() {
  call void @printf(...) willreturn
  unreachable
}

Since we know that printf will always return, and because LLVM’s UB can time-travel, it is legal to remove the call to printf altogether and optimize the function to simply:

define void @fn() {
  unreachable
}

Deferred UB

Deferred UB is a lighter form of UB. It enables instructions to be executed speculatively while marking some corner cases as having erroneous values. Deferred UB should be used for cases where the semantics offered by common CPUs differ, but the CPU does not trap.

As an example, consider the shift instructions. The x86 and ARM architectures offer different semantics when the shift amount is equal to or greater than the bitwidth. We could solve this tension in one of two ways: 1) pick one of the x86/ARM semantics for LLVM, which would make the code emitted for the other architecture slower; 2) define that case as yielding poison. LLVM chose the latter option. For frontends for languages like C or C++ (e.g., clang), they can map shifts in the source program directly to a shift in LLVM IR, since the semantics of C and C++ define such shifts as UB. For languages that offer strong semantics, they must use the value of the shift conditionally, e.g.:

define i32 @x86_shift(i32 %a, i32 %b) {
  %mask = and i32 %b, 31
  %shift = shl i32 %a, %mask
  ret i32 %shift
}

There are two deferred UB values in LLVM: undef and poison, which we describe next.

Undef Values

Warning

Undef values are deprecated and should be used only when strictly necessary. Uses of undef values should be restricted to representing loads of uninitialized memory. This is the only part of the IR semantics that cannot be replaced with alternatives yet (work in ongoing).

An undef value represents any value of a given type. Moreover, each use of an instruction that depends on undef can observe a different value. For example:

define i32 @fn() {
  %add = add i32 undef, 0
  %ret = add i32 %add, %add
  ret i32 %ret
}

Unsurprisingly, the first addition yields undef. However, the result of the second addition is more subtle. We might be tempted to think that it yields an even number. But it might not be! Since each (transitive) use of undef can observe a different value, the second addition is equivalent to add i32 undef, undef, which is equivalent to undef. Hence, the function above is equivalent to:

define i32 @fn() {
  ret i32 undef
}

Each call to this function may observe a different value, namely any 32-bit number (even and odd).

Because each use of undef can observe a different value, some optimizations are wrong if we are not sure a value is not undef. Consider a function that multiplies a number by 2:

define i32 @fn(i32 %v) {
  %mul2 = mul i32 %v, 2
  ret i32 %mul2
}

This function is guaranteed to return an even number, even if %v is undef. However, as we’ve seen above, the following function does not:

define i32 @fn(i32 %v) {
  %mul2 = add i32 %v, %v
  ret i32 %mul2
}

This optimization is wrong just because undef values exist, even if they are not used in this part of the program as LLVM has no way to tell if %v is undef or not.

Looking at the value lattice, undef values can only be replaced with either a freeze instruction or a concrete value. A consequence is that giving undef as an operand to an instruction that triggers UB for some values of that operand makes the program UB. For example, udiv %x, undef is UB since we replace undef with 0 (udiv %x, 0), becoming obvious that it is UB.

Poison Values

Poison values are a stronger form of deferred UB than undef. They still allow instructions to be executed speculatively, but they taint the whole expression DAG (with some exceptions), akin to floating point NaN values.

Example:

define i32 @fn(i32 %a, i32 %b, i32 %c) {
  %add = add nsw i32 %a, %b
  %ret = add nsw i32 %add, %c
  ret i32 %ret
}

The nsw attribute in the additions indicates that the operation yields poison if there is a signed overflow. If the first addition overflows, %add is poison and thus %ret is also poison since it taints the whole expression DAG.

Poison values can be replaced with any value of type (undef, concrete values, or a freeze instruction).

Propagation of Poison Through Select

Most instructions return poison if any of their inputs is poison. A notable exception is the select instruction, which is poison if and only if the condition is poison or the selected value is poison. This means that select acts as a barrier for poison propagation, which impacts which optimizations can be performed.

For example, consider the following function:

define i1 @fn(i32 %x, i32 %y) {
  %cmp1 = icmp ne i32 %x, 0
  %cmp2 = icmp ugt i32 %x, %y
  %and = select i1 %cmp1, i1 %cmp2, i1 false
  ret i1 %and
}

It is not correct to optimize the select into an and because when %cmp1 is false, the select is only poison if %x is poison, while the and below is poison if either %x or %y are poison.

define i1 @fn(i32 %x, i32 %y) {
  %cmp1 = icmp ne i32 %x, 0
  %cmp2 = icmp ugt i32 %x, %y
  %and = and i1 %cmp1, %cmp2     ;; poison if %x or %y are poison
  ret i1 %and
}

However, the optimization is possible if all operands of the values are used in the condition (notice the flipped operands in the select):

define i1 @fn(i32 %x, i32 %y) {
  %cmp1 = icmp ne i32 %x, 0
  %cmp2 = icmp ugt i32 %x, %y
  %and = select i1 %cmp2, i1 %cmp1, i1 false
  ; ok to replace with:
  %and = and i1 %cmp1, %cmp2
  ret i1 %and
}

The Freeze Instruction

Both undef and poison values sometimes propagate too much down an expression DAG. Undef values because each transitive use can observe a different value, and poison values because they make the whole DAG poison. There are some cases where it is important to stop such propagation. This is where the freeze instruction comes in.

Take the following example function:

 define i32 @fn(i32 %n, i1 %c) {
 entry:
   br label %loop

loop:
   %i = phi i32 [ 0, %entry ], [ %i2, %loop.end ]
   %cond = icmp ule i32 %i, %n
   br i1 %cond, label %loop.cont, label %exit

loop.cont:
   br i1 %c, label %then, label %else

 then:
   ...
   br label %loop.end

 else:
   ...
   br label %loop.end

 loop.end:
   %i2 = add i32 %i, 1
   br label %loop

 exit:
   ...
 }

Imagine we want to perform loop unswitching on the loop above since the branch condition inside the loop is loop invariant. We would obtain the following IR:

 define i32 @fn(i32 %n, i1 %c) {
 entry:
   br i1 %c, label %then, label %else

then:
   %i = phi i32 [ 0, %entry ], [ %i2, %then.cont ]
   %cond = icmp ule i32 %i, %n
   br i1 %cond, label %then.cont, label %exit

then.cont:
   ...
   %i2 = add i32 %i, 1
   br label %then

else:
   %i3 = phi i32 [ 0, %entry ], [ %i4, %else.cont ]
   %cond = icmp ule i32 %i3, %n
   br i1 %cond, label %else.cont, label %exit

else.cont:
   ...
   %i4 = add i32 %i3, 1
   br label %else

 exit:
   ...
 }

There is a subtle catch: when the function is called with %n being zero, the original function did not branch on %c, while the optimized one does. Branching on a deferred UB value is immediate UB, hence the transformation is wrong in general because %c may be undef or poison.

Cases like this need a way to tame deferred UB values. This is exactly what the freeze instruction is for! When given a concrete value as argument, freeze is a no-op, returning the argument as-is. When given an undef or poison value, freeze returns a non-deterministic value of the type. This is not the same as undef: the value returned by freeze is the same for all users.

Branching on a value returned by freeze is always safe since it either evaluates to true or false consistently. We can make the loop unswitching optimization above correct as follows:

define i32 @fn(i32 %n, i1 %c) {
entry:
  %c2 = freeze i1 %c
  br i1 %c2, label %then, label %else

Writing Tests Without Undefined Behavior

When writing tests, it is important to ensure that they don’t trigger UB unnecessarily. Some automated test reduces sometimes use undef or poison values as dummy values, but this is considered a bad practice if this leads to triggering UB.

For example, imagine that we want to write a test and we don’t care about the particular divisor value because our optimization kicks in regardless:

 define i32 @fn(i8 %a) {
   %div = udiv i8 %a, poison
   ...
}

The issue with this test is that it triggers immediate UB. This prevents verification tools like Alive from validating the correctness of the optimization. Hence, it is considered a bad practice to have tests with unnecessary immediate UB (unless that is exactly what the test is for). The test above should use a dummy function argument instead of using poison:

 define i32 @fn(i8 %a, i8 %dummy) {
   %div = udiv i8 %a, %dummy
   ...
}

Common sources of immediate UB in tests include branching on undef/poison conditions and dereferencing undef/poison/null pointers.

Note

If you need a placeholder value to pass as an argument to an instruction that may trigger UB, add a new argument to the function rather than using undef or poison.

Summary

Undefined behavior (UB) in LLVM IR consists of two well-defined concepts: immediate and deferred UB (undef and poison values). Passing deferred UB values to certain operations leads to immediate UB. This can be avoided in some cases through the use of the freeze instruction.

The lattice of values in LLVM is: immediate UB > poison > undef > freeze(poison) > concrete value. It is only valid to transform values from the left to the right (e.g., a poison value can be replaced with a concrete value, but not the other way around).

Undef is now deprecated and should be used only to represent loads of uninitialized memory.