Skip to content

Commit 2df4e3a

Browse files
dfa1claude
andcommitted
fix(zstd): bound frame sizes and overflow-check total
decode summed untrusted per-frame uncompressed_size into a long with no validation, then arena.allocate'd it; a crafted metadata could wrap the total to a small positive (under-allocation) or claim a huge size (allocation DoS). The (int) narrowing of uncompressed_size at the asSlice call site could also go negative/truncated, throwing a raw IndexOutOfBoundsException instead of a VortexException. Validate each frame via IoBounds.toIntSize and accumulate with Math.addExact, surfacing overflow as VortexException. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1d8ddeb commit 2df4e3a

1 file changed

Lines changed: 13 additions & 2 deletions

File tree

reader/src/main/java/io/github/dfa1/vortex/reader/decode/ZstdEncodingDecoder.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.github.dfa1.vortex.core.model.PType;
55
import io.github.dfa1.vortex.core.error.VortexException;
66
import io.github.dfa1.vortex.core.model.EncodingId;
7+
import io.github.dfa1.vortex.core.io.IoBounds;
78
import io.github.dfa1.vortex.core.io.PTypeIO;
89
import io.github.dfa1.vortex.core.proto.ProtoZstdMetadata;
910
import io.github.dfa1.vortex.reader.array.Array;
@@ -63,7 +64,17 @@ public Array decode(DecodeContext ctx) {
6364
int frameCount = meta.frames().size();
6465
long totalUncompressed = 0;
6566
for (int i = 0; i < frameCount; i++) {
66-
totalUncompressed += meta.frames().get(i).uncompressed_size();
67+
// Validate each frame's declared size (rejects negative / >2 GB) and accumulate
68+
// overflow-safely, so a crafted metadata cannot wrap the total to a small positive
69+
// value and under-allocate, nor drive arena.allocate negative. The per-frame cap also
70+
// guards the (int) narrowing at the asSlice call site in decompressFrames.
71+
int frameSize = IoBounds.toIntSize(meta.frames().get(i).uncompressed_size());
72+
try {
73+
totalUncompressed = Math.addExact(totalUncompressed, frameSize);
74+
} catch (ArithmeticException e) {
75+
throw new VortexException(EncodingId.VORTEX_ZSTD,
76+
"total uncompressed size overflows", e);
77+
}
6778
}
6879

6980
MemorySegment decompressed = decompressFrames(ctx, meta, frameCount, totalUncompressed);
@@ -163,7 +174,7 @@ private static MemorySegment decompressFrames(
163174
long outOffset = 0;
164175
for (int i = 0; i < frameCount; i++) {
165176
MemorySegment src = asNative(ctx.buffer(frameBufferBase + i), scratch);
166-
int uncompSize = (int) meta.frames().get(i).uncompressed_size();
177+
int uncompSize = IoBounds.toIntSize(meta.frames().get(i).uncompressed_size());
167178
MemorySegment dst = out.asSlice(outOffset, uncompSize);
168179
long written = dictionary == null
169180
? dctx.decompress(dst, src)

0 commit comments

Comments
 (0)