Medium severity5.9NVD Advisory· Published Jan 2, 2025· Updated Apr 15, 2026
CVE-2024-8447
CVE-2024-8447
Description
A security issue was discovered in the LRA Coordinator component of Narayana. When Cancel is called in LRA, an execution time of approximately 2 seconds occurs. If Join is called with the same LRA ID within that timeframe, the application may crash or hang indefinitely, leading to a denial of service.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.jboss.narayana.rts:lra-coordinator-jarMaven | < 7.1.0.Final | 7.1.0.Final |
Patches
1eb778412de23JBTM-3911 Replace synchronized in favor of Reentrant Lock
7 files changed · +517 −303
rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/domain/model/LongRunningAction.java+23 −14 modified@@ -726,23 +726,32 @@ protected LRAStatus toLRAStatus(int atomicActionStatus) { public LRAParticipantRecord enlistParticipant(URI coordinatorUrl, String participantUrl, String recoveryUrlBase, long timeLimit, String compensatorData) throws UnsupportedEncodingException { - LRAParticipantRecord participant = findLRAParticipant(participantUrl, false); - - if (participant != null) { - participant.setCompensatorData(compensatorData); - return participant; // must have already been enlisted + ReentrantLock lock = tryLockTransaction(); + if (lock == null) { + LRALogger.i18nLogger.warn_enlistment(); + return null; } - - participant = doEnlistParticipant(coordinatorUrl, participantUrl, recoveryUrlBase, - timeLimit, compensatorData); - - if (participant != null) { - // need to remember that there is a new participant - deactivate(); // if it fails the superclass will have logged a warning - savedIntentionList = true; // need this clean up if the LRA times out + else { + try { + LRAParticipantRecord participant = findLRAParticipant(participantUrl, false); + if (participant != null) { + participant.setCompensatorData(compensatorData); + return participant; // must have already been enlisted + } + participant = doEnlistParticipant(coordinatorUrl, participantUrl, recoveryUrlBase, timeLimit, + compensatorData); + if (participant != null) { + // need to remember that there is a new participant + deactivate(); // if it fails the superclass will have logged a warning + savedIntentionList = true; // need this clean up if the LRA times out + } + return participant; + } + finally { + lock.unlock(); + } } - return participant; } private LRAParticipantRecord doEnlistParticipant(URI coordinatorUrl, String participantUrl, String recoveryUrlBase,
rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/domain/service/LRAService.java+1 −1 modified@@ -328,7 +328,7 @@ public int leave(URI lraId, String compensatorUrl) { } } - public synchronized int joinLRA(StringBuilder recoveryUrl, URI lra, long timeLimit, + public int joinLRA(StringBuilder recoveryUrl, URI lra, long timeLimit, String compensatorUrl, String linkHeader, String recoveryUrlBase, StringBuilder compensatorData) { if (lra == null) {
rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/domain/model/LRATestBase.java+286 −0 added@@ -0,0 +1,286 @@ +/* + Copyright The Narayana Authors + SPDX-License-Identifier: Apache-2.0 + */ + +package io.narayana.lra.coordinator.domain.model; + +import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_CONTEXT_HEADER; +import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_PARENT_CONTEXT_HEADER; +import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_RECOVERY_HEADER; + +import java.io.File; +import java.net.URI; +import java.time.temporal.ChronoUnit; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; + +import org.eclipse.microprofile.lra.annotation.AfterLRA; +import org.eclipse.microprofile.lra.annotation.Compensate; +import org.eclipse.microprofile.lra.annotation.Complete; +import org.eclipse.microprofile.lra.annotation.Forget; +import org.eclipse.microprofile.lra.annotation.LRAStatus; +import org.eclipse.microprofile.lra.annotation.ParticipantStatus; +import org.eclipse.microprofile.lra.annotation.ws.rs.LRA; +import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer; +import org.jboss.resteasy.test.TestPortProvider; +import org.junit.rules.TestName; + +import com.arjuna.ats.arjuna.common.arjPropertyManager; + +import io.narayana.lra.logging.LRALogger; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +public class LRATestBase { + + protected static UndertowJaxrsServer server; + static final AtomicInteger compensateCount = new AtomicInteger(0); + static final AtomicInteger completeCount = new AtomicInteger(0); + static final AtomicInteger forgetCount = new AtomicInteger(0); + static final long LRA_SHORT_TIMELIMIT = 10L; + private static LRAStatus status = LRAStatus.Active; + private static final AtomicInteger acceptCount = new AtomicInteger(0); + + @Path("/test") + public static class Participant { + private Response getResult(boolean cancel, URI lraId) { + Response.Status status = cancel ? Response.Status.INTERNAL_SERVER_ERROR : Response.Status.OK; + + return Response.status(status).entity(lraId.toASCIIString()).build(); + } + + @GET + @Path("start-end") + @LRA(value = LRA.Type.REQUIRED) + public Response doInLRA(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI contextId, + @DefaultValue("0") @QueryParam("accept") Integer acceptCount, + @DefaultValue("false") @QueryParam("cancel") Boolean cancel) { + LRATestBase.acceptCount.set(acceptCount); + + return getResult(cancel, contextId); + } + + @GET + @Path("start") + @LRA(value = LRA.Type.REQUIRED, end = false) + public Response startInLRA(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI contextId, + @HeaderParam(LRA_HTTP_PARENT_CONTEXT_HEADER) URI parentLRA, + @DefaultValue("0") @QueryParam("accept") Integer acceptCount, + @DefaultValue("false") @QueryParam("cancel") Boolean cancel) { + LRATestBase.acceptCount.set(acceptCount); + + return getResult(cancel, contextId); + } + + @PUT + @Path("end") + @LRA(value = LRA.Type.MANDATORY, + cancelOnFamily = Response.Status.Family.SERVER_ERROR) + public Response endLRA(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI contextId, + @HeaderParam(LRA_HTTP_PARENT_CONTEXT_HEADER) URI parentLRA, + @DefaultValue("0") @QueryParam("accept") Integer acceptCount, + @DefaultValue("false") @QueryParam("cancel") Boolean cancel) { + LRATestBase.acceptCount.set(acceptCount); + + return getResult(cancel, contextId); + } + + @GET + @Path("time-limit") + @Produces(MediaType.APPLICATION_JSON) + @LRA(value = LRA.Type.REQUIRED, timeLimit = 500, timeUnit = ChronoUnit.MILLIS) + public Response timeLimit(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId) { + try { + // sleep for longer than specified in the attribute 'timeLimit' + // (go large, ie 2 seconds, to avoid time issues on slower systems) + Thread.sleep(2000); + } catch (InterruptedException e) { + LRALogger.logger.debugf("Interrupted because time limit elapsed", e); + } + return Response.status(Response.Status.OK).entity(lraId.toASCIIString()).build(); + } + + @GET + @Path("timed-action") + @LRA(value = LRA.Type.REQUIRED, end = false, timeLimit = LRA_SHORT_TIMELIMIT) // the default unit is SECONDS + public Response actionWithLRA(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI contextId, + @DefaultValue("false") @QueryParam("cancel") Boolean cancel) { + status = LRAStatus.Active; + + server.stop(); //simulate a server crash + + return getResult(cancel, contextId); + } + + @LRA(value = LRA.Type.NESTED, end = false) + @PUT + @Path("nested") + public Response nestedLRA(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI contextId, + @HeaderParam(LRA_HTTP_PARENT_CONTEXT_HEADER) URI parentLRA, + @DefaultValue("false") @QueryParam("cancel") Boolean cancel) { + return getResult(cancel, contextId); + } + + @LRA(value = LRA.Type.NESTED) + @PUT + @Path("nested-with-close") + public Response nestedLRAWithClose(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI contextId, + @HeaderParam(LRA_HTTP_PARENT_CONTEXT_HEADER) URI parentId, + @DefaultValue("false") @QueryParam("cancel") Boolean cancel) { + return getResult(cancel, contextId); + } + + @PUT + @Path("multiLevelNestedActivity") + @LRA(value = LRA.Type.MANDATORY, end = false) + public Response multiLevelNestedActivity( + @HeaderParam(LRA_HTTP_RECOVERY_HEADER) URI recoveryId, + @HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI nestedLRAId, + @QueryParam("nestedCnt") @DefaultValue("1") Integer nestedCnt) { + // invoke resources that enlist nested LRAs + String[] lras = new String[nestedCnt + 1]; + lras[0] = nestedLRAId.toASCIIString(); + IntStream.range(1, lras.length).forEach(i -> lras[i] = restPutInvocation(nestedLRAId,"nestedActivity", "")); + + return Response.ok(String.join(",", lras)).build(); + } + + @PUT + @Path("nestedActivity") + @LRA(value = LRA.Type.NESTED, end = true) + public Response nestedActivity(@HeaderParam(LRA_HTTP_RECOVERY_HEADER) URI recoveryId, + @HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI nestedLRAId) { + return Response.ok(nestedLRAId.toASCIIString()).build(); + } + + @GET + @Path("status") + public Response getStatus() { + return Response.ok(status.name()).build(); + } + + @PUT + @Path("/complete") + @Complete + public Response complete(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI contextLRA, + @HeaderParam(LRA_HTTP_PARENT_CONTEXT_HEADER) URI parentLRA) { + if (acceptCount.getAndDecrement() <= 0) { + completeCount.incrementAndGet(); + acceptCount.set(0); + return Response.status(Response.Status.OK).entity(ParticipantStatus.Completed).build(); + } + + return Response.status(Response.Status.ACCEPTED).entity(ParticipantStatus.Completing).build(); + } + + @PUT + @Path("/compensate") + @Compensate + public Response compensate(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI contextLRA, + @HeaderParam(LRA_HTTP_PARENT_CONTEXT_HEADER) URI parentLRA) { + if (acceptCount.getAndDecrement() <= 0) { + compensateCount.incrementAndGet(); + acceptCount.set(0); + return Response.status(Response.Status.OK).entity(ParticipantStatus.Compensated).build(); + } + + return Response.status(Response.Status.ACCEPTED).entity(ParticipantStatus.Compensating).build(); + } + + @PUT + @Path("after") + @AfterLRA + public Response lraEndStatus(LRAStatus endStatus) { + status = endStatus; + + return Response.ok().build(); + } + + @DELETE + @Path("/forget") + @Produces(MediaType.APPLICATION_JSON) + @Forget + public Response forgetWork(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId, + @HeaderParam(LRA_HTTP_RECOVERY_HEADER) URI recoveryId) { + forgetCount.incrementAndGet(); + + return Response.ok().build(); + } + + @GET + @Path("forget-count") + public int getForgetCount() { + return forgetCount.get(); + } + + @PUT + @Path("reset-accepted") + public Response reset() { + LRATestBase.acceptCount.set(0); + + return Response.ok("").build(); + } + + private String restPutInvocation(URI lraURI, String path, String bodyText) { + String id = ""; + Client client = ClientBuilder.newClient(); + try { + try (Response response = client + .target(TestPortProvider.generateURL("/base/test")) + .path(path) + .request() + .header(LRA_HTTP_CONTEXT_HEADER, lraURI) + .put(Entity.text(bodyText))) { + if (response.hasEntity()) { // read the entity (to force close on the response) + id = response.readEntity(String.class); + } + if (response.getStatus() != Response.Status.OK.getStatusCode()) { + throw new WebApplicationException(id + ": error on REST PUT for LRA '" + lraURI + + "' at path '" + path + "' and body '" + bodyText + "'", response); + } + } + + return id; + } finally { + client.close(); + } + } + } + + protected void clearObjectStore(TestName testName) { + final String objectStorePath = arjPropertyManager.getObjectStoreEnvironmentBean().getObjectStoreDir(); + final File objectStoreDirectory = new File(objectStorePath); + + clearDirectory(objectStoreDirectory, testName); + } + + protected void clearDirectory(final File directory, TestName testName) { + final File[] files = directory.listFiles(); + + if (files != null) { + for (final File file : Objects.requireNonNull(directory.listFiles())) { + if (file.isDirectory()) { + clearDirectory(file, testName); + } + + if (!file.delete()) { + LRALogger.logger.infof("%s: unable to delete file %s", testName, file.getName()); + } + } + } + } +} \ No newline at end of file
rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/domain/model/LRATest.java+32 −287 modified@@ -5,22 +5,28 @@ package io.narayana.lra.coordinator.domain.model; -import com.arjuna.ats.arjuna.common.arjPropertyManager; -import io.narayana.lra.LRAData; -import io.narayana.lra.client.NarayanaLRAClient; -import io.narayana.lra.coordinator.api.Coordinator; -import io.narayana.lra.coordinator.domain.service.LRAService; -import io.narayana.lra.coordinator.internal.LRARecoveryModule; -import io.narayana.lra.filter.ServerLRAFilter; -import io.narayana.lra.logging.LRALogger; -import io.narayana.lra.provider.ParticipantStatusOctetStreamProvider; -import org.eclipse.microprofile.lra.annotation.AfterLRA; -import org.eclipse.microprofile.lra.annotation.Compensate; -import org.eclipse.microprofile.lra.annotation.Complete; -import org.eclipse.microprofile.lra.annotation.Forget; +import static io.narayana.lra.LRAConstants.COORDINATOR_PATH_NAME; +import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_CONTEXT_HEADER; +import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_RECOVERY_HEADER; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.net.URI; +import java.net.URISyntaxException; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; + import org.eclipse.microprofile.lra.annotation.LRAStatus; -import org.eclipse.microprofile.lra.annotation.ParticipantStatus; -import org.eclipse.microprofile.lra.annotation.ws.rs.LRA; import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer; import org.jboss.resteasy.test.TestPortProvider; import org.junit.After; @@ -31,16 +37,16 @@ import org.junit.Test; import org.junit.rules.TestName; +import io.narayana.lra.LRAData; +import io.narayana.lra.client.NarayanaLRAClient; +import io.narayana.lra.coordinator.api.Coordinator; +import io.narayana.lra.coordinator.domain.service.LRAService; +import io.narayana.lra.coordinator.internal.LRARecoveryModule; +import io.narayana.lra.filter.ServerLRAFilter; +import io.narayana.lra.logging.LRALogger; +import io.narayana.lra.provider.ParticipantStatusOctetStreamProvider; import jakarta.ws.rs.ApplicationPath; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.DefaultValue; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.NotFoundException; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; @@ -50,257 +56,18 @@ import jakarta.ws.rs.core.Link; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import java.io.File; -import java.net.URI; -import java.net.URISyntaxException; -import java.time.temporal.ChronoUnit; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.StringTokenizer; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.IntStream; -import static io.narayana.lra.LRAConstants.COORDINATOR_PATH_NAME; -import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_CONTEXT_HEADER; -import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_PARENT_CONTEXT_HEADER; -import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_RECOVERY_HEADER; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +public class LRATest extends LRATestBase { -public class LRATest { - private static UndertowJaxrsServer server; private static LRAService service; - static final AtomicInteger compensateCount = new AtomicInteger(0); - static final AtomicInteger completeCount = new AtomicInteger(0); - static final AtomicInteger forgetCount = new AtomicInteger(0); - - static final long LRA_SHORT_TIMELIMIT = 10L; - - private static LRAStatus status = LRAStatus.Active; - private static final AtomicInteger acceptCount = new AtomicInteger(0); - private NarayanaLRAClient lraClient; private Client client; private String coordinatorPath; @Rule public TestName testName = new TestName(); - @Path("/test") - public static class Participant { - private Response getResult(boolean cancel, URI lraId) { - Response.Status status = cancel ? Response.Status.INTERNAL_SERVER_ERROR : Response.Status.OK; - - return Response.status(status).entity(lraId.toASCIIString()).build(); - } - - @GET - @Path("start-end") - @LRA(value = LRA.Type.REQUIRED) - public Response doInLRA(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI contextId, - @DefaultValue("0") @QueryParam("accept") Integer acceptCount, - @DefaultValue("false") @QueryParam("cancel") Boolean cancel) { - LRATest.acceptCount.set(acceptCount); - - return getResult(cancel, contextId); - } - - @GET - @Path("start") - @LRA(value = LRA.Type.REQUIRED, end = false) - public Response startInLRA(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI contextId, - @HeaderParam(LRA_HTTP_PARENT_CONTEXT_HEADER) URI parentLRA, - @DefaultValue("0") @QueryParam("accept") Integer acceptCount, - @DefaultValue("false") @QueryParam("cancel") Boolean cancel) { - LRATest.acceptCount.set(acceptCount); - - return getResult(cancel, contextId); - } - - @PUT - @Path("end") - @LRA(value = LRA.Type.MANDATORY, - cancelOnFamily = Response.Status.Family.SERVER_ERROR) - public Response endLRA(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI contextId, - @HeaderParam(LRA_HTTP_PARENT_CONTEXT_HEADER) URI parentLRA, - @DefaultValue("0") @QueryParam("accept") Integer acceptCount, - @DefaultValue("false") @QueryParam("cancel") Boolean cancel) { - LRATest.acceptCount.set(acceptCount); - - return getResult(cancel, contextId); - } - - @GET - @Path("time-limit") - @Produces(MediaType.APPLICATION_JSON) - @LRA(value = LRA.Type.REQUIRED, timeLimit = 500, timeUnit = ChronoUnit.MILLIS) - public Response timeLimit(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId) { - try { - // sleep for longer than specified in the attribute 'timeLimit' - // (go large, ie 2 seconds, to avoid time issues on slower systems) - Thread.sleep(2000); - } catch (InterruptedException e) { - LRALogger.logger.debugf("Interrupted because time limit elapsed", e); - } - return Response.status(Response.Status.OK).entity(lraId.toASCIIString()).build(); - } - - @GET - @Path("timed-action") - @LRA(value = LRA.Type.REQUIRED, end = false, timeLimit = LRA_SHORT_TIMELIMIT) // the default unit is SECONDS - public Response actionWithLRA(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI contextId, - @DefaultValue("false") @QueryParam("cancel") Boolean cancel) { - status = LRAStatus.Active; - - server.stop(); //simulate a server crash - - return getResult(cancel, contextId); - } - - @LRA(value = LRA.Type.NESTED, end = false) - @PUT - @Path("nested") - public Response nestedLRA(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI contextId, - @HeaderParam(LRA_HTTP_PARENT_CONTEXT_HEADER) URI parentLRA, - @DefaultValue("false") @QueryParam("cancel") Boolean cancel) { - return getResult(cancel, contextId); - } - - @LRA(value = LRA.Type.NESTED) - @PUT - @Path("nested-with-close") - public Response nestedLRAWithClose(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI contextId, - @HeaderParam(LRA_HTTP_PARENT_CONTEXT_HEADER) URI parentId, - @DefaultValue("false") @QueryParam("cancel") Boolean cancel) { - return getResult(cancel, contextId); - } - - @PUT - @Path("multiLevelNestedActivity") - @LRA(value = LRA.Type.MANDATORY, end = false) - public Response multiLevelNestedActivity( - @HeaderParam(LRA_HTTP_RECOVERY_HEADER) URI recoveryId, - @HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI nestedLRAId, - @QueryParam("nestedCnt") @DefaultValue("1") Integer nestedCnt) { - // invoke resources that enlist nested LRAs - String[] lras = new String[nestedCnt + 1]; - lras[0] = nestedLRAId.toASCIIString(); - IntStream.range(1, lras.length).forEach(i -> lras[i] = restPutInvocation(nestedLRAId,"nestedActivity", "")); - - return Response.ok(String.join(",", lras)).build(); - } - - @PUT - @Path("nestedActivity") - @LRA(value = LRA.Type.NESTED, end = true) - public Response nestedActivity(@HeaderParam(LRA_HTTP_RECOVERY_HEADER) URI recoveryId, - @HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI nestedLRAId) { - return Response.ok(nestedLRAId.toASCIIString()).build(); - } - - @GET - @Path("status") - public Response getStatus() { - return Response.ok(status.name()).build(); - } - - @PUT - @Path("/complete") - @Complete - public Response complete(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI contextLRA, - @HeaderParam(LRA_HTTP_PARENT_CONTEXT_HEADER) URI parentLRA) { - if (acceptCount.getAndDecrement() <= 0) { - completeCount.incrementAndGet(); - acceptCount.set(0); - return Response.status(Response.Status.OK).entity(ParticipantStatus.Completed).build(); - } - - return Response.status(Response.Status.ACCEPTED).entity(ParticipantStatus.Completing).build(); - } - - @PUT - @Path("/compensate") - @Compensate - public Response compensate(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI contextLRA, - @HeaderParam(LRA_HTTP_PARENT_CONTEXT_HEADER) URI parentLRA) { - if (acceptCount.getAndDecrement() <= 0) { - compensateCount.incrementAndGet(); - acceptCount.set(0); - return Response.status(Response.Status.OK).entity(ParticipantStatus.Compensated).build(); - } - - return Response.status(Response.Status.ACCEPTED).entity(ParticipantStatus.Compensating).build(); - } - - @PUT - @Path("after") - @AfterLRA - public Response lraEndStatus(LRAStatus endStatus) { - status = endStatus; - - return Response.ok().build(); - } - - @DELETE - @Path("/forget") - @Produces(MediaType.APPLICATION_JSON) - @Forget - public Response forgetWork(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI lraId, - @HeaderParam(LRA_HTTP_RECOVERY_HEADER) URI recoveryId) { - forgetCount.incrementAndGet(); - - return Response.ok().build(); - } - - @GET - @Path("forget-count") - public int getForgetCount() { - return forgetCount.get(); - } - - @PUT - @Path("reset-accepted") - public Response reset() { - LRATest.acceptCount.set(0); - - return Response.ok("").build(); - } - - private String restPutInvocation(URI lraURI, String path, String bodyText) { - String id = ""; - Client client = ClientBuilder.newClient(); - try { - try (Response response = client - .target(TestPortProvider.generateURL("/base/test")) - .path(path) - .request() - .header(LRA_HTTP_CONTEXT_HEADER, lraURI) - .put(Entity.text(bodyText))) { - if (response.hasEntity()) { // read the entity (to force close on the response) - id = response.readEntity(String.class); - } - if (response.getStatus() != Response.Status.OK.getStatusCode()) { - throw new WebApplicationException(id + ": error on REST PUT for LRA '" + lraURI - + "' at path '" + path + "' and body '" + bodyText + "'", response); - } - } - - return id; - } finally { - client.close(); - } - } - } - @ApplicationPath("base") public static class LRAParticipant extends Application { @Override @@ -333,7 +100,7 @@ public void before() { LRALogger.logger.debugf("Starting test %s", testName); server = new UndertowJaxrsServer().start(); - clearObjectStore(); + clearObjectStore(testName); lraClient = new NarayanaLRAClient(); compensateCount.set(0); @@ -353,7 +120,7 @@ public void after() { LRALogger.logger.debugf("Finished test %s", testName); lraClient.close(); client.close(); - clearObjectStore(); + clearObjectStore(testName); server.stop(); } @@ -904,26 +671,4 @@ private static String makeLink(String uriPrefix, String key) { .build().toString(); } - private void clearObjectStore() { - final String objectStorePath = arjPropertyManager.getObjectStoreEnvironmentBean().getObjectStoreDir(); - final File objectStoreDirectory = new File(objectStorePath); - - clearDirectory(objectStoreDirectory); - } - - private void clearDirectory(final File directory) { - final File[] files = directory.listFiles(); - - if (files != null) { - for (final File file : Objects.requireNonNull(directory.listFiles())) { - if (file.isDirectory()) { - clearDirectory(file); - } - - if (!file.delete()) { - LRALogger.logger.infof("%s: unable to delete file %s", testName, file.getName()); - } - } - } - } } \ No newline at end of file
rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/domain/model/LRAWithParticipantsTest.java+170 −0 added@@ -0,0 +1,170 @@ +/* + Copyright The Narayana Authors + SPDX-License-Identifier: Apache-2.0 + */ +package io.narayana.lra.coordinator.domain.model; + +import static io.narayana.lra.LRAConstants.COORDINATOR_PATH_NAME; +import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_CONTEXT_HEADER; +import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_PARENT_CONTEXT_HEADER; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; + +import java.net.URI; +import java.time.temporal.ChronoUnit; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +import org.eclipse.microprofile.lra.annotation.Compensate; +import org.eclipse.microprofile.lra.annotation.ParticipantStatus; +import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer; +import org.jboss.resteasy.test.TestPortProvider; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; + +import io.narayana.lra.client.NarayanaLRAClient; +import io.narayana.lra.coordinator.api.Coordinator; +import io.narayana.lra.filter.ServerLRAFilter; +import io.narayana.lra.logging.LRALogger; +import io.narayana.lra.provider.ParticipantStatusOctetStreamProvider; +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Response; + +public class LRAWithParticipantsTest extends LRATestBase { + + @Rule + public TestName testName = new TestName(); + private UndertowJaxrsServer server; + private NarayanaLRAClient lraClient; + private static ReentrantLock lock = new ReentrantLock(); + private static boolean joinAttempted; + private static boolean compensateCalled; + @Path("/test") + public static class ParticipantExtended extends Participant { + + @PUT + @Path("/compensate") + @Compensate + public Response compensate(@HeaderParam(LRA_HTTP_CONTEXT_HEADER) URI contextLRA, + @HeaderParam(LRA_HTTP_PARENT_CONTEXT_HEADER) URI parentLRA) { + synchronized (lock) { + compensateCalled = true; + lock.notify(); + } + synchronized (lock) { + while (!joinAttempted) { + try { + lock.wait(); + } + catch (InterruptedException e) { + fail("Could not wait"); + } + } + } + return Response.status(Response.Status.ACCEPTED).entity(ParticipantStatus.Compensating).build(); + } + } + @ApplicationPath("service2") + public static class Service2 extends Application { + + @Override + public Set<Class<?>> getClasses() { + HashSet<Class<?>> classes = new HashSet<>(); + classes.add(ParticipantExtended.class); + classes.add(ServerLRAFilter.class); + classes.add(ParticipantStatusOctetStreamProvider.class); + return classes; + } + } + @ApplicationPath("service3") + public static class Service3 extends Service2 { + } + @ApplicationPath("service4") + public static class Service4 extends Service2 { + } + @ApplicationPath("/") + public static class LRACoordinator extends Application { + + @Override + public Set<Class<?>> getClasses() { + HashSet<Class<?>> classes = new HashSet<>(); + classes.add(Coordinator.class); + return classes; + } + } + @BeforeClass + public static void start() { + System.setProperty("lra.coordinator.url", TestPortProvider.generateURL('/' + COORDINATOR_PATH_NAME)); + } + + @Before + public void before() { + LRALogger.logger.debugf("Starting test %s", testName); + server = new UndertowJaxrsServer().start(); + clearObjectStore(testName); + lraClient = new NarayanaLRAClient(); + server.deploy(LRACoordinator.class); + server.deployOldStyle(Service2.class); + server.deployOldStyle(Service3.class); + server.deployOldStyle(Service4.class); + } + + @After + public void after() { + LRALogger.logger.debugf("Finished test %s", testName); + lraClient.close(); + clearObjectStore(testName); + server.stop(); + } + + @Test + public void testJoinAfterTimeout() { + // lraClient calls POST /lra-coordinator/start to start a Saga. + // this simulates the service 1 from the JBTM-3908 + URI lraId = lraClient.startLRA(null, "testTimeLimit", 1000L, ChronoUnit.MILLIS); + // Service 2 calls PUT /lra-coordinator/{LraId} to join the Saga. + lraClient.joinLRA(lraId, null, URI.create("http://localhost:8081/service2/test"), null); + // Service 3 calls PUT /lra-coordinator/{LraId} to join the same Saga. + lraClient.joinLRA(lraId, null, URI.create("http://localhost:8081/service3/test"), null); + // A timeout exception occurs in Service 1, leading it to call PUT + // /lra-coordinator/{LraId}/cancel to cancel the Saga. + // The LRA Coordinator calls the compensation API /saga/compensate registered + // by Service 2 and Service 3. + try { + TimeUnit.SECONDS.sleep(1); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + synchronized (lock) { + while (!compensateCalled) { + try { + lock.wait(); + } + catch (InterruptedException e) { + fail("Could not wait"); + } + } + // Service 2 receives the /saga/compensate call and begins compensating. + // Before compensate call is finished, Service 4 calls PUT + // /lra-coordinator/{LraId} to attempt to join the Saga. + // Exception is thrown because a timed-out lra cannot be joined + assertThrows(WebApplicationException.class, () -> { + lraClient.joinLRA(lraId, null, URI.create("http://localhost:8081/service4/test"), null); + }); + joinAttempted = true; + lock.notify(); + } + } +} \ No newline at end of file
rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/tools/osb/mbean/ObjStoreBrowserLRATest.java+2 −1 modified@@ -90,7 +90,8 @@ public void lraMBean() throws Exception { @Test public void lraMBeanRemoval() throws Exception { - LongRunningAction lra = new LongRunningAction(new Uid()); + String lraUrl = "http://localhost:8080/lra"; + LongRunningAction lra = LRARecoveryModule.getService().startLRA(lraUrl, null, "client", Long.MAX_VALUE); OSEntryBean lraOSEntryBean = null; try { lra.begin(Long.MAX_VALUE); // Creating the LRA records in the log store.
rts/lra/service-base/src/main/java/io/narayana/lra/logging/LraI18nLogger.java+3 −0 modified@@ -165,6 +165,9 @@ String info_failedToEnlistingLRANotFound(URL lraId, URI coordinatorUri, int coor @Message(id = 25039, value = "Invalid argument passed to method: %s") String error_invalidArgument(String reason); + @LogMessage(level = WARN) + @Message(id = 25040, value = "Lock not acquired, enlistment failed: cannot enlist participant, cannot lock transaction") + void warn_enlistment(); /* Allocate new messages directly above this notice. - id: use the next id number in numeric sequence. Don't reuse ids.
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- github.com/advisories/GHSA-qq9f-q439-2574ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-8447ghsaADVISORY
- access.redhat.com/errata/RHSA-2025:3357nvdWEB
- access.redhat.com/errata/RHSA-2025:3358nvdWEB
- access.redhat.com/errata/RHSA-2025:7620nvdWEB
- access.redhat.com/security/cve/CVE-2024-8447nvdWEB
- bugzilla.redhat.com/show_bug.cginvdWEB
- github.com/jbosstm/narayana/commit/eb778412de230afc4687a2df43641280494156c5ghsaWEB
- github.com/jbosstm/narayana/pull/2293nvdWEB
News mentions
0No linked articles in our index yet.