From e9f30d47c6d9277baace83be10eaca724520cac3 Mon Sep 17 00:00:00 2001 From: elharo Date: Thu, 30 Jul 2026 12:49:05 +0000 Subject: [PATCH] Fix #12607: DefaultMaven session scope leak on constructor exception Wrap sessionScope.enter() in a try-finally block so that sessionScope.exit() is always called, even when the MavenChainedWorkspaceReader constructor throws. Previously, sessionScope.enter() was called outside the try-finally block. If the intervening constructor threw, sessionScope.exit() was never executed, leaking the session scope's thread-local state. Fixes gh-12607 --- .../java/org/apache/maven/DefaultMaven.java | 24 +++--- .../maven/DefaultMavenSessionScopeTest.java | 83 +++++++++++++++++++ 2 files changed, 96 insertions(+), 11 deletions(-) create mode 100644 impl/maven-core/src/test/java/org/apache/maven/DefaultMavenSessionScopeTest.java diff --git a/impl/maven-core/src/main/java/org/apache/maven/DefaultMaven.java b/impl/maven-core/src/main/java/org/apache/maven/DefaultMaven.java index 23e85a14b01e..ba5ea30d1db0 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/DefaultMaven.java +++ b/impl/maven-core/src/main/java/org/apache/maven/DefaultMaven.java @@ -210,20 +210,22 @@ private MavenExecutionResult doExecute(MavenExecutionRequest request) { // so that @SessionScoped components can be @Injected into AbstractLifecycleParticipants. // sessionScope.enter(); - MavenChainedWorkspaceReader chainedWorkspaceReader = - new MavenChainedWorkspaceReader(request.getWorkspaceReader(), ideWorkspaceReader); - try (CloseableSession closeableSession = newCloseableSession(request, chainedWorkspaceReader)) { - MavenSession session = new MavenSession(closeableSession, request, result); - session.setSession(defaultSessionFactory.newSession(session)); + try { + MavenChainedWorkspaceReader chainedWorkspaceReader = + new MavenChainedWorkspaceReader(request.getWorkspaceReader(), ideWorkspaceReader); + try (CloseableSession closeableSession = newCloseableSession(request, chainedWorkspaceReader)) { + MavenSession session = new MavenSession(closeableSession, request, result); + session.setSession(defaultSessionFactory.newSession(session)); - sessionScope.seed(MavenSession.class, session); - sessionScope.seed(RepositorySystemSession.class, closeableSession); // fixed in Maven 3.10.x - sessionScope.seed(Session.class, session.getSession()); - sessionScope.seed(InternalMavenSession.class, InternalMavenSession.from(session.getSession())); + sessionScope.seed(MavenSession.class, session); + sessionScope.seed(RepositorySystemSession.class, closeableSession); // fixed in Maven 3.10.x + sessionScope.seed(Session.class, session.getSession()); + sessionScope.seed(InternalMavenSession.class, InternalMavenSession.from(session.getSession())); - legacySupport.setSession(session); + legacySupport.setSession(session); - return doExecute(request, session, result, chainedWorkspaceReader); + return doExecute(request, session, result, chainedWorkspaceReader); + } } finally { sessionScope.exit(); } diff --git a/impl/maven-core/src/test/java/org/apache/maven/DefaultMavenSessionScopeTest.java b/impl/maven-core/src/test/java/org/apache/maven/DefaultMavenSessionScopeTest.java new file mode 100644 index 000000000000..d336b140e84a --- /dev/null +++ b/impl/maven-core/src/test/java/org/apache/maven/DefaultMavenSessionScopeTest.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven; + +import java.io.File; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.util.List; + +import org.apache.maven.api.services.Lookup; +import org.apache.maven.execution.BuildResumptionAnalyzer; +import org.apache.maven.execution.BuildResumptionDataRepository; +import org.apache.maven.execution.DefaultMavenExecutionRequest; +import org.apache.maven.execution.MavenExecutionRequest; +import org.apache.maven.execution.MavenExecutionResult; +import org.apache.maven.graph.GraphBuilder; +import org.apache.maven.internal.impl.DefaultSessionFactory; +import org.apache.maven.lifecycle.internal.ExecutionEventCatapult; +import org.apache.maven.plugin.LegacySupport; +import org.apache.maven.resolver.RepositorySystemSessionFactory; +import org.apache.maven.session.scope.internal.SessionScope; +import org.eclipse.aether.repository.WorkspaceReader; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class DefaultMavenSessionScopeTest { + + @Test + void testSessionScopeIsExitedOnWorkspaceReaderError() throws Exception { + WorkspaceReader badReader = mock(WorkspaceReader.class); + when(badReader.getRepository()).thenReturn(null); + + File localRepoDir = Files.createTempDirectory("local-repo").toFile(); + localRepoDir.deleteOnExit(); + MavenExecutionRequest request = new DefaultMavenExecutionRequest() + .setLocalRepositoryPath(localRepoDir) + .setWorkspaceReader(badReader); + + SessionScope sessionScope = new SessionScope(); + + DefaultMaven defaultMaven = new DefaultMaven( + mock(Lookup.class), + mock(ExecutionEventCatapult.class), + mock(LegacySupport.class), + sessionScope, + mock(RepositorySystemSessionFactory.class), + mock(GraphBuilder.class), + mock(BuildResumptionAnalyzer.class), + mock(BuildResumptionDataRepository.class), + null, + mock(DefaultSessionFactory.class), + null); + + MavenExecutionResult result = defaultMaven.execute(request); + + assertFalse(result.getExceptions().isEmpty()); + + Field valuesField = SessionScope.class.getSuperclass().getDeclaredField("values"); + valuesField.setAccessible(true); + List values = (List) valuesField.get(sessionScope); + assertTrue(values.isEmpty(), "Session scope must be exited even on workspace reader error"); + } +}