fix: align typed column buffers to T in decode paths

cast_slice::<u8, T> panics with TargetAlignmentGreaterAndInputNotAligned
on KDB IPC payloads where a variable-length column leaves a numeric
column at a misaligned wire offset. The sync decode path's alignment
fallback used Bytes::copy_from_slice (Vec<u8> layout, align=1), which
only happens to work because most allocators over-align byte blocks --
not guaranteed by Rust's allocator API. The async pipelined path went
through read_bytes(len * size) directly, with no alignment branch at
all, and panicked in arrow projection's as_*_slice on Windows release
builds under AsyncPool.query.

Both paths now back typed columns with Vec<T> (Layout::array::<T>
guarantees align_of::<T>()), exposed as bytes::Bytes via a new
AlignedTBuf<T> AsRef<[u8]> owner passed to Bytes::from_owner. Sync
fallback uses the same wrapper. Pipelined typed reads route through
a new read_typed_bytes::<T> helper that swaps in for every typed
Primitive arm in decode_vector_async.

Regression test in pipelined::tests constructs a table with an odd-
length symbol column followed by Long, exercising the previously
panicking path.
This commit is contained in:
Cam Zalewski 2026-05-20 14:13:41 +01:00
parent 53ac90fe84
commit f24af467ec
2 changed files with 114 additions and 18 deletions

View file

@ -70,6 +70,16 @@ impl Default for DecodeOptions {
}
}
/// Owns a `Vec<T>` and exposes it as `&[u8]` so `bytes::Bytes::from_owner`
/// can keep a T-aligned allocation alive while presenting a byte view.
struct AlignedTBuf<T: bytemuck::Pod>(Vec<T>);
impl<T: bytemuck::Pod> AsRef<[u8]> for AlignedTBuf<T> {
fn as_ref(&self) -> &[u8] {
bytemuck::cast_slice(&self.0)
}
}
struct BodyReader {
bytes: bytes::Bytes,
offset: usize,
@ -128,8 +138,13 @@ impl BodyReader {
/// Returns a `Bytes` wrapper of `count * size_of::<T>()` bytes, aligned for `T`.
///
/// If the current offset is already aligned for `T`, this is zero-copy
/// (a `Bytes::slice`). Otherwise it copies into a new aligned allocation.
fn read_bytes_aligned<T: bytemuck::Pod>(&mut self, count: usize) -> CoreResult<bytes::Bytes> {
/// (a `Bytes::slice`). Otherwise it copies into a `Vec<T>`-backed allocation,
/// guaranteeing T-alignment regardless of the global allocator's behavior
/// for `Vec<u8>` (whose layout only requires align=1).
fn read_bytes_aligned<T>(&mut self, count: usize) -> CoreResult<bytes::Bytes>
where
T: bytemuck::Pod + Send + Sync + 'static,
{
let byte_len = count
.checked_mul(std::mem::size_of::<T>())
.ok_or(CoreError::LengthOverflow(count))?;
@ -143,11 +158,12 @@ impl BodyReader {
let ptr = self.bytes[self.offset..].as_ptr();
let align = std::mem::align_of::<T>();
let result = if (ptr as usize) % align == 0 {
// Already aligned — zero-copy slice.
self.bytes.slice(self.offset..end)
} else {
// Misaligned — must copy into an aligned allocation.
bytes::Bytes::copy_from_slice(&self.bytes[self.offset..end])
let mut aligned = vec![T::zeroed(); count];
let dst: &mut [u8] = bytemuck::cast_slice_mut(&mut aligned);
dst.copy_from_slice(&self.bytes[self.offset..end]);
bytes::Bytes::from_owner(AlignedTBuf(aligned))
};
self.offset = end;
Ok(result)