Many programming languages, including C89, specify that the behavior of a program in terms of sequentially executed steps, whose behavior is in turn defined in terms of the program's state when the step is executed, without regard for any program states that may have occurred in the past or might occur in the future. In such a language, once an implementation starts executing a program fragment like:
extern unsigned short arr[65537];
unsigned test(unsigned q)
{
unsigned short i=0;
while (arr[i] != q)
i++;
if (q < 65536)
arr[q] = 2;
return i;
}
program actions would be limited to reading and writing to i
or reading the first 65536 elements of arr
unless or until it reached a state where arr[i]
was equal to q
.
In some situations, it may be difficult for a compiler to determine whether a loop will terminate, but easy for a compiler to recognize that none of the actions within a loop interact in any way with any other program actions which would be performed after the loop executed.
Execution of such loops could never serve any useful purpose in cases where they terminate, and would generally not be useful in cases where they fail to terminate, and it would thus be advantageous to eliminate them. In order to avoid having to write detailed rules about when compilers may eliminate such loops, some language standards simply treat any failure of a side-effect-free loop to terminate as "anything can happen" Undefined Behavior.
This allows some useful transformations "as-if" rules, but also allows compilers to turn code that would have been memory safe (and thus free of Arbitrary Code Execution exploits) if executed as described, or even if loops had been cleanly omitted, with code that is no longer memory safe and could thus be exploited by a malefactor to execute arbitrary code.
Such a compiler, given the above code, might omit both the loop and the following if
test, performing an unconditional store to arr[q]
even if q
was larger than 65535.
When processing tasks that involve input from potentially untrustworthy sources, and are required to be free of Arbitrary Code Execution exploits, what advantages are there--aside from saving a little ink in a language specification--to treating endless loops as "anything can happen" Undefined Behavior rather than specifying that a compiler need only treat a loop or other region of code with a single statically reachable exit as sequenced relative to surrounding code if some individual action (other than a branch) would be sequenced likewise?
Treating endless loops as "anything can happen" UB may simplify the design of compilers specialized for tasks that will never receive input from untrustworthy sources.
But for other tasks, it would seem to allow a compiler fewer optimization opportunities than would be available in a language where programmers could rely upon seemingly-side-effect-free loops that might fail to terminate having no side effects beyond possibly causing program execution to get "stuck".
What advantages, if any, does treating endless loops as "anything can happen" UB, as opposed to allowing more limited reordering, offer when processing tasks involving potentially untrustworthy inputs?
int1*int2
falls within the range ofint
writes the expression as(int)(1u*int1*int2)/int3
, an optimizer that knows thatint2
will be 30 andint3
will be 15 would be unable to use that knowledge to simply the expression toint1*2
, but if the programmer written the expression as simplyint1*int2/int3
, the optimizer would have been perform that optimization which, in isolation, would make the code more efficient while still meeting requirements, but there would be no way of knowing... $\endgroup$