Hi Chris,

On 01/06/2015 12:10 PM, Chris Hegarty wrote:
On 6 Jan 2015, at 07:27, Peter Levart <peter.lev...@gmail.com> wrote:
Hi Brian,
On 01/05/2015 09:51 PM, Brian Goetz wrote:
Thanks Peter(s): I think it's just about time to re-sync on the goals, since 
there are an infinitude of issues with serialization, only some of which we can 
address.

To that end, let me toss out some of the results of our recent explorations 
with improving serialization, where we started out on something not unlike what 
Peter is suggesting, and dialed back.

One thing we can do to positively improve the serialization experience is to 
reduce the set of situations where users have to write explicit readObject() 
methods.  In the JDK, we end up having to do so for all manner of 
security-motivated reasons, and such code is hard to write correctly, hard to 
validate, hard to audit, and hard to evolve.

After an audit of our codebase (thanks Chris!), we identified some of the 
biggest offenders on this score:

- Validating invariants
- Ensuring confinement

The latter comes up in cases where there's a field:

class Foo {
    private final Bar bar = new BarImpl();
}

which implicitly has an invariant: the Bar referenced by Foo.bar is a) exactly 
of type BarImpl and not some other subclass and b) there is exactly one 
reference to that BarImpl (assuming it doesn't escape Foo.)

Note that both of these still use the default deserialization; having to 
override readObject() just to do some extra checking is error-prone and messy.  
So I think the goal of a separate validation method which is called 
automatically is a good one, since it means we can ditch some percentage of 
otherwise-needless readObject implementations (which then have to jump through 
hoops to set final fields.)
readObject() can be used on both ends of the spectrum or anywhere in-between.
It can be used for explicit deserialization or just for validation of default 
deserialized state.

Well, that "in-between" is actually not really available (unless explicit (de)serialization part uses "block data mode", which is not very evolvable). But I think this could be enhanced (see below).

Would separate validation method give us anything more or simplify things? 
readObject() can be used just as:

private void readObject(ObjectInputStream in) throws IOException, 
ClassNotFoundException {
    in.defaultReadObject();
    ...
    just validation
    ...
}
One issue with the above pattern is that defaultReadObject will read and set 
the fields before the explicit validation code, ‘just validate’, is executed. 
So if validation fails, throws an appropriate exception, the object may be left 
in an inconsistent state.


Right.

What about the following addition to ObjectInputStream.GetField/ObjectOutputStream.PutField API. For example:


public class Interval implements Serializable {

    // current fields
    private final int lo, hi;

    private static final ObjectStreamField[] serialPersistentFields = {
        new ObjectStreamField("lo", Integer.TYPE),
        new ObjectStreamField("hi", Integer.TYPE),
        // legacy 'len' field
        new ObjectStreamField("len", Integer.TYPE)
    };

    private static void validate(int lo, int hi) {
        // invariant
        if (lo > hi)
            throw new IllegalArgumentException("lo:" + lo + " > hi:" + hi);
    }

    public Interval(int lo, int hi) {
        validate(lo, hi);
        this.lo = lo;
        this.hi = hi;
    }

    public int getLo() { return lo; }

    public int getHi() { return hi; }

    public int getLen() { return hi - lo; }

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { ObjectInputStream.GetField fields = in.readFields(); // this already validates the types
        // validate 'lo' and 'hi' fields invariant
        int lo = fields.get("lo", 0);
        int hi = fields.get("hi", 0);
        validate(lo, hi);
        // validate legacy 'len' field
        int len = fields.get("len", hi - lo);
if (len != hi - lo) throw new IllegalArgumentException("len:" + len + " is wrong");
        // set current fields from read data
        fields.defaultReadFields(); // this is new API!
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        ObjectOutputStream.PutField fields = out.putFields();
        // put current fields
        fields.defaultPutFields(); // this is new API!
        // put legacy len field
        fields.put("len", hi - lo);
        // write-out buffered puts
        out.writeFields();
    }
}


I've taken a quick look at what might be needed to do that, and it appears very simple:


http://cr.openjdk.java.net/~plevart/misc/ObjectIOStream.GetPutFields/webrev/



One approach, with existing serialization, is to use the GetField API rather 
than defaultReadObject. Read the field values into locals and validate before 
setting. But this again falls foul for final fields.

The above addition to [Get|Put]Fields API is a mix of explicit validation and default setters which seems to cover final fields.


As an aside: I have been looking at ObjectInputStream, and related classes, 
recently. The current default implementation of readObject sets non-primitive 
field values one at a time, without first checking that all their types are 
assignable. In many cases the setting of field values can be deferred until 
after they have been validated, at a minimum that the non-primitive types are 
assignable. Whole hierarchy failure atomicity in many cases, or at a minimum 
per class descriptor failure atomicity, can be achieved ( either all fields 
will be set, or contain their default values ). Preliminary webrev [1].

 From this exploration, it seems like the natural place for an invariant 
checker is between the reading and reconstitution of field values, and the 
assigning of them. If the invariant checker could be called by the 
serialization mechanism then the users code can use final fields, without 
needing to muck around with reflection, and also have the added benefit of, in 
many cases, not potential leaving the object in an inconsistent state.

To me, these benefits seems worthwhile, and justify a separate invariant 
checker mechanism.

The type checking for failure atomicity is a good addition to OIS.defaultReadObject() method behaviour. The separate invariant checker mechanism would have to expose field values before setting them to the fields of object being deserialized. The GetFields API lends itself to that purpose. Whether to reserve another private 'validateReadObject' special method signature for that purpose is a question. The above is an alternative that uses readObject() for both - default field assignment and explicit validation.

Regards, Peter


I agree that being able to play nicely with final fields is also an important 
goal here.
Explicit deserialization and final fields don't play nicely together currently, 
yes. Sometimes data-racey-publication thread-safety is sacrificed just because 
explicit deserialization would complicate code too much by using reflection.
Given my above comments, certainly in the case of invariant checking, we should 
be able to make this experience a little better ( confining the use of 
reflectively setting final fields and ensuring their safe publication in one 
place, the Serialization implementation ).

-Chris.

[1] http://cr.openjdk.java.net/~chegar/failureAtomicity/


Reply via email to