diff --git a/java/ql/integration-tests/java/query-suite/java-code-quality-extended.qls.expected b/java/ql/integration-tests/java/query-suite/java-code-quality-extended.qls.expected index cdd6769ab46f..6d0319431d6f 100644 --- a/java/ql/integration-tests/java/query-suite/java-code-quality-extended.qls.expected +++ b/java/ql/integration-tests/java/query-suite/java-code-quality-extended.qls.expected @@ -67,15 +67,18 @@ ql/java/ql/src/Likely Bugs/Concurrency/CallsToRunnableRun.ql ql/java/ql/src/Likely Bugs/Concurrency/DateFormatThreadUnsafe.ql ql/java/ql/src/Likely Bugs/Concurrency/DoubleCheckedLocking.ql ql/java/ql/src/Likely Bugs/Concurrency/DoubleCheckedLockingWithInitRace.ql +ql/java/ql/src/Likely Bugs/Concurrency/Escaping.ql ql/java/ql/src/Likely Bugs/Concurrency/FutileSynchOnField.ql ql/java/ql/src/Likely Bugs/Concurrency/NonSynchronizedOverride.ql ql/java/ql/src/Likely Bugs/Concurrency/NotifyNotNotifyAll.ql +ql/java/ql/src/Likely Bugs/Concurrency/SafePublication.ql ql/java/ql/src/Likely Bugs/Concurrency/ScheduledThreadPoolExecutorZeroThread.ql ql/java/ql/src/Likely Bugs/Concurrency/SleepWithLock.ql ql/java/ql/src/Likely Bugs/Concurrency/StartInConstructor.ql ql/java/ql/src/Likely Bugs/Concurrency/SynchOnBoxedType.ql ql/java/ql/src/Likely Bugs/Concurrency/SynchSetUnsynchGet.ql ql/java/ql/src/Likely Bugs/Concurrency/SynchWriteObject.ql +ql/java/ql/src/Likely Bugs/Concurrency/ThreadSafe.ql ql/java/ql/src/Likely Bugs/Finalization/NullifiedSuperFinalize.ql ql/java/ql/src/Likely Bugs/Frameworks/JUnit/BadSuiteMethod.ql ql/java/ql/src/Likely Bugs/Frameworks/JUnit/JUnit5MissingNestedAnnotation.ql diff --git a/java/ql/integration-tests/java/query-suite/java-code-quality.qls.expected b/java/ql/integration-tests/java/query-suite/java-code-quality.qls.expected index eb502feb6bac..56e2daa58ce0 100644 --- a/java/ql/integration-tests/java/query-suite/java-code-quality.qls.expected +++ b/java/ql/integration-tests/java/query-suite/java-code-quality.qls.expected @@ -30,10 +30,13 @@ ql/java/ql/src/Likely Bugs/Comparison/WrongNanComparison.ql ql/java/ql/src/Likely Bugs/Concurrency/CallsToRunnableRun.ql ql/java/ql/src/Likely Bugs/Concurrency/DoubleCheckedLocking.ql ql/java/ql/src/Likely Bugs/Concurrency/DoubleCheckedLockingWithInitRace.ql +ql/java/ql/src/Likely Bugs/Concurrency/Escaping.ql ql/java/ql/src/Likely Bugs/Concurrency/NonSynchronizedOverride.ql +ql/java/ql/src/Likely Bugs/Concurrency/SafePublication.ql ql/java/ql/src/Likely Bugs/Concurrency/ScheduledThreadPoolExecutorZeroThread.ql ql/java/ql/src/Likely Bugs/Concurrency/SynchOnBoxedType.ql ql/java/ql/src/Likely Bugs/Concurrency/SynchSetUnsynchGet.ql +ql/java/ql/src/Likely Bugs/Concurrency/ThreadSafe.ql ql/java/ql/src/Likely Bugs/Frameworks/JUnit/JUnit5MissingNestedAnnotation.ql ql/java/ql/src/Likely Bugs/Inheritance/NoNonFinalInConstructor.ql ql/java/ql/src/Likely Bugs/Likely Typos/ContainerSizeCmpZero.ql diff --git a/java/ql/lib/semmle/code/java/Concurrency.qll b/java/ql/lib/semmle/code/java/Concurrency.qll index 7322f16068c0..da2783bc3080 100644 --- a/java/ql/lib/semmle/code/java/Concurrency.qll +++ b/java/ql/lib/semmle/code/java/Concurrency.qll @@ -2,6 +2,57 @@ overlay[local?] module; import java +import semmle.code.java.frameworks.Mockito + +/** + * A Java type representing a lock. + * We identify a lock type as one that has both `lock` and `unlock` methods. + */ +class LockType extends RefType { + LockType() { + this.getAMethod().hasName("lock") and + this.getAMethod().hasName("unlock") + } + + /** Gets a method that is locking this lock type. */ + private Method getLockMethod() { + result.getDeclaringType() = this and + result.hasName(["lock", "lockInterruptibly", "tryLock"]) + } + + /** Gets a method that is unlocking this lock type. */ + private Method getUnlockMethod() { + result.getDeclaringType() = this and + result.hasName("unlock") + } + + /** Gets an `isHeldByCurrentThread` method of this lock type. */ + private Method getIsHeldByCurrentThreadMethod() { + result.getDeclaringType() = this and + result.hasName("isHeldByCurrentThread") + } + + /** Gets a call to a method that is locking this lock type. */ + MethodCall getLockAccess() { + result.getMethod() = this.getLockMethod() and + // Not part of a Mockito verification call + not result instanceof MockitoVerifiedMethodCall + } + + /** Gets a call to a method that is unlocking this lock type. */ + MethodCall getUnlockAccess() { + result.getMethod() = this.getUnlockMethod() and + // Not part of a Mockito verification call + not result instanceof MockitoVerifiedMethodCall + } + + /** Gets a call to a method that checks if the lock is held by the current thread. */ + MethodCall getIsHeldByCurrentThreadAccess() { + result.getMethod() = this.getIsHeldByCurrentThreadMethod() and + // Not part of a Mockito verification call + not result instanceof MockitoVerifiedMethodCall + } +} /** * Holds if `e` is synchronized by a local synchronized statement `sync` on the variable `v`. @@ -49,3 +100,136 @@ class SynchronizedCallable extends Callable { ) } } + +/** + * This module provides predicates, chiefly `locallyMonitors`, to check if a given expression is synchronized on a specific monitor. + */ +module Monitors { + /** + * A monitor is any object that is used to synchronize access to a shared resource. + * This includes locks as well as variables used in synchronized blocks (including `this`). + */ + newtype TMonitor = + /** Either a lock or a variable used in a synchronized block. */ + TVariableMonitor(Variable v) { + v.getType() instanceof LockType or locallySynchronizedOn(_, _, v) + } or + /** An instance reference used as a monitor. */ + TInstanceMonitor(RefType thisType) { locallySynchronizedOnThis(_, thisType) } or + /** A class used as a monitor. */ + TClassMonitor(RefType classType) { locallySynchronizedOnClass(_, classType) } + + /** + * A monitor is any object that is used to synchronize access to a shared resource. + * This includes locks as well as variables used in synchronized blocks (including `this`). + */ + class Monitor extends TMonitor { + /** Gets the location of this monitor. */ + abstract Location getLocation(); + + /** Gets a textual representation of this element. */ + abstract string toString(); + } + + /** + * A variable used as a monitor. + * The variable is either a lock or is used in a synchronized block. + * E.g `synchronized (m) { ... }` or `m.lock();` + */ + class VariableMonitor extends Monitor, TVariableMonitor { + override Location getLocation() { result = this.getVariable().getLocation() } + + override string toString() { result = "VariableMonitor(" + this.getVariable().toString() + ")" } + + /** Gets the variable being used as a monitor. */ + Variable getVariable() { this = TVariableMonitor(result) } + } + + /** + * An instance reference used as a monitor. + * Either via `synchronized (this) { ... }` or by marking a non-static method as `synchronized`. + */ + class InstanceMonitor extends Monitor, TInstanceMonitor { + override Location getLocation() { result = this.getThisType().getLocation() } + + override string toString() { result = "InstanceMonitor(" + this.getThisType().toString() + ")" } + + /** Gets the instance reference being used as a monitor. */ + RefType getThisType() { this = TInstanceMonitor(result) } + } + + /** + * A class used as a monitor. + * This is achieved by marking a static method as `synchronized`. + */ + class ClassMonitor extends Monitor, TClassMonitor { + override Location getLocation() { result = this.getClassType().getLocation() } + + override string toString() { result = "ClassMonitor(" + this.getClassType().toString() + ")" } + + /** Gets the class being used as a monitor. */ + RefType getClassType() { this = TClassMonitor(result) } + } + + /** Holds if the expression `e` is synchronized on the monitor `m`. */ + predicate locallyMonitors(Expr e, Monitor m) { + exists(Variable v | v = m.(VariableMonitor).getVariable() | + locallyLockedOn(e, v) + or + locallySynchronizedOn(e, _, v) + ) + or + locallySynchronizedOnThis(e, m.(InstanceMonitor).getThisType()) + or + locallySynchronizedOnClass(e, m.(ClassMonitor).getClassType()) + } + + /** Gets the control flow node that must dominate `e` when `e` is synchronized on a lock. */ + ControlFlowNode getNodeToBeDominated(Expr e) { + // If `e` is the LHS of an assignment, use the control flow node for the assignment + exists(Assignment asgn | asgn.getDest() = e | result = asgn.getControlFlowNode()) + or + // if `e` is not the LHS of an assignment, use the default control flow node + not exists(Assignment asgn | asgn.getDest() = e) and + result = e.getControlFlowNode() + } + + /** A field storing a lock. */ + class LockField extends Field { + LockField() { this.getType() instanceof LockType } + + /** Gets a call to a method locking the lock stored in this field. */ + MethodCall getLockCall() { + result.getQualifier() = this.getRepresentative().getAnAccess() and + result = this.getType().(LockType).getLockAccess() + } + + /** Gets a call to a method unlocking the lock stored in this field. */ + MethodCall getUnlockCall() { + result.getQualifier() = this.getRepresentative().getAnAccess() and + result = this.getType().(LockType).getUnlockAccess() + } + + /** + * Gets a variable representing this field. + * It can be the field itself or a local variable initialized to the field. + */ + private Variable getRepresentative() { + result = this + or + result.getInitializer() = this.getAnAccess() + } + } + + /** Holds if `e` is synchronized on the `Lock` `lock` by a locking call. */ + predicate locallyLockedOn(Expr e, LockField lock) { + exists(MethodCall lockCall, MethodCall unlockCall | + lockCall = lock.getLockCall() and + unlockCall = lock.getUnlockCall() + | + dominates(lockCall.getControlFlowNode(), unlockCall.getControlFlowNode()) and + dominates(lockCall.getControlFlowNode(), getNodeToBeDominated(e)) and + postDominates(unlockCall.getControlFlowNode(), getNodeToBeDominated(e)) + ) + } +} diff --git a/java/ql/lib/semmle/code/java/ConflictingAccess.qll b/java/ql/lib/semmle/code/java/ConflictingAccess.qll new file mode 100644 index 000000000000..ceff3e4ffa3a --- /dev/null +++ b/java/ql/lib/semmle/code/java/ConflictingAccess.qll @@ -0,0 +1,286 @@ +/** + * Provides classes and predicates for detecting conflicting accesses in the sense of the Java Memory Model. + */ +overlay[local?] +module; + +import java +import Concurrency + +/** Provides predicates, chiefly `isModifying`, to check if a given expression modifies a shared resource. */ +module Modification { + import semmle.code.java.dataflow.FlowSummary + + /** Holds if the field access `a` modifies a shared resource. */ + predicate isModifying(FieldAccess a) { + a.isVarWrite() + or + exists(Call c | c.(MethodCall).getQualifier() = a | isModifyingCall(c)) + or + exists(ArrayAccess aa, Assignment asa | aa.getArray() = a | asa.getDest() = aa) + } + + /** Holds if the call `c` modifies a shared resource. */ + predicate isModifyingCall(Call c) { + exists(SummarizedCallable sc, string output | sc.getACall() = c | + sc.propagatesFlow(_, output, _, _) and + output.matches("Argument[this]%") + ) + } +} + +/** Holds if the type `t` is thread-safe. */ +predicate isThreadSafeType(Type t) { + t.(RefType).getSourceDeclaration().getName().matches(["Atomic%", "Concurrent%"]) + or + t.(RefType).getSourceDeclaration().getName() = "ThreadLocal" + or + // Anything in `java.util.concurrent` is thread safe. + // See https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/package-summary.html#MemoryVisibility + t.(RefType).getPackage().getName() = "java.util.concurrent" + or + t instanceof ClassAnnotatedAsThreadSafe +} + +/** Holds if the expression `e` is a thread-safe initializer. */ +private predicate isThreadSafeInitializer(Expr e) { + exists(string name | + e.(Call).getCallee().getSourceDeclaration().hasQualifiedName("java.util", "Collections", name) + | + name.matches("synchronized%") + ) +} + +/** + * A field that is exposed to potential data races. + * We require the field to be in a class that is annotated as `@ThreadSafe`. + */ +class ExposedField extends Field { + ExposedField() { + this.getDeclaringType() instanceof ClassAnnotatedAsThreadSafe and + not this.isVolatile() and + // field is not a lock + not this.getType() instanceof LockType and + // field is not thread-safe + not isThreadSafeType(this.getType()) and + not isThreadSafeType(this.getInitializer().getType()) and + // the initializer guarantees thread safety + not isThreadSafeInitializer(this.getInitializer()) + } +} + +/** + * A field access that is exposed to potential data races. + * We require the field to be in a class that is annotated as `@ThreadSafe`. + */ +class ExposedFieldAccess extends FieldAccess { + ExposedFieldAccess() { + // access is to an exposed field + this.getField() instanceof ExposedField and + // access is not the initializer of the field + not this.(VarWrite).getASource() = this.getField().getInitializer() and + // access not in a constructor + not this.getEnclosingCallable() = this.getField().getDeclaringType().getAConstructor() and + // not a field on a local variable + not this.getQualifier+().(VarAccess).getVariable() instanceof LocalVariableDecl and + // not the variable mentioned in a synchronized statement + not this = any(SynchronizedStmt sync).getExpr() + } +} + +/** + * A class annotated as `@ThreadSafe`. + * Provides predicates to check for concurrency issues. + */ +class ClassAnnotatedAsThreadSafe extends Class { + ClassAnnotatedAsThreadSafe() { this.getAnAnnotation().getType().getName() = "ThreadSafe" } + + // We wish to find conflicting accesses that are reachable from public methods + // and to know which monitors protect them. + // + // It is very easy and natural to write a predicate for conflicting accesses, + // but that would be binary, and hence not suited for reachability analysis. + // + // It is also easy to state that all accesses to a field are protected by a single monitor, + // but that would require a forall, which is not suited for recursion. + // (The recursion occurs for example as you traverse the access path and keep requiring that all tails are protected.) + // + // We therefore use a dual solution: + // - We write a unary recursive predicate for accesses that are not protected by any monitor. + // Any such write access, reachable from a public method, is conflicting with itself. + // And any such read will be conflicting with any publicly reachable write access (locked or not). + // + // - We project the above predicate down to fields, so we can find fields with unprotected accesses. + // - From this we can derive a unary recursive predicate for fields whose accesses are protected by exactly one monitor. + // The predicate tracks the monitor. + // If such a field has two accesses protected by different monitors, we have a concurrency issue. + // This can be determined by simple counting at the end of the recursion. + // Technically, we only have a concurrency issue if there is a write access, + // but if you are locking your reads with different locks, you likely made a typo. + // + // - From the above, we can derive a unary recursive predicate for fields whose accesses are protected by at least one monitor. + // This predicate tracks all the monitors that protect accesses to the field. + // This is combined with a predicate that checks if any access escapes a given monitor. + // If all the monitors that protect accesses to a field are escaped by at least one access, + // we have a concurrency issue. + // This can be determined by a single forall at the end of the recursion. + // + // With this formulation we avoid binary predicates and foralls in recursion. + // + // Cases where a field access is not protected by any monitor + /** + * Holds if the field access `a` to the field `f` is not protected by any monitor, and it can be reached via the expression `e` in the method `m`. + * We maintain the invariant that `m = e.getEnclosingCallable()`. + */ + private predicate unlockedAccess( + ExposedField f, Expr e, Method m, ExposedFieldAccess a, boolean write + ) { + m.getDeclaringType() = this and + ( + // base case + f.getDeclaringType() = this and + m = e.getEnclosingCallable() and + a.getField() = f and + a = e and + (if Modification::isModifying(a) then write = true else write = false) + or + // recursive case + exists(MethodCall c, Expr e0, Method m0 | this.unlockedAccess(f, e0, m0, a, write) | + m = c.getEnclosingCallable() and + not m0.isPublic() and + c.getCallee().getSourceDeclaration() = m0 and + c = e and + not Monitors::locallyMonitors(e0, _) + ) + ) + } + + /** Holds if the class has an unlocked access to the field `f` via the expression `e` in the method `m`. */ + private predicate hasUnlockedAccess(ExposedField f, Expr e, Method m, boolean write) { + this.unlockedAccess(f, e, m, _, write) + } + + /** Holds if the field access `a` to the field `f` is not protected by any monitor, and it can be reached via the expression `e` in the public method `m`. */ + predicate unlockedPublicAccess( + ExposedField f, Expr e, Method m, ExposedFieldAccess a, boolean write + ) { + this.unlockedAccess(f, e, m, a, write) and + m.isPublic() and + not Monitors::locallyMonitors(e, _) + } + + /** Holds if the class has an unlocked access to the field `f` via the expression `e` in the public method `m`. */ + private predicate hasUnlockedPublicAccess(ExposedField f, Expr e, Method m, boolean write) { + this.unlockedPublicAccess(f, e, m, _, write) + } + + // Cases where all accesses to a field are protected by exactly one monitor + // + /** + * Holds if the class has an access, locked by exactly one monitor, to the field `f` via the expression `e` in the method `m`. + */ + private predicate hasOnelockedAccess( + ExposedField f, Expr e, Method m, boolean write, Monitors::Monitor monitor + ) { + //base + this.hasUnlockedAccess(f, e, m, write) and + Monitors::locallyMonitors(e, monitor) + or + // recursive case + exists(MethodCall c, Method m0 | this.hasOnelockedAccess(f, _, m0, write, monitor) | + m = c.getEnclosingCallable() and + not m0.isPublic() and + c.getCallee().getSourceDeclaration() = m0 and + c = e and + // consider allowing idempotent monitors + not Monitors::locallyMonitors(e, _) and + m.getDeclaringType() = this + ) + } + + /** Holds if the class has an access, locked by exactly one monitor, to the field `f` via the expression `e` in the public method `m`. */ + private predicate hasOnelockedPublicAccess( + ExposedField f, Expr e, Method m, boolean write, Monitors::Monitor monitor + ) { + this.hasOnelockedAccess(f, e, m, write, monitor) and + m.isPublic() and + not this.hasUnlockedPublicAccess(f, e, m, write) + } + + /** Holds if the field `f` has more than one access, all locked by a single monitor, but different monitors are used. */ + predicate singleMonitorMismatch(ExposedField f) { + 2 <= strictcount(Monitors::Monitor monitor | this.hasOnelockedPublicAccess(f, _, _, _, monitor)) + } + + // Cases where all accesses to a field are protected by at least one monitor + // + /** Holds if the class has an access, locked by at least one monitor, to the field `f` via the expression `e` in the method `m`. */ + private predicate hasOnepluslockedAccess( + ExposedField f, Expr e, Method m, boolean write, Monitors::Monitor monitor + ) { + //base + this.hasOnelockedAccess(f, e, m, write, monitor) and + not this.singleMonitorMismatch(f) and + not this.hasUnlockedPublicAccess(f, _, _, _) + or + // recursive case + exists(MethodCall c, Method m0, Monitors::Monitor monitor0 | + this.hasOnepluslockedAccess(f, _, m0, write, monitor0) and + m = c.getEnclosingCallable() and + not m0.isPublic() and + c.getCallee().getSourceDeclaration() = m0 and + c = e and + m.getDeclaringType() = this + | + monitor = monitor0 + or + Monitors::locallyMonitors(e, monitor) + ) + } + + /** Holds if the class has a write access to the field `f` that can be reached via a public method. */ + predicate hasPublicWriteAccess(ExposedField f) { + this.hasUnlockedPublicAccess(f, _, _, true) + or + this.hasOnelockedPublicAccess(f, _, _, true, _) + or + exists(Method m | m.getDeclaringType() = this and m.isPublic() | + this.hasOnepluslockedAccess(f, _, m, true, _) + ) + } + + /** Holds if the class has an access, not protected by the monitor `m`, to the field `f` via the expression `e` in the method `m`. */ + private predicate escapesMonitor( + ExposedField f, Expr e, Method m, boolean write, Monitors::Monitor monitor + ) { + //base + this.hasOnepluslockedAccess(f, _, _, _, monitor) and + this.hasUnlockedAccess(f, e, m, write) and + not Monitors::locallyMonitors(e, monitor) + or + // recursive case + exists(MethodCall c, Method m0 | this.escapesMonitor(f, _, m0, write, monitor) | + m = c.getEnclosingCallable() and + not m0.isPublic() and + c.getCallee().getSourceDeclaration() = m0 and + c = e and + not Monitors::locallyMonitors(e, monitor) and + m.getDeclaringType() = this + ) + } + + /** Holds if the class has an access, not protected by the monitor `m`, to the field `f` via the expression `e` in the public method `m`. */ + private predicate escapesMonitorPublic( + ExposedField f, Expr e, Method m, boolean write, Monitors::Monitor monitor + ) { + this.escapesMonitor(f, e, m, write, monitor) and + m.isPublic() + } + + /** Holds if no monitor protects all accesses to the field `f`. */ + predicate notFullyMonitored(ExposedField f) { + forex(Monitors::Monitor monitor | this.hasOnepluslockedAccess(f, _, _, _, monitor) | + this.escapesMonitorPublic(f, _, _, _, monitor) + ) + } +} diff --git a/java/ql/src/Likely Bugs/Concurrency/Escaping.qhelp b/java/ql/src/Likely Bugs/Concurrency/Escaping.qhelp new file mode 100644 index 000000000000..a8a614dbe369 --- /dev/null +++ b/java/ql/src/Likely Bugs/Concurrency/Escaping.qhelp @@ -0,0 +1,34 @@ + + + + + +

+In a thread-safe class, non-final fields should generally be private (or possibly volatile) to ensure that they cannot be accessed by other threads in an unsafe manner. +

+ +
+ + +

+If the field does not change, mark it as final. If the field is mutable, mark it as private and provide properly synchronized accessors.

+ +
+ + + + +
  • + Java Language Specification, chapter 17: + Threads and Locks. +
  • +
  • + Java concurrency package: + java.util.concurrent. +
  • + + +
    +
    diff --git a/java/ql/src/Likely Bugs/Concurrency/Escaping.ql b/java/ql/src/Likely Bugs/Concurrency/Escaping.ql new file mode 100644 index 000000000000..a2f3e0f7b463 --- /dev/null +++ b/java/ql/src/Likely Bugs/Concurrency/Escaping.ql @@ -0,0 +1,26 @@ +/** + * @name Escaping + * @description In a thread-safe class, care should be taken to avoid exposing mutable state. + * @kind problem + * @problem.severity warning + * @precision high + * @id java/escaping + * @tags quality + * reliability + * concurrency + */ + +import java +import semmle.code.java.ConflictingAccess + +from Field f, ClassAnnotatedAsThreadSafe c +where + f = c.getAField() and + not f.isFinal() and // final fields do not change + not f.isPrivate() and + // We believe that protected fields are also dangerous + // Volatile fields cannot cause data races, but it is dubious to allow changes. + // For now, we ignore volatile fields, but there are likely bugs to be caught here. + not f.isVolatile() +select f, "The class $@ is marked as thread-safe, but this field is potentially escaping.", c, + c.getName() diff --git a/java/ql/src/Likely Bugs/Concurrency/SafePublication.java b/java/ql/src/Likely Bugs/Concurrency/SafePublication.java new file mode 100644 index 000000000000..76980412e8cc --- /dev/null +++ b/java/ql/src/Likely Bugs/Concurrency/SafePublication.java @@ -0,0 +1,17 @@ +public class SafePublication { + private volatile Object value; + private final int server_id; + + public SafePublication() { + value = new Object(); // Safely published as volatile + server_id = 1; // Safely published as final + } + + public synchronized Object getValue() { + return value; + } + + public int getServerId() { + return server_id; + } +} \ No newline at end of file diff --git a/java/ql/src/Likely Bugs/Concurrency/SafePublication.qhelp b/java/ql/src/Likely Bugs/Concurrency/SafePublication.qhelp new file mode 100644 index 000000000000..4b4225304111 --- /dev/null +++ b/java/ql/src/Likely Bugs/Concurrency/SafePublication.qhelp @@ -0,0 +1,57 @@ + + + + + +

    +In a thread-safe class, values must be published safely to avoid inconsistent or unexpected behavior caused by visibility issues between threads. If a value is not safely published, one thread may see a stale or partially constructed value written by another thread, leading to subtle concurrency bugs. +

    +

    +In particular, values of primitive types should not be initialised to anything but their default values (which for Object is null) unless this happens in a static context. +

    +

    +Techniques for safe publication include: +

    + + +
    + + +

    +Choose a safe publication technique that fits your use case. If the value only needs to be written once, say for a singleton, consider using the final keyword. If the value is mutable and needs to be shared across threads, consider using synchronized blocks or methods, or using a thread-safe collection from java.util.concurrent. +

    + +
    + + +

    In the following example, the values of value and server_id are not safely published. The constructor creates a new object and assigns it to the field value. However, the field is not declared as volatile or final, and there are no synchronization mechanisms in place to ensure that the value is fully constructed before it is published. A different thread may see the default value null. Similarly, the field server_id may be observed to be 0.

    + + + +

    To fix this example, we declare the field value as volatile. This will ensure that all changes to the field are visible to all threads. The field server_id is only meant to be written once, so we only need the write inside the constructor to be visible to other threads; declaring it final guarantees this:

    + + + +
    + + + +
  • + Java Language Specification, chapter 17: + Threads and Locks. +
  • +
  • + Java concurrency package: + java.util.concurrent. +
  • + + +
    +
    diff --git a/java/ql/src/Likely Bugs/Concurrency/SafePublication.ql b/java/ql/src/Likely Bugs/Concurrency/SafePublication.ql new file mode 100644 index 000000000000..5e2a89ce3721 --- /dev/null +++ b/java/ql/src/Likely Bugs/Concurrency/SafePublication.ql @@ -0,0 +1,93 @@ +/** + * @name Safe publication + * @description A field of a thread-safe class is not safely published. + * @kind problem + * @problem.severity warning + * @precision high + * @id java/safe-publication + * @tags quality + * reliability + * concurrency + */ + +import java +import semmle.code.java.ConflictingAccess + +/** + * Holds if `v` should be the default value for the field `f`. + * That is, `v` is an initial (or constructor) assignment of `f`. + */ +predicate shouldBeDefaultValueFor(Field f, Expr v) { + v = f.getAnAssignedValue() and + ( + v = f.getInitializer() + or + v.getEnclosingCallable() = f.getDeclaringType().getAConstructor() + ) +} + +/** + * Gets the default value for the field `f`. + * See https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html + * for the default values of the primitive types. + * The default value for non-primitive types is null. + */ +bindingset[result] +Expr getDefaultValue(Field f) { + f.getType().hasName("byte") and result.(IntegerLiteral).getIntValue() = 0 + or + f.getType().hasName("short") and result.(IntegerLiteral).getIntValue() = 0 + or + f.getType().hasName("int") and result.(IntegerLiteral).getIntValue() = 0 + or + f.getType().hasName("long") and + ( + result.(LongLiteral).getValue() = "0" or + result.(IntegerLiteral).getValue() = "0" + ) + or + f.getType().hasName("float") and result.(FloatLiteral).getValue() = "0.0" + or + f.getType().hasName("double") and result.(DoubleLiteral).getValue() = "0.0" + or + f.getType().hasName("char") and result.(CharacterLiteral).getCodePointValue() = 0 + or + f.getType().hasName("boolean") and result.(BooleanLiteral).getBooleanValue() = false + or + not f.getType().getName() in [ + "byte", "short", "int", "long", "float", "double", "char", "boolean" + ] and + result instanceof NullLiteral +} + +/** + * Holds if all constructor or initial assignments (if any) are to the default value. + * That is, assignments by the declaration: + * int x = 0; OK + * int x = 3; not OK + * or inside a constructor: + * public c(a) { + * x = 0; OK + * x = 3; not OK + * x = a; not OK + * } + */ +predicate isAssignedDefaultValue(Field f) { + forall(Expr v | shouldBeDefaultValueFor(f, v) | v = getDefaultValue(f)) +} + +predicate isSafelyPublished(Field f) { + f.isFinal() or // NOTE: For non-primitive types, 'final' alone does not guarantee safe publication unless the object is immutable or safely constructed. Consider reviewing the handling of non-primitive fields for safe publication. + f.isStatic() or + f.isVolatile() or + isThreadSafeType(f.getType()) or + isThreadSafeType(f.getInitializer().getType()) or + isAssignedDefaultValue(f) +} + +from Field f, ClassAnnotatedAsThreadSafe c +where + f = c.getAField() and + not isSafelyPublished(f) +select f, "The class $@ is marked as thread-safe, but this field is not safely published.", c, + c.getName() diff --git a/java/ql/src/Likely Bugs/Concurrency/ThreadSafe.qhelp b/java/ql/src/Likely Bugs/Concurrency/ThreadSafe.qhelp new file mode 100644 index 000000000000..cafcb3cdd799 --- /dev/null +++ b/java/ql/src/Likely Bugs/Concurrency/ThreadSafe.qhelp @@ -0,0 +1,33 @@ + + + + + +

    +In a thread-safe class, all field accesses that can be caused by calls to public methods must be properly synchronized.

    + +
    + + +

    +Protect the field access with a lock. Alternatively mark the field as volatile if the write operation is atomic. You can also choose to use a data type that guarantees atomic access. If the field is immutable, mark it as final.

    + +
    + + + + +
  • + Java Language Specification, chapter 17: + Threads and Locks. +
  • +
  • + Java concurrency package: + java.util.concurrent. +
  • + + +
    +
    diff --git a/java/ql/src/Likely Bugs/Concurrency/ThreadSafe.ql b/java/ql/src/Likely Bugs/Concurrency/ThreadSafe.ql new file mode 100644 index 000000000000..fafa4701ed22 --- /dev/null +++ b/java/ql/src/Likely Bugs/Concurrency/ThreadSafe.ql @@ -0,0 +1,51 @@ +/** + * @name Not thread-safe + * @description This class is not thread-safe. It is annotated as `@ThreadSafe`, but it has a + * conflicting access to a field that is not synchronized with the same monitor. + * @kind problem + * @problem.severity warning + * @precision high + * @id java/not-threadsafe + * @tags quality + * reliability + * concurrency + */ + +import java +import semmle.code.java.ConflictingAccess + +predicate unmonitoredAccess(ExposedFieldAccess a, string msg, Expr entry, string entry_desc) { + exists(ClassAnnotatedAsThreadSafe cls, ExposedField f | + cls.unlockedPublicAccess(f, entry, _, a, true) + or + cls.unlockedPublicAccess(f, entry, _, a, false) and + cls.hasPublicWriteAccess(f) + ) and + msg = + "This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe." and + entry_desc = "this expression" +} + +predicate notFullyMonitoredField( + ExposedField f, string msg, ClassAnnotatedAsThreadSafe cls, string cls_name +) { + ( + // Technically there has to be a write access for a conflict to exist. + // But if you are locking your reads with different locks, you likely made a typo, + // so in this case we alert without requiring `cls.has_public_write_access(f)` + cls.singleMonitorMismatch(f) + or + cls.notFullyMonitored(f) and + cls.hasPublicWriteAccess(f) + ) and + msg = + "This field is not properly synchronized in that no single monitor covers all accesses, but the class $@ is annotated as @ThreadSafe." and + cls_name = cls.getName() +} + +from Top alert_element, Top alert_context, string alert_msg, string context_desc +where + unmonitoredAccess(alert_element, alert_msg, alert_context, context_desc) + or + notFullyMonitoredField(alert_element, alert_msg, alert_context, context_desc) +select alert_element, alert_msg, alert_context, context_desc diff --git a/java/ql/src/Likely Bugs/Concurrency/UnreleasedLock.ql b/java/ql/src/Likely Bugs/Concurrency/UnreleasedLock.ql index 118593e31fe8..c7d33eff4a99 100644 --- a/java/ql/src/Likely Bugs/Concurrency/UnreleasedLock.ql +++ b/java/ql/src/Likely Bugs/Concurrency/UnreleasedLock.ql @@ -16,47 +16,7 @@ import java import semmle.code.java.controlflow.Guards import semmle.code.java.dataflow.SSA -import semmle.code.java.frameworks.Mockito - -class LockType extends RefType { - LockType() { - this.getAMethod().hasName("lock") and - this.getAMethod().hasName("unlock") - } - - Method getLockMethod() { - result.getDeclaringType() = this and - (result.hasName("lock") or result.hasName("tryLock")) - } - - Method getUnlockMethod() { - result.getDeclaringType() = this and - result.hasName("unlock") - } - - Method getIsHeldByCurrentThreadMethod() { - result.getDeclaringType() = this and - result.hasName("isHeldByCurrentThread") - } - - MethodCall getLockAccess() { - result.getMethod() = this.getLockMethod() and - // Not part of a Mockito verification call - not result instanceof MockitoVerifiedMethodCall - } - - MethodCall getUnlockAccess() { - result.getMethod() = this.getUnlockMethod() and - // Not part of a Mockito verification call - not result instanceof MockitoVerifiedMethodCall - } - - MethodCall getIsHeldByCurrentThreadAccess() { - result.getMethod() = this.getIsHeldByCurrentThreadMethod() and - // Not part of a Mockito verification call - not result instanceof MockitoVerifiedMethodCall - } -} +import semmle.code.java.Concurrency predicate lockBlock(LockType t, BasicBlock b, int locks) { locks = strictcount(int i | b.getNode(i).asExpr() = t.getLockAccess()) diff --git a/java/ql/src/Likely Bugs/Concurrency/UnsafePublication.java b/java/ql/src/Likely Bugs/Concurrency/UnsafePublication.java new file mode 100644 index 000000000000..0b7ea3309815 --- /dev/null +++ b/java/ql/src/Likely Bugs/Concurrency/UnsafePublication.java @@ -0,0 +1,17 @@ +public class UnsafePublication { + private Object value; + private int server_id; + + public UnsafePublication() { + value = new Object(); // Not safely published, other threads may see the default value null + server_id = 1; // Not safely published, other threads may see the default value 0 + } + + public Object getValue() { + return value; + } + + public int getServerId() { + return server_id; + } +} \ No newline at end of file diff --git a/java/ql/src/change-notes/2025-06-22-query-escaping.md b/java/ql/src/change-notes/2025-06-22-query-escaping.md new file mode 100644 index 000000000000..f33de2e8556f --- /dev/null +++ b/java/ql/src/change-notes/2025-06-22-query-escaping.md @@ -0,0 +1,4 @@ +--- +category: newQuery +--- +* Added a new query, `java/escaping`, to detect values escaping from classes marked as `@ThreadSafe`. \ No newline at end of file diff --git a/java/ql/src/change-notes/2025-06-22-query-not-thread-safe.md b/java/ql/src/change-notes/2025-06-22-query-not-thread-safe.md new file mode 100644 index 000000000000..d5dd07446097 --- /dev/null +++ b/java/ql/src/change-notes/2025-06-22-query-not-thread-safe.md @@ -0,0 +1,4 @@ +--- +category: newQuery +--- +* Added a new query, `java/not-threadsafe`, to detect data races in classes marked as `@ThreadSafe`. \ No newline at end of file diff --git a/java/ql/src/change-notes/2025-06-22-query-safe-publication.md b/java/ql/src/change-notes/2025-06-22-query-safe-publication.md new file mode 100644 index 000000000000..23b64c970b31 --- /dev/null +++ b/java/ql/src/change-notes/2025-06-22-query-safe-publication.md @@ -0,0 +1,4 @@ +--- +category: newQuery +--- +* Added a new query, `java/safe-publication`, to detect unsafe publication in classes marked as `@ThreadSafe`. \ No newline at end of file diff --git a/java/ql/test/query-tests/Escaping/Escaping.expected b/java/ql/test/query-tests/Escaping/Escaping.expected new file mode 100644 index 000000000000..e066b29dae4f --- /dev/null +++ b/java/ql/test/query-tests/Escaping/Escaping.expected @@ -0,0 +1,3 @@ +| Escaping.java:3:7:3:7 | x | The class $@ is marked as thread-safe, but this field is potentially escaping. | Escaping.java:2:14:2:21 | Escaping | Escaping | +| Escaping.java:4:14:4:14 | y | The class $@ is marked as thread-safe, but this field is potentially escaping. | Escaping.java:2:14:2:21 | Escaping | Escaping | +| Escaping.java:9:18:9:18 | b | The class $@ is marked as thread-safe, but this field is potentially escaping. | Escaping.java:2:14:2:21 | Escaping | Escaping | diff --git a/java/ql/test/query-tests/Escaping/Escaping.java b/java/ql/test/query-tests/Escaping/Escaping.java new file mode 100644 index 000000000000..9d3b568369ad --- /dev/null +++ b/java/ql/test/query-tests/Escaping/Escaping.java @@ -0,0 +1,17 @@ +@ThreadSafe +public class Escaping { + int x; //$ Alert + public int y = 0; //$ Alert + private int z = 3; + final int w = 0; + public final int u = 4; + private final long a = 5; + protected long b = 0; //$ Alert + protected final long c = 0L; + volatile long d = 3; + protected volatile long e = 3L; + + public void methodLocal() { + int i; + } +} \ No newline at end of file diff --git a/java/ql/test/query-tests/Escaping/Escaping.qlref b/java/ql/test/query-tests/Escaping/Escaping.qlref new file mode 100644 index 000000000000..846d88a1e0aa --- /dev/null +++ b/java/ql/test/query-tests/Escaping/Escaping.qlref @@ -0,0 +1,2 @@ +query: Likely Bugs/Concurrency/Escaping.ql +postprocess: utils/test/InlineExpectationsTestQuery.ql \ No newline at end of file diff --git a/java/ql/test/query-tests/Escaping/ThreadSafe.java b/java/ql/test/query-tests/Escaping/ThreadSafe.java new file mode 100644 index 000000000000..1a4534cc78f4 --- /dev/null +++ b/java/ql/test/query-tests/Escaping/ThreadSafe.java @@ -0,0 +1,2 @@ +public @interface ThreadSafe { +} \ No newline at end of file diff --git a/java/ql/test/query-tests/SafePublication/SafePublication.expected b/java/ql/test/query-tests/SafePublication/SafePublication.expected new file mode 100644 index 000000000000..fbb54ff7b8cc --- /dev/null +++ b/java/ql/test/query-tests/SafePublication/SafePublication.expected @@ -0,0 +1,7 @@ +| SafePublication.java:5:9:5:9 | z | The class $@ is marked as thread-safe, but this field is not safely published. | SafePublication.java:2:14:2:28 | SafePublication | SafePublication | +| SafePublication.java:6:9:6:9 | w | The class $@ is marked as thread-safe, but this field is not safely published. | SafePublication.java:2:14:2:28 | SafePublication | SafePublication | +| SafePublication.java:7:9:7:9 | u | The class $@ is marked as thread-safe, but this field is not safely published. | SafePublication.java:2:14:2:28 | SafePublication | SafePublication | +| SafePublication.java:11:10:11:10 | d | The class $@ is marked as thread-safe, but this field is not safely published. | SafePublication.java:2:14:2:28 | SafePublication | SafePublication | +| SafePublication.java:12:10:12:10 | e | The class $@ is marked as thread-safe, but this field is not safely published. | SafePublication.java:2:14:2:28 | SafePublication | SafePublication | +| SafePublication.java:14:11:14:13 | arr | The class $@ is marked as thread-safe, but this field is not safely published. | SafePublication.java:2:14:2:28 | SafePublication | SafePublication | +| SafePublication.java:17:10:17:11 | cc | The class $@ is marked as thread-safe, but this field is not safely published. | SafePublication.java:2:14:2:28 | SafePublication | SafePublication | diff --git a/java/ql/test/query-tests/SafePublication/SafePublication.java b/java/ql/test/query-tests/SafePublication/SafePublication.java new file mode 100644 index 000000000000..9c1d031987b0 --- /dev/null +++ b/java/ql/test/query-tests/SafePublication/SafePublication.java @@ -0,0 +1,29 @@ +@ThreadSafe +public class SafePublication { + int x; + int y = 0; + int z = 3; //$ Alert + int w; //$ Alert + int u; //$ Alert + long a; + long b = 0; + long c = 0L; + long d = 3; //$ Alert + long e = 3L; //$ Alert + + int[] arr = new int[3]; //$ Alert + float f = 0.0f; + double dd = 00.0d; + char cc = 'a'; //$ Alert + char ok = '\u0000'; + + public SafePublication(int a) { + x = 0; + w = 3; // not ok + u = a; // not ok + } + + public void methodLocal() { + int i; + } +} \ No newline at end of file diff --git a/java/ql/test/query-tests/SafePublication/SafePublication.qlref b/java/ql/test/query-tests/SafePublication/SafePublication.qlref new file mode 100644 index 000000000000..51bf2ced9660 --- /dev/null +++ b/java/ql/test/query-tests/SafePublication/SafePublication.qlref @@ -0,0 +1,2 @@ +query: Likely Bugs/Concurrency/SafePublication.ql +postprocess: utils/test/InlineExpectationsTestQuery.ql \ No newline at end of file diff --git a/java/ql/test/query-tests/SafePublication/ThreadSafe.java b/java/ql/test/query-tests/SafePublication/ThreadSafe.java new file mode 100644 index 000000000000..1a4534cc78f4 --- /dev/null +++ b/java/ql/test/query-tests/SafePublication/ThreadSafe.java @@ -0,0 +1,2 @@ +public @interface ThreadSafe { +} \ No newline at end of file diff --git a/java/ql/test/query-tests/ThreadSafe/ThreadSafe.expected b/java/ql/test/query-tests/ThreadSafe/ThreadSafe.expected new file mode 100644 index 000000000000..3d73caaffe56 --- /dev/null +++ b/java/ql/test/query-tests/ThreadSafe/ThreadSafe.expected @@ -0,0 +1,45 @@ +| examples/C.java:14:9:14:14 | this.y | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/C.java:14:9:14:14 | this.y | this expression | +| examples/C.java:15:9:15:14 | this.y | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/C.java:15:9:15:14 | this.y | this expression | +| examples/C.java:16:9:16:14 | this.y | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/C.java:16:9:16:14 | this.y | this expression | +| examples/C.java:16:18:16:23 | this.y | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/C.java:16:18:16:23 | this.y | this expression | +| examples/C.java:20:9:20:14 | this.y | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/C.java:20:9:20:14 | this.y | this expression | +| examples/C.java:21:9:21:14 | this.y | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/C.java:21:9:21:14 | this.y | this expression | +| examples/C.java:22:9:22:14 | this.y | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/C.java:22:9:22:14 | this.y | this expression | +| examples/C.java:22:18:22:23 | this.y | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/C.java:22:18:22:23 | this.y | this expression | +| examples/C.java:26:9:26:14 | this.y | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/C.java:26:9:26:14 | this.y | this expression | +| examples/C.java:30:13:30:13 | y | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/C.java:30:13:30:13 | y | this expression | +| examples/C.java:33:9:33:9 | y | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/C.java:33:9:33:9 | y | this expression | +| examples/DeepPaths.java:8:17:8:17 | y | This field is not properly synchronized in that no single monitor covers all accesses, but the class $@ is annotated as @ThreadSafe. | examples/DeepPaths.java:7:14:7:22 | DeepPaths | DeepPaths | +| examples/FaultyTurnstileExample.java:18:5:18:9 | count | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/FaultyTurnstileExample.java:18:5:18:9 | count | this expression | +| examples/FaultyTurnstileExample.java:26:15:26:19 | count | This field is not properly synchronized in that no single monitor covers all accesses, but the class $@ is annotated as @ThreadSafe. | examples/FaultyTurnstileExample.java:23:7:23:29 | FaultyTurnstileExample2 | FaultyTurnstileExample2 | +| examples/FlawedSemaphore.java:15:14:15:18 | state | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/FlawedSemaphore.java:15:14:15:18 | state | this expression | +| examples/FlawedSemaphore.java:18:7:18:11 | state | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/FlawedSemaphore.java:18:7:18:11 | state | this expression | +| examples/LockExample.java:18:15:18:20 | length | This field is not properly synchronized in that no single monitor covers all accesses, but the class $@ is annotated as @ThreadSafe. | examples/LockExample.java:14:14:14:24 | LockExample | LockExample | +| examples/LockExample.java:19:15:19:31 | notRelatedToOther | This field is not properly synchronized in that no single monitor covers all accesses, but the class $@ is annotated as @ThreadSafe. | examples/LockExample.java:14:14:14:24 | LockExample | LockExample | +| examples/LockExample.java:20:17:20:23 | content | This field is not properly synchronized in that no single monitor covers all accesses, but the class $@ is annotated as @ThreadSafe. | examples/LockExample.java:14:14:14:24 | LockExample | LockExample | +| examples/LockExample.java:44:5:44:10 | length | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/LockExample.java:44:5:44:10 | length | this expression | +| examples/LockExample.java:45:5:45:11 | content | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/LockExample.java:45:5:45:11 | content | this expression | +| examples/LockExample.java:45:13:45:18 | length | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/LockExample.java:45:13:45:18 | length | this expression | +| examples/LockExample.java:49:5:49:10 | length | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/LockExample.java:49:5:49:10 | length | this expression | +| examples/LockExample.java:62:5:62:10 | length | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/LockExample.java:62:5:62:10 | length | this expression | +| examples/LockExample.java:65:5:65:11 | content | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/LockExample.java:65:5:65:11 | content | this expression | +| examples/LockExample.java:65:13:65:18 | length | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/LockExample.java:65:13:65:18 | length | this expression | +| examples/LockExample.java:69:5:69:10 | length | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/LockExample.java:69:5:69:10 | length | this expression | +| examples/LockExample.java:71:5:71:11 | content | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/LockExample.java:71:5:71:11 | content | this expression | +| examples/LockExample.java:71:13:71:18 | length | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/LockExample.java:71:13:71:18 | length | this expression | +| examples/LockExample.java:76:5:76:10 | length | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/LockExample.java:76:5:76:10 | length | this expression | +| examples/LockExample.java:79:5:79:11 | content | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/LockExample.java:79:5:79:11 | content | this expression | +| examples/LockExample.java:79:13:79:18 | length | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/LockExample.java:79:13:79:18 | length | this expression | +| examples/LockExample.java:112:5:112:21 | notRelatedToOther | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/LockExample.java:112:5:112:21 | notRelatedToOther | this expression | +| examples/LockExample.java:119:5:119:21 | notRelatedToOther | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/LockExample.java:119:5:119:21 | notRelatedToOther | this expression | +| examples/LockExample.java:124:5:124:21 | notRelatedToOther | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/LockExample.java:124:5:124:21 | notRelatedToOther | this expression | +| examples/LockExample.java:145:5:145:21 | notRelatedToOther | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/LockExample.java:145:5:145:21 | notRelatedToOther | this expression | +| examples/LockExample.java:153:5:153:21 | notRelatedToOther | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/LockExample.java:153:5:153:21 | notRelatedToOther | this expression | +| examples/ManyLocks.java:8:17:8:17 | y | This field is not properly synchronized in that no single monitor covers all accesses, but the class $@ is annotated as @ThreadSafe. | examples/ManyLocks.java:7:14:7:22 | ManyLocks | ManyLocks | +| examples/SyncLstExample.java:45:5:45:7 | lst | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/SyncLstExample.java:45:5:45:7 | lst | this expression | +| examples/SyncStackExample.java:37:5:37:7 | stc | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/SyncStackExample.java:37:5:37:7 | stc | this expression | +| examples/SynchronizedAndLock.java:10:17:10:22 | length | This field is not properly synchronized in that no single monitor covers all accesses, but the class $@ is annotated as @ThreadSafe. | examples/SynchronizedAndLock.java:7:14:7:32 | SynchronizedAndLock | SynchronizedAndLock | +| examples/Test.java:52:5:52:10 | this.y | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/Test.java:24:5:24:18 | setYPrivate(...) | this expression | +| examples/Test.java:60:5:60:10 | this.y | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/Test.java:60:5:60:10 | this.y | this expression | +| examples/Test.java:74:5:74:10 | this.y | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/Test.java:74:5:74:10 | this.y | this expression | +| examples/Test.java:74:14:74:14 | y | This field access (publicly accessible via $@) is not protected by any monitor, but the class is annotated as @ThreadSafe. | examples/Test.java:74:14:74:14 | y | this expression | diff --git a/java/ql/test/query-tests/ThreadSafe/ThreadSafe.qlref b/java/ql/test/query-tests/ThreadSafe/ThreadSafe.qlref new file mode 100644 index 000000000000..eba9a6745542 --- /dev/null +++ b/java/ql/test/query-tests/ThreadSafe/ThreadSafe.qlref @@ -0,0 +1,2 @@ +query: Likely Bugs/Concurrency/ThreadSafe.ql +postprocess: utils/test/InlineExpectationsTestQuery.ql \ No newline at end of file diff --git a/java/ql/test/query-tests/ThreadSafe/examples/Alias.java b/java/ql/test/query-tests/ThreadSafe/examples/Alias.java new file mode 100644 index 000000000000..802bae2fbb35 --- /dev/null +++ b/java/ql/test/query-tests/ThreadSafe/examples/Alias.java @@ -0,0 +1,21 @@ +package examples; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@ThreadSafe +public class Alias { + private int y; + + private final ReentrantLock lock = new ReentrantLock(); + + public void notMismatch() { + final ReentrantLock lock = this.lock; + lock.lock(); + try { + y = 42; + } finally { + this.lock.unlock(); + } + } +} \ No newline at end of file diff --git a/java/ql/test/query-tests/ThreadSafe/examples/App.java b/java/ql/test/query-tests/ThreadSafe/examples/App.java new file mode 100644 index 000000000000..1c085ee6179a --- /dev/null +++ b/java/ql/test/query-tests/ThreadSafe/examples/App.java @@ -0,0 +1,14 @@ +/* + * This Java source file was generated by the Gradle 'init' task. + */ +package examples; + +public class App { + public String getGreeting() { + return "Hello World!"; + } + + public static void main(String[] args) { + System.out.println(new App().getGreeting()); + } +} diff --git a/java/ql/test/query-tests/ThreadSafe/examples/C.java b/java/ql/test/query-tests/ThreadSafe/examples/C.java new file mode 100644 index 000000000000..92c6b82800c3 --- /dev/null +++ b/java/ql/test/query-tests/ThreadSafe/examples/C.java @@ -0,0 +1,72 @@ +package examples; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@ThreadSafe +public class C { + + private int y; + private Lock lock = new ReentrantLock(); + private Lock lock2 = new ReentrantLock(); + + public void m() { + this.y = 0; // $ Alert + this.y += 1; // $ Alert + this.y = this.y - 1; // $ Alert + } + + public void n4() { + this.y = 0; // $ Alert + this.y += 1; // $ Alert + this.y = this.y - 1; // $ Alert + } + + public void setY(int y) { + this.y = y; // $ Alert + } + + public void test() { + if (y == 0) { // $ Alert + lock.lock(); + } + y = 0; // $ Alert + lock.unlock(); + } + + public void n() { + this.lock.lock(); + this.y = 0; + this.y += 1; + this.y = this.y - 1; + this.lock.unlock(); + } + + public void callTestLock2() { + lock2.lock(); + setY(1); + lock2.unlock(); + } + + public void n2() { + lock.lock(); + this.y = 0; + this.y += 1; + this.y = this.y - 1; + lock.unlock(); + } + + public void n3() { + lock.lock(); + y = 0; + y += 1; + y = y - 1; + lock.unlock(); + } + + public void callTest() { + lock.lock(); + setY(1); + lock.unlock(); + } +} \ No newline at end of file diff --git a/java/ql/test/query-tests/ThreadSafe/examples/DeepPaths.java b/java/ql/test/query-tests/ThreadSafe/examples/DeepPaths.java new file mode 100644 index 000000000000..095087a50182 --- /dev/null +++ b/java/ql/test/query-tests/ThreadSafe/examples/DeepPaths.java @@ -0,0 +1,61 @@ +package examples; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@ThreadSafe +public class DeepPaths { + private int y; // $ Alert + + private final Lock lock1 = new ReentrantLock(); + private final Lock lock2 = new ReentrantLock(); + private final Lock lock3 = new ReentrantLock(); + + public void layer1Locked() { + lock1.lock(); + this.layer2Locked(); + lock1.unlock(); + } + + private void layer2Locked() { + lock2.lock(); + this.layer3Unlocked(); + lock2.unlock(); + } + + private void layer3Locked() { + lock3.lock(); + y++; + lock3.unlock(); + } + + public void layer1Skip() { + lock2.lock(); + this.layer3Locked(); + lock2.unlock(); + } + + public void layer1Indirect() { + this.layer2(); + } + + private void layer2() { + this.layer2Locked(); + } + + public void layer1Unlocked() { + this.layer2Unlocked(); + } + + private void layer2Unlocked() { + this.layer3(); + } + + private void layer3() { + this.layer3Locked(); + } + + private void layer3Unlocked() { + y++; + } +} diff --git a/java/ql/test/query-tests/ThreadSafe/examples/FaultyTurnstileExample.java b/java/ql/test/query-tests/ThreadSafe/examples/FaultyTurnstileExample.java new file mode 100644 index 000000000000..20b258135f6d --- /dev/null +++ b/java/ql/test/query-tests/ThreadSafe/examples/FaultyTurnstileExample.java @@ -0,0 +1,40 @@ +package examples; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@ThreadSafe +class FaultyTurnstileExample { + private Lock lock = new ReentrantLock(); + private int count = 0; + + public void inc() { + lock.lock(); + count++; // $ MISSING: Alert + lock.unlock(); + } + + public void dec() { + count--; // $ Alert + } +} + +@ThreadSafe +class FaultyTurnstileExample2 { + private Lock lock1 = new ReentrantLock(); + private Lock lock2 = new ReentrantLock(); + private int count = 0; // $ Alert + + public void inc() { + lock1.lock(); + count++; + lock1.unlock(); + } + + public void dec() { + lock2.lock(); + count--; + lock2.unlock(); + } +} + diff --git a/java/ql/test/query-tests/ThreadSafe/examples/FlawedSemaphore.java b/java/ql/test/query-tests/ThreadSafe/examples/FlawedSemaphore.java new file mode 100644 index 000000000000..a73b45e60ed6 --- /dev/null +++ b/java/ql/test/query-tests/ThreadSafe/examples/FlawedSemaphore.java @@ -0,0 +1,30 @@ +package examples; + +@ThreadSafe +public class FlawedSemaphore { + private final int capacity; + private int state; + + public FlawedSemaphore(int c) { + capacity = c; + state = 0; + } + + public void acquire() { + try { + while (state == capacity) { // $ Alert + this.wait(); + } + state++; // $ Alert + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public void release() { + synchronized (this) { + state--; // State can become negative + this.notifyAll(); + } + } +} diff --git a/java/ql/test/query-tests/ThreadSafe/examples/LockCorrect.java b/java/ql/test/query-tests/ThreadSafe/examples/LockCorrect.java new file mode 100644 index 000000000000..9c6c5abce56d --- /dev/null +++ b/java/ql/test/query-tests/ThreadSafe/examples/LockCorrect.java @@ -0,0 +1,51 @@ +package examples; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@ThreadSafe +public class LockCorrect { + private Lock lock1 = new ReentrantLock(); + + private int length = 0; + private int notRelatedToOther = 10; + private int[] content = new int[10]; + private int thisSynchronized = 0; + + public void add(int value) { + lock1.lock(); + length++; + content[length] = value; + lock1.unlock(); + } + + public void removeCorrect() { + lock1.lock(); + content[length] = 0; + length--; + lock1.unlock(); + } + + public void synchronizedOnLock1() { + synchronized(lock1) { + notRelatedToOther++; + } + } + + public void synchronizedOnLock12() { + synchronized(lock1) { + notRelatedToOther++; + } + } + + public synchronized void x() { + thisSynchronized++; + } + + public void x1() { + synchronized(this) { + thisSynchronized++; + } + } + +} diff --git a/java/ql/test/query-tests/ThreadSafe/examples/LockExample.java b/java/ql/test/query-tests/ThreadSafe/examples/LockExample.java new file mode 100644 index 000000000000..1e1792d0d2e4 --- /dev/null +++ b/java/ql/test/query-tests/ThreadSafe/examples/LockExample.java @@ -0,0 +1,156 @@ +// This example shows that we only get one alert "per concurrency problem": +// For each problematic variable, we get one alert at the earliest conflicting write. +// If the variable is involved in several different monitors, we get an alert for each monitor that +// is not correctly used. +// A single alert can have many related locations, since each conflicting access which is not +// properly synchronized is a related location. +// This leads to many lines in the .expected file. +package examples; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@ThreadSafe +public class LockExample { + private Lock lock1 = new ReentrantLock(); + private Lock lock2 = new ReentrantLock(); + + private int length = 0; // $ Alert + private int notRelatedToOther = 10; // $ Alert + private int[] content = new int[10]; // $ Alert + + public void add(int value) { + lock1.lock(); + length++; + content[length] = value; + lock1.unlock(); + } + + public void removeCorrect() { + lock1.lock(); + length--; + content[length] = 0; + lock1.unlock(); + } + + public void notTheSameLockAsAdd() { // use locks, but different ones + lock2.lock(); + length--; + content[length] = 0; + lock2.unlock(); + } + + public void noLock() { // no locks + length--; // $ Alert + content[length] = 0; // $ Alert + } + + public void fieldUpdatedOutsideOfLock() { // adjusts length without lock + length--; // $ Alert + + lock1.lock(); + content[length] = 0; + lock1.unlock(); + } + + public synchronized void synchronizedLock() { // no locks, but with synchronized + length--; + content[length] = 0; + } + + public void onlyLocked() { // never unlocked, only locked + length--; // $ Alert + + lock1.lock(); + content[length] = 0; // $ Alert + } + + public void onlyUnlocked() { // never locked, only unlocked + length--; // $ Alert + + content[length] = 0; // $ Alert + lock1.unlock(); + } + + public void notSameLock() { + length--; // $ Alert + + lock2.lock();// Not the same lock + content[length] = 0; // $ Alert + lock1.unlock(); + } + + public void updateCount() { + lock2.lock(); + notRelatedToOther++; + lock2.unlock(); + } + + public void updateCountTwiceCorrect() { + lock2.lock(); + notRelatedToOther++; + lock2.unlock(); + lock1.lock(); + notRelatedToOther++; + lock1.unlock(); + } + + public void updateCountTwiceDifferentLocks() { + lock2.lock(); + notRelatedToOther++; + lock2.unlock(); + lock1.lock(); + notRelatedToOther++; + lock2.unlock(); + } + + public void updateCountTwiceLock() { + lock2.lock(); + notRelatedToOther++; + lock2.unlock(); + lock1.lock(); + notRelatedToOther++; // $ Alert + } + + public void updateCountTwiceUnLock() { + lock2.lock(); + notRelatedToOther++; + lock2.unlock(); + notRelatedToOther++; // $ Alert + lock1.unlock(); + } + + public void synchronizedNonRelatedOutside() { + notRelatedToOther++; // $ Alert + + synchronized(this) { + length++; + } + } + + public void synchronizedNonRelatedOutside2() { + int x = 0; + x++; + + synchronized(this) { + length++; + } + } + + public void synchronizedNonRelatedOutside3() { + synchronized(this) { + length++; + } + + notRelatedToOther = 1; // $ Alert + } + + public void synchronizedNonRelatedOutside4() { + synchronized(lock1) { + length++; + } + + notRelatedToOther = 1; // $ Alert + } + +} diff --git a/java/ql/test/query-tests/ThreadSafe/examples/LoopyCallGraph.java b/java/ql/test/query-tests/ThreadSafe/examples/LoopyCallGraph.java new file mode 100644 index 000000000000..caea22ac8511 --- /dev/null +++ b/java/ql/test/query-tests/ThreadSafe/examples/LoopyCallGraph.java @@ -0,0 +1,33 @@ +package examples; + +import java.util.Random; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@ThreadSafe +public class LoopyCallGraph { + private Lock lock = new ReentrantLock(); + private int count = 0; + private Random random = new Random(); + + public void entry() { + if (random.nextBoolean()) { + increase(); // this could look like an unprotected path to a call to dec() + } else { + lock.lock(); + dec(); + lock.unlock(); + } + } + + private void increase() { + lock.lock(); + count = 10; + lock.unlock(); + entry(); // this could look like an unprotected path to a call to dec() + } + + private void dec() { + count--; + } +} \ No newline at end of file diff --git a/java/ql/test/query-tests/ThreadSafe/examples/ManyLocks.java b/java/ql/test/query-tests/ThreadSafe/examples/ManyLocks.java new file mode 100644 index 000000000000..a7e19b3424bf --- /dev/null +++ b/java/ql/test/query-tests/ThreadSafe/examples/ManyLocks.java @@ -0,0 +1,37 @@ +package examples; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@ThreadSafe +public class ManyLocks { + private int y; // $ Alert + + private final Lock lock1 = new ReentrantLock(); + private final Lock lock2 = new ReentrantLock(); + private final Lock lock3 = new ReentrantLock(); + + public void inc() { + lock1.lock(); + lock2.lock(); + y++; + lock2.unlock(); + lock1.unlock(); + } + + public void dec() { + lock2.lock(); + lock3.lock(); + y--; + lock3.unlock(); + lock2.unlock(); + } + + public void reset() { + lock1.lock(); + lock3.lock(); + y = 0; + lock3.unlock(); + lock1.unlock(); + } +} \ No newline at end of file diff --git a/java/ql/test/query-tests/ThreadSafe/examples/SyncLstExample.java b/java/ql/test/query-tests/ThreadSafe/examples/SyncLstExample.java new file mode 100644 index 000000000000..04bc1c3c454b --- /dev/null +++ b/java/ql/test/query-tests/ThreadSafe/examples/SyncLstExample.java @@ -0,0 +1,47 @@ +package examples; + +import java.util.List; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@ThreadSafe +public class SyncLstExample { + private Lock lock = new ReentrantLock(); + private List lst; + + public SyncLstExample(List lst) { + this.lst = lst; + } + + public void add(T item) { + lock.lock(); + lst.add(item); + lock.unlock(); + } + + public void remove(int i) { + lock.lock(); + lst.remove(i); + lock.unlock(); + } +} + +@ThreadSafe +class FaultySyncLstExample { + private Lock lock = new ReentrantLock(); + private List lst; + + public FaultySyncLstExample(List lst) { + this.lst = lst; + } + + public void add(T item) { + lock.lock(); + lst.add(item); + lock.unlock(); + } + + public void remove(int i) { + lst.remove(i); // $ Alert + } +} diff --git a/java/ql/test/query-tests/ThreadSafe/examples/SyncStackExample.java b/java/ql/test/query-tests/ThreadSafe/examples/SyncStackExample.java new file mode 100644 index 000000000000..31e8cebee3e7 --- /dev/null +++ b/java/ql/test/query-tests/ThreadSafe/examples/SyncStackExample.java @@ -0,0 +1,39 @@ +package examples; + +import java.util.Stack; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@ThreadSafe +public class SyncStackExample { + private Lock lock = new ReentrantLock(); + private Stack stc = new Stack(); + + public void push(T item) { + lock.lock(); + stc.push(item); + lock.unlock(); + } + + public void pop() { + lock.lock(); + stc.pop(); + lock.unlock(); + } +} + +@ThreadSafe +class FaultySyncStackExample { + private Lock lock = new ReentrantLock(); + private Stack stc = new Stack(); + + public void push(T item) { + lock.lock(); + stc.push(item); + lock.unlock(); + } + + public void pop() { + stc.pop(); // $ Alert + } +} diff --git a/java/ql/test/query-tests/ThreadSafe/examples/SynchronizedAndLock.java b/java/ql/test/query-tests/ThreadSafe/examples/SynchronizedAndLock.java new file mode 100644 index 000000000000..52b35a84bb70 --- /dev/null +++ b/java/ql/test/query-tests/ThreadSafe/examples/SynchronizedAndLock.java @@ -0,0 +1,21 @@ +package examples; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@ThreadSafe +public class SynchronizedAndLock { + private Lock lock = new ReentrantLock(); + + private int length = 0; // $ Alert + + public void add(int value) { + lock.lock(); + length++; + lock.unlock(); + } + + public synchronized void subtract() { + length--; + } +} diff --git a/java/ql/test/query-tests/ThreadSafe/examples/Test.java b/java/ql/test/query-tests/ThreadSafe/examples/Test.java new file mode 100644 index 000000000000..e6b0567ef89b --- /dev/null +++ b/java/ql/test/query-tests/ThreadSafe/examples/Test.java @@ -0,0 +1,76 @@ +package examples; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@ThreadSafe +public class Test { + /** + * Escaping field due to public visibility. + */ + int publicField; + + private int y; + final int immutableField = 1; + + // As of the below examples with synchronized as well. Except the incorrectly placed lock. + + private Lock lock = new ReentrantLock(); + + /** + * Calls the a method where y field escapes. + * @param y + */ + public void setYAgainInCorrect(int t) { + setYPrivate(t); + } + + /** + * Locks the method where y field escapes. + * @param y + */ + public void setYAgainCorrect(int y) { + lock.lock(); + setYPrivate(y); + lock.unlock(); + } + + /** + * No escaping y field. Locks the y before assignment. + * @param y + */ + public void setYCorrect(int y) { + lock.lock(); + this.y = y; + lock.unlock(); + } + + /** + * No direct escaping, since it method is private. Only escaping if another public method uses this. + * @param y + */ + private void setYPrivate(int y) { + this.y = y; // $ Alert + } + + /** + * Incorrectly locks y. + * @param y + */ + public void setYWrongLock(int y) { + this.y = y; // $ Alert + lock.lock(); + lock.unlock(); + } + + public synchronized int getImmutableField() { + return immutableField; + } + + public synchronized int getImmutableField2() { + return immutableField; + } + + public void testMethod() { + this.y = y + 2; // $ Alert + } +} diff --git a/java/ql/test/query-tests/ThreadSafe/examples/Test2.java b/java/ql/test/query-tests/ThreadSafe/examples/Test2.java new file mode 100644 index 000000000000..731af5ecf679 --- /dev/null +++ b/java/ql/test/query-tests/ThreadSafe/examples/Test2.java @@ -0,0 +1,22 @@ +package examples; + +import java.util.ArrayList; + +// Note: Not marked as thread-safe +// We inherit from this in Test3Super.java +public class Test2 { + int x; + protected ArrayList lst = new ArrayList<>(); + + public Test2() { + this.x = 0; + } + + public void changeX() { + this.x = x + 1; + } + + public void changeLst() { + lst.add("Hello"); + } +} diff --git a/java/ql/test/query-tests/ThreadSafe/examples/Test3Super.java b/java/ql/test/query-tests/ThreadSafe/examples/Test3Super.java new file mode 100644 index 000000000000..5a48e20bc056 --- /dev/null +++ b/java/ql/test/query-tests/ThreadSafe/examples/Test3Super.java @@ -0,0 +1,17 @@ +package examples; + +@ThreadSafe +public class Test3Super extends Test2 { // We might want an alert here for the inherited unsafe methods. + + public Test3Super() { + super.x = 0; + } + + public void y() { + super.x = 0; //$ MISSING: Alert + } + + public void yLst() { + super.lst.add("Hello!"); //$ MISSING: Alert + } +} diff --git a/java/ql/test/query-tests/ThreadSafe/examples/ThreadSafe.java b/java/ql/test/query-tests/ThreadSafe/examples/ThreadSafe.java new file mode 100644 index 000000000000..fc0a645c442c --- /dev/null +++ b/java/ql/test/query-tests/ThreadSafe/examples/ThreadSafe.java @@ -0,0 +1,4 @@ +package examples; + +public @interface ThreadSafe { +} \ No newline at end of file diff --git a/java/ql/test/query-tests/ThreadSafe/examples/TurnstileExample.java b/java/ql/test/query-tests/ThreadSafe/examples/TurnstileExample.java new file mode 100644 index 000000000000..90ea98a77d9e --- /dev/null +++ b/java/ql/test/query-tests/ThreadSafe/examples/TurnstileExample.java @@ -0,0 +1,23 @@ +package examples; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@ThreadSafe +public class TurnstileExample { + private Lock lock = new ReentrantLock(); + private int count = 0; + + public void inc() { + Lock l = lock; + l.lock(); + count++; + l.unlock(); + } + + public void dec() { + lock.lock(); + count--; + lock.unlock(); + } +} \ No newline at end of file