This vulnerabilities described in ISO/IEC 24772-1:2024 clause 6.3 is applicable to C++. The “endianness” of integer types and packing of bit fields are implementation-defined properties and not portable.
The standard library type std::endian
allows to portably check the endianness of a platform and code can use this information to operate on individual bytes of a machine word in the correct order.
There is no portable mapping from bitfields in a struct to individual bits in a machine word. Therefore, C++ bitfields should not be used to directly map to bits in hardware, even though the compiler provides suitable mapping and manipulation operations. A further complication is that accessing a bitfield can often not easily be performed atomically, because the non-participating bits of a memory location need to be read before the relevant bits can be mutated through masking, and the whole memory location has to be written again. It is possible to simulate bitfields with a defined layout through library class types that implement the required masking operations.
For individual bits std::bitset<N>
and std::vector<bool>
can provide suitable representations at run time, but don’t support a direct mapping to machine words. However, be aware that std::vector<bool>
does not in general behave like a std::vector
which can cause generic code to misbehave.
C++ provides a rich set of bitwise operators that can be used to address the issues of bit manipulation in a portable way. However, the shift operation can result in undefined behavior [EWF] when shifting by a negative or too large value, or when shifting a signed operand. It is advisable to use bit operations only on appropriate unsigned integral types with a known width while being careful of potential integral promotion that might change a small unsigned operand type to be promoted to a signed integer type. When bitwise memory operations are needed, it is good practice to encapsulate such operations in a class type’s member functions.
For representing individual bitmasks values employed in bit operations, it is advisable to put the corresponding named constants in an enumeration type with the appropriate underlying type for easier recall.
While a bit shift of an integral value can be viewed as a multiplication or division by a power of two, it should not be used in arithmetic expression to implement such an operation. Compilers will automatically implement such a multiplication in the most efficient way, there is no need to obfuscate multiplication and division as shift operations.
Except for specific situations (trivial types), objects of class type can not be assumed to have a layout appropriate to be manipulated on a byte or bitwise level. Depending on the size and alignment of its data members a class type might have padding bytes between members. The absence of padding in a trivially copyable type T
can be checked with a static_assert(std::has_unique_object_representations_v<T>)
.
std::bit_cast
can be used to reinterpret suitably sized trivial types on a bitwise level, i.e., for accessing the binary representation of a floating point value. However, for types with padding bits that do do not participate in an object’s value representation, the corresponding bits in a bit_cast
result have indeterminate values. If those bits are used to compare for equality, with the function memcmp
for instance, the padding bits may differ and cause false negatives.
Malicious code could use such padding bits as a secret channel which might be accessed through copying the underlying bytes.
See C++ Core Guidelines ES101 use unsigned types for bit manipulation.
To avoid the vulnerability or mitigate its ill effects, C++ software developers can:
Follow the avoidance mechanisms of ISO/IEC 24772-3 clause 6.3.2
Avoid the use of shift operations to implement multiplication or division by powers of two.
Use built-in bitwise operations only with operands of unsigned integral types or enums with an unsigned underlying type.
When performing bitwise operations on operands of an unsigned type that gets promoted to a signed integral type, cast the result to the corresponding unsigned type immediately.
Ensure that the right-hand operand of a shift operation is not negative or greater than the number of bits the left-hand operand. Use static analysis tools or other means to prove that the right hand operand of the shift is not out of bounds.
Prefer std::bit_cast
over other means such as reinterpret_cast
to access the binary representation of an object.
Prefer abstractions in a class type encapsulating bit manipulations using unsigned types of well-defined width, such as std::bitset
, over non-portable bitfields.
Forbid the use of std::memcmp
, std::memcpy
, or std::memmove
on objects of type T
unless std::has_unique_object_representations_v<T>
is true.