Skip to content

Commit 67ba307

Browse files
committed
handle issue with releasing buffers
1 parent ab0f53b commit 67ba307

1 file changed

Lines changed: 56 additions & 20 deletions

File tree

src/main/java/tools/jackson/core/util/BufferRecycler.java

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package tools.jackson.core.util;
22

3+
import java.lang.invoke.MethodHandles;
4+
import java.lang.invoke.VarHandle;
35
import java.util.Objects;
4-
import java.util.concurrent.atomic.AtomicReferenceArray;
56

67
/**
78
* This is a small utility class, whose main functionality is to allow
@@ -18,8 +19,24 @@
1819
* Rewritten in 2.16 to work with {@link RecyclerPool} abstraction.
1920
*/
2021
public class BufferRecycler
21-
implements RecyclerPool.WithPool<BufferRecycler>
22+
implements RecyclerPool.WithPool<BufferRecycler>
2223
{
24+
/**
25+
* VarHandle for atomic acquire/release access to Object[] array elements.
26+
* Used for the byte and char buffer slot arrays to apply minimal required
27+
* memory ordering semantics (acquire on alloc, release on release) rather
28+
* than the unconditional volatile barriers that AtomicReferenceArray imposes.
29+
*/
30+
private static final VarHandle BUFFER_VH;
31+
32+
static {
33+
try {
34+
BUFFER_VH = MethodHandles.arrayElementVarHandle(Object[].class);
35+
} catch (Exception e) {
36+
throw new ExceptionInInitializerError(e);
37+
}
38+
}
39+
2340
/**
2441
* Tag-on interface to allow various other types to expose {@link BufferRecycler}
2542
* they are constructed with.
@@ -93,11 +110,13 @@ public interface Gettable {
93110
private final static int[] BYTE_BUFFER_LENGTHS = new int[] { 8000, 8000, 2000, 2000 };
94111
private final static int[] CHAR_BUFFER_LENGTHS = new int[] { 4000, 4000, 200, 200 };
95112

96-
// Note: changed from simple array in 2.10:
97-
protected final AtomicReferenceArray<byte[]> _byteBuffers;
113+
// Note: changed from AtomicReferenceArray in 3.2 to Object[] + VarHandle for
114+
// finer-grained memory ordering control (acquire/release instead of full volatile).
115+
protected final Object[] _byteBuffers;
98116

99-
// Note: changed from simple array in 2.10:
100-
protected final AtomicReferenceArray<char[]> _charBuffers;
117+
// Note: changed from AtomicReferenceArray in 3.2 to Object[] + VarHandle for
118+
// finer-grained memory ordering control (acquire/release instead of full volatile).
119+
protected final Object[] _charBuffers;
101120

102121
private RecyclerPool<BufferRecycler> _pool;
103122

@@ -123,8 +142,8 @@ public BufferRecycler() {
123142
* @param cbCount Number of {@code char[]} buffers to allocate
124143
*/
125144
protected BufferRecycler(int bbCount, int cbCount) {
126-
_byteBuffers = new AtomicReferenceArray<>(bbCount);
127-
_charBuffers = new AtomicReferenceArray<>(cbCount);
145+
_byteBuffers = new Object[bbCount];
146+
_charBuffers = new Object[cbCount];
128147
}
129148

130149
/**
@@ -157,19 +176,30 @@ public byte[] allocByteBuffer(int ix, int minSize) {
157176
if (minSize < DEF_SIZE) {
158177
minSize = DEF_SIZE;
159178
}
160-
byte[] buffer = _byteBuffers.getAndSet(ix, null);
179+
// getAndSetAcquire: atomically claims the slot and applies acquire semantics so
180+
// that all writes made by the thread that stored this buffer are visible to us.
181+
byte[] buffer = (byte[]) BUFFER_VH.getAndSetAcquire(_byteBuffers, ix, null);
161182
if (buffer == null || buffer.length < minSize) {
162183
buffer = balloc(minSize);
163184
}
164185
return buffer;
165186
}
166187

167188
public void releaseByteBuffer(int ix, byte[] buffer) {
168-
// 13-Jan-2024, tatu: [core#1186] Replace only if beneficial:
169-
byte[] oldBuffer = _byteBuffers.get(ix);
170-
if ((oldBuffer == null) || buffer.length > oldBuffer.length) {
171-
// Could use CAS, but should not really matter
172-
_byteBuffers.set(ix, buffer);
189+
// CAS loop fixes the TOCTOU race in the original get+set pattern.
190+
// compareAndSet with release semantics publishes all our writes to the next
191+
// thread that acquires this buffer.
192+
// Modified to do a max number of attempts to recycle (hardcoded to 3)
193+
for (int i = 0; i < 3; i++) {
194+
byte[] current = (byte[]) BUFFER_VH.getAcquire(_byteBuffers, ix);
195+
if (current != null && current.length >= buffer.length) {
196+
// Slot already holds a buffer at least as large; no benefit in replacing it.
197+
return;
198+
}
199+
if (BUFFER_VH.compareAndSet(_byteBuffers, ix, current, buffer)) {
200+
return;
201+
}
202+
// CAS lost the race; retry with the newly observed value.
173203
}
174204
}
175205

@@ -188,19 +218,25 @@ public char[] allocCharBuffer(int ix, int minSize) {
188218
if (minSize < DEF_SIZE) {
189219
minSize = DEF_SIZE;
190220
}
191-
char[] buffer = _charBuffers.getAndSet(ix, null);
221+
// getAndSetAcquire: atomically claims the slot with acquire semantics.
222+
char[] buffer = (char[]) BUFFER_VH.getAndSetAcquire(_charBuffers, ix, null);
192223
if (buffer == null || buffer.length < minSize) {
193224
buffer = calloc(minSize);
194225
}
195226
return buffer;
196227
}
197228

198229
public void releaseCharBuffer(int ix, char[] buffer) {
199-
// 13-Jan-2024, tatu: [core#1186] Replace only if beneficial:
200-
char[] oldBuffer = _charBuffers.get(ix);
201-
if ((oldBuffer == null) || buffer.length > oldBuffer.length) {
202-
// Could use CAS, but should not really matter
203-
_charBuffers.set(ix, buffer);
230+
// CAS loop fixes the TOCTOU race in the original get+set pattern.
231+
// compareAndSet with release semantics publishes our writes to the next thread.
232+
while (true) {
233+
char[] current = (char[]) BUFFER_VH.getAcquire(_charBuffers, ix);
234+
if (current != null && current.length >= buffer.length) {
235+
return;
236+
}
237+
if (BUFFER_VH.compareAndSet(_charBuffers, ix, current, buffer)) {
238+
return;
239+
}
204240
}
205241
}
206242

0 commit comments

Comments
 (0)