11
#include<cstring>

struct A {
    char a;
    int b;
};

int main() {
    A* a = new A();
    a->a = 1;
    unsigned char m[sizeof(A)];
    std::memcpy(m, a, sizeof(A));
    return m[1];
}

Is this program guaranteed to exit with status 0 in C++, aside from possible exceptions due to allocation failure and assuming there is at least one padding byte between a and b in A?

new A() does value-initialization which zeros all members and padding bytes of the A object. For C, 6.2.6.1p6 in N1570 (C11 draft) seemed to imply to me that padding bytes are in an unspecified state after assignment to a member, although I may be misinterpreting this (see comments). But in any case I don't see any rule allowing this in the C++ standard (drafts).


Motivated by this stating that the padding from a zero-initialized structure may leak information if followed by assignment to a member in the second (non-compliant) example. Note however that the description of that example is wrong anyway since it actually does aggregate-initialization, not value-initialization and therefore no zero-initialization.


Here are two similar versions of the code that I had in the question earlier, but which probably have UB due to unrelated issues with the method I use to inspect the object representation (see comments):

#include<new>

struct A {
    char a;
    int b;
};

int main() {
    unsigned char* m = new unsigned char[sizeof(A)];
    A* a = new(m) A();
    a->a = 1;
    return m[1];
}

and

struct A {
    char a;
    int b;
};

int main() {
    A* a = new A();
    a->a = 1;
    return reinterpret_cast<unsigned char*>(a)[1];
}
user17732522
  • 53,019
  • 2
  • 56
  • 105
  • 3
    @Eljay See https://eel.is/c++draft/dcl.init#general-6.2 in conjunction with https://eel.is/c++draft/dcl.init#general-9.1.2 and https://eel.is/c++draft/dcl.init#general-16.4. And yes, this is a bug in GCC and ICC at least, see [this question](https://stackoverflow.com/q/70979077/17732522). – user17732522 Feb 19 '22 at 19:24
  • 1
    Nitpick: What if your implementation requires no alignment for `int`, so that there are no padding bytes, but does have padding bits in the object representation of `int`, so that the int `0` has a nonzero first byte? – Nate Eldredge Feb 19 '22 at 19:52
  • @NateEldredge I have edited the question to remove the possibility. Although that is another interesting question I suppose, whether padding bits of scalar types need to be set to zero by zero-initialization. It seems to not be specified. – user17732522 Feb 19 '22 at 20:06
  • Both programs have UB. – Language Lawyer Feb 20 '22 at 00:03
  • @LanguageLawyer Because of which parts of them? – user17732522 Feb 20 '22 at 00:08
  • `m[1]` (when L2R conversion is applied to this) and `reinterpret_cast(a)[1]` ([expr.add]/6, as usual) – Language Lawyer Feb 20 '22 at 00:11
  • @LanguageLawyer And the third version I added, or do I misunderstand the technicalities you are seeing issues with? – user17732522 Feb 20 '22 at 00:27
  • I think the third is better – Language Lawyer Feb 20 '22 at 00:38
  • One place where this might come up is: thread 1 writes to `foo.a` while thread 2 concurrently reads the padding bytes, without synchronization. Is there a data race? – Nate Eldredge Feb 20 '22 at 00:41
  • @NateEldredge I suppose a similar question for C (without having looked in the standard first): What guarantees that modification of two fields of a structure by two threads doesn't touch the same padding byte causing a data race? – user17732522 Feb 20 '22 at 00:49
  • @user17732522: Well, simply the fact that two members of the same struct are different objects, thus different memory locations [3.14], thus accesses to one do not conflict with the other [5.1.2.4p4] and so no data race is allowed to occur. So an implementation would either have to ensure that accesses to different members never touch the same padding, or else if they do, that no ill effects actually occur. I think the issue can only arise if one thread explicitly accesses the padding bytes. – Nate Eldredge Feb 20 '22 at 00:58
  • @NateEldredge Right. I think (again without having checked) the C++ standard has equivalent wording for this case. – user17732522 Feb 20 '22 at 01:08
  • What rule did you see in any C standard or draft about this? – Solomon Ucko Mar 11 '22 at 02:31
  • @SolomonUcko For example 6.2.6.1p6 in N1570 (C11 draft). – user17732522 Mar 11 '22 at 02:34
  • @user17732522 I might be misinterpreting it, since the wording is somewhat confusing, but it sounds like that that rule refers to assigning the whole structure or union (which could itself be a member of another structure or union), rather than assigning to a member of it? This is further evidenced by note 51, "Thus, for example, structure assignment need not copy any padding bits." – Solomon Ucko Mar 11 '22 at 02:43
  • @SolomonUcko Right, I think I could be misinterpreting this. If that is the case, then I suppose I could ask the same question for C as well. I always assumed that the compiler could e.g. write over padding bytes following a member if that is more efficient than limiting the write to the member, but never looked into the spec for this before asking this question. – user17732522 Mar 11 '22 at 02:49
  • @SolomonUcko There isn't much context in the answer, but in [this question](https://stackoverflow.com/questions/70292731/is-the-compiler-allowed-to-modify-padding-bytes) the paragraph I referenced is interpreted the same way I did. – user17732522 Mar 11 '22 at 02:57

3 Answers3

0

It seems like code such as memcpy is implicitly allowed to use any value for the padding bits. Realistically, memcpy would just copy whatever's there, but other code, such as field assignment, could change it, if it's considered unobservable.

https://eel.is/c++draft/basic.types.general states: (emphasis added)

For any object (other than a potentially-overlapping subobject) of trivially copyable type T, whether or not the object holds a valid value of type T, the underlying bytes ([intro.memory]) making up the object can be copied into an array of char, unsigned char, or std​::​byte ([cstddef.syn]).30 If the content of that array is copied back into the object, the object shall subsequently hold its original value.

The object representation of an object of type T is the sequence of N unsigned char objects taken up by the object of type T, where N equals sizeof(T). The value representation of an object of type T is the set of bits that participate in representing a value of type T. Bits in the object representation that are not part of the value representation are padding bits. For trivially copyable types, the value representation is a set of bits in the object representation that determines a value, which is one discrete element of an implementation-defined set of values.32

  1. By using, for example, the library functions ([headers]) std​::​memcpy or std​::​memmove.
  1. The intent is that the memory model of C++ is compatible with that of ISO/IEC 9899 Programming Language C.
Solomon Ucko
  • 5,724
  • 3
  • 24
  • 45
  • I think I can see that this could allow the object representation to be different after copying back to an object, but I am not convinced that this applies to my example where I am only copying to a character array. Whether or not sentence 2 is there, I would still expect sentence 1 to imply that the character array will hold the same object representation (not only value representation) as the original. – user17732522 Mar 10 '22 at 22:18
0

The value of padding bytes is undefined which is a serious security problem.

And with something undefined the compiler may do whatever it likes. So in simple structs that get zero initialized you can bet the compiler will simply initialize it all as skipping the padding would be slower. But as soon as you have custom initialization of some members all bets are off.

You should watch

J. Bialek, S. Block “Killing Uninitialized Memory: Protecting the OS Without Destroying Performance”

for a much longer discussion of the problem and what they did to mitigate it in Windows. clang has a similar mitigation as well.

Goswin von Brederlow
  • 11,875
  • 2
  • 24
  • 42
  • I skipped through the video and the main point seems to be that it is important to zero-initialize everything, but in the example I gave I do zero-initialize the structure. My question is whether this is safe even if I _afterwards_ assign to a member. The linked video seems to assume that, when I though it wasn't. – user17732522 Mar 12 '22 at 13:59
0

First, dynamically allocated memory (i.e. variables like "A* a = new A()" in your main function) is not automatically set to zero in C/C++. It usually contains garbage from previous variables. Since your variable is declared at the very beginning of the program, you are lucky that it is null.

For example, if you change the code like this, then the a->b field will contain garbage:

int main() {
    
    volatile int* garbage = new int(12345678); // allocate memory on the heap and initialize it with the value 12345678
    std::cout << (*garbage) << std::endl; // use the variable so that the compiler does not remove it when optimizing
    delete garbage;
    
    A* a = new A(); // allocate memory in the same place
    a->a = 1;
    std::cout << a->b << std::endl; // will output the garbage from the garbage variable

...

In order to avoid such undefined (random) behavior, you need to add a constructor for the structure, which will be called when it is created:

A()
{
    memset(this, 0, sizeof(A)); // explicit nulling
}

Finally, in order to get a guaranteed result (WISIWIG) when placing structure fields in memory, it is necessary to instruct the compiler not to align them (fields) on the machine word boundary to speed up reading / writing. The format of the directive is compiler dependent: C++ struct alignment question

Here is a complete example program for the GCC compiler:

#include <cstring>
#include <iostream>
#include <cassert>

// to disable field alignment, you must include this directive in the description of the structure
#define __PACKED__ __attribute__((packed))

struct A {
    A()
    {
        memset(this, 0, sizeof(A));
    }
    
    char a;
    int b;
}__PACKED__; // now it is a packed structure (no alignment inserts between fields)

int main() {
    
    volatile int* garbage = new int(12345678);
    std::cout << (*garbage) << std::endl;
    delete garbage;
    
    A* a = new A();
    a->a = 1;
    std::cout << a->b << std::endl; // now here's 0
    
    unsigned char m[sizeof(A)];
    std::memcpy(m, a, sizeof(A));

    assert(m[0] == 1);
    assert(*(int*)(m+1) == 0);
    assert(sizeof(A) == sizeof(char)+sizeof(int));
    
    std::cout << "return value: " << int(m[1]) << std::endl;
    return m[1];
}
qqNade
  • 1,942
  • 8
  • 10
  • `new A()` (in contrast to `new A`) is value-initialization and therefore will zero-initialize the whole structure. The last output in your first program must always result in `0`. Identifiers containing double underscore are reserved and may not be used as macro name. Also, I do not intend to have the struct packed. The question is explicitly about the behavior of the padding. Please also note that the question is tagged `language-lawyer` and is therefore about what exactly the standard specifies, not how specific compilers behave. – user17732522 Mar 12 '22 at 14:51
  • You don't need to `cout<<(*garbage)`. Once you've declared it `volatile`, it won't be optimized and removed. – Batman Mar 16 '22 at 18:19