Safe Serialization Under Mutual Suspicion/"Reversing" Evaluation

As we've seen, we make serializers, unserializers, and other transformers like expression simplifiers by composing a recognizer with a builder. The interface between the two is the DEBuilder API, explained in Appendix A: The Data-E Manual. Since most of the API is a straightforward reflection of the Data-E grammar productions, if you wish, you may safely skip these details and proceed here by example.

Evaluating Data-E
The semantics of Data-E are defined by the semantics of its evaluation as an E  program. We could unserialize using the full E  evaluator. However, this is inefficient both as an implementation and as an explanation. Instead, here is the Data-E evaluator as a builder, implementing exactly this subset of E 's semantics.

As we see, the E.call(..) underlined above is where all the object construction is done. All the rest is plumbing to hook the up the references among these objects.

The only extra parameter to the above code, in addition to those specified by the DEBuilder API, is the scope parameter to makeBuilder(..). Typically, we will express unserialization-time policy choices using only this hook. With a bit of pre-planning at serialization time, this can be a surprisingly powerful hook, and will often prove adequate.

Unevaluating to Data-E
We are now ready for the heart of serialization -- the Data-E subgraph recognizer. It has two parameters for expressing policy -- the uncallerList and the unscopeMap.

Since we are evaling &quot;in reverse&quot;, we need the inverse of a scope, which we call an unscope. An unscope maps from arbitrary values to a description of the &quot;variable name&quot; presumed to hold that reference. In the unscope table passed in as unscopeMap, each description is a normal variable name string, as would be used to look the value up in a scope. On each recognize(..)</tt>, the &quot;.diverge</tt>&quot; makes a private copy of the unscopeMap</tt> we put in the variable unscope</tt>, which we use from there. This private unscope table gets additional mappings from values to integers representing temporary variable indices.

The uncallerList</tt> is used to obtain a portrayal of each object, as we explain below.

During traversal, for every reference a subgraph recognizer already associates with a variable, whether from the original unscopeMap</tt> argument or because it has already been traversed, it builds a reference to that variable. Otherwise, it first builds a new pair of temporary variables for a promise and its resolver, and associates the promise variable as naming the new reference. In that context, it then builds code to generate a reconstruction of that reference. Finally, using defrec</tt> it builds code to resolve the previously generated promise to the reconstructed value.

Traversal as Uncalling
Once again, most of the code above is plumbing, to hook references up correctly. The actual traversal step where objects are &quot;taken apart&quot; -- the inverse of the builder's E.call(..)</tt> step -- is the underlined call to each uncaller</tt>. Each uncaller</tt> returns either null, indicating a failure to portray the object, or a triple corresponding to the three arguments to E.call(..)</tt> -- a receiver, a verb (message name), and a list of arguments. Such a triple portrays the object for purpose of reconstruction. It says that a reconstruction of the object would be an E.call(..)</tt> performed in the reconstructing context using (a reconstruction of) the receiver, the verb, and (reconstructions of) the arguments. The uncallerList</tt> functions as a search path -- each uncaller is tried until one succeeds or the list is exhausted. If none succeed, then the recognition as a whole is terminated with a thrown exception.

The default uncallerList</tt> consists of the minimalUncaller</tt> shown below and the import__uriGetter</tt>:

The minimalUncaller</tt> simply asks an object to provide its own portrayal. Our earlier <tt>generationCounter</tt> is an example of an object that overrides <tt>__optUncall</tt> to provide its own self portrait. We say that such an object is transparent -- it provides this portrayal to any of its clients. The <tt>minimalUncaller</tt> can only portray transparent objects. Other uncallers are for portraying non-transparent objects. Some, such as the <tt>import__uriGetter</tt>, are a special category of uncaller called a Loader. These also have a <tt>.get(String)</tt> method that acts as the inverse of their <tt>.optUncall(..)</tt> method. For example, since <tt>StringBuffer</tt> is a safe class, it can be imported using the <tt>import__uriGetter</tt>: As explained earlier, the above code uses the URI-expression. It is just syntactic shorthand for: The resulting object is a maker -- its protocol consists of (the enabled subset of) the public constructors and static methods of the class <tt>StringBuffer</tt>. That's why we name it <tt>makeStringBuffer</tt> -- it acts mostly as a function for making <tt>StringBuffer</tt>s.

Going the other way So we see that the <tt>import__uriGetter</tt> is in effect saying In order to reconstruct <tt>makeStringBuffer</tt>, send the &quot;<tt>.get</tt>&quot; message to me, the <tt>import__uriGetter</tt>, with the string <tt>&quot; java.lang.makeStringBuffer &quot;</tt> as argument. Loaders will normally follow this pattern, varying only the contents of the string argument.

Putting all this together, and remembering that <tt>deSrcKit</tt> will depict using the URI-expression shorthand when it can, we have Note that the <tt>makeStringBuffer</tt> reconstructed by these means isn't necessarily equivalent to the original. Rather, <tt>import__uriGetter</tt> embodies the policy choice that the reconstruction should be whatever object is importable by the same name in the reconstruction context. If this context represents a different version of the system, in which the object imported by this name acts differently, this policy choice would have us live with the consequences, including the possible failure to reconstruct. This is often the right engineering decision, and corresponds closely to the decisions built into other serialization systems, such as JOSS's handling of classes [ref Shapiro].

We now have all the basic ingredients needed to explain and address the security issues raised by serialization.

= Corresponding Concepts in Conventional Serialization = In our terminology, like Data-E, JOSS also solicits from each object not its depiction, but its portrayal in terms of other objects. Mallet can only claim to have a reference to Alice by producing a reference to Alice, which he can only do if he actually has such a reference. If an object simply implements <tt>Serializable</tt> and does nothing else, then its internal implementation doubles as its self-portrait. However, an object can offer a nominated replacement -- another object to be serialized in its stead, whose portrayal thereby serves as the original object's self-portrait. The serializer may use the nominated replacement. Or it may appoint its own replacement, by overriding the <tt>replaceObject(..)</tt> method of <tt>ObjectOutputStream</tt>, just as our serializer can appoint its own portrayal by adding an uncaller to the uncaller list. The resulting depiction is a literal picture only of the graph of appointed replacements.

JOSS provides similar flexibility during unserialization, with objects offering a nominated resolution to take their place in the unserialized graph, with the unserializer potentially substituting an appointed resolution. Given cyclic graphs and the non-redirectability of Java references, this cannot be implemented correctly in Java. Using promises, we can easily implement the equivalent correctly in <font color="#009000">E  for Data-E (and likewise in any other object-capability language with delayed references), but we haven't yet needed this flexibility during unserialization.

This is wikified from Part 2: "Reversing" Evaluation