You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Soundness: a field initialised in its declaration keeps that state in other methods, even after the constructor changed it #446
Impact: a protocol violation that crashes at runtime is accepted. When a field is initialised in its declaration (private Res res = new Res();), the state from that initialiser is used for the field in every method. That holds even after the constructor has moved the object to another state. Typestate on fields should start unknown in each method, as it does for a field assigned only in the constructor.
Minimal reproducer
importliquidjava.specification.StateRefinement;
importliquidjava.specification.StateSet;
@StateSet({"open", "closed"})
classRes {
@StateRefinement(to = "open(this)")
publicRes() {}
@StateRefinement(from = "open(this)", to = "closed(this)")
publicvoidclose() {}
@StateRefinement(from = "open(this)")
publicvoiduse() {}
}
publicclassRepro {
privateResres = newRes(); // open here...Repro() {
res.close(); // ...closed once the constructor has run
}
voidwork() {
res.use(); // use() needs open: should be an error (or at least "unknown")
}
}
Expected: an error at res.use(), since res is closed (or its state is unknown in work).
Actual (main @ f7f366c, and 1a506fe): Correct! Passed Verification.
Real-world scenario. GUI code keeps a javax.swing.undo.UndoManager in a field initialised at its declaration and configures it in the constructor. Here, with an external spec of UndoManager where setLimit is refused after end():
importjavax.swing.undo.UndoManager;
publicclassU16_FieldInitEndedCanUndoSetLimit {
privateUndoManagerundoManager = newUndoManager(); // initializer: the verifier keeps start hereU16_FieldInitEndedCanUndoSetLimit() {
this.undoManager.end();
}
voidshrink() {
if (this.undoManager.canUndo()) { // true after end(); the verifier still believes startthis.undoManager.setLimit(5); // throws RuntimeException: setLimit after end()
}
}
publicstaticvoidmain(String[] args) {
newU16_FieldInitEndedCanUndoSetLimit().shrink();
}
}
On the JVM:java.lang.RuntimeException: Attempt to call UndoManager.setLimit() after UndoManager.end() has been called (UndoManager.java:289).
LiquidJava (f7f366c), with the spec from co-specification (examples/undomanager/specs/4-tables-UndoManagerRefinements.java): Correct! Passed Verification.
Contrast: the same class with private UndoManager undoManager;, assigned in the constructor (this.undoManager = new UndoManager(); this.undoManager.end();), is correctly rejected at setLimit(5) (co-specification probe U13, on 1a506fe). Only the declaration initialiser leaks its state.
With a spec where every DatagramSocket constructor ends bound(this) and bind needs unbound(this), the verifier accepts open(): the = -1 initialiser makes localPort != -1 false inside open(), so the branch with the bug looks dead. Deleting = -1 (and nothing else) makes it reject line bind(...) with found ... localPort != -1 && bound(sendSocket) ... but expected unbound(sendSocket).
Verifier commit 1a506fe; spec and client at liquid-java/co-specification examples/datagramsocket (exercise E_OSS_A1_06).
Activity
CatarinaGamboa commentedon Oct 29, 2026
Another real-world instance, where this hides a real bug (not just a false alarm).
From N0BOY/FT8CN
IcomUdpClient.java(MIT), trimmed:With a spec where every
DatagramSocketconstructor endsbound(this)andbindneedsunbound(this), the verifier acceptsopen(): the= -1initialiser makeslocalPort != -1false insideopen(), so the branch with the bug looks dead. Deleting= -1(and nothing else) makes it reject linebind(...)withfound ... localPort != -1 && bound(sendSocket) ... but expected unbound(sendSocket).Verifier commit 1a506fe; spec and client at liquid-java/co-specification
examples/datagramsocket(exercise E_OSS_A1_06).