Date: Fri, 21 Oct 2022 12:32:59 +0100
On Thu, 20 Oct 2022 at 18:04, Edward Catmur <ecatmur_at_[hidden]> wrote:
>
>
> On Thu, 20 Oct 2022 at 17:38, <language.lawyer_at_[hidden]> wrote:
>
>> >>> This issue came up on a StackOverflow question:
>> >>>
>> >>>
>> >>
>> https://stackoverflow.com/questions/74080177/double-free-in-the-c-standard-library-using-only-stdfunction-and-stdshared/74080676
>> >>>
>> >>> This led to a discussion here:
>> >>>
>> >>> https://chat.stackoverflow.com/rooms/248862/discuss-lwg2224
>> >>>
>> >>> Now, https://eel.is/c++draft/res.on.objects states
>> >>>
>> >>> "If an object of a standard library type is accessed, and the
>> beginning
>> >> of the object's lifetime does not happen before the access, or the
>> access
>> >> does not happen before the end of the object's lifetime, the behavior
>> is
>> >> undefined unless otherwise specified."
>> >>>
>> >>> According to a non-normative note in
>> >> https://eel.is/c++draft/defns.access,
>> >>>
>> >>> "Only glvalues of scalar type can be used to access objects. ...
>> >> Attempts to read or modify an object of class type typically invoke a
>> >> constructor or assignment operator; such invocations do not themselves
>> >> constitute accesses, although they may involve accesses of scalar
>> >> subobjects."
>> >>>
>> >>> The question is, can objects of class type be accessed?
>> >>
>> >> Can, in some sense. But it would be UB. E.g.
>> >>
>> >> struct S
>> >> {
>> >> char buf[sizeof(int)];
>> >> } s;
>> >>
>> >> reinterpret_cast<int&>(s) = 0; // kinda accesses `s`, but this violates
>> >> strict aliasing, so the behavior is actually undefined
>> >>
>> >
>> > It accesses `s`'s storage, sure.
>>
>> What do you mean by explicitly writing «storage»? You disagree with that
>> the expression accesses `s`?
>>
>
> Yes, since `s` is not a glvalue of scalar type.
>
> > But a read would not have undefined behavior.
>>
>> [basic.lval]/11 apply uniformly to both kinds of access: reads and writes.
>>
>
> Oh oops, didn't spot that the reinterpret_cast was to int. If it was to a
> storage-like type, that would be well-defined, no?
>
> >> Is it the intent that an access to a scalar subobject also constitutes
>> an
>> >> access to the object itself?
>> >>
>> >> No
>> >>
>> >>> If not, then it is impossible for objects of most library types to be
>> >> accessed.
>> >>
>> >> There are other things which are undefined if an object (or rather a
>> >> pointer/glvalue referring to it) is used before the lifetime has
>> started
>> >> https://timsong-cpp.github.io/cppwp/n4868/basic.life#6.sentence-5
>> >> https://timsong-cpp.github.io/cppwp/n4868/basic.life#7.sentence-4
>> >
>> >
>> > That doesn't apply to objects of class type within
>> > construction/destruction; it only applies before their constructor
>> starts
>> > or after the destructor ends. During construction/destruction, we are
>> > referred to [class.cdtor].
>>
>> I know.
>>
>> >> If so, then objects of many library types are unavoidably accessed by
>> >> their own destructor, after the end of their lifetime. Either way,
>> >> [res.on.objects] doesn't work.
>> >>
>> >> Treat [res.on.objects] as a non-normative reminder about
>> above-mentioned
>> >> parts from [basic.life]
>> >> (But in general, there are lots of nonsensical wording in the library
>> >> part, if read it from strict core part POV.)
>> >>
>> >
>> > The library desires to be more restrictive than the language here. It is
>> > possible to write user code that is robust against recursive invocation
>> (by
>> > ensuring that invariants hold before calling any operation not directly
>> > under the control of the user code, and by ensuring that all and any
>> > recursive modifications called from therein are handled in some defined
>> > way). But we don't want to constrain the library to that extent, so we
>> add
>> > UB under [constraints].
>>
>> AFAIK, recursive invocations are handled by
>> https://timsong-cpp.github.io/cppwp/n4868/reentrancy (plus some LWG
>> issue saying that invoking method A on a library object during execution of
>> method B on the same object is also (should also be) covered by this
>> paragraph as a recursive invocation)
>>
>
> That's LWG 2414, no?
>
> So you do not need [res.on.objects] to restrict a user destructor in
>> calling a vector's methods during that vector's destruction which caused
>> the invocation of the user's destructor.
>>
>
> You're saying that with LWG 2414 resolved, [res.on.objects]/2 becomes a
> dead letter? If so, agree that this looks like an improvement, and fixes
> the issue of a library class being able to access its internals during
> cdestruction.
>
> One issue is that ctdors are not non-static member functions, so that
> proposed resolution (to LWG 2414) would need to be extended to mention
> those as well.
>
> Another is the virtual member function issue (though that's probably not a
> problem in practice; I don't *think* the Standard has any virtual member
> functions not accessing object state).
>
> But most crucially, we're still left with the issue of whether invoking a
> non-static member function (or cdtor) is or causes access. Again, the
> proposed language in LWG 2414 says "During the execution of a standard
> library non-static member function F on an object, if that object is
> accessed [...]", implying that that object is of a class type, so not a
> scalar, so per se cannot be accessed.
>
So, what I would suggest would be:
1. Adopt the proposed resolution to LWG 2414, with the following amendments:
"During the execution of a standard library ^constructor, destructor or^
non-static member function F on an object, if ^any of the subobjects of^
that object is accessed through a glvalue [...]"
2. Change [res.on.objects]/2 to a /Note:/
3. Add language (after [res.on.objects]/2?) to forbid dynamic polymorphic
access during cdestruction:
"Before the beginning of the lifetime of an object x of standard library
type, and after the end of its lifetime, the following have undefined
behavior unless otherwise specified:
- forming a pointer or reference to a direct or indirect virtual base class
subobject of x;
- forming a pointer or reference to a direct or indirect non-static data
member of a direct or indirect virtual base class subobject of x;
- directly or indirectly calling a virtual member function on x;
- invoking the `typeid` operator with operand `x`;
- invoking `dynamic_cast` with operand `x`."
>
>
> On Thu, 20 Oct 2022 at 17:38, <language.lawyer_at_[hidden]> wrote:
>
>> >>> This issue came up on a StackOverflow question:
>> >>>
>> >>>
>> >>
>> https://stackoverflow.com/questions/74080177/double-free-in-the-c-standard-library-using-only-stdfunction-and-stdshared/74080676
>> >>>
>> >>> This led to a discussion here:
>> >>>
>> >>> https://chat.stackoverflow.com/rooms/248862/discuss-lwg2224
>> >>>
>> >>> Now, https://eel.is/c++draft/res.on.objects states
>> >>>
>> >>> "If an object of a standard library type is accessed, and the
>> beginning
>> >> of the object's lifetime does not happen before the access, or the
>> access
>> >> does not happen before the end of the object's lifetime, the behavior
>> is
>> >> undefined unless otherwise specified."
>> >>>
>> >>> According to a non-normative note in
>> >> https://eel.is/c++draft/defns.access,
>> >>>
>> >>> "Only glvalues of scalar type can be used to access objects. ...
>> >> Attempts to read or modify an object of class type typically invoke a
>> >> constructor or assignment operator; such invocations do not themselves
>> >> constitute accesses, although they may involve accesses of scalar
>> >> subobjects."
>> >>>
>> >>> The question is, can objects of class type be accessed?
>> >>
>> >> Can, in some sense. But it would be UB. E.g.
>> >>
>> >> struct S
>> >> {
>> >> char buf[sizeof(int)];
>> >> } s;
>> >>
>> >> reinterpret_cast<int&>(s) = 0; // kinda accesses `s`, but this violates
>> >> strict aliasing, so the behavior is actually undefined
>> >>
>> >
>> > It accesses `s`'s storage, sure.
>>
>> What do you mean by explicitly writing «storage»? You disagree with that
>> the expression accesses `s`?
>>
>
> Yes, since `s` is not a glvalue of scalar type.
>
> > But a read would not have undefined behavior.
>>
>> [basic.lval]/11 apply uniformly to both kinds of access: reads and writes.
>>
>
> Oh oops, didn't spot that the reinterpret_cast was to int. If it was to a
> storage-like type, that would be well-defined, no?
>
> >> Is it the intent that an access to a scalar subobject also constitutes
>> an
>> >> access to the object itself?
>> >>
>> >> No
>> >>
>> >>> If not, then it is impossible for objects of most library types to be
>> >> accessed.
>> >>
>> >> There are other things which are undefined if an object (or rather a
>> >> pointer/glvalue referring to it) is used before the lifetime has
>> started
>> >> https://timsong-cpp.github.io/cppwp/n4868/basic.life#6.sentence-5
>> >> https://timsong-cpp.github.io/cppwp/n4868/basic.life#7.sentence-4
>> >
>> >
>> > That doesn't apply to objects of class type within
>> > construction/destruction; it only applies before their constructor
>> starts
>> > or after the destructor ends. During construction/destruction, we are
>> > referred to [class.cdtor].
>>
>> I know.
>>
>> >> If so, then objects of many library types are unavoidably accessed by
>> >> their own destructor, after the end of their lifetime. Either way,
>> >> [res.on.objects] doesn't work.
>> >>
>> >> Treat [res.on.objects] as a non-normative reminder about
>> above-mentioned
>> >> parts from [basic.life]
>> >> (But in general, there are lots of nonsensical wording in the library
>> >> part, if read it from strict core part POV.)
>> >>
>> >
>> > The library desires to be more restrictive than the language here. It is
>> > possible to write user code that is robust against recursive invocation
>> (by
>> > ensuring that invariants hold before calling any operation not directly
>> > under the control of the user code, and by ensuring that all and any
>> > recursive modifications called from therein are handled in some defined
>> > way). But we don't want to constrain the library to that extent, so we
>> add
>> > UB under [constraints].
>>
>> AFAIK, recursive invocations are handled by
>> https://timsong-cpp.github.io/cppwp/n4868/reentrancy (plus some LWG
>> issue saying that invoking method A on a library object during execution of
>> method B on the same object is also (should also be) covered by this
>> paragraph as a recursive invocation)
>>
>
> That's LWG 2414, no?
>
> So you do not need [res.on.objects] to restrict a user destructor in
>> calling a vector's methods during that vector's destruction which caused
>> the invocation of the user's destructor.
>>
>
> You're saying that with LWG 2414 resolved, [res.on.objects]/2 becomes a
> dead letter? If so, agree that this looks like an improvement, and fixes
> the issue of a library class being able to access its internals during
> cdestruction.
>
> One issue is that ctdors are not non-static member functions, so that
> proposed resolution (to LWG 2414) would need to be extended to mention
> those as well.
>
> Another is the virtual member function issue (though that's probably not a
> problem in practice; I don't *think* the Standard has any virtual member
> functions not accessing object state).
>
> But most crucially, we're still left with the issue of whether invoking a
> non-static member function (or cdtor) is or causes access. Again, the
> proposed language in LWG 2414 says "During the execution of a standard
> library non-static member function F on an object, if that object is
> accessed [...]", implying that that object is of a class type, so not a
> scalar, so per se cannot be accessed.
>
So, what I would suggest would be:
1. Adopt the proposed resolution to LWG 2414, with the following amendments:
"During the execution of a standard library ^constructor, destructor or^
non-static member function F on an object, if ^any of the subobjects of^
that object is accessed through a glvalue [...]"
2. Change [res.on.objects]/2 to a /Note:/
3. Add language (after [res.on.objects]/2?) to forbid dynamic polymorphic
access during cdestruction:
"Before the beginning of the lifetime of an object x of standard library
type, and after the end of its lifetime, the following have undefined
behavior unless otherwise specified:
- forming a pointer or reference to a direct or indirect virtual base class
subobject of x;
- forming a pointer or reference to a direct or indirect non-static data
member of a direct or indirect virtual base class subobject of x;
- directly or indirectly calling a virtual member function on x;
- invoking the `typeid` operator with operand `x`;
- invoking `dynamic_cast` with operand `x`."
Received on 2022-10-21 11:33:12