The problem with encapsulating C.val + autoboxing

Remi Forax forax at univ-mlv.fr
Mon Jul 25 20:33:29 UTC 2022


One of the idea of encapsulating C.val is that even with a value class C with no good default, accessing C.val is useful.
That's true but a unfortunate consequence is that it makes leaking T in a generic code a security issue.

It is in fact more secure to use two classes C and CFlat, one with no good default and the other which allow the default value, when using with generics.

Here is an example, let say we have a value class Month (1 to 12) and an identity class Agenda that contains several months.
WE can declare Month like this, with a package-private value companion.

value record Month(int value) {
  /*package-private*/ companion val;

  public Month {
    if (value < 1 || value > 12) throw new IAE();
  }
}

So we can flatten the Month when stored in a list

class Agenda {
  private final MyList<Month.val> months = new MyList<>();

  public void add(Hour hour) {
    Objects.requireNonNull(hour);
    months.add(hour);
  }

  public Hour getFirst() {
    return months.isEmpty()? null: months.getFirst();
  }
}

Is this code safe ? The trouble is that it depends on the implementation of MyList, by example with

class MyList<E> {
  private E[] array = new E[16];
  private int size;

  public boolean isEmpty() { return size == 0; }

  public void add(E element) {
    array[size++] = element;
  }

  public E getFirst() {
    // Objects.checkIndex(0, size);
    return array[0];
  }
}

MyList.getFirst() leaks E.default, so the implementation of Agenda is not safe.
Using the encapsulation to hide C.val is only safe if the generics code never leak E.default.

Weirdly, if i use different classes to represent C and C.val, i do not have that issue.

class Agenda {
  value record MonthFlat(int value) {
    public companion val;
  }

  private final MyList<MonthFlat> months = new MyList<>();

  public void add(Hour hour) {
    Objects.requireNonNull(hour);
    months.add(new MonthFlat(hour.value()));
  }

  public Hour getFirst() {
    return months.isEmpty()? null: new Month(months.getFirst().value);
  }
}

because unlike the autoboxing conversion between Month.val to Month, the conversion from MonthFlat to Month does not bypass the constructor.

Rémi



More information about the valhalla-spec-observers mailing list