Repository navigation
MINOR: Bounds check source buffer in BaseFixedWidthVector.copyFrom - #1376
Support-itecz wants to merge 2 commits into
Conversation
| from.getDataBuffer().memoryAddress() + (long) fromIndex * typeWidth, | ||
| this.getDataBuffer().memoryAddress() + (long) thisIndex * typeWidth, | ||
| typeWidth); | ||
| from.getDataBuffer() |
There was a problem hiding this comment.
This hardens only BaseFixedWidthVector.copyFrom. The same unchecked pattern, MemoryUtil.copyMemory on raw memoryAddress() plus an offset, remains in other places in vector/src/main. These include DecimalVector, Decimal256Vector, BitVectorHelper, and VectorAppender`.
If the goal is to make malformed IPC input fail with IndexOutOfBoundsException instead of an out-of-bounds read, can we say which of these are in scope?
If they are reachable from loadFieldBuffers style input, I would fix them here.
There was a problem hiding this comment.
VectorAppender and BitVectorHelper.concatBits are reachable: the delta vector can be one loaded via loadFieldBuffers, and nothing checks its data, view, type or validity buffer against its value count before the raw copy. I added a source checkBytes before each of those copies and both concatBits inputs, plus two appender tests (short data buffer, short validity buffer) that fail without the change. The offset copies were already bounded by the getInt/getLong of the last offset right above them. DecimalVector/Decimal256Vector setSafe and setBigEndianSafe already checkBytes both buffers before copying, so I left those alone. The description now lists what is covered.
| from.getDataBuffer().memoryAddress() + (long) fromIndex * typeWidth, | ||
| this.getDataBuffer().memoryAddress() + (long) thisIndex * typeWidth, | ||
| typeWidth); | ||
| from.getDataBuffer() |
There was a problem hiding this comment.
Oh, also, copyFrom is called once per element, and copyFromSafe loops and appenders call it in bulk. ArrowBuf.getBytes(long, ArrowBuf, long, int) adds bounds checks on both buffers plus argument validation, in place of a single raw copy.
Do you have a before/after number, for instance the existing JMH benchmarks for copyFrom? I would like to see the cost here.
Also, this only protects when bounds checking is on. With arrow.enable_unsafe_memory_access=true, getBytes skips validation and the read is unchecked again. That is probably acceptable, since the flag disables checks by design, but please say so in the description.
There was a problem hiding this comment.
Ran Float8Benchmarks.copyFromBenchmark (1024 copies per op, 2 forks x 8 iterations on a laptop, so the error bars are wide):
- checks on: main 13.98 ± 6.03 us/op, this PR 19.89 ± 5.18 us/op
enable_unsafe_memory_access=true: main 8.51 ± 4.87, this PR 8.79 ± 4.83
So about 6 ns per value with checks on and nothing measurable with them off. I also tried checkBytes on both buffers plus the original raw copy instead of getBytes, and it came out the same (20.38 ± 11.60), so I kept getBytes. The appender changes are one range check per buffer per append, not per value.
Agreed on the unsafe flag. With it set the source check is skipped and the read is unchecked again. That is in the description now.
| body.setZero(0, body.capacity()); | ||
| body.setInt(0, 1); | ||
| body.setInt(12, 0xBAD); | ||
| // Field node claims 4 values, but the data buffer only holds one, as a malformed IPC |
There was a problem hiding this comment.
The source is loaded with a zero-length validity buffer (body.slice(0, 0)) and a null count of 0. The first copyFromSafe(0, 0, source) then needs isSet(0) to read that buffer without throwing.
Is that guaranteed, or does it work only because of how isSet handles this case today?
If a later change makes isSet check the buffer length, this test fails for a reason unrelated to the data-buffer bounds check it's meant to cover.
Giving the source a real validity buffer (all bits set) would make the test say what it means.
There was a problem hiding this comment.
Good catch. Today it works because loadValidityBuffer replaces an empty validity buffer with an all-ones one when the null count is 0, not because of isSet, but the test should not rely on that. The source now gets a real 8-byte validity buffer with all 4 bits set, and the new appender tests do the same.
What's Changed
BaseFixedWidthVector.copyFromcopies the value with a rawMemoryUtil.copyMemoryfromfrom.getDataBuffer().memoryAddress() + fromIndex * typeWidth, so neither the source nor the target buffer is bounds checked, even with bounds checking enabled. A fixed-width vector loaded from an IPC record batch whose field node length is larger than its data buffer (for example 4 int values over a 4-byte buffer) passesisNull.copyFrom/copyFromSafe/TransferPair.copyValueSafeon an index past the buffer then read native memory beyond it, whileget()on the same index throwsIndexOutOfBoundsException. In the new test the copied value is an int sitting 12 bytes past the start of the data slice.The copy now goes through
ArrowBuf.getBytes(index, dst, dstIndex, length), which checks both buffers, the same wayBaseVariableWidthVector.copyFromalready does. Copies of valid data are unchanged.VectorAppenderandBitVectorHelper.concatBitshad the same pattern on the delta vector's buffers, and the delta can come straight fromloadFieldBuffers. They nowcheckBytesthe source range before each raw copy (fixed-width data, variable-width and large variable-width data, view buffer, union and dense union type buffers, and bothconcatBitsinputs). The offset buffer copies were already covered by thegetInt/getLongof the last offset just above them.DecimalVector/Decimal256VectorsetSafeandsetBigEndianSafealreadycheckBytesboth buffers before their raw copies, so they're left as is.These checks only apply with bounds checking on. With
arrow.enable_unsafe_memory_access=truethe source-side check is skipped, so a malformed batch reads out of bounds again, which is what that flag opts into.Float8Benchmarks.copyFromBenchmark(1024copyFromcalls per op, JDK 26, 2 forks x 8 iterations, laptop so the error bars are wide):enable_unsafe_memory_access=trueSo roughly 6 ns per copied value with checks on and no measurable change with them off. The appender and
concatBitschecks are one range check per buffer per append, not per value.TestCopyFrom.testCopyFromFixedWidthSourceBufferTooShortand the newTestVectorAppender.testAppendFixedWidthVectorSourceBufferTooShort/testAppendValidityBufferTooShortfail without the change and pass with it. Thearrow-vectorandarrow-algorithmtest suites pass locally along with spotless and checkstyle.