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..7fbf0647b277 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. */ + Method getLockMethod() { + result.getDeclaringType() = this and + result.hasName(["lock", "lockInterruptibly", "tryLock"]) + } + + /** Gets a method that is unlocking this lock type. */ + Method getUnlockMethod() { + result.getDeclaringType() = this and + result.hasName("unlock") + } + + /** Gets an `isHeldByCurrentThread` method of this lock type. */ + 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,123 @@ 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()) + } + + /** Holds if `localLock` refers to `lock`. */ + predicate represents(Field lock, Variable localLock) { + lock.getType() instanceof LockType and + ( + localLock = lock + or + localLock.getInitializer() = lock.getAnAccess() + ) + } + + /** 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() + } + + /** Holds if `e` is synchronized on the `Lock` `lock` by a locking call. */ + predicate locallyLockedOn(Expr e, Field lock) { + lock.getType() instanceof LockType and + exists(Variable localLock, MethodCall lockCall, MethodCall unlockCall | + represents(lock, localLock) and + lockCall.getQualifier() = localLock.getAnAccess() and + lockCall = lock.getType().(LockType).getLockAccess() and + unlockCall.getQualifier() = localLock.getAnAccess() and + unlockCall = lock.getType().(LockType).getUnlockAccess() + | + 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..f075bb3d1989 --- /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. */ +predicate isThreadSafeInitializer(Expr e) { + e.(Call) + .getCallee() + .getSourceDeclaration() + .getQualifiedName() + .matches("java.util.Collections.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()`. + */ + predicate unlocked_access(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.unlocked_access(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`. */ + predicate has_unlocked_access(ExposedField f, Expr e, Method m, boolean write) { + this.unlocked_access(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 unlocked_public_access( + ExposedField f, Expr e, Method m, ExposedFieldAccess a, boolean write + ) { + this.unlocked_access(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`. */ + predicate has_unlocked_public_access(ExposedField f, Expr e, Method m, boolean write) { + this.unlocked_public_access(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`. + */ + predicate has_onelocked_access( + ExposedField f, Expr e, Method m, boolean write, Monitors::Monitor monitor + ) { + //base + this.has_unlocked_access(f, e, m, write) and + Monitors::locallyMonitors(e, monitor) + or + // recursive case + exists(MethodCall c, Method m0 | this.has_onelocked_access(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`. */ + predicate has_onelocked_public_access( + ExposedField f, Expr e, Method m, boolean write, Monitors::Monitor monitor + ) { + this.has_onelocked_access(f, e, m, write, monitor) and + m.isPublic() and + not this.has_unlocked_public_access(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 single_monitor_mismatch(ExposedField f) { + 2 <= + strictcount(Monitors::Monitor monitor | this.has_onelocked_public_access(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`. */ + predicate has_onepluslocked_access( + ExposedField f, Expr e, Method m, boolean write, Monitors::Monitor monitor + ) { + //base + this.has_onelocked_access(f, e, m, write, monitor) and + not this.single_monitor_mismatch(f) and + not this.has_unlocked_public_access(f, _, _, _) + or + // recursive case + exists(MethodCall c, Method m0, Monitors::Monitor monitor0 | + this.has_onepluslocked_access(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 has_public_write_access(ExposedField f) { + this.has_unlocked_public_access(f, _, _, true) + or + this.has_onelocked_public_access(f, _, _, true, _) + or + exists(Method m | m.getDeclaringType() = this and m.isPublic() | + this.has_onepluslocked_access(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`. */ + predicate escapes_monitor( + ExposedField f, Expr e, Method m, boolean write, Monitors::Monitor monitor + ) { + //base + this.has_onepluslocked_access(f, _, _, _, monitor) and + this.has_unlocked_access(f, e, m, write) and + not Monitors::locallyMonitors(e, monitor) + or + // recursive case + exists(MethodCall c, Method m0 | this.escapes_monitor(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, 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`. */ + predicate escapes_monitor_public( + ExposedField f, Expr e, Method m, boolean write, Monitors::Monitor monitor + ) { + this.escapes_monitor(f, e, m, write, monitor) and + m.isPublic() + } + + /** Holds if no monitor protects all accesses to the field `f`. */ + predicate not_fully_monitored(ExposedField f) { + forex(Monitors::Monitor monitor | this.has_onepluslocked_access(f, _, _, _, monitor) | + this.escapes_monitor_public(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..64341017890f --- /dev/null +++ b/java/ql/src/Likely Bugs/Concurrency/SafePublication.java @@ -0,0 +1,11 @@ +public class SafePublication { + private Object value; + + public synchronized void produce() { + value = new Object(); // Safely published using synchronization + } + + public synchronized Object getValue() { + return value; + } +} \ 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..a24977e67301 --- /dev/null +++ b/java/ql/src/Likely Bugs/Concurrency/SafePublication.qhelp @@ -0,0 +1,62 @@ + + + + + +

    +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 value of value is not safely published. The produce method + creates a new object and assigns it to the field value. However, the field is not + declared as volatile, and there are no synchronization mechanisms in place to ensure + that the value is fully constructed before it is published.

    + + + +

    To fix this example, declare the field value as volatile, or use + synchronized blocks or methods to ensure that the value is fully constructed before it is + published. We illustrate the latter with the following example:

    + + + +
    + + + +
  • + 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..89e9cbdb169b --- /dev/null +++ b/java/ql/src/Likely Bugs/Concurrency/ThreadSafe.ql @@ -0,0 +1,56 @@ +/** + * @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 unmonitored_access( + ClassAnnotatedAsThreadSafe cls, ExposedFieldAccess a, Expr entry, string msg, string entry_desc +) { + exists(ExposedField f | + cls.unlocked_public_access(f, entry, _, a, true) + or + cls.unlocked_public_access(f, entry, _, a, false) and + cls.has_public_write_access(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 not_fully_monitored_field( + ClassAnnotatedAsThreadSafe cls, ExposedField f, string msg, 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.single_monitor_mismatch(f) + or + cls.not_fully_monitored(f) and + cls.has_public_write_access(f) + ) and + msg = + "The 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 + ClassAnnotatedAsThreadSafe cls, Top alert_element, Top alert_context, string alert_msg, + string context_desc +where + unmonitored_access(cls, alert_element, alert_context, alert_msg, context_desc) + or + not_fully_monitored_field(cls, alert_element, alert_msg, context_desc) and + alert_context = cls +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..ddf8c8b400f5 --- /dev/null +++ b/java/ql/src/Likely Bugs/Concurrency/UnsafePublication.java @@ -0,0 +1,12 @@ +@ThreadSafe +public class UnsafePublication { + private Object value; + + public void produce() { + value = new Object(); // Not safely published, other threads may see the default value null + } + + public Object getValue() { + return value; + } +} \ 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..ae2bd4ea5c8a --- /dev/null +++ b/java/ql/test/query-tests/ThreadSafe/ThreadSafe.expected @@ -0,0 +1,43 @@ +| 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/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 | The 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 | The 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 | The 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 | The 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/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 | The 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/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/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/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