18

I was reading through the C11 standard. As per the C11 standard undefined behavior is classified into four different types. The parenthesized numbers refer to the subclause of the C Standard (C11) that identifies the undefined behavior.

Example 1: The program attempts to modify a string literal (6.4.5). This undefined behavior is classified as: Undefined Behavior (information/confirmation needed)

Example 2 : An lvalue does not designate an object when evaluated (6.3.2.1). This undefined behavior is classified as: Critical Undefined Behavior

Example 3: An object has its stored value accessed other than by an lvalue of an allowable type (6.5). This undefined behavior is classified as: Bounded Undefined Behavior

Example 4: The string pointed to by the mode argument in a call to the fopen function does not exactly match one of the specified character sequences (7.21.5.3). This undefined behavior is classified as: Possible Conforming Language Extension

What is the meaning of the classifications? What do these classification convey to the programmer?

Boann
  • 48,794
  • 16
  • 117
  • 146
  • 3
    I had not realized that there were different classifications of Undefined Behavior. Looking over the list I would think that this is really about which kinds of Undefined Behavior have a kind of industry default behavior used by many or most or the most common compilers and which kinds of Undefined Behavior do not. Another part of this may be which of these are fundamental issues that are intertwined with the actual C memory model and C abstract model of operation versus which of these are possible issues with the Standard Library. – Richard Chambers Nov 04 '17 at 18:49
  • 1
    The standard itself does not much make such a classification. "Undefined behavior" means what the term means in its first sense: the behavior of a certain program is not defined by the standard. Don't read more into this. Sometimes this is done implicitly (nothing is said about a certain thing), or explicitly. For the latter, there is different language and different motivation. The start of clause 4 "Conformance" gives you a good overview about this. – Jens Gustedt Nov 04 '17 at 20:43
  • 1
    As @JensGustedt correctly points out, these classifications do not appear in the C standard. Indeed, C11 section 4 paragraph 2 lists three ways the standard can indicate undefined behavior, and says, "There is no difference in emphasis among these three; they all describe ‘‘behavior that is undefined’’." Please update your question to indicate where these classifications come from. – Keith Thompson Nov 08 '17 at 21:29
  • @KeithThompson: Annex L gives a definition of "Critical Undefined Behavior" and specifies that for implementations that define `__STDC_ANALYZABLE__` all other forms are to be considered "bounded". While the Annex is badly worded, I think the clear intention is that *quality* implementations which define `__STDC_ANALYZABLE__` will refrain from processing "bounded" forms of Undefined Behavior in ways that would create security vulnerabilities in cases where a platform's "natural" behavior would not. – supercat Nov 10 '17 at 18:36

5 Answers5

10

I only have access to a draft of the standard, but from what I’m reading, it seems like this classification of undefined behavior isn’t mandated by the standard and only matters from the perspective of compilers and environments that specifically indicate that they want to create C programs that can be more easily analyzed for different classes of errors. (These environments have to define a special symbol __STDC_ANALYZABLE__.)

It seems like the key idea here is an “out of bounds write,” which is defined as a write operation that modifies data that isn’t otherwise allocated as part of an object. For example, if you clobber the bytes of an existing variable accidentally, that’s not an out of bounds write, but if you jumped to a random region of memory and decorated it with your favorite bit pattern you’d be performing an out of bounds write.

A specific behavior is bounded undefined behavior if the result is undefined, but won’t ever do an out of bounds write. In other words, the behavior is undefined, but you won’t jump to a random address not associated with any objects or allocated space and put bytes there. A behavior is critical undefined behavior if you get undefined behavior that cannot promise that it won’t do an out-of-bounds write.

The standard then goes on to talk about what can lead to critical undefined behavior. By default undefined behaviors are bounded undefined behaviors, but there are exceptions for UB that result from memory errors like like accessing deallocated memory or using an uninitialized pointer, which have critical undefined behavior. Remember, though, that these classifications only exist and have meaning in the context of implementations of C that choose to specifically separate out these sorts of behaviors. Unless your C environment guarantees it’s analyzable, all undefined behaviors can potentially do absolutely anything!

My guess is that this is intended for environments like building drivers or kernel plugins where you’d like to be able to analyze a piece of code and say “well, if you're going to shoot someone in the foot, it had better be your foot that you’re shooting and not mine!” If you compile a C program with these constraints, the runtime environment can instrument the very few operations that are allowed to be critical undefined behavior and have those operations trap to the OS, and assume that all other undefined behaviors will at most destroy memory that’s specifically associated with the program itself.

templatetypedef
  • 362,284
  • 104
  • 897
  • 1,065
  • I'd regard the kinds of optimizations that `__STDC_ANALYZABLE__` is intended to forbid as inappropriate not just in kernel code, but in most programs that may need to process input from untrustworthy sources. There may be some cases where such optimizations might allow performance improvements that could not be achieved more effectively via safer means, but for most kinds of programs the benefits of such aggressive optimizations would be limited. – supercat Nov 10 '17 at 19:40
7

All of these are cases where the behaviour is undefined, i.e. the standard "imposes no requirements". Traditionally, within undefined behaviour and considering one implementation (i.e. C compiler + C standard library), one could see two kinds of undefined behaviour:

  • constructs for which the behaviour would not be documented, or would be documented to cause a crash, or the behaviour would be erratic,
  • constructs that the standard left undefined but for which the implementation defines some useful behaviour.

Sometimes these can be controlled by compiler switches. E.g. example 1 usually always causes bad behaviour - a trap, or crash, or modifies a shared value. Earlier versions of GCC allowed one to have modifiable string literals with -fwritable-strings; therefore if that switch was given, the implementation defined the behaviour in that case.

C11 added an optional orthogonal classification: bounded undefined behaviour and critical undefined behaviour. Bounded undefined behaviour is that which does not perform an out-of-bounds store, i.e. it cannot cause values being written in arbitrary locations in memory. Any undefined behaviour that is not bounded undefined behaviour is critical undefined behaviour.

Iff __STDC_ANALYZABLE__ is defined, the implementation will conform to the appendix L, which has this definitive list of critical undefined behaviour:

  • An object is referred to outside of its lifetime (6.2.4).
  • A store is performed to an object that has two incompatible declarations (6.2.7),
  • A pointer is used to call a function whose type is not compatible with the referenced type (6.2.7, 6.3.2.3, 6.5.2.2).
  • An lvalue does not designate an object when evaluated (6.3.2.1).
  • The program attempts to modify a string literal (6.4.5).
  • The operand of the unary * operator has an invalid value (6.5.3.2).
  • Addition or subtraction of a pointer into, or just beyond, an array object and an integer type produces a result that points just beyond the array object and is used as the operand of a unary * operator that is evaluated (6.5.6).
  • An attempt is made to modify an object defined with a const-qualified type through use of an lvalue with non-const-qualified type (6.7.3).
  • An argument to a function or macro defined in the standard library has an invalid value or a type not expected by a function with variable number of arguments (7.1.4).
  • The longjmp function is called with a jmp_buf argument where the most recent invocation of the setjmp macro in the same invocation of the program with the corresponding jmp_buf argument is nonexistent, or the invocation was from another thread of execution, or the function containing the invocation has terminated execution in the interim, or the invocation was within the scope of an identifier with variably modified type and execution has left that scope in the interim (7.13.2.1).
  • The value of a pointer that refers to space deallocated by a call to the free or realloc function is used (7.22.3).
  • A string or wide string utility function accesses an array beyond the end of an object (7.24.1, 7.29.4).

For the bounded undefined behaviour, the standard imposes no requirements other than that an out-of-bounds write is not allowed to happen.

The example 1: modification of a string literal is also. classified as critical undefined behaviour. The example 4 is critical undefined behaviour too - the value is not one expected by the standard library.


For example 4, the standard hints that while the behaviour is undefined in case of mode that is not defined by the standard, there are implementations that might define behaviour for other flags. For example glibc supports many more mode flags, such as c, e, m and x, and allow setting the character encoding of the input with ,ccs=charset modifier (and putting the stream into wide mode right away).

5

Some programs are intended solely for use with input that is known to be valid, or at least come from trustworthy sources. Others are not. Certain kinds of optimizations which might be useful when processing only trusted data are stupid and dangerous when used with untrusted data. The authors of Annex L unfortunately wrote it excessively vaguely, but the clear intention is to allow compilers that they won't do certain kinds of "optimizations" that are stupid and dangerous when using data from untrustworthy sources.

Consider the function (assume "int" is 32 bits):

int32_t triplet_may_be_interesting(int32_t a, int32_t b, int32_t c)
{ 
  return a*b > c;
}

invoked from the context:

#define SCALE_FACTOR 123456
int my_array[20000];
int32_t foo(uint16_t x, uint16_t y)
{
  if (x < 20000)
    my_array[x]++;
  if (triplet_may_be_interesting(x, SCALE_FACTOR, y))
    return examine_triplet(x, SCALE_FACTOR, y);
  else
    return 0;
}

When C89 was written, the most common way a 32-bit compiler would process that code would have been to do a 32-bit multiply and then do a signed comparison with y. A few optimizations are possible, however, especially if a compiler in-lines the function invocation:

  1. On platforms where unsigned compares are faster than signed compares, a compiler could infer that since none of a, b, or c can be negative, the arithmetical value of a*b is non-negative, and it may thus use an unsigned compare instead of a signed comparison. This optimization would be allowable even if __STDC_ANALYZABLE__ is non-zero.

  2. A compiler could likewise infer that if x is non-zero, the arithmetical value of x*123456 will be greater than every possible value of y, and if x is zero, then x*123456 won't be greater than any. It could thus replace the second if condition with simply if (x). This optimization is also allowable even if __STDC_ANALYzABLE__ is non-zero.

  3. A compiler whose authors either intend it for use only with trusted data, or else wrongly believe that cleverness and stupidity are antonyms, could infer that since any value of x larger than 17395 will result in an integer overflow, x may be safely presumed to be 17395 or less. It could thus perform my_array[x]++; unconditionally. A compiler may not define __STDC_ANALYZABLE__ with a non-zero value if it would perform this optimization. It is this latter kind of optimization which Annex L is designed to address. If an implementation can guarantee that the effect of overflow will be limited to yielding a possibly-meaningless value, it may be cheaper and easier for code to deal with the possibly of the value being meaningless than to prevent the overflow. If overflow could instead cause objects to behave as though their values were corrupted by future computations, however, there would be no way a program could deal with things like overflow after the fact, even in cases where the result of the computation would end up being irrelevant.

In this example, if the effect of integer overflow would be limited to yielding a possibly-meaningless value, and if calling examine_triplet() unnecessarily would waste time but would otherwise be harmless, a compiler may be able to usefully optimize triplet_may_be_interesting in ways that would not be possible if it were written to avoid integer overflow at all costs. Aggressive "optimization" will thus result in less efficient code than would be possible with a compiler that instead used its freedom to offer some loose behavioral guarantees.

Annex L would be much more useful if it allowed implementations to offer specific behavioral guarantees (e.g. overflow will yield a possibly-meaningless result, but have no other side-effects). No single set of guarantees would be optimal for all programs, but the amount of text Annex L spent on its impractical proposed trapping mechanism could have been better spent specifying macros to indicate what guarantees various implementations could offer.

supercat
  • 77,689
  • 9
  • 166
  • 211
3

According to cppreference :

Critical undefined behavior

Critical UB is undefined behavior that might perform a memory write or a volatile memory read out of bounds of any object. A program that has critical undefined behavior may be susceptible to security exploits.

Only the following undefined behaviors are critical:

  • access to an object outside of its lifetime (e.g. through a dangling pointer)
  • write to an object whose declarations are not compatible
  • function call through a function pointer whose type is not compatible with the type of the function it points to
  • lvalue expression is evaluated, but does not designate an object attempted modification of a string literal
  • dereferencing an invalid (null, indeterminate, etc) or past-the-end pointer
  • modification of a const object through a non-const pointer
  • call to a standard library function or macro with an invalid argument
  • call to a variadic standard library function with unexpected argument type (e.g. call to printf with an argument of the type that doesn't match its conversion specifier)
  • longjmp where there is no setjmp up the calling scope, across threads, or from within the scope of a VM type.
  • any use of the pointer that was deallocated by free or realloc
  • any string or wide string library function accesses an array out of bounds

Bounded undefined behavior

Bounded UB is undefined behavior that cannot perform an illegal memory write, although it may trap and may produce or store indeterminate values.

All undefined behavior not listed as critical is bounded, including

  • multithreaded data races
  • use of a indeterminate values with automatic storage duration
  • strict aliasing violations
  • misaligned object access
  • signed integer overflow
  • unsequenced side-effects modify the same scalar or modify and read the same scalar
  • floating-to-integer or pointer-to-integer conversion overflow
  • bitwise shift by a negative or too large bit count
  • integer division by zero
  • use of a void expression
  • direct assignment or memcpy of inexactly-overlapped objects
  • restrict violations
  • etc.. ALL undefined behavior that's not in the critical list.
msc
  • 33,420
  • 29
  • 119
  • 214
  • Well, that's particularly badly written, it is not that bounded undefined behaviour couldn't lead to security exploits, even without out of bound writes... – Antti Haapala -- Слава Україні Nov 04 '17 at 19:05
  • Several of the bounded errors can produce values that might later be used in indexing or pointer arithmetic and can *then* cause a critical error anyway. So an illegal memory write doesn't happen now, only later? – Bo Persson Nov 05 '17 at 00:07
  • (My comment is in reference to the excerpt from cppreference) – Antti Haapala -- Слава Україні Nov 05 '17 at 06:32
  • @AnttiHaapala: It is not well written. There are many situations where it would be acceptable for code to behave in a wide variety of ways when given invalid input, provided it refrains from doing certain things. Code which must behave in precisely-defined fashion for all inputs is often more expensive to write and slower to execute than code which can simply ignore cases where the exact output doesn't matter. Unfortunately, Annex L fails to specify any situations in which any particular corner cases could safely be ignored. I think it is intended to forbid optimizations that could... – supercat Nov 08 '17 at 18:41
  • ...only be justified by having UB transcend laws of time and causality (e.g. the fact that a program will, in future, perform "bounded" UB if x is greater than 23 would not justify a compiler omitting a comparison that tests whether x is less than 30, since the only way such behavior would be justifiable would be if the future UB could alter the value of x in odd fashion; non-critical UB isn't supposed to behave as though it clobbers arbitrary storage). – supercat Nov 08 '17 at 18:45
2

"I was reading through the C11 standard. As per the C11 standard undefined behavior is classified into four different types."

I wonder what you were actually reading. The 2011 ISO C standard does not mention these four different classifications of undefined behavior. In fact it's quite explicit in not making any distinctions among different kinds of undefined behavior.

Here's ISO C11 section 4 paragraph 2:

If a "shall" or "shall not" requirement that appears outside of a constraint or runtime-constraint is violated, the behavior is undefined. Undefined behavior is otherwise indicated in this International Standard by the words "undefined behavior" or by the omission of any explicit definition of behavior. There is no difference in emphasis among these three; they all describe "behavior that is undefined".

All the examples you cite are undefined behavior, which, as far as the Standard is concerned, means nothing more or less than:

behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this International Standard imposes no requirements

If you have some other reference, that discusses different kinds of undefined behavior, please update your question to cite it. Your question would then be about what that document means by its classification system, not (just) about the ISO C standard.

Some of the wording in your question appears similar to some of the information in C11 Annex L, "Analyzability" (which is optional for conforming C11 implementations), but your first example refers to "Undefined Behavior (information/confirmation needed)", and the word "confirmation" appears nowhere in the ISO C standard.

Keith Thompson
  • 254,901
  • 44
  • 429
  • 631
  • 1
    It's possible for one part of the Standard to expressly decline to impose requirements on an action, while another part imposes behavioral requirements on that action in certain circumstances or on certain implementations. For example, computing `1.0/x` when x is zero would invoke Undefined Behavior on some implementations, but would be required to yield NaN on implementations that define `__STDC_IEC_559__`. I think the clear intention of Annex L is that it imposes requirements upon implementations that claim to support it, even when the Standard would otherwise not impose any. – supercat Nov 09 '17 at 16:19