Skip to content

Class: create primitive classes without calling forName - #43

Open
tautschnig wants to merge 2 commits into
masterfrom
fix-primitive-class-clinit
Open

tautschnig wants to merge 2 commits into
masterfrom
fix-primitive-class-clinit

Conversation

@tautschnig

@tautschnig tautschnig commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator

Class: create primitive classes without calling forName

Class.forName calls CProver.notModelled() and then throws an
AssertionError, as its model is incomplete. getPrimitiveClass(String),
getPrimitiveClass(int) and getSuperclass() still called forName, and
the static initializers of all boxed types call getPrimitiveClass (e.g.,
Integer. calls getPrimitiveClass(4)). Thus, JBMC reported a
failing assertion in forName for any program that uses a boxed type, and
did not explore any paths beyond that call.

Create the Class objects in getPrimitiveClass directly instead.
isPrimitive(), which compares the name with string literals by
reference, still works, as the names are the same literals.

getSuperclass() now returns null, which matches the existing comment
that the model assumes there is no superclass. Returning a new Class
object instead would make Enum.getDeclaringClass() return a different
object on each call, so that E.A.getDeclaringClass() ==
E.B.getDeclaringClass() would not hold.

With JBMC, assertions about Integer.TYPE.isPrimitive() and getName(),
and about HashMap and ArrayList of Integer, now verify, and a false
assertion about a HashMap is still reported as failing.

Byte, Short, Float: use the right primitive class for TYPE

Byte.TYPE, Short.TYPE and Float.TYPE were initialised with
getPrimitiveClass(7), which is the class of double, so that, e.g.,
Byte.TYPE.getName() returned "double". Use the indices of byte (2),
short (3) and float (6) instead, and mark the change from the JDK
source, which uses getPrimitiveClass(String), as Integer.java does.

Copilot AI review requested due to automatic review settings July 29, 2026 00:36

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR prevents internal java.lang.Class helpers (primitive-class lookup and getSuperclass()) from calling Class.forName(String), which is intentionally CProver.notModelled() + AssertionError in this model library. This avoids spurious verification failures triggered during boxed-primitive <clinit> execution (e.g., Integer.TYPE initialization).

Changes:

  • Add a private cproverClassWithName(String) factory to create Class instances with a known name without routing through forName.
  • Route getPrimitiveClass(String), getPrimitiveClass(int), and getSuperclass() through the new factory.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/main/java/java/lang/Class.java Outdated
Comment on lines +405 to +407
public Class getSuperclass(){
// TODO: here we assume no superclass which may not be correct
return Class.forName(null);
return cproverClassWithName(null);
tautschnig and others added 2 commits November 4, 2026 11:30
Class.forName calls CProver.notModelled() and then throws an
AssertionError, as its model is incomplete. getPrimitiveClass(String),
getPrimitiveClass(int) and getSuperclass() still called forName, and
the static initializers of all boxed types call getPrimitiveClass (e.g.,
Integer.<clinit> calls getPrimitiveClass(4)). Thus, JBMC reported a
failing assertion in forName for any program that uses a boxed type, and
did not explore any paths beyond that call.

Create the Class objects in getPrimitiveClass directly instead.
isPrimitive(), which compares the name with string literals by
reference, still works, as the names are the same literals.

getSuperclass() now returns null, which matches the existing comment
that the model assumes there is no superclass. Returning a new Class
object instead would make Enum.getDeclaringClass() return a different
object on each call, so that E.A.getDeclaringClass() ==
E.B.getDeclaringClass() would not hold.

With JBMC, assertions about Integer.TYPE.isPrimitive() and getName(),
and about HashMap and ArrayList of Integer, now verify, and a false
assertion about a HashMap is still reported as failing.

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
Byte.TYPE, Short.TYPE and Float.TYPE were initialised with
getPrimitiveClass(7), which is the class of double, so that, e.g.,
Byte.TYPE.getName() returned "double". Use the indices of byte (2),
short (3) and float (6) instead, and mark the change from the JDK
source, which uses getPrimitiveClass(String), as Integer.java does.

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
@tautschnig
tautschnig force-pushed the fix-primitive-class-clinit branch from b6ac7e5 to 115d0d2 Compare November 4, 2026 11:48
tautschnig added a commit that referenced this pull request Nov 4, 2026
Commit 75beb6b made Class.forName call CProver.notModelled(), as it does
not run static initializers. Object.getClass() is implemented as
Class.forName(CProver.classIdentifier(this)), so every path through
getClass() was cut off: for example, assertions after a call to
Object.toString(), Enum.compareTo, or an equals() method that compares
getClass() results were reported as SUCCESS without being checked.

The model before that commit, which kept Class objects in two static
arrays of size 8, had problems as well:
- With more than 8 classes, forName returned a nondeterministic object.
- Class literals (Foo.class) were not in the arrays, so
  Class.forName("Foo") == Foo.class was false.
- With JBMC's --nondet-static, the non-final static arrays are
  initialised nondeterministically, which made the search for a class
  name compare it with nondeterministic strings: JBMC's nondet-static
  regression test did not terminate within 15 minutes with that model,
  and finishes within a second when the arrays are final.

Keep all Class objects in a linked list instead, whose head is stored in
a final array so that --nondet-static leaves it alone.
cproverInitializeClassLiteral, which JBMC calls to initialise each class
literal, adds the literal to the list, and forName adds a new Class
object only if none with that name exists yet. Thus there is one Class
object per class name, as in the JVM. forName now throws
NullPointerException for a null name, as the JVM does. It still does
not run static initializers, and does not throw ClassNotFoundException
for names of classes that do not exist; its documentation now says so.

Each lookup compares the name with those of all Class objects created
so far using String.equals, which is expensive for JBMC's string solver
when there are many of them: a program that calls getClass() on objects
of 10 different classes took 81 seconds. JBMC therefore implements
getClass() for all classes except java.lang.Object itself without going
through forName (diffblue/cbmc#9390), so that this lookup is only used
for explicit calls of Class.forName and for getClass() on objects of
type java.lang.Object. For class literals to be added to the list, JBMC
must initialise class literals after the static fields of
java.lang.Class, which diffblue/cbmc#9390 also does; with older versions
of JBMC, Class.forName("Foo") == Foo.class remains false.

This builds on #43, which makes getPrimitiveClass and getSuperclass no
longer call forName.

With these models (and #49 for building with JDK 25), 3 of JBMC's
regression tests fail, compared with 113 with the current master branch:
two depend on the numbering of dynamic objects in traces, and one fails
JBMC's trace validation also without this change. This holds both with
and without diffblue/cbmc#9390. With #43 and #49, but without this
change, 3 tests fail as well, but two of them because getClass() on an
object of type java.lang.Object reaches CProver.notModelled().

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
@tautschnig tautschnig changed the title Class: don't route internal callers through the notModelled forName Class: create primitive classes without calling forName Nov 4, 2026
@tautschnig
tautschnig marked this pull request as ready for review November 4, 2026 12:22
tautschnig added a commit that referenced this pull request Nov 4, 2026
Commit 75beb6b made Class.forName call CProver.notModelled(), as it does
not run static initializers. Object.getClass() is implemented as
Class.forName(CProver.classIdentifier(this)), so that JBMC reports a
failing assertion in forName for every call of getClass(), and does not
check any assertion after such a call, e.g., after a call of
Object.toString(), Enum.compareTo, or an equals() method that compares
getClass() results.

The model before that commit, which kept Class objects in two static
arrays of size 8, had problems as well:
- With more than 8 classes, forName returned a nondeterministic object.
- Class literals (Foo.class) were not in the arrays, so
  Class.forName("Foo") == Foo.class was false.
- With JBMC's --nondet-static, the non-final static arrays are
  initialised nondeterministically, which made the search for a class
  name compare it with nondeterministic strings: JBMC's nondet-static
  regression test did not terminate within 15 minutes with that model,
  and finishes within a second when the arrays are final.

Keep all Class objects in two linked lists instead, whose heads are
stored in final arrays so that --nondet-static leaves them alone:
cproverInitializeClassLiteral, which JBMC calls to initialise each class
literal, adds the literal to the first list, and forName adds a new
Class object to the second list only if none with that name exists in
either list yet. Thus there is one Class object per class name, as in
the JVM. forName now throws
NullPointerException for a null name, as the JVM does. It still does
not run static initializers, and does not throw ClassNotFoundException
for names of classes that do not exist; its documentation now says so.

Each lookup compares the name with those of all Class objects created
so far using String.equals, which is expensive for JBMC's string solver
when there are many of them: a program that calls getClass() on objects
of 10 different classes took 81 seconds. JBMC therefore implements
getClass() for all classes except java.lang.Object itself without going
through forName (diffblue/cbmc#9390), so that this lookup is only used
for explicit calls of Class.forName and for getClass() on objects of
type java.lang.Object. For class literals to be added to the list, JBMC
must initialise class literals after the static fields of
java.lang.Class, which diffblue/cbmc#9390 also does; with older versions
of JBMC, Class.forName("Foo") == Foo.class remains false.

For classes whose class literal the program does not refer to, JBMC
(with diffblue/cbmc#9390) does not create a class literal, but calls the
new method cproverGetClassObject on the first call of getClass() for the
class. It returns the Class object that forName created for the class,
if any, so that Class.forName("Foo") == new Foo().getClass() holds also
if forName is called first, and a new one in the second list otherwise.
Objects are only ever added at the head of a list, and only once: as
symbolic execution generally does not resolve the name comparisons,
moving objects between lists or adding an object to a list a second
time would make JBMC consider cyclic lists, so that it could not bound
the loops over them.

There are no locks: JBMC's model of monitors requires the monitor count
to be zero, and atomic sections cannot be used either, as JBMC's static
initialization code uses atomic sections, which cannot be nested.

This builds on #43, which makes getPrimitiveClass and getSuperclass no
longer call forName.

With these models (and #49 for building with JDK 25), 3 of JBMC's
regression tests fail, compared with 113 with the current master branch:
two depend on the numbering of dynamic objects in traces, and one fails
JBMC's trace validation also without this change. This holds both with
and without diffblue/cbmc#9390. With #43 and #49, but without this
change, 3 tests fail as well, but two of them because getClass() on an
object of type java.lang.Object reaches CProver.notModelled().

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants