CVE-2024-1023
Description
A vulnerability in the Eclipse Vert.x toolkit results in a memory leak due to using Netty FastThreadLocal data structures. Specifically, when the Vert.x HTTP client establishes connections to different hosts, triggering the memory leak. The leak can be accelerated with intimate runtime knowledge, allowing an attacker to exploit this vulnerability. For instance, a server accepting arbitrary internet addresses could serve as an attack vector by connecting to these addresses, thereby accelerating the memory leak.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
io.vertx:vertx-coreMaven | >= 4.5.0, < 4.5.2 | 4.5.2 |
io.vertx:vertx-coreMaven | >= 4.4.5, < 4.4.7 | 4.4.7 |
Patches
2dd6f64302b56The CombinerExecutor class creates an instance of FastThreadLocal for each combiner executor leading to an increase of the InternalThreadLocalMap index. Consequently each thread local map of FastThreadLocalThread will get a new map sized accordingly, leading to an eventual memory leak.
3 files changed · +179 −22
src/main/java/io/vertx/core/net/impl/pool/CombinerExecutor.java+39 −10 modified@@ -13,6 +13,8 @@ import io.netty.util.concurrent.FastThreadLocal; import io.netty.util.internal.PlatformDependent; +import java.util.HashMap; +import java.util.Map; import java.util.Queue; import java.util.concurrent.atomic.AtomicInteger; @@ -29,11 +31,19 @@ public class CombinerExecutor<S> implements Executor<S> { private final AtomicInteger s = new AtomicInteger(); private final S state; - protected static final class InProgressTail { + protected static final class InProgressTail<S> { + + final CombinerExecutor<S> combiner; Task task; + Map<CombinerExecutor<S>, Task> others; + + public InProgressTail(CombinerExecutor<S> combiner, Task task) { + this.combiner = combiner; + this.task = task; + } } - private final FastThreadLocal<InProgressTail> current = new FastThreadLocal<>(); + private static final FastThreadLocal<InProgressTail<?>> current = new FastThreadLocal<>(); public CombinerExecutor(S state) { this.state = state; @@ -72,23 +82,42 @@ public void submit(Action<S> action) { } } while (!q.isEmpty() && s.compareAndSet(0, 1)); if (head != null) { - InProgressTail inProgress = current.get(); + InProgressTail<S> inProgress = (InProgressTail<S>) current.get(); if (inProgress == null) { - inProgress = new InProgressTail(); + inProgress = new InProgressTail<>(this, tail); current.set(inProgress); - inProgress.task = tail; try { // from now one cannot trust tail anymore head.runNextTasks(); + assert inProgress.others == null || inProgress.others.isEmpty(); } finally { current.remove(); } } else { - assert inProgress.task != null; - Task oldNextTail = inProgress.task.replaceNext(head); - assert oldNextTail == null; - inProgress.task = tail; - + if (inProgress.combiner == this) { + Task oldNextTail = inProgress.task.replaceNext(head); + assert oldNextTail == null; + inProgress.task = tail; + } else { + Map<CombinerExecutor<S>, Task> map = inProgress.others; + if (map == null) { + map = inProgress.others = new HashMap<>(1); + } + Task task = map.get(this); + if (task == null) { + map.put(this, tail); + try { + // from now one cannot trust tail anymore + head.runNextTasks(); + } finally { + map.remove(this); + } + } else { + Task oldNextTail = task.replaceNext(head); + assert oldNextTail == null; + map.put(this, tail); + } + } } } }
src/test/java/io/vertx/core/net/impl/pool/ConnectionPoolTest.java+74 −5 modified@@ -20,7 +20,6 @@ import io.vertx.core.impl.VertxInternal; import io.vertx.core.impl.WorkerContext; import io.vertx.test.core.VertxTestBase; -import org.junit.Ignore; import org.junit.Test; import java.util.*; @@ -31,10 +30,8 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.IntConsumer; import java.util.stream.Collectors; import java.util.stream.IntStream; -import java.util.stream.Stream; public class ConnectionPoolTest extends VertxTestBase { @@ -962,14 +959,14 @@ public void testPostTasksTrampoline() throws Exception { List<Integer> res = Collections.synchronizedList(new LinkedList<>()); AtomicInteger seq = new AtomicInteger(); CountDownLatch latch = new CountDownLatch(numAcquires); + int[] count = new int[1]; ConnectionPool<Connection> pool = ConnectionPool.pool(new PoolConnector<Connection>() { - int count = 0; int reentrancy = 0; @Override public void connect(EventLoopContext context, Listener listener, Handler<AsyncResult<ConnectResult<Connection>>> handler) { assertEquals(0, reentrancy++); try { - int val = count++; + int val = count[0]++; if (val == 0) { // Queue extra requests for (int i = 0;i < numAcquires;i++) { @@ -979,6 +976,7 @@ public void connect(EventLoopContext context, Listener listener, Handler<AsyncRe latch.countDown(); })); } + assertEquals(1, count[0]); } handler.handle(Future.failedFuture("failure")); } finally { @@ -994,10 +992,81 @@ public boolean isValid(Connection connection) { int num = seq.getAndIncrement(); pool.acquire(ctx, 0, onFailure(err -> res.add(num))); awaitLatch(latch); + assertEquals(1 + numAcquires, count[0]); List<Integer> expected = IntStream.range(0, numAcquires + 1).boxed().collect(Collectors.toList()); assertEquals(expected, res); } + @Test + public void testConcurrentPostTasksTrampoline() throws Exception { + AtomicReference<ConnectionPool<Connection>> ref1 = new AtomicReference<>(); + AtomicReference<ConnectionPool<Connection>> ref2 = new AtomicReference<>(); + EventLoopContext ctx = vertx.createEventLoopContext(); + List<Integer> res = Collections.synchronizedList(new LinkedList<>()); + CountDownLatch latch = new CountDownLatch(4); + ConnectionPool<Connection> pool1 = ConnectionPool.pool(new PoolConnector<Connection>() { + int count = 0; + int reentrancy = 0; + @Override + public void connect(EventLoopContext context, Listener listener, Handler<AsyncResult<ConnectResult<Connection>>> handler) { + assertEquals(0, reentrancy++); + try { + int val = count++; + if (val == 0) { + ref1.get().acquire(ctx, 0, onFailure(err -> { + res.add(1); + latch.countDown(); + })); + ref2.get().acquire(ctx, 0, onFailure(err -> { + res.add(2); + latch.countDown(); + })); + } + handler.handle(Future.failedFuture("failure")); + } finally { + reentrancy--; + } + } + @Override + public boolean isValid(Connection connection) { + return true; + } + }, new int[]{1}, 2); + ConnectionPool<Connection> pool2 = ConnectionPool.pool(new PoolConnector<Connection>() { + int count = 0; + int reentrancy = 0; + @Override + public void connect(EventLoopContext context, Listener listener, Handler<AsyncResult<ConnectResult<Connection>>> handler) { + assertEquals(0, reentrancy++); + try { + int val = count++; + if (val == 0) { + ref2.get().acquire(ctx, 0, onFailure(err -> { + res.add(3); + latch.countDown(); + })); + ref1.get().acquire(ctx, 0, onFailure(err -> { + res.add(4); + latch.countDown(); + })); + } + handler.handle(Future.failedFuture("failure")); + } finally { + reentrancy--; + } + } + @Override + public boolean isValid(Connection connection) { + return true; + } + }, new int[]{1}, 2); + ref1.set(pool1); + ref2.set(pool2); + pool1.acquire(ctx, 0, onFailure(err -> res.add(0))); + awaitLatch(latch); +// assertEquals(Arrays.asList(0, 2, 1, 3, 4), res); + } + static class Connection { public Connection() { }
src/test/java/io/vertx/core/net/impl/pool/SynchronizationTest.java+66 −7 modified@@ -10,11 +10,15 @@ */ package io.vertx.core.net.impl.pool; +import io.netty.util.concurrent.FastThreadLocal; import io.vertx.test.core.AsyncTestBase; import org.junit.Test; import java.lang.management.ManagementFactory; import java.lang.management.ThreadMXBean; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicBoolean; @@ -67,13 +71,55 @@ public void run() { } @Test - public void testFoo() throws Exception { + public void testActionReentrancy2() throws Exception { + List<Integer> log = new LinkedList<>(); + Executor<Object> combiner1 = new CombinerExecutor<>(new Object()); + Executor<Object> combiner2 = new CombinerExecutor<>(new Object()); + int[] reentrancy = new int[2]; + combiner1.submit(state1 -> taskOf(() -> { + assertEquals(0, reentrancy[0]++); + combiner1.submit(state2 -> taskOf(() -> { + assertEquals(0, reentrancy[0]++); + log.add(0); + reentrancy[0]--; + })); + combiner2.submit(state2 -> taskOf(() -> { + assertEquals(0, reentrancy[1]++); + log.add(1); + combiner1.submit(state3 -> taskOf(() -> { + assertEquals(0, reentrancy[0]++); + log.add(2); + reentrancy[0]--; + })); + combiner2.submit(state3 -> taskOf(() -> { + assertEquals(0, reentrancy[1]++); + log.add(3); + reentrancy[1]--; + })); + reentrancy[1]--; + })); + reentrancy[0]--; + })); + assertEquals(0, reentrancy[0]); + assertEquals(0, reentrancy[1]); + assertEquals(Arrays.asList(1, 3, 0, 2), log); + } + static Task taskOf(Runnable runnable) { + return new Task() { + @Override + public void run() { + runnable.run(); + } + }; + } + @Test + public void testFoo() throws Exception { int numThreads = 8; int numIter = 1_000 * 100; Executor<Object> sync = new CombinerExecutor<>(new Object()); - Executor.Action action = s -> { + Executor.Action<Object> action = s -> { burnCPU(10); return null; }; @@ -96,11 +142,6 @@ public void testFoo() throws Exception { } } - - - - - public static class Utils { public static long res = 0; // value sink public static long ONE_MILLI_IN_NANO = 1000000; @@ -170,4 +211,22 @@ public void run() { }); assertEquals(3, order.get()); } + + @Test + public void testFastThreadLocalStability() { + CombinerExecutor<Void> executor = new CombinerExecutor<>(null); + int expected = io.netty.util.internal.InternalThreadLocalMap.lastVariableIndex(); + AtomicInteger counter = new AtomicInteger(); + for (int i = 0;i < 1000;i++) { + executor = new CombinerExecutor<>(null); + executor.submit(state -> new Task() { + @Override + public void run() { + counter.incrementAndGet(); + } + }); + assertEquals(i + 1, counter.get()); + } + assertEquals(expected, io.netty.util.internal.InternalThreadLocalMap.lastVariableIndex()); + } }
665ceba38444The CombinerExecutor class creates an instance of FastThreadLocal for each combiner executor leading to an increase of the InternalThreadLocalMap index. Consequently each thread local map of FastThreadLocalThread will get a new map sized accordingly, leading to an eventual memory leak.
3 files changed · +179 −19
src/main/java/io/vertx/core/net/impl/pool/CombinerExecutor.java+39 −10 modified@@ -13,6 +13,8 @@ import io.netty.util.concurrent.FastThreadLocal; import io.netty.util.internal.PlatformDependent; +import java.util.HashMap; +import java.util.Map; import java.util.Queue; import java.util.concurrent.atomic.AtomicInteger; @@ -29,11 +31,19 @@ public class CombinerExecutor<S> implements Executor<S> { private final AtomicInteger s = new AtomicInteger(); private final S state; - protected static final class InProgressTail { + protected static final class InProgressTail<S> { + + final CombinerExecutor<S> combiner; Task task; + Map<CombinerExecutor<S>, Task> others; + + public InProgressTail(CombinerExecutor<S> combiner, Task task) { + this.combiner = combiner; + this.task = task; + } } - private final FastThreadLocal<InProgressTail> current = new FastThreadLocal<>(); + private static final FastThreadLocal<InProgressTail<?>> current = new FastThreadLocal<>(); public CombinerExecutor(S state) { this.state = state; @@ -72,23 +82,42 @@ public void submit(Action<S> action) { } } while (!q.isEmpty() && s.compareAndSet(0, 1)); if (head != null) { - InProgressTail inProgress = current.get(); + InProgressTail<S> inProgress = (InProgressTail<S>) current.get(); if (inProgress == null) { - inProgress = new InProgressTail(); + inProgress = new InProgressTail<>(this, tail); current.set(inProgress); - inProgress.task = tail; try { // from now one cannot trust tail anymore head.runNextTasks(); + assert inProgress.others == null || inProgress.others.isEmpty(); } finally { current.remove(); } } else { - assert inProgress.task != null; - Task oldNextTail = inProgress.task.replaceNext(head); - assert oldNextTail == null; - inProgress.task = tail; - + if (inProgress.combiner == this) { + Task oldNextTail = inProgress.task.replaceNext(head); + assert oldNextTail == null; + inProgress.task = tail; + } else { + Map<CombinerExecutor<S>, Task> map = inProgress.others; + if (map == null) { + map = inProgress.others = new HashMap<>(1); + } + Task task = map.get(this); + if (task == null) { + map.put(this, tail); + try { + // from now one cannot trust tail anymore + head.runNextTasks(); + } finally { + map.remove(this); + } + } else { + Task oldNextTail = task.replaceNext(head); + assert oldNextTail == null; + map.put(this, tail); + } + } } } }
src/test/java/io/vertx/core/net/impl/pool/ConnectionPoolTest.java+74 −2 modified@@ -960,14 +960,14 @@ public void testPostTasksTrampoline() throws Exception { List<Integer> res = Collections.synchronizedList(new LinkedList<>()); AtomicInteger seq = new AtomicInteger(); CountDownLatch latch = new CountDownLatch(numAcquires); + int[] count = new int[1]; ConnectionPool<Connection> pool = ConnectionPool.pool(new PoolConnector<Connection>() { - int count = 0; int reentrancy = 0; @Override public void connect(ContextInternal context, Listener listener, Handler<AsyncResult<ConnectResult<Connection>>> handler) { assertEquals(0, reentrancy++); try { - int val = count++; + int val = count[0]++; if (val == 0) { // Queue extra requests for (int i = 0;i < numAcquires;i++) { @@ -977,6 +977,7 @@ public void connect(ContextInternal context, Listener listener, Handler<AsyncRes latch.countDown(); })); } + assertEquals(1, count[0]); } handler.handle(Future.failedFuture("failure")); } finally { @@ -992,10 +993,81 @@ public boolean isValid(Connection connection) { int num = seq.getAndIncrement(); pool.acquire(ctx, 0, onFailure(err -> res.add(num))); awaitLatch(latch); + assertEquals(1 + numAcquires, count[0]); List<Integer> expected = IntStream.range(0, numAcquires + 1).boxed().collect(Collectors.toList()); assertEquals(expected, res); } + @Test + public void testConcurrentPostTasksTrampoline() throws Exception { + AtomicReference<ConnectionPool<Connection>> ref1 = new AtomicReference<>(); + AtomicReference<ConnectionPool<Connection>> ref2 = new AtomicReference<>(); + ContextInternal ctx = vertx.createEventLoopContext(); + List<Integer> res = Collections.synchronizedList(new LinkedList<>()); + CountDownLatch latch = new CountDownLatch(4); + ConnectionPool<Connection> pool1 = ConnectionPool.pool(new PoolConnector<Connection>() { + int count = 0; + int reentrancy = 0; + @Override + public void connect(ContextInternal context, Listener listener, Handler<AsyncResult<ConnectResult<Connection>>> handler) { + assertEquals(0, reentrancy++); + try { + int val = count++; + if (val == 0) { + ref1.get().acquire(ctx, 0, onFailure(err -> { + res.add(1); + latch.countDown(); + })); + ref2.get().acquire(ctx, 0, onFailure(err -> { + res.add(2); + latch.countDown(); + })); + } + handler.handle(Future.failedFuture("failure")); + } finally { + reentrancy--; + } + } + @Override + public boolean isValid(Connection connection) { + return true; + } + }, new int[]{1}, 2); + ConnectionPool<Connection> pool2 = ConnectionPool.pool(new PoolConnector<Connection>() { + int count = 0; + int reentrancy = 0; + @Override + public void connect(ContextInternal context, Listener listener, Handler<AsyncResult<ConnectResult<Connection>>> handler) { + assertEquals(0, reentrancy++); + try { + int val = count++; + if (val == 0) { + ref2.get().acquire(ctx, 0, onFailure(err -> { + res.add(3); + latch.countDown(); + })); + ref1.get().acquire(ctx, 0, onFailure(err -> { + res.add(4); + latch.countDown(); + })); + } + handler.handle(Future.failedFuture("failure")); + } finally { + reentrancy--; + } + } + @Override + public boolean isValid(Connection connection) { + return true; + } + }, new int[]{1}, 2); + ref1.set(pool1); + ref2.set(pool2); + pool1.acquire(ctx, 0, onFailure(err -> res.add(0))); + awaitLatch(latch); +// assertEquals(Arrays.asList(0, 2, 1, 3, 4), res); + } + static class Connection { public Connection() { }
src/test/java/io/vertx/core/net/impl/pool/SynchronizationTest.java+66 −7 modified@@ -10,11 +10,15 @@ */ package io.vertx.core.net.impl.pool; +import io.netty.util.concurrent.FastThreadLocal; import io.vertx.test.core.AsyncTestBase; import org.junit.Test; import java.lang.management.ManagementFactory; import java.lang.management.ThreadMXBean; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicBoolean; @@ -67,13 +71,55 @@ public void run() { } @Test - public void testFoo() throws Exception { + public void testActionReentrancy2() throws Exception { + List<Integer> log = new LinkedList<>(); + Executor<Object> combiner1 = new CombinerExecutor<>(new Object()); + Executor<Object> combiner2 = new CombinerExecutor<>(new Object()); + int[] reentrancy = new int[2]; + combiner1.submit(state1 -> taskOf(() -> { + assertEquals(0, reentrancy[0]++); + combiner1.submit(state2 -> taskOf(() -> { + assertEquals(0, reentrancy[0]++); + log.add(0); + reentrancy[0]--; + })); + combiner2.submit(state2 -> taskOf(() -> { + assertEquals(0, reentrancy[1]++); + log.add(1); + combiner1.submit(state3 -> taskOf(() -> { + assertEquals(0, reentrancy[0]++); + log.add(2); + reentrancy[0]--; + })); + combiner2.submit(state3 -> taskOf(() -> { + assertEquals(0, reentrancy[1]++); + log.add(3); + reentrancy[1]--; + })); + reentrancy[1]--; + })); + reentrancy[0]--; + })); + assertEquals(0, reentrancy[0]); + assertEquals(0, reentrancy[1]); + assertEquals(Arrays.asList(1, 3, 0, 2), log); + } + static Task taskOf(Runnable runnable) { + return new Task() { + @Override + public void run() { + runnable.run(); + } + }; + } + @Test + public void testFoo() throws Exception { int numThreads = 8; int numIter = 1_000 * 100; Executor<Object> sync = new CombinerExecutor<>(new Object()); - Executor.Action action = s -> { + Executor.Action<Object> action = s -> { burnCPU(10); return null; }; @@ -96,11 +142,6 @@ public void testFoo() throws Exception { } } - - - - - public static class Utils { public static long res = 0; // value sink public static long ONE_MILLI_IN_NANO = 1000000; @@ -170,4 +211,22 @@ public void run() { }); assertEquals(3, order.get()); } + + @Test + public void testFastThreadLocalStability() { + CombinerExecutor<Void> executor = new CombinerExecutor<>(null); + int expected = io.netty.util.internal.InternalThreadLocalMap.lastVariableIndex(); + AtomicInteger counter = new AtomicInteger(); + for (int i = 0;i < 1000;i++) { + executor = new CombinerExecutor<>(null); + executor.submit(state -> new Task() { + @Override + public void run() { + counter.incrementAndGet(); + } + }); + assertEquals(i + 1, counter.get()); + } + assertEquals(expected, io.netty.util.internal.InternalThreadLocalMap.lastVariableIndex()); + } }
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
16- github.com/advisories/GHSA-5667-3wch-7q7wghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-1023ghsaADVISORY
- access.redhat.com/errata/RHSA-2024:1662nvdWEB
- access.redhat.com/errata/RHSA-2024:1706nvdWEB
- access.redhat.com/errata/RHSA-2024:2088nvdWEB
- access.redhat.com/errata/RHSA-2024:2833nvdWEB
- access.redhat.com/errata/RHSA-2024:3527nvdWEB
- access.redhat.com/errata/RHSA-2024:3989nvdWEB
- access.redhat.com/errata/RHSA-2024:4884nvdWEB
- access.redhat.com/security/cve/CVE-2024-1023nvdWEB
- bugzilla.redhat.com/show_bug.cginvdWEB
- github.com/eclipse-vertx/vert.x/commit/665ceba38444e3929bb7b9a2a0bae2cb603fe81bghsaWEB
- github.com/eclipse-vertx/vert.x/commit/dd6f64302b56cd4d3dcf61efaaf174b5f6ce676dghsaWEB
- github.com/eclipse-vertx/vert.x/issues/5078nvdWEB
- github.com/eclipse-vertx/vert.x/pull/5080nvdWEB
- github.com/eclipse-vertx/vert.x/pull/5082nvdWEB
News mentions
0No linked articles in our index yet.