[E-Lang] Immutable map operations
Mark S. Miller
markm@caplet.com
Sun, 01 Apr 2001 23:00:36 -0800
At 05:30 PM Sunday 4/1/01, hal@finney.org wrote:
>Isn't it risky to rely for security on the fact that an object will only
>respond to messages with particular names?
Yes.
>It seems like this kind of
>problem could arise in other contexts.
True.
>I remember one of my concerns
>with the MintMaker code was that Assays and Purses responded to some of
>the same messages and in some contexts you could substitute one for the
>other, causing surprising results.
An excellent example!
>Is this a serious problem, or can it be easily avoided?
In short, I'm not sure if it can be easily avoided, so for now it should be
considered a serious problem. Perhaps the serious problem. This is the
kind of issue that cries out for static (type-checking-like) security
checking technology. In the meantime, design rules help a lot, such as "No
subtypes that add authority."
In long:
First, let's contrast E with a hypothetical capability secure variant of
Java (which I'll refer to as "SafeJ"). This is essentially Java with all
the parts that break capability security removed, and with the static type
safety left in. Even without doing much else, this is already pretty close
to a usable programming language. (The mission of Original-E was, among
other things, to make that language usable. Unfortunately, Javasoft stood
in the way. Btw, I've got safej.{com,org} in case we ever want to revive
this effort.)
The above problem is somewhat worse in E than it would be in SafeJ, precisely
because of an issue you allude to above: ".. messages with particular
names". In Java (and SafeJ), you only get dynamic polymorphism (runtime
substitutability) according to message name as declared within declared
types. In E, it's just the message name. SafeJ could prevent accidental
polymorphism not prevented by E.
(Note: The taxonomy is actually a bit more complicated, as Joule is
dynamically typed like E, but does polymorphism according to declared types
like SafeJ, and it would prevent accidental polymorphism in the same sense
that SafeJ would. Btw, Joule's technique for doing this was inspired by
Jonathan Rees' earlier language, T.)
Conventional static type declarations can help catch such bugs, but only
imperfectly, and mostly not by preventing accidental polymorphism. A common
object programming practice in statically typed languages, especially under
maintenance, is to provide the least specific (super-most) type that enables
the program to compile without extra casts. Besides being the least work,
it leaves the program maximally reusable.
Under this practice, neither of these bugs (the BlobMaker and the MintMaker)
would have been caught in SafeJ, as the problematic polymorphism was within
the programmer's model of the types in his code, and therefore the types he
probably would have declared. Neither bug was due to accidental polymorphism.
Instead, the programmer should leverage the notion of types-as-contracts,
and declare a specific enough type to express the contractual properties
he's counting on. If the BlobMaker were instead declared as
def ConstList := <import:org.erights.e.elib.tables.ConstList> asType()
class BlobMaker(list :ConstList) :any {
def Blob {
to get(index) :any { list[index] }
to getStuff() :ConstList { list }
.... list ...
}
}
either of these new ":ConstList" guards would have caught the problem.
Likewise for a guard that simply insists on immutability. (MarcS's
suggested ":pbc", which stands for PassByConstruction, would have worked in
this case, although pbc objects are necessarily immutable. A ":PassByCopy"
or ":immutable" guard would work, once we have such guards.)
Static vs Dynamic, SafeJ vs E
So E can express these constraints as well. E has two weaknesses compared
SafeJ:
1) The above accidental vs declared polymorphism issue. Experience in
dynamically types OO languages such as Smalltalk say this is more a problem
in theory than in practice, but this question hasn't been examined in a
security context.
2) Until we have a static type inference tool we can apply to E (elint), the
Guard violation above will only be caught at runtime, whereas in SafeJ it
might have been caught at compile time. This creates a greater
vulnerability to denial of service, but not other security problems. (And E
does not generally claim to enable resistance to denial of service.)
In compensation, E has some comparative strengths:
a) By being a runtime check, the guard can engage in arbitrary computation,
and so check conditions infeasible to determine statically (like the range
checks in the Ode's MintMaker).
b) In concert with auditors, guards can verify not only that an object's
code alleges that it satisfies some contract, but that it actually does
satisfy a (restricted category of) contract. For the above case, since
Alice and BlobMaker are on the same side, it probably would have been
adequate for 'list' to allege itself to be immutable. But for other
situations, most notably confinement, you want the assurance from something
you do trust (the ":confined" auditor/guard) that something you don't trust
is confined.
Design Rules for Safer Capability Programming
Even without static security checkers, we can substantially reduce the
danger by adopting certain design rules when designing our abstractions. In
a Joule context, I believe, Dean first coined "No subtypes that add
authority." The BlobMaker bug is directly due to my violation of that rule
in designing E's collection classes: both FlexList and ConstList are
subtypes of EList, and most of their protocol is defined up in EList. The
SafeJ/ELib programmer is in essentially as much danger from this bug
as is the E programmer.
This design rule is in direct violation of OO conventional wisdom: If type B
is fully conventionally upwards compatible from type A, then it's
conventionally fine to make B a subtype of A. The problem is that adding
functionality is conventionally considered upwards compatible. The type-as-
contract is conventionally understood in terms of what the object must be
able to do. For security purposes, we need declarations-as-contracts that
also say what the object must not be able to do. The cost of this design
rule is less polymorphism, and therefore less opportunities for reuse. This
is the only currently known issue for which good capability design conflicts
with the conventional wisdom of good OO design.
The problem with conventional "upwards compatibility" occurs both across
types and across versions. A later version of a system may be
conventionally upwards compatible with the earlier version, while doing new
things that violate the *inabilities* other code was counting on. Like
conventional type checking, conventional regression testing is powerless to
detect these violations of required inabilities. The corresponding
versioning rule may be "Later versions should not both add authority and
claim compatibility with earlier versions."
I don't think the polymorphism-abuse bug you found in the MintMaker would
have been caught or fixed by the above design rule. I have remained anxious
to return to the analysis of that incident to try to understand more
generally what the problem is, how we fell into it and, and what design
practices and/or static security checkers would have helped. It's a great
example.
Cheers,
--MarkM