NullPointerException in SonarJava LocksNotUnlockedCheck when analyzing RedisDistributedLock

Hi Sonar team,

I encountered an analyzer crash while scanning a Java class that implements a distributed lock with a local ReentrantLock fallback.

Environment:

[INFO] --- sonar:5.5.0.6356:sonar (default-cli) @ wiki ---
Warning:  Using an unspecified version instead of an explicit plugin version may introduce breaking analysis changes at an unwanted time. It is highly recommended to use an explicit version, e.g. 'org.sonarsource.scanner.maven:sonar-maven-plugin:5.5.0.6356'.
[INFO] Java 25.0.2 Azul Systems, Inc. (64-bit)
[INFO] Linux 4.18.0-477.10.1.el8_8.x86_64 (amd64)
[INFO] Communicating with SonarQube Community Build 26.2.0.119303
[INFO] JRE provisioning: os[linux], arch[x86_64]
[INFO] Starting SonarScanner Engine...

OS: RockyLinux 8
Arch: x86_64
Scanner: Maven
CI Platform: Github/Gitlab

Code Example:

package com.***.wiki.infrastructure.cache;

import com.***.wiki.common.constant.CacheKeys;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

@Component
public class RedisDistributedLock {

    private final StringRedisTemplate redisTemplate;
    private final DefaultRedisScript<Long> releaseLockRedisScript;
    private final ConcurrentMap<String, ReentrantLock> localLocks = new ConcurrentHashMap<>();
    private final ConcurrentMap<String, String> localLockTokens = new ConcurrentHashMap<>();

    public RedisDistributedLock(StringRedisTemplate redisTemplate,
                                DefaultRedisScript<Long> releaseLockRedisScript) {
        this.redisTemplate = redisTemplate;
        this.releaseLockRedisScript = releaseLockRedisScript;
    }

    public LockToken lock(String key, Duration leaseDuration, Duration waitDuration) {
        String lockKey = CacheKeys.LOCK_PREFIX + key;
        String token = UUID.randomUUID().toString();

        long deadlineNanos = System.nanoTime() + waitDuration.toNanos();
        while (System.nanoTime() < deadlineNanos) {
            try {
                Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, token, leaseDuration);
                if (Boolean.TRUE.equals(success)) {
                    return new LockToken(lockKey, token);
                }
            } catch (Exception ex) {
                return localLock(lockKey, token, waitDuration);
            }
            try {
                TimeUnit.MILLISECONDS.sleep(25);
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        return null;
    }

    public void unlock(LockToken token) {
        if (token == null) {
            return;
        }
        if (token.key().startsWith("local:")) {
            String localKey = token.key().substring("local:".length());
            String expected = localLockTokens.get(localKey);
            if (token.value().equals(expected)) {
                localLockTokens.remove(localKey);
                ReentrantLock localLock = localLocks.get(localKey);
                if (localLock != null && localLock.isHeldByCurrentThread()) {
                    localLock.unlock();
                }
            }
            return;
        }
        try {
            redisTemplate.execute(releaseLockRedisScript, List.of(token.key()), token.value());
        } catch (Exception ignored) {
            // Best effort unlock for Redis mode.
        }
    }

    private LockToken localLock(String lockKey, String token, Duration waitDuration) {
        ReentrantLock localLock = localLocks.computeIfAbsent(lockKey, key -> new ReentrantLock());
        try {
            boolean acquired = localLock.tryLock(waitDuration.toMillis(), TimeUnit.MILLISECONDS);
            if (!acquired) {
                return null;
            }
            localLockTokens.put(lockKey, token);
            return new LockToken("local:" + lockKey, token);
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
            return null;
        }
    }

    public record LockToken(String key, String value) {
    }
}

Actual result:

The Java analyzer crashes with NullPointerException during symbolic execution instead of reporting an issue normally.

original error log:

Error:  Unable to run check class org.sonar.java.se.SymbolicExecutionVisitor -  on file 'src/main/java/com/***/wiki/infrastructure/cache/RedisDistributedLock.java', To help improve the SonarSource Java Analyzer, please report this problem to SonarSource: see https://community.sonarsource.com/
java.lang.NullPointerException
	at java.base/java.util.Objects.requireNonNull(Unknown Source)
	at org.sonarsource.analyzer.commons.collections.AVLTree.get(AVLTree.java:80)
	at org.sonar.java.se.ProgramState.addConstraint(ProgramState.java:318)
	at org.sonar.java.se.ProgramState.addConstraintTransitively(ProgramState.java:297)
	at org.sonar.java.se.checks.LocksNotUnlockedCheck$PostStatementVisitor.visitMethodInvocationWithIdentifierTarget(LocksNotUnlockedCheck.java:153)
	at org.sonar.java.se.checks.LocksNotUnlockedCheck$PostStatementVisitor.visitMethodInvocation(LocksNotUnlockedCheck.java:144)
	at org.sonar.java.model.expression.MethodInvocationTreeImpl.accept(MethodInvocationTreeImpl.java:101)
	at org.sonar.java.se.checks.LocksNotUnlockedCheck.checkPostStatement(LocksNotUnlockedCheck.java:175)
	at org.sonar.java.se.CheckerDispatcher.executePost(CheckerDispatcher.java:110)
	at org.sonar.java.se.CheckerDispatcher.addTransition(CheckerDispatcher.java:101)
	at org.sonar.java.se.CheckerDispatcher.executePost(CheckerDispatcher.java:122)
	at org.sonar.java.se.CheckerDispatcher.addTransition(CheckerDispatcher.java:101)
	at org.sonar.java.se.CheckerDispatcher.executePost(CheckerDispatcher.java:122)
	at org.sonar.java.se.CheckerDispatcher.addTransition(CheckerDispatcher.java:101)
	at org.sonar.java.se.checks.NullDereferenceCheck.checkPostStatement(NullDereferenceCheck.java:399)
	at org.sonar.java.se.CheckerDispatcher.executePost(CheckerDispatcher.java:110)
	at org.sonar.java.se.CheckerDispatcher.addTransition(CheckerDispatcher.java:101)
	at org.sonar.java.se.CheckerDispatcher.executeCheckPostStatement(CheckerDispatcher.java:73)
	at org.sonar.java.se.ExplodedGraphWalker.executeMethodInvocation(ExplodedGraphWalker.java:847)
	at org.sonar.java.se.ExplodedGraphWalker.visit(ExplodedGraphWalker.java:668)
	at org.sonar.java.se.ExplodedGraphWalker.execute(ExplodedGraphWalker.java:259)
	at org.sonar.java.se.ExplodedGraphWalker.visitMethod(ExplodedGraphWalker.java:216)
	at org.sonar.java.se.SymbolicExecutionVisitor.execute(SymbolicExecutionVisitor.java:68)
	at org.sonar.java.se.xproc.BehaviorCache.get(BehaviorCache.java:92)
	at org.sonar.java.se.xproc.BehaviorCache.get(BehaviorCache.java:75)
	at org.sonar.java.se.ExplodedGraphWalker.executeMethodInvocation(ExplodedGraphWalker.java:811)
	at org.sonar.java.se.ExplodedGraphWalker.visit(ExplodedGraphWalker.java:668)
	at org.sonar.java.se.ExplodedGraphWalker.execute(ExplodedGraphWalker.java:259)
	at org.sonar.java.se.ExplodedGraphWalker.visitMethod(ExplodedGraphWalker.java:216)
	at org.sonar.java.se.ExplodedGraphWalker.visitMethod(ExplodedGraphWalker.java:208)
	at org.sonar.java.se.SymbolicExecutionVisitor.execute(SymbolicExecutionVisitor.java:71)
	at org.sonar.java.se.SymbolicExecutionVisitor.visitMethod(SymbolicExecutionVisitor.java:57)
	at org.sonar.java.model.declaration.MethodTreeImpl.accept(MethodTreeImpl.java:228)
	at org.sonar.plugins.java.api.tree.BaseTreeVisitor.scan(BaseTreeVisitor.java:37)
	at org.sonar.plugins.java.api.tree.BaseTreeVisitor.scan(BaseTreeVisitor.java:31)
	at org.sonar.plugins.java.api.tree.BaseTreeVisitor.visitClass(BaseTreeVisitor.java:67)
	at org.sonar.java.model.declaration.ClassTreeImpl.accept(ClassTreeImpl.java:238)
	at org.sonar.plugins.java.api.tree.BaseTreeVisitor.scan(BaseTreeVisitor.java:37)
	at org.sonar.plugins.java.api.tree.BaseTreeVisitor.scan(BaseTreeVisitor.java:31)
	at org.sonar.plugins.java.api.tree.BaseTreeVisitor.visitCompilationUnit(BaseTreeVisitor.java:49)
	at org.sonar.java.model.JavaTree$CompilationUnitTreeImpl.accept(JavaTree.java:203)
	at org.sonar.plugins.java.api.tree.BaseTreeVisitor.scan(BaseTreeVisitor.java:37)
	at org.sonar.java.se.SymbolicExecutionVisitor.scanFile(SymbolicExecutionVisitor.java:52)
	at org.sonar.java.model.VisitorsBridge.lambda$runScanner$0(VisitorsBridge.java:286)
	at org.sonar.java.model.VisitorsBridge.runScanner(VisitorsBridge.java:291)
	at org.sonar.java.model.VisitorsBridge.runScanner(VisitorsBridge.java:286)
	at org.sonar.java.model.VisitorsBridge.visitFile(VisitorsBridge.java:269)
	at org.sonar.java.ast.JavaAstScanner.simpleScan(JavaAstScanner.java:169)
	at org.sonar.java.ast.JavaAstScanner.simpleScan(JavaAstScanner.java:157)
	at org.sonar.java.JavaFrontend.scanAsBatchCallback(JavaFrontend.java:247)
	at org.sonar.java.JavaFrontend.lambda$scanBatch$0(JavaFrontend.java:238)
	at org.sonar.java.model.JParserConfig$Batch$1.acceptAST(JParserConfig.java:187)
	at org.eclipse.jdt.core.dom.CompilationUnitResolver.resolve(CompilationUnitResolver.java:1134)
	at org.eclipse.jdt.core.dom.CompilationUnitResolver.resolve(CompilationUnitResolver.java:778)
	at org.eclipse.jdt.core.dom.CompilationUnitResolver$ECJCompilationUnitResolver.resolve(CompilationUnitResolver.java:84)
	at org.eclipse.jdt.core.dom.ASTParser.createASTs(ASTParser.java:1109)
	at org.sonar.java.model.JParserConfig$Batch.parse(JParserConfig.java:173)
	at org.sonar.java.JavaFrontend.scanBatch(JavaFrontend.java:238)
	at org.sonar.java.JavaFrontend.scanInBatches(JavaFrontend.java:228)
	at org.sonar.java.JavaFrontend.scanAsBatch(JavaFrontend.java:195)
	at org.sonar.java.JavaFrontend.scan(JavaFrontend.java:160)
	at org.sonar.plugins.java.JavaSensor.execute(JavaSensor.java:124)
	at org.sonar.scanner.sensor.AbstractSensorWrapper.analyse(AbstractSensorWrapper.java:69)
	at org.sonar.scanner.sensor.ModuleSensorsExecutor.execute(ModuleSensorsExecutor.java:88)
	at org.sonar.scanner.sensor.ModuleSensorsExecutor.lambda$execute$1(ModuleSensorsExecutor.java:61)
	at org.sonar.scanner.sensor.ModuleSensorsExecutor.withModuleStrategy(ModuleSensorsExecutor.java:79)
	at org.sonar.scanner.sensor.ModuleSensorsExecutor.execute(ModuleSensorsExecutor.java:61)
	at org.sonar.scanner.scan.SpringModuleScanContainer.doAfterStart(SpringModuleScanContainer.java:80)
	at org.sonar.core.platform.SpringComponentContainer.startComponents(SpringComponentContainer.java:227)
	at org.sonar.core.platform.SpringComponentContainer.execute(SpringComponentContainer.java:206)
	at org.sonar.scanner.scan.SpringProjectScanContainer.scan(SpringProjectScanContainer.java:212)
	at org.sonar.scanner.scan.SpringProjectScanContainer.scanRecursively(SpringProjectScanContainer.java:208)
	at org.sonar.scanner.scan.SpringProjectScanContainer.doAfterStart(SpringProjectScanContainer.java:178)
	at org.sonar.core.platform.SpringComponentContainer.startComponents(SpringComponentContainer.java:227)
	at org.sonar.core.platform.SpringComponentContainer.execute(SpringComponentContainer.java:206)
	at org.sonar.scanner.bootstrap.SpringScannerContainer.doAfterStart(SpringScannerContainer.java:344)
	at org.sonar.core.platform.SpringComponentContainer.startComponents(SpringComponentContainer.java:227)
	at org.sonar.core.platform.SpringComponentContainer.execute(SpringComponentContainer.java:206)
	at org.sonar.scanner.bootstrap.SpringGlobalContainer.doAfterStart(SpringGlobalContainer.java:143)
	at org.sonar.core.platform.SpringComponentContainer.startComponents(SpringComponentContainer.java:227)
	at org.sonar.core.platform.SpringComponentContainer.execute(SpringComponentContainer.java:206)
	at org.sonar.scanner.bootstrap.ScannerMain.runScannerEngine(ScannerMain.java:157)
	at org.sonar.scanner.bootstrap.ScannerMain.run(ScannerMain.java:72)
	at org.sonar.scanner.bootstrap.ScannerMain.main(ScannerMain.java:56)

Expected result

I would expect either:
a normal issue report from the relevant rule, or no issue but not an analyzer crash.

Hi,

Welcome to the community and thanks for this report!

Can you give your context for this? I.e. are you on SonarQube Cloud? SonarQube for IDE (flavor and version)? SonarQube self-managed (flavor and version)?

 
Thx,
Ann

I mean above is the version
SonarQube self-managed Community Build v26.2.0.119303
and deploy using docker, this is the image id, release about 4 weeks ago:

6d79dc68eeb4

Hi,

Thanks for confirmation that you’re on a recent(ish - 26.3 is out :smiley: ) version. I’ve flagged this for the language experts.

 
Ann

Hi,

Thanks for the report and the detailed code sample.

We’ve been able to reproduce the crash on our end. The NullPointerException triggered by the symbolic execution of your distributed lock fallback is clearly a bug, and we’ll be tackling it to ensure the analyzer handles this pattern gracefully.

You can follow our progress on this issue via our public Jira ticket here: Jira

We appreciate your contribution to making the Java SE analyzer more robust!

Best regards,

Romain

2 Likes