Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using volatile twice in an R-value

The statement:

volatile unsigned char * volatile p = (volatile unsigned char * volatile)v;

Generates a warning C4197 in MSVC v14.1:

Warning C4197: 'volatile unsigned char *volatile ': top-level volatile in cast is ignored

The 2011 C standard (section [N1570] 6.7.3 4.) states: “The properties associated with qualified types are meaningful only for expressions, that are l-values”, thus the top-level volatile in this cast is ignored and generates this warning.

The author of this code states, that it doesn't violate the C standard and is required to prevent some GCC optimizations. He illustrates the problem with the code at: https://godbolt.org/g/xP4eGz

#include <stddef.h>

static void memset_s(void * v, size_t n) {
  volatile unsigned char * p = (volatile unsigned char *)v;
  for(size_t i = 0; i < n; ++i) {
    p[i] = 0;
  }
}

void f1() {
  unsigned char x[4];
  memset_s(x, sizeof x);
}

static void memset_s_volatile_pnt(void * v, size_t n) {
  volatile unsigned char * volatile p = (volatile unsigned char * volatile)v;
  for(size_t i = 0; i < n; ++i) {
    p[i] = 0;
  }
}

void f1_volatile_pnt() {
  unsigned char x[4];
  memset_s_volatile_pnt(x, sizeof x);
}

...where he shows that the function f1() compiles to nothing (just a ret instruction) but f1_volatile_pnt() compiles to instructions that do the intended job.

QUESTION: Is there a way to properly write this code so that it is compiled correctly by GCC and in accordance with the 2011 C standard (section [N1570] 6.7.3 4.) so it does not generate the warning with MSVC and ICC ? ...without #ifdef...

For the context of this problem, see: https://github.com/jedisct1/libsodium/issues/687

like image 390
George Robinson Avatar asked Mar 03 '18 13:03

George Robinson


2 Answers

Conclusion

To make the code volatile unsigned char * volatile p = (volatile unsigned char * volatile) v; compile in C or in C++ without warnings while retaining the author’s intent, remove the second volatile in the cast:

volatile unsigned char * volatile p = (volatile unsigned char *) v;

The cast is unnecessary in C, but the question asks that the code be compilable without warning in MSVC, which compiles as C++, not C, so the cast is needed. In C alone, if the statement could be (assuming v is void * or is compatible with the type of p):

volatile unsigned char * volatile p = v;

Why Qualify a Pointer as Volatile

The original source contains this code:

volatile unsigned char *volatile pnt_ =
    (volatile unsigned char *volatile) pnt;
size_t i = (size_t) 0U;

while (i < len) {
    pnt_[i++] = 0U;

The apparent desire of this code is to ensure that memory is cleared for security purposes. Normally, if C code assigns zero to some object x and never reads x before a subsequent assignment or program termination, the compiler will, when optimizing, remove the assignment of zero. The author does not want this optimization to occur; they apparently intend to ensure that memory is actually cleared. Clearing memory can reduce opportunities for an attacker to read the memory (through side channels, by exploiting bugs, by gaining physical possession of the computer, or other means).

Suppose we have some buffer x that is an array of unsigned char. If x were defined with volatile, it is a volatile object, and the compiler always implements writes to it; it never removes them during optimization.

On the other hand, if x is not defined with volatile, but we put its address in a pointer p that has a type pointer to volatile unsigned char, what happens when we write *p = 0? As R.. points out, if the compiler can see that p points into x, it knows that the object being modified is not volatile, and therefore the compiler is not required to actually write to memory if it can otherwise optimize away the assignment. This is because the C standard defines volatile in terms of accessing volatile objects, not merely accessing memory through a pointer that has a type of “pointer to volatile something.”

To ensure the compiler actually writes to x, the author of this code declares p to be volatile. What this means is that, in *p = 0, the compiler cannot know that p points into x. The compiler is required to load the value of p from whatever memory it has assigned for p; it must assume p may have changed from the value that pointed into x.

Further, when p is declared volatile unsigned char *volatile p, the compiler must assume that the place pointed to by p is volatile. (Technically, when it loads the value of p, it could examine it, discover it is in fact pointing into x or some other memory known not to be volatile, and then treat it as non-volatile. But this would be an extraordinary effort by the compiler, and we can assume it does not happen.)

Therefore, if the code were:

volatile unsigned char *pnt_ = pnt;
size_t i = (size_t) 0U;

while (i < len) {
    pnt_[i++] = 0U;

then, whenever the compiler can see that pnt in fact points to non-volatile memory and that memory is not read before it is later written, the compiler may remove this code during optimization. However, if the code is:

volatile unsigned char *volatile pnt_ = pnt;
size_t i = (size_t) 0U;

while (i < len) {
    pnt_[i++] = 0U;

then, in each iteration of the loop, the compiler must:

  • Load pnt_ from the memory allocated for it.
  • Calculate the destination address.
  • Write zero to that address (unless the compiler goes to the extraordinary trouble of determining the address is non-volatile).

Thus, the purpose of the second volatile is to hide from the compiler the fact that the pointer points to non-volatile memory.

Although this accomplishes the author’s goal, it has the undesired effects of forcing the compiler to reload the pointer in each iteration of the loop and preventing the compiler from optimizing the loop by writing to the destination several bytes at a time.

Because of this undesired effect, I have another answer to the question “Is there a way to properly write this code so that it is compiled correctly by GCC and in accordance with the 2011 C standard (section [N1570] 6.7.3 4.) so it does not generate the warning with MSVC and ICC ?”: I would implement memset_s in assembly language and ask the compiler developers also to build it into the compiler.

Casting a Value

Consider the definition:

volatile unsigned char * volatile p = (volatile unsigned char * volatile) v;

We have seen above that the definition of p as volatile unsigned char * volatile is needed to accomplish the author’s goal, although it is an unfortunate workaround to shortcomings in C. However, what about the cast, (volatile unsigned char * volatile).

First, the cast is unnecessary, as the value of v will be automatically converted to the type of p. To avoid the warning in MSVC, the cast can simply be removed, leaving the definition as volatile unsigned char * volatile p = v;.

Given that the cast is there, the question asks whether the second volatile has any meaning. The C standard explicitly says “The properties associated with qualified types are meaningful only for expressions that are lvalues.” (C 2011 [N1570] 6.7.3 4.)

volatile means something unknown to the compiler can change the value of an object or accessing the object can cause something unknown to the compiler to occur. For example, if there is a volatile int a in the program, that implies the object identified by a can be changed by some means not known to the compiler. It could be changed by some special hardware on the computer, by a debugger, by the operating system, or by other means.

volatile qualifies an object. An object is a region of data storage in memory that can represent values.

In expressions, we have values. For example, some int values are 3, 5, or −1. Values cannot be volatile. They are not storage in memory; they are abstract mathematical values. The number 3 can never change; it is always 3.

The cast (volatile unsigned char * volatile) says to cast something to be a volatile pointer to a volatile unsigned char. It is fine to point to a volatile unsigned char—a pointer points to something in memory. But what does it mean to be a volatile pointer? A pointer is just a value; it is an address. Values do not have memory, they are not objects, so they cannot be volatile. So the second volatile in the cast (volatile unsigned char * volatile) has no effect in standard C. It is conforming C code, but the qualifier has no effect.

like image 69
Eric Postpischil Avatar answered Oct 15 '22 00:10

Eric Postpischil


There is fundamentally no way to express what the author wants to express. The first version of the code is rightly optimized down to nothing by some compilers, because the underlying object unsigned char x[4] is not volatile; accessing it through pointer-to-volatile does not magically make it volatile.

The second version of the code is a hack that happens to achieve what the author wants, but at significant additional cost, and in the real world might be counter-productive. If (in a non-toy, fleshed-out example) the array x had only been used such that the compiler was able to keep it entirely in registers, the hacks in memset_s_volatile_pnt would force it to be spilled out to actual memory on the stack, only then to be clobbered, and memset_s_volatile_pnt would not be able to do anything to get rid of the copies in the original registers. A cheaper way to achieve the same thing would be just calling normal memset on x, then passing x to an external function whose definition the compiler can't see (to be safest, an external function in a different shared library).

Safe memory clearing is not expressible in C; it needs compiler/language-level extensions. The best way to do it in C+POSIX is just to do all the handling of sensitive data in a separate process whose lifetime is limited to the duration the sensitive data is needed and rely on memory protection boundaries to ensure it never leaks anywhere else.

If you just want to get rid of the warning, though, the solution is easy. Simply change:

volatile unsigned char * volatile p = (volatile unsigned char * volatile)v;

to:

volatile unsigned char * volatile p = (volatile unsigned char *)v;
like image 4
R.. GitHub STOP HELPING ICE Avatar answered Oct 15 '22 01:10

R.. GitHub STOP HELPING ICE