Software Engineer.

1,530 words

You'll only receive email when CAFEBABE publishes a new post


Notes and Format

  • What: Convert an object into a stream of bytes by storing the state of the object.
  • What gets stored in the byte stream:
    • The member variables/fields
    • function signature (but NOT the function's code) impacts the bytes created for the object
  • How: "The default serialization mechanism for an object writes the class of the
    object, the class signature, and the values of all non-transient and
    non-static fields. References to other objects (except in transient or
    static fields) cause those objects to be written also. Multiple references
    to a single object are encoded using a reference sharing mechanism so that
    graphs of objects can be restored to the same shape as when the original was
  • How: "The default deserialization mechanism for objects restores the contents
    of each field to the value and type it had when it was written. Fields
    declared as transient or static are ignored by the deserialization process.
    References to other objects cause those objects to be read from the stream
    as necessary. Graphs of objects are restored correctly using a reference
    sharing mechanism. New objects are always allocated when deserializing,
    which prevents existing objects from being overwritten."

    "Reading an object is analogous to running the constructors of a new
    object. Memory is allocated for the object and initialized to zero (NULL).
    No-arg constructors are invoked for the non-serializable classes and then
    the fields of the serializable classes are restored from the stream starting
    with the serializable class closest to java.lang.object and finishing with
    the object's most specific class. "  

  • Important: With respect to byte streams, notes on closing the streams:
    • For an output stream, calling close() ensures that the data gets written to its destination (file or whathaveyou); not closing the stream will result in the data being in the buffer without being written to its destination.
    • For an input stream, a file descriptor is associated with the stream, and is stored in the file descriptor table; consequently, the file descriptor table can run out of space if there is a lot of unclosed file streams, and may stop the program from opening new files -- which can be problematic. Close your input file streams.
  • Important: it introduces vulnreability in the code since the serialized byte stream can be easily edited to change bytes such that some invariant of the serialized object is broken.
  • source

Stream Format:

  • Stream Format For Classes

    Magic NumberVersion UIDclassNameserialVersionUIDhandleclassDescFlags bytefield1TypeCode field1NamefieldNTypeCode fieldNName <classNameForFieldOfObjectType>

  • Stream Format For Objects        

Magic NumberVersion UIDclassNameserialVersionUIDhandleclassDescFlags bytefield1TypeCode field1NamefieldNTypeCode fieldNName <classNameForFieldOfObjectType>

classData[]: fieldsInClass, bytesForPrimitives

magic version contents

content contents content

object: classes, outputStream Objects, Enums

blockdata: primitive dataTypes

  • "All of the write methods for primitive types encode their values using a DataOutputStream to put them in the standard stream format. The bytes are buffered into block data records so they can be distinguished from the encoding of objects. This buffering allows primitive data to be skipped if necessary for class versioning. It also allows the stream to be parsed without invoking class-specific methods."

Codes used in the serialized data:


B = byte

C = char

D = double

F = float

I = integer

J =long

S = short

Z = boolean


[ = array

L = object

final static short STREAMMAGIC = (short)0xaced;
    final static short STREAMVERSION = 5;
    final static byte TCNULL = (byte)0x70;
    final static byte TCREFERENCE = (byte)0x71;
    final static byte TCCLASSDESC = (byte)0x72;
    final static byte TCOBJECT = (byte)0x73;
    final static byte TCSTRING = (byte)0x74;
    final static byte TCARRAY = (byte)0x75;
    final static byte TCCLASS = (byte)0x76;
    final static byte TCBLOCKDATA = (byte)0x77;
    final static byte TCENDBLOCKDATA = (byte)0x78;
    final static byte TCRESET = (byte)0x79;
    final static byte TCBLOCKDATALONG = (byte)0x7A;
    final static byte TCEXCEPTION = (byte)0x7B;
    final static byte TCLONGSTRING = (byte) 0x7C;
    final static byte TCPROXYCLASSDESC = (byte) 0x7D;
    final static byte TC_ENUM = (byte) 0x7E;
    final static  int   baseWireHandle = 0x7E0000;

Serialization Vulnerability Demonstration Code:
<add after lunch>

Volatile and Synchronized

Gist: a volatile variable isn't enough synchronization.

In Java, a "volatile" field is often presented as a weaker form of synchronization: a field that's specifically indicated to the compiler and runtime as "not-to-be-reordered", which, consequently, doesn't get cached; it is guaranteed to return the most recent write on the field to any threads accessing that field.

Because "most recent write" doesn't always imply "every write", using volatile as a form of synchronization could become problematic.

Consider the code below where the starting balance (represented by volatile member variable balance) is 0; two threads (the main thread, and another created (thread) change the balance to add 100 and 51000 respectively. After execution, one'd expect the final balance to be 51100 (100+51000) for a synchronized operation; however, that isn't what happens: on my machine, the final balance takes values like 51100, 51099, 51000 etc. See the output following the code.

import java.lang.Thread;
/* Over several executions of the program,
 a write to the  volatile variable "balance" 
 either by the main thread or by the created thread 
 is lost i.e. overwritten*/
public class ThreadTest
volatile int balance = 0;
public void deposit(int val)
balance = balance+val;

public void credit(int val)
balance = balance-val;
public void debit(int val) 
Thread internalThread = new Thread(new Runnable(){
public void run()
System.out.println("pre-change balance in second thread is: "+Integer.toString(balance));

System.out.println("post-change balance in second thread is: "+Integer.toString(balance));
catch(Exception ioe)
System.out.println("error in debit thread run.");

catch(Exception ioe)
System.out.println("error in debit().");

public static void main(String[] args)  throws InterruptedException
System.out.println("Main Thread started");
ThreadTest tt = new ThreadTest();

System.out.println("Main Thread starting balance is: "+Integer.toString(tt.balance));

// alter balance from a new thread

// alter balance from the main thread
for(int i = 1;i<=51000; i++)

System.out.println("Main Thread changed balance is: "+Integer.toString(tt.balance));

[my-pc java]$ java ThreadTest  
Main Thread started
Main Thread starting balance is: 0
pre-change balance in second thread is: 1420
post-change balance in second thread is: 2837
Main Thread changed balance is: 51000
[my-pc java]$ java ThreadTest        
Main Thread started
Main Thread starting balance is: 0
pre-change balance in second thread is: 935
post-change balance in second thread is: 1619
Main Thread changed balance is: 51100

In the same code, simply marking the method "deposit()" as synchronized ensures that the final balance is always 51100. Of course, the choice of using a synchronized method or a synchronized block is situational.