C++ Logo

sg12

Advanced search

Re: [ub] memcpy blessing a new object of statically unknown type

From: Hubert Tong <hubert.reinterpretcast_at_[hidden]>
Date: Sat, 9 Jan 2016 13:24:13 -0500
On Sat, Jan 9, 2016 at 1:35 AM, David Krauss <david_work_at_[hidden]> wrote:

>
> On 2016–01–09, at 11:35 AM, Hubert Tong <hubert.reinterpretcast_at_[hidden]>
> wrote:
>
> Yes, std::launder requires a static type; however, it does not limit the
> ability of memcpy to operate without knowing the type of the object being
> copied. The std::launder call is involved *after* the completion of memcpy
> to access the object that the memcpyinitialized.
>
>
> Right, but in type erasure, the static type must be determined by
> inspecting the blob somehow. (Stashing a discriminating value elsewhere is
> one solution, but it’s more common and often more efficient to use an
> abstract base class or a discriminator inside a union.)
>
> My library would launder the erasure_base subobject to retrieve its
> dispatch table, but then it’d be stuck. Dispatching to a derived class
> would lead back to UB.
>
While std::launder requires a static (as in, bound at compile-time) type,
it does not require that the object retrieved is a complete object.
Following the std::launder, using plain-old static_cast to convert from the
base to the derived type is fine (assuming the static_cast would otherwise
be well-formed).


>
> One workaround could be to launder the same address repeatedly as the type
> becomes better resolved. For example, the call wrapper could launder a base
> class address, then perform an indirect call, then the callee could launder
> again to the derived class. For the common case of virtual dynamic
> dispatch, this sounds like it would incur UB before first line of the
> callee. My library doesn’t use virtual, but similar ones do. If only
> complete objects can be laundered, devirtualization could kick in… or
> launder could refuse to handle an abstract class at all. The workaround
> would also imply an excessive number of derived-type launder calls, which
> could compromise optimization by suggesting that bitwise manipulations are
> occurring when none are. Reloading the dispatch table pointer costs cycles.
>
> Perhaps a second style of laundering could implement a compromise. First, auto
> &header = *launder(header_ptr) gets a fully-formed header object from a
> blob, and then auto &whole = launder_extend<whole_type>(header) revises
> the object identity to make header a subobject sharing its address with
> another already-fully-formed object of type whole. (For example, header could
> be a base, a union member, or an initial struct member.) The
> launder_extend function differs in that it acts only if its argument was
> believed to be a complete object (i.e. fresh from launder), and it only
> launders the remainder of the new complete object. To solve the virtual issue,
> do not let launder imply that its result is most-derived. Perhaps, let
> virtual dispatch implicitly do launder_extend.
>
> This scheme leaves launder open-ended so a polymorphic object or union
> can be used, yet still laundered further. A simple implementation can opt
> to treat launder_extend the same as launder.
>
> Example:
> struct discriminator { int value; };
> struct foo { discriminator d; int i; };
> struct bar { discriminator d; float f; };
> union foobar { foo a; bar b; };
> struct baz : discriminator { double x; };
> struct bad : discriminator { virtual ~ bad(); };
>
> void unpack( discriminator * p ) {
> std::launder( p ); // OK: now we can access p.
>
Launder does not cause side-effects. This line does nothing.
Try:
discriminator *pp = std::launder(p); // OK: pp points to a discriminator
object if the preconditions of launder are met


> int disc = p->value;
>
Use pp, but yes.


> if ( disc == 0 ) {
> auto & f = std::launder_extend< foo >( * p ); // OK: now we can
> access a foo.
>
auto &f = *reinterpret_cast<foo *>(pp); // OK: we had a valid pointer to
the first non-static data member of a standard-layout struct (refer to CWG
notes from Kona)

        int q = p->value; // Load may be elided. Value is already in disc,
> equal to zero.
>
Use pp, but yes.

        auto & fb = std::launder_extend< foobar >( * f ); // OK: a further
> extension to a super-object.
>
auto &fb = reinterpret_cast<foobar &>(f); // OK: we have a valid "pointer"
to a non-static data member of a union (refer to CWG notes from Kona)
or
auto &fb = *reinterpret_cast<foobar *>(pp); // OK: the applies transitivity
rules

        auto & f2 = std::launder_extend< foo >( * p ); // OK, no-op: p was
> already extended, becoming a subobject.
>
Same as f.

        auto & b = std::launder_extend< bar >( * p ); // UB: there’s
> already a different object there.
>
I think the issue of non-active members of a union was still in flux during
Kona.


> auto & i = std::launder_extend< int >( * p ); // Library
> precondition violation: invalid object extension.
>
auto &i = *reinterpret_cast<int *>(pp); // OK


> } else if ( disc == 1 ) {
> baz & z = std::launder_extend< baz >( * p ); // OK, but
> implementation-dependent in theory.
>
baz &z = *static_cast<baz *>(pp); // OK


> } else if ( disc == 2 ) {
>
This case is likely to go wrong before it gets here:
bad b;
discriminator *pb = reinterpret_cast<discriminator *>(&b); // result
unspecified


> // Library precondition violation: no discriminator subobject
> shares an address with class bad.
> bad & x = std::launder_extend< bad >( * p );
> }
> }
>
>
> _______________________________________________
> ub mailing list
> ub_at_[hidden]
> http://www.open-std.org/mailman/listinfo/ub
>
>

Received on 2016-01-09 19:24:17