CertC++-DCL55

Avoid information leakage when passing a class object across a trust boundary

Required inputs: IR

The C++ Standard, [class.mem], paragraph 13 [ ISO/IEC 14882-2014], describes the layout of non-static data members of a non-union class, specifying the following:

Nonstatic data members of a (non-union) class with the same access control are allocated so that later members have higher addresses within a class object. The order of allocation of non-static data members with different access control is unspecified. Implementation alignment requirements might cause two adjacent members not to be allocated immediately after each other; so might requirements for space for managing virtual functions and virtual base classes.

Further, [class.bit], paragraph 1, in part, states the following:

Allocation of bit-fields within a class object is implementation-defined. Alignment of bit-fields is implementation-defined. Bit-fields are packed into some addressable allocation unit.

Thus, padding bits may be present at any location within a class object instance (including at the beginning of the object, in the case of an unnamed bit-field as the first member declared in a class). Unless initialized by zero-initialization, padding bits contain indeterminate values that may contain sensitive information.

When passing a pointer to a class object instance across a trust boundary to a different trusted domain, the programmer must ensure that the padding bits of such an object do not contain sensitive information.

Noncompliant Code Example

This noncompliant code example runs in kernel space and copies data from  arg to user space. However, padding bits may be used within the object, for example, to ensure the proper alignment of class data members. These padding bits may contain sensitive information that may then be leaked when the data is copied to user space, regardless of how the data is copied.

#include <cstddef>
 
struct test {
  int a;
  char b;
  int c;
};
 
// Safely copy bytes to user space
extern int copy_to_user(void *dest, void *src, std::size_t size);
 
void do_stuff(void *usr_buf) {
  test arg{1, 2, 3};
  copy_to_user(usr_buf, &arg, sizeof(arg));
}
Noncompliant Code Example

In this noncompliant code example, arg is value-initialized through direct initialization. Because test does not have a user-provided default constructor, the value-initialization is preceded by a zero-initialization that guarantees the padding bits are initialized to 0 before any further initialization occurs. It is akin to using std::memset() to initialize all of the bits in the object to 0.

#include <cstddef>
 
struct test {
  int a;
  char b;
  int c;
};
 
// Safely copy bytes to user space
extern int copy_to_user(void *dest, void *src, std::size_t size);
 
void do_stuff(void *usr_buf) {
  test arg{};
 
  arg.a = 1;
  arg.b = 2;
  arg.c = 3;
 
  copy_to_user(usr_buf, &arg, sizeof(arg));
}

However, compilers are free to implement  arg.b = 2 by setting the low byte of a 32-bit register to  2, leaving the high bytes unchanged, and storing all 32 bits of the register into memory. This could leak the high-order bytes resident in the register to a user.

Compliant Solution

This compliant solution serializes the structure data before copying it to an untrusted context.

#include <cstddef>
#include <cstring>

struct test {
  int a;
  char b;
  int c;
};

// Safely copy bytes to user space.
extern int copy_to_user(void *dest, void *src, std::size_t size);

void do_stuff(void *usr_buf) {
  test arg{1, 2, 3};
  // May be larger than strictly needed.
  unsigned char buf[sizeof(arg)];
  std::size_t offset = 0;

  std::memcpy(buf + offset, &arg.a, sizeof(arg.a));
  offset += sizeof(arg.a);
  std::memcpy(buf + offset, &arg.b, sizeof(arg.b));
  offset += sizeof(arg.b);
  std::memcpy(buf + offset, &arg.c, sizeof(arg.c));
  offset += sizeof(arg.c);

  copy_to_user(usr_buf, buf, offset /* size of info copied */);
}

This code ensures that no uninitialized padding bits are copied to unprivileged users. The structure copied to user space is now a packed structure and the  copy_to_user() function would need to unpack it to recreate the original, padded structure.

Compliant Solution (Padding Bytes)

Padding bits can be explicitly declared as fields within the structure. This solution is not portable, however, because it depends on the implementation and target memory architecture. The following solution is specific to the x86-32 architecture.

#include <cstddef>

struct test {
  int a;
  char b;
  char padding_1, padding_2, padding_3;
  int c;
 
  test(int a, char b, int c) : a(a), b(b),
    padding_1(0), padding_2(0), padding_3(0),
    c(c) {}
};
// Ensure c is the next byte after the last padding byte.
static_assert(offsetof(test, c) == offsetof(test, padding_3) + 1,
              "Object contains intermediate padding");
// Ensure there is no trailing padding.
static_assert(sizeof(test) == offsetof(test, c) + sizeof(int),
              "Object contains trailing padding");



// Safely copy bytes to user space.
extern int copy_to_user(void *dest, void *src, std::size_t size);

void do_stuff(void *usr_buf) {
  test arg{1, 2, 3};
  copy_to_user(usr_buf, &arg, sizeof(arg));
}

The  static_assert() declaration accepts a constant expression and an  error message. The expression is evaluated at compile time and, if false, the compilation is terminated and the error message is used as the diagnostic. The explicit insertion of the padding bytes into the  struct should ensure that no additional padding bytes are added by the compiler, and consequently both static assertions should be true. However, it is necessary to validate these assumptions to ensure that the solution is correct for a particular implementation.

Noncompliant Code Example

In this noncompliant code example, padding bits may abound, including

  • alignment padding bits after a virtual method table or virtual base class data to align a subsequent data member,
  • alignment padding bits to position a subsequent data member on a properly aligned boundary,
  • alignment padding bits to position data members of varying access control levels.
  • bit-field padding bits when the sequential set of bit-fields does not fill an entire allocation unit,
  • bit-field padding bits when two adjacent bit-fields are declared with different underlying types,
  • padding bits when a bit-field is declared with a length greater than the number of bits in the underlying allocation unit, or
  • padding bits to ensure a class instance will be appropriately aligned for use within an array.

This code example runs in kernel space and copies data from  arg to user space. However, the padding bits within the object instance may contain sensitive information that will then be leaked when the data is copied to user space.

#include <cstddef>

class base {
public:
  virtual ~base() = default;
};

class test : public virtual base {
  alignas(32) double h;
  char i;
  unsigned j : 80;
protected:
  unsigned k;
  unsigned l : 4;
  unsigned short m : 3;
public:
  char n;
  double o;

  test(double h, char i, unsigned j, unsigned k, unsigned l, unsigned short m,
       char n, double o) :
    h(h), i(i), j(j), k(k), l(l), m(m), n(n), o(o) {}

  virtual void foo();
};

// Safely copy bytes to user space.
extern int copy_to_user(void *dest, void *src, std::size_t size);

void do_stuff(void *usr_buf) {
  test arg{0.0, 1, 2, 3, 4, 5, 6, 7.0};
  copy_to_user(usr_buf, &arg, sizeof(arg));
}

Padding bits are implementation-defined, so the layout of the class object may differ between compilers or architectures. When compiled with GCC5.3.0 for x86-32, the  test object requires 96 bytes of storage to accommodate 29 bytes of data (33 bytes including the vtable) and has the following layout.

Offset (bytes (bits)) Storage Size (bytes (bits)) Reason
Offset Storage Size Reason
0 1 (32) vtable pointer
56 (448) 4 (32) unsigned k
4 (32) 28 (224) data member alignment padding
60 (480) 0 (4) unsigned l : 4
32 (256) 8 (64) double h
60 (484) 0 (3) unsigned short m : 3
40 (320) 1 (8) char i
60 (487) 0 (1) unused bit-field bits
41 (328) 3 (24) data member alignment padding
61 (488) 1 (8) char n
44 (352) 4 (32) unsigned j : 80
62 (496) 2 (16) data member alignment padding
48 (384) 6 (48) extended bit-field size padding
64 (512) 8 (64) double o
54 (432) 2 (16) alignment padding
72 (576) 24 (192) class alignment padding
Compliant Solution

Due to the complexity of the data structure, this compliant solution serializes the object data before copying it to an untrusted context instead of attempting to account for all of the padding bytes manually.

#include <cstddef>
#include <cstring>

class base {
public:
  virtual ~base() = default;
};
class test : public virtual base {
  alignas(32) double h;
  char i;
  unsigned j : 80;
protected:
  unsigned k;
  unsigned l : 4;
  unsigned short m : 3;
public:
  char n;
  double o;

  test(double h, char i, unsigned j, unsigned k, unsigned l, unsigned short m,
       char n, double o) :
    h(h), i(i), j(j), k(k), l(l), m(m), n(n), o(o) {}

  virtual void foo();
  bool serialize(unsigned char *buffer, std::size_t &size) {
    if (size < sizeof(test)) {
      return false;
    }

    std::size_t offset = 0;
    std::memcpy(buffer + offset, &h, sizeof(h));
    offset += sizeof(h);
    std::memcpy(buffer + offset, &i, sizeof(i));
    offset += sizeof(i);
    unsigned loc_j = j; // Only sizeof(unsigned) bits are valid, so this is not narrowing.
    std::memcpy(buffer + offset, &loc_j, sizeof(loc_j));
    offset += sizeof(loc_j);
    std::memcpy(buffer + offset, &k, sizeof(k));
    offset += sizeof(k);
    unsigned char loc_l = l & 0b1111;
    std::memcpy(buffer + offset, &loc_l, sizeof(loc_l));
    offset += sizeof(loc_l);
    unsigned short loc_m = m & 0b111;
    std::memcpy(buffer + offset, &loc_m, sizeof(loc_m));
    offset += sizeof(loc_m);
    std::memcpy(buffer + offset, &n, sizeof(n));
    offset += sizeof(n);
    std::memcpy(buffer + offset, &o, sizeof(o));
    offset += sizeof(o);

    size -= offset;
    return true;
  }
};

// Safely copy bytes to user space.
extern int copy_to_user(void *dest, void *src, size_t size);

void do_stuff(void *usr_buf) {
  test arg{0.0, 1, 2, 3, 4, 5, 6, 7.0};

  // May be larger than strictly needed, will be updated by
  // calling serialize() to the size of the buffer remaining.
  std::size_t size = sizeof(arg);
  unsigned char buf[sizeof(arg)];
  if (arg.serialize(buf, size)) {
    copy_to_user(usr_buf, buf, sizeof(test) - size);
  } else {
    // Handle error
  }
}

This code ensures that no uninitialized padding bits are copied to unprivileged users. The structure copied to user space is now a packed structure and the  copy_to_user() function would need to unpack it to re-create the original, padded structure.

Risk Assessment

Padding bits might inadvertently contain sensitive data such as pointers to kernel data structures or passwords. A pointer to such a structure could be passed to other functions, causing information leakage.

Rule Severity Likelihood Remediation Cost Priority Level
DCL55-CPP Low Unlikely High P1 L3
Related Guidelines
SEI CERT C Coding Standard  DCL39-C. Avoid information leakage when passing a structure across a trust boundary 
Bibliography
[ ISO/IEC 14882-2014] Subclause 8.5, "Initializers"
Subclause 9.2, "Class Members"
Subclause 9.6, "Bit-fields"
Excerpt from SEI CERT C++ Coding Standard [https://cmu-sei.github.io/secure-coding-standards/sei-cert-cpp-coding-standard/rules/declarations-and-initialization-dcl/dcl55-cpp], Copyright (C) 1995-2026 Carnegie Mellon University. See section 9.4. "3rd-Party Licenses" in the documentation for full details.

Possible Messages

Key

Text

Severity

Disabled

pointer_to_padded_composite_as_argument

Pointer to composite type ‘{}’{} is passed as argument: potential information leak.

None

False

Options

trust_boundary_functions

trust_boundary_functions

Type: set[bauhaus.analysis.config.FunctionName] | dict[bauhaus.analysis.config.FunctionName, set[int]]

Default:

{
   'fwrite': {0},
   'memcpy': {1},
   'send': {1},
   'sendmsg': {1},
   'sendto': {1},
   'write': {1}
}
When names of functions are provided in a map, only calls to these functions are reported when a pointer to some struct/class with padding is passed as argument at a position of the given set. If the set of argument positions is empty all positions are checked. Passing names of functions as a set is just for backwards compatibility and corresponds to a map with empty argument positions.