C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Fixing std::bit_cast padding bit issues

From: Andrey Semashev <andrey.semashev_at_[hidden]>
Date: Fri, 16 Jan 2026 19:37:18 +0300
On 16 Jan 2026 18:17, Jan Schultke wrote:
>
> On Fri, 16 Jan 2026 at 15:55, Andrey Semashev via Std-Proposals <std-
> proposals_at_[hidden] <mailto:std-proposals_at_[hidden]>> wrote:
>
> On 16 Jan 2026 17:20, Jan Schultke via Std-Proposals wrote:
> >
> > They are not copied or preserved with most copies, but they /are/
> > accessible via things like memcpy and access using character or
> > std::byte pointers. So sometimes they are relevant despite
> not being
> > part of the value of the object.
> >
> > You can copy them with memcpy (only at run-time), but examining the
> > value results in undefined behavior.
>
> This should not be the case. I believe, the reason for it being an UB
> was that integers used to allow trap values, which are no longer
> allowed. Certainly, not for raw bytes.
>
> The reason is also that granting the implementation the freedom to not
> copy around padding bytes is an optimization opportunity,

The implementation cannot not copy the padding bits when the user
invokes memcpy. It could theoretically do something funny with them when
the user invokes bit_cast, but that is still acceptable if the result is
usable (specifically, those bits of the result that were not produced
from the input padding bits).

The proposal doesn't limit implementations wrt. handling padding bits
any more than it is already limited now. In particular, it may still
avoid copying padding when the user copies values around.

> and relying on
> the values of padding bytes is a massive (security-critical) bug anyway.

Noone is proposing to rely on padding bits. On the contrary, I and David
propose to add an utility for clearing padding bits, which, if anything,
could improve security.

> There are still floating-point types where certain representations
> result in CPU traps when used in an operation, if you find that to be
> more convincing of a reason.

Yes. And the possibility of producing a trap value of such a type exists
whether or not it is produced via padding or value representation bits
in the source.

> memcpy has well defined behavior, regardless of how it's implemented. If
> the program says memcpy then the source is copied as is to the target,
> with padding bits and everything. I do not see a fundamental reason why
> then the target should not be allowed to be used.
>
> Because the source could also not be used. For all intents and purposes,
> padding bits are eternally indeterminate quantum bits, and querying
> their value results in your CPU exploding.

This premise is too disconnected from reality.

Look, even the standard acknowledges that objects are represented with
bytes stored in some kind of storage. There's no magical quantum bits
that make your CPU explode. You can examine the bytes in that storage,
including the padding. The standard is reluctant to define the contents
of those bytes because it allows the implementation to take liberties in
their handling and (in earlier standards) they could end up being a trap
representation, which is no longer allowed. We're not asking to take
away those liberties, just admit the reality and say that reading from
the padding bits (using bit_cast) produces unspecified result, unless
those bits were previously cleared.

> And BTW, passing data via registers is not "doing god knows what", as
> the data is stored in registers as is.
>
> It is doing god knows what. When a type has four value bytes and four
> padding bytes, moving it around may be done via 32-bit register move.
> Whether that preserves the upper 32 bits or wipes them (like on x86) is
> a CPU architecture implementation detail, and not anything you can
> portably rely on.

Zero or sign extension falls into "doing something other than just
moving data" that I addressed below.

> Whether the compiler generates code that does something other than
> just moving the data, is irrelevant
> because it is not allowed to modify the value representation as part
> of memcpy, and padding bits in the source are unspecified anyway.
>
> Padding bits are not part of the value representation, and the
> implementation is free to only copy some of the bytes with memcpy
> because there exists no way of observing the values of padding bytes
> anyway. That is, under as-if rule optimization.

memcpy must write something to the target bits where the source padding
bits map onto. You could argue that memcpy could magically produce
garbage there instead of actually reading it from source (although no
implementation does such silly things, to my knowledge), but that would
still be acceptable if the user doesn't use those garbage bits. And I
maintain, the user should be allowed to do that, I see no reason why he
shouldn't.

> > It's also reasonable to expect the compiler to spot a branch based on
> > indeterminate bits within memcmp and optimize your code away entirely.
> > It's also reasonable to expect UBSan to terminate your program
> when you
> > memcmp a padded type, or for the compiler to treat such a call as
> > equivalent to std::unreachable() and not even emit IR for it.
>
> You are talking about tools that implement the standard. The tools will
> be updated once the standard changes, which is what we're discussing
> here.
>
> Yes, but it's entirely unrealistic to expect implementations to make
> massive changes to how they handle padding bits, seeing that the current
> model works reasonably well. To my understanding, LLVM does not even
> have an object representation for objects during constant evaluation; it
> is generated on the fly when you std::bit_cast.

I think, at this point we're only discussing runtime. At least, the
clear_padding function I proposed earlier was purposely not marked
constexpr.

At some point in the future we may discuss handling padding in
constexpr, but for now I'm only concerned with runtime.

> > There is even the problem of types like _BitInt(3) where the padding
> > doesn't nicely bundle into whole bytes. It is unclear how those
> could be
> > handled by pad_to_zero, other than to bit-cast and zero the padding
> > bits, which is exactly what I'm proposing.
>
> Padding bits are padding bits, whether they span the whole byte or not.
> Thus, clear_padding/pad_to_zero would mask them to zero (e.g. with a
> bitwise AND instruction).
>
> That's just undefined behavior, and it probably should stay undefined
> behavior.

So what you're saying is that there is no legal way to bit_cast a
_BitInt(3) to something else (e.g. uint8_t), that this is a good thing?
I'm sorry, but I disagree.

> Internally, the result of reading indeterminate/padding bits may be an
> LLVM poison value, and to my understanding, multiplying with zero or
> doing a bitwise AND with zero does not "unpoison" values. That is what
> you're conceptually asking for though.

Doing a bitwise AND in C++ might not "unpoison" it, but invoking
clear_padding would. I only mentioned bitwise AND *instruction* as a
means of how this call could be implemented in hardware.

> In general, I cannot empathize with this desire to manipulate and query
> padding bits. The current object model treats them as almost not
> existing, and it's a model that enables numerous optimizations and
> allows for UBSan to protect people from their own stupidity and from
> writing security-critical bugs.

But padding bits exist and are exposable in various ways. Denying this
is living in a delusional world and results in people writing
non-portable and non-conforming code anyway.

Received on 2026-01-16 16:37:22