Topic: DR: Member pointers allow circumventing protected member access
Author: litb <Schaub-Johannes@web.de>
Date: Thu, 25 Jun 2009 16:26:59 CST Raw View
As it turns out, you can access the protected members of any class by
taking a member pointer and then applying it to an object of that
class. The example below seems to be well-formed according to 11.2
[class.access.base]/5, and to the additional rules of 11.5
[class.protected]/1, even though there is no object of a derived class
involved at all.
struct X {
protected:
int m;
};
int& getM(X &x) {
struct Voyeur : X { using X::m; };
return x.*(&Voyeur::m);
}
int main() {
X x;
getM(x) = 0;
}
This can also be achieved by a static member function within the
derived class.
--
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html ]
Author: Greg Herlihy <greghe@mac.com>
Date: Fri, 26 Jun 2009 23:10:42 CST Raw View
On Jun 25, 3:26 pm, litb <Schaub-Johan...@web.de> wrote:
> As it turns out, you can access the protected members of any class by
> taking a member pointer and then applying it to an object of that
> class. The example below seems to be well-formed according to 11.2
> [class.access.base]/5, and to the additional rules of 11.5
> [class.protected]/1, even though there is no object of a derived class
> involved at all.
>
> struct X {
> protected:
> int m;
>
> };
>
> int& getM(X &x) {
> struct Voyeur : X { using X::m; };
> return x.*(&Voyeur::m);
>
> }
>
> int main() {
> X x;
> getM(x) = 0;
>
> }
C++ access controls apply to the names of the class's data members -
and not to the data member objects themselves. Therefore, a routine
with access to a data member's name is certainly free to return a
pointer to that data member. Therefore, the above program does not
demonstrate any defect in in the language, but instead behaves as it
should.
Moreover, there is no reason to forbid pointers to protected or
private data members (since a C++ program that does not wish to have
such pointers can simply refrain from creating them, whereas a program
that did need to create pointers to protected or private data members
- would be pointlessly prevented from doing so).
Greg
--
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html ]
Author: Nick Hounsome <nick.hounsome@googlemail.com>
Date: Fri, 26 Jun 2009 23:32:45 CST Raw View
On 25 June, 23:26, litb <Schaub-Johan...@web.de> wrote:
> As it turns out, you can access the protected members of any class by
> taking a member pointer and then applying it to an object of that
> class. The example below seems to be well-formed according to 11.2
> [class.access.base]/5, and to the additional rules of 11.5
> [class.protected]/1, even though there is no object of a derived class
> involved at all.
>
> struct X {
> protected:
> int m;
>
> };
>
> int& getM(X &x) {
> struct Voyeur : X { using X::m; };
> return x.*(&Voyeur::m);
>
> }
>
> int main() {
> X x;
> getM(x) = 0;
>
> }
>
> This can also be achieved by a static member function within the
> derived class.
]
This is just one of the problems with inheritance - There is no way
you can prevent your children from selling the familly jewels.
Usually I would expect it to be a bad design that used protected data
- not so much because of the access issues but because it means that
you are
no longer in control of class invariants and you can never again
change the implementation to one that doesn't use 'm'.
If you really need access to whatever 'm' is supposed to represent
then it should be via protected access methods.
--
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html ]
Author: litb <Schaub-Johannes@web.de>
Date: Sat, 27 Jun 2009 18:06:09 CST Raw View
On 27 Jun., 07:10, Greg Herlihy <gre...@mac.com> wrote:
> On Jun 25, 3:26 pm, litb <Schaub-Johan...@web.de> wrote:
>
>
>
> > As it turns out, you can access the protected members of any class by
> > taking a member pointer and then applying it to an object of that
> > class. The example below seems to be well-formed according to 11.2
> > [class.access.base]/5, and to the additional rules of 11.5
> > [class.protected]/1, even though there is no object of a derived class
> > involved at all.
>
> > struct X {
> > protected:
> > int m;
>
> > };
>
> > int& getM(X &x) {
> > struct Voyeur : X { using X::m; };
> > return x.*(&Voyeur::m);
>
> > }
>
> > int main() {
> > X x;
> > getM(x) = 0;
>
> > }
>
> C++ access controls apply to the names of the class's data members -
> and not to the data member objects themselves. Therefore, a routine
> with access to a data member's name is certainly free to return a
> pointer to that data member. Therefore, the above program does not
> demonstrate any defect in in the language, but instead behaves as it
> should.
>
Some people seem to have misunderstood the DR. First the whole purpose
of the DR is that this accesses a protected member without undefined
behavior happening (as opposed to "clever" things like "#define
private public" that violate the ODR). And second, what is the purpose
of "protected", if the only difference is that instead of accessing a
member using a.f, you access the member using getF(a), with getF being
written trivially in one line of code?
Here is a better testcase by another guy. I wish my testcases would be
equally clear :)
// snip
struct X { protected: int m; };
struct Y: X {
// error: X::m is protected within this context
static int & getM_naive(X & x) { return x.m; }
// ok, no problem..
static int & getM_sneaky(X & x) { return x.*(&Y::m); }
};
// snap
In my view, the problem is that things like &Y::m yield a X::*, and do
not preserve the use of Y as the nested name specifier. The standard
then has to accept applications of it on a X, because it doesn't know
how the member pointer was taken in the first case. It's also not
feasible to make it have type X::*, because that means we cannot apply
it on things like the following:
X &x = some_Y;
// if &Y::m had type T Y::*, this would fail
x.*&Y::m = ...;
I propose to add text like the following into 5.5[expr.mptr.oper]/4:
"If the dynamic type of the object expression is not of type the same
type as the nested name specifier used to obtain the member pointer or
not of a type derived from it, then the program is ill-formed; no
diagnostic required"
--
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html ]
Author: "Alf P. Steinbach" <alfps@start.no>
Date: Sun, 28 Jun 2009 17:40:24 CST Raw View
* litb:
>
> In my view, the problem is that things like &Y::m yield a X::*, and do
> not preserve the use of Y as the nested name specifier. The standard
> then has to accept applications of it on a X, because it doesn't know
> how the member pointer was taken in the first case. It's also not
> feasible to make it have type X::*, because that means we cannot apply
> it on things like the following:
>
> X &x = some_Y;
> // if &Y::m had type T Y::*, this would fail
> x.*&Y::m = ...;
I can't see why anyone would want to have that allowed without explicit casting?
After all, if you know that x is really a Y (or derived), and hardcode that,
then just declare x as a Y in the first place.
> I propose to add text like the following into 5.5[expr.mptr.oper]/4:
>
> "If the dynamic type of the object expression is not of type the same
> type as the nested name specifier used to obtain the member pointer or
> not of a type derived from it, then the program is ill-formed; no
> diagnostic required"
I can't see that adding even more UB and optional diagnostics solves anything;
compilers can already diagnose at will without any specific permission for any
particular case.
What's needed or IMO desirable is enforcement by the type rules, so that the
same access rules apply for member pointers as for other code.
And for that, 5.3.1/2 about the type of an address operator expression should
be changed.
Currently it says
'If the member is a non-static member of class C of type T, the type of the
result is "pointer to member of class C of type T"'
And one reasonable resolution is
'If the member is a non-static member of class C1 of type T, the type of the
result is "pointer to member of class C2 of type T", where C2 is the class
nominated by the qualified-id [note: C2 is the same as or derived from C1]'
and in the following example changing "has type int A::*" to "has type B::*".
Impact on existing code: explicit casting needs to be added to code that uses
the type system loophole, probably a number of bugs will be discovered. :-)
Cheers,
- Alf
--
Due to hosting requirements I need visits to <url: http://alfps.izfree.com/>.
No ads, and there is some C++ stuff! :-) Just going there is good. Linking
to it is even better! Thanks in advance!
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html ]
Author: Greg Herlihy <greghe@mac.com>
Date: Sun, 28 Jun 2009 17:41:44 CST Raw View
On Jun 27, 5:06 pm, litb <Schaub-Johan...@web.de> wrote:
> Some people seem to have misunderstood the DR. First the whole purpose
> of the DR is that this accesses a protected member without undefined
> behavior happening (as opposed to "clever" things like "#define
> private public" that violate the ODR).
Access controls are strictly a compile-time check - they have no
effect on a program's behavior. Note that in the example program, the
code that obtained the pointer to the data member did have access to
that protected member..
> And second, what is the purpose
> of "protected", if the only difference is that instead of accessing a
> member using a.f, you access the member using getF(a), with getF being
> written trivially in one line of code?
The difference is significant: the getF() form indicates that
accessing the protected members (of a different object) is deliberate.
After all, if a class method is ablre to access its own protected
members, then surely, it should somehow be able to access the
protected members of other instances of the same class.
However, to distinguish between accessing the protected data of the
current instance versus other instances, a class method must use a
member pointer in order to access the protected members of a different
instance. This extra step helps to ensure that the access to the
protected members is deliberate.
> Here is a better testcase by another guy. I wish my testcases would be
> equally clear :)
>
> // snip
> struct X { protected: int m; };
>
> struct Y: X {
> // error: X::m is protected within this context
> static int & getM_naive(X & x) { return x.m; }
> // ok, no problem..
> static int & getM_sneaky(X & x) { return x.*(&Y::m); }};
>
> // snap
>
> In my view, the problem is that things like &Y::m yield a X::*, and do
> not preserve the use of Y as the nested name specifier.
No, &Y::m yields a member pointer whose type is in fact "int Y::*", so
the type "Y" is encoded in the member pointer itself.
> The standard
> then has to accept applications of it on a X, because it doesn't know
> how the member pointer was taken in the first case. It's also not
> feasible to make it have type X::*, because that means we cannot apply
> it on things like the following:
>
> X &x = some_Y;
> // if &Y::m had type T Y::*, this would fail
> x.*&Y::m = ...;
x.&Y::m does fail in any context that does not have access to Y's
protected data members.
> I propose to add text like the following into 5.5[expr.mptr.oper]/4:
>
> "If the dynamic type of the object expression is not of type the same
> type as the nested name specifier used to obtain the member pointer or
> not of a type derived from it, then the program is ill-formed; no
> diagnostic required"
In C++, member pointers can be applied only to objects of the class
specified by the member pointer (or a derived class). A program that
applies a member pointer to an object of some other class is already
ill-formed - and a diagnostic is required.
Greg
--
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html ]
Author: litb <Schaub-Johannes@web.de>
Date: Mon, 29 Jun 2009 10:07:15 CST Raw View
On Jun 29, 1:41 am, Greg Herlihy <gre...@mac.com> wrote:
> On Jun 27, 5:06 pm, litb <Schaub-Johan...@web.de> wrote:
>
> > Some people seem to have misunderstood the DR. First the whole purpose
> > of the DR is that this accesses a protected member without undefined
> > behavior happening (as opposed to "clever" things like "#define
> > private public" that violate the ODR).
>
> Access controls are strictly a compile-time check - they have no
> effect on a program's behavior. Note that in the example program, the
> code that obtained the pointer to the data member did have access to
> that protected member..
>
How does this apply to my statement that this DR concerns
circumventing "protected" without UB? Undefined behavior can happen at
compile time. Think of unbound template instantiations, non-empty file
without newline at the end, ... . Undefined behavior means your
implementation can do anything. The compiler is part of that.
> > And second, what is the purpose
> > of "protected", if the only difference is that instead of accessing a
> > member using a.f, you access the member using getF(a), with getF being
> > written trivially in one line of code?
>
> The difference is significant: the getF() form indicates that
> accessing the protected members (of a different object) is deliberate.
>
protected access control means only the derived class/friends has
access to the members of the inherited members. But as the DR shows,
you can easily go and modify std::stack's adapted deque or other
intimate implementation details. This can't be the Standard's
intention. Whether the access to the protected member looks deliberate
or not doesn't matter.
> After all, if a class method is ablre to access its own protected
> members, then surely, it should somehow be able to access the
> protected members of other instances of the same class.
>
In the example i showed, i accessed a protected member of some
arbitrary typed object. Whether i use an intermediary class to achieve
that or not shouldn't matter at all. The result should be that the
member stays protected.
> However, to distinguish between accessing the protected data of the
> current instance versus other instances, a class method must use a
> member pointer in order to access the protected members of a different
> instance. This extra step helps to ensure that the access to the
> protected members is deliberate.
>
This doesn't make sense to me. Either the access to objects not in the
same hierarchy is allowed, or it is forbidden. In this case, it is
allowed by the Standard, and it seems to be just because of a lack of
mechanism to forbid it. I don't think it's deliberately allowed with
the use of member pointers, just to make the access more difficult or
complicated.
>
>
> > Here is a better testcase by another guy. I wish my testcases would be
> > equally clear :)
>
> > // snip
> > struct X { protected: int m; };
>
> > struct Y: X {
> > // error: X::m is protected within this context
> > static int & getM_naive(X & x) { return x.m; }
> > // ok, no problem..
> > static int & getM_sneaky(X & x) { return x.*(&Y::m); }};
>
> > // snap
>
> > In my view, the problem is that things like &Y::m yield a X::*, and do
> > not preserve the use of Y as the nested name specifier.
>
> No, &Y::m yields a member pointer whose type is in fact "int Y::*", so
> the type "Y" is encoded in the member pointer itself.
>
Please check your claims before you post it to a DR discussion,
especially if the reporter claims otherwise. You are wrong, of course.
> > The standard
> > then has to accept applications of it on a X, because it doesn't know
> > how the member pointer was taken in the first case. It's also not
> > feasible to make it have type X::*, because that means we cannot apply
> > it on things like the following:
>
> > X &x = some_Y;
> > // if &Y::m had type T Y::*, this would fail
> > x.*&Y::m = ...;
>
> x.&Y::m does fail in any context that does not have access to Y's
> protected data members.
>
As the comment says, it would if &Y::m would have type T Y::*, it
doesn't with current C++ semantics.
> > I propose to add text like the following into 5.5[expr.mptr.oper]/4:
>
> > "If the dynamic type of the object expression is not of type the same
> > type as the nested name specifier used to obtain the member pointer or
> > not of a type derived from it, then the program is ill-formed; no
> > diagnostic required"
>
> In C++, member pointers can be applied only to objects of the class
> specified by the member pointer (or a derived class). A program that
> applies a member pointer to an object of some other class is already
> ill-formed - and a diagnostic is required.
>
Yes, you are right. But I didn't claim that this is allowed (in fact,
i showed a failing example).
--
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html ]
Author: "Alf P. Steinbach" <alfps@start.no>
Date: Mon, 29 Jun 2009 10:06:42 CST Raw View
* Greg Herlihy:
> On Jun 27, 5:06 pm, litb <Schaub-Johan...@web.de> wrote:
>>
>> // snip
>> struct X { protected: int m; };
>>
>> struct Y: X {
>> // error: X::m is protected within this context
>> static int & getM_naive(X & x) { return x.m; }
>> // ok, no problem..
>> static int & getM_sneaky(X & x) { return x.*(&Y::m); }};
>>
>> // snap
>>
>> In my view, the problem is that things like &Y::m yield a X::*, and do
>> not preserve the use of Y as the nested name specifier.
>
> No, &Y::m yields a member pointer whose type is in fact "int Y::*", so
> the type "Y" is encoded in the member pointer itself.
That's wrong.
Cheers & hth.,
- Alf
--
Due to hosting requirements I need visits to <url: http://alfps.izfree.com/>.
No ads, and there is some C++ stuff! :-) Just going there is good. Linking
to it is even better! Thanks in advance!
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html ]
Author: litb <Schaub-Johannes@web.de>
Date: Mon, 29 Jun 2009 10:43:32 CST Raw View
On 29 Jun., 01:40, "Alf P. Steinbach" <al...@start.no> wrote:
> * litb:
>
>
>
> > In my view, the problem is that things like &Y::m yield a X::*, and do
> > not preserve the use of Y as the nested name specifier. The standard
> > then has to accept applications of it on a X, because it doesn't know
> > how the member pointer was taken in the first case. It's also not
> > feasible to make it have type X::*, because that means we cannot apply
> > it on things like the following:
>
> > X &x = some_Y;
> > // if &Y::m had type T Y::*, this would fail
> > x.*&Y::m = ...;
>
> I can't see why anyone would want to have that allowed without explicit casting?
>
> After all, if you know that x is really a Y (or derived), and hardcode that,
> then just declare x as a Y in the first place.
>
I think you are right. And when we pass &Y::m to a template, that
invokes the pointer on a vector of X*, then we could still cast up
manually.
> > I propose to add text like the following into 5.5[expr.mptr.oper]/4:
>
> > "If the dynamic type of the object expression is not of type the same
> > type as the nested name specifier used to obtain the member pointer or
> > not of a type derived from it, then the program is ill-formed; no
> > diagnostic required"
>
> I can't see that adding even more UB and optional diagnostics solves anything;
> compilers can already diagnose at will without any specific permission for any
> particular case.
>
I wanted to draw such code invalid, disregarding from what the
compiler does. So that such stuff isn't allowed anymore. But my
proposed solution is probably a bit messy anyway, because it makes a
restriction on something not really carried over by the type system.
This would probably keep being a "theoretic ill-formed that's never
diagnosed", and i agree with you such things should be avoided if
that's what you are pointing at.
> What's needed or IMO desirable is enforcement by the type rules, so that the
> same access rules apply for member pointers as for other code.
>
> And for that, 5.3.1/2 about the type of an address operator expression should
> be changed.
>
> Currently it says
>
> 'If the member is a non-static member of class C of type T, the type of the
> result is "pointer to member of class C of type T"'
>
> And one reasonable resolution is
>
> 'If the member is a non-static member of class C1 of type T, the type of the
> result is "pointer to member of class C2 of type T", where C2 is the class
> nominated by the qualified-id [note: C2 is the same as or derived from C1]'
>
> and in the following example changing "has type int A::*" to "has type B::*".
>
> Impact on existing code: explicit casting needs to be added to code that uses
> the type system loophole, probably a number of bugs will be discovered. :-)
>
I like your solution. I don't know how much code would be broken by
this, though. Any idea why this isn't so in the Standard already?
--
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html ]