VYPR
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.

PackageAffected versionsPatched versions
org.jboss.narayana.rts:lra-coordinator-jarMaven
< 7.1.0.Final7.1.0.Final

Patches

1
eb778412de23

JBTM-3911 Replace synchronized in favor of Reentrant Lock

https://github.com/jbosstm/narayanaMarco Sappé GriotSep 11, 2024via ghsa
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

News mentions

0

No linked articles in our index yet.