Skip to content

Commit 754eb3d

Browse files
glasstigerclaude
andcommitted
Guard the sent-dict mirror against int overflow
The I/O thread's lifetime-monotonic symbol-dictionary mirror is sized with int math: accumulateSentDict passed sentDictBytesLen + regionBytes (an int sum) to ensureSentDictCapacity, and the grow step doubled capacity*2, also int. On a pathological, very-high-cardinality connection the sum overflows negative -- so the capacity check passes and copyMemory scribbles past the buffer (silent heap corruption) -- and capacity*2 overflows negative near 1 GB, degrading the doubling to exact-fit reallocs. Reaching this needs ~200M+ distinct symbols on one connection, far past any real workload, but the failure mode is silent corruption. ensureSentDictCapacity now takes a long, the caller passes a long sum, and the method throws a LineSenderException above an int-addressable ceiling (Integer.MAX_VALUE - 8) instead of overflowing, growing in long math clamped to that ceiling. Defensive only -- not reachable at realistic symbol cardinality, so there is no scale test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 282ac09 commit 754eb3d

1 file changed

Lines changed: 24 additions & 5 deletions

File tree

core/src/main/java/io/questdb/client/cutlass/qwp/client/sf/cursor/CursorWebSocketSendLoop.java

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ public final class CursorWebSocketSendLoop implements QuietCloseable {
133133
*/
134134
public static final long DEFAULT_POISON_MIN_ESCALATION_WINDOW_MILLIS = 5_000L;
135135
private static final Logger LOG = LoggerFactory.getLogger(CursorWebSocketSendLoop.class);
136+
// Hard ceiling for the lifetime-monotonic sent-dictionary mirror. The mirror
137+
// fields are int, so it cannot exceed Integer.MAX_VALUE bytes; reaching even
138+
// this needs ~200M+ distinct symbols on a single connection, far past any real
139+
// workload. The guard exists so that pathological growth fails loudly instead
140+
// of overflowing the int capacity math into a heap-corrupting copyMemory.
141+
private static final int MAX_SENT_DICT_BYTES = Integer.MAX_VALUE - 8;
136142
/**
137143
* Throttle "reconnect attempt N failed" WARN logs to one per 5 s.
138144
*/
@@ -2035,19 +2041,32 @@ private void accumulateSentDict(long payloadAddr, int payloadLen) {
20352041
}
20362042
}
20372043
int regionBytes = (int) (p - regionStart);
2038-
ensureSentDictCapacity(sentDictBytesLen + regionBytes);
2044+
// long sum: sentDictBytesLen + regionBytes can exceed Integer.MAX_VALUE on
2045+
// a very-high-cardinality lifetime connection; ensureSentDictCapacity then
2046+
// fails loudly rather than overflowing to a negative int (which would make
2047+
// the capacity check pass and copyMemory scribble past the buffer).
2048+
ensureSentDictCapacity((long) sentDictBytesLen + regionBytes);
20392049
Unsafe.getUnsafe().copyMemory(regionStart, sentDictBytesAddr + sentDictBytesLen, regionBytes);
20402050
sentDictBytesLen += regionBytes;
20412051
sentDictCount += (int) deltaCount;
20422052
}
20432053

2044-
private void ensureSentDictCapacity(int required) {
2054+
private void ensureSentDictCapacity(long required) {
20452055
if (sentDictBytesCapacity >= required) {
20462056
return;
20472057
}
2048-
int newCap = Math.max(sentDictBytesCapacity * 2, Math.max(4096, required));
2049-
sentDictBytesAddr = Unsafe.realloc(sentDictBytesAddr, sentDictBytesCapacity, newCap, MemoryTag.NATIVE_DEFAULT);
2050-
sentDictBytesCapacity = newCap;
2058+
if (required > MAX_SENT_DICT_BYTES) {
2059+
throw new LineSenderException("symbol dictionary mirror exceeds the maximum size ["
2060+
+ "required=" + required + ", max=" + MAX_SENT_DICT_BYTES + ']');
2061+
}
2062+
// Grow in long to avoid the capacity*2 int overflow (negative) that would
2063+
// otherwise degrade the doubling near 1 GB; clamp to the int ceiling.
2064+
long newCap = Math.max((long) sentDictBytesCapacity * 2, Math.max(4096L, required));
2065+
if (newCap > MAX_SENT_DICT_BYTES) {
2066+
newCap = MAX_SENT_DICT_BYTES;
2067+
}
2068+
sentDictBytesAddr = Unsafe.realloc(sentDictBytesAddr, sentDictBytesCapacity, (int) newCap, MemoryTag.NATIVE_DEFAULT);
2069+
sentDictBytesCapacity = (int) newCap;
20512070
}
20522071

20532072
private long readVarintAt(long p, long limit) {

0 commit comments

Comments
 (0)