Skip to content

GH-50762: [C++][Feather] Avoid reading outside sliced bitmap buffers - #50763

Open
tcmartin wants to merge 1 commit into
apache:mainfrom
tcmartin:gh-50762-feather-bitmap-bounds
Open

tcmartin wants to merge 1 commit into
apache:mainfrom
tcmartin:gh-50762-feather-bitmap-bounds

Conversation

@tcmartin

@tcmartin tcmartin commented Jul 31, 2026 •

Copy link
Copy Markdown

Rationale for this change

The Feather V1 bitmap shift loop unconditionally read the byte after the final
source byte. For an exact-sized sliced bitmap, this caused a one-byte
out-of-bounds read and copied data outside the logical slice into unused bits of
the serialized bitmap.

Closes #50762.

What changes are included in this PR?

  • Keep bitmap length in bits while writing shifted Boolean and validity
    bitmaps.
  • Stop before reading a source byte that is not required by the logical slice.
  • Clear unused trailing bits in the serialized bitmap.
  • Add an exact-sized sliced-Boolean regression test that checks the serialized
    byte and round-trip result.

Are these changes tested?

Yes.

  • Full arrow-feather-test suite under ASan/UBSan: 84 passed, 5 expected
    skips.
  • Standalone exact-sized-buffer reproducer under ASan/UBSan: offset-zero
    control and offset-one trigger both complete cleanly after the fix.

Are there any user-facing changes?

No public API changes. Feather V1 output no longer incorporates bits outside a
logical bitmap slice.

This PR contains a Critical Fix. It prevents an out-of-bounds read and
incorrect data from being copied into serialized output when the documented
array-buffer contract is upheld.

AI assistance

I used an AI coding assistant to help trace the writer, draft the implementation
and regression test, and run the verification commands. I reviewed the complete
diff, reproduced the original fault under ASan, verified the bit-boundary
logic, and ran the full Feather test suite. No maintainer was tagged or pinged.

Track logical bit lengths separately from byte lengths when shifting sliced bitmaps, stop before the first byte outside the declared slice, and clear unused output bits. Add an exact-sized-buffer regression test that checks both serialized output and round-trip behavior.

Constraint: Preserve the streaming writer and Feather V1 byte layout.

Rejected: Copy the entire bitmap into a temporary allocation | adds allocation and abandons the existing bounded streaming design.

Confidence: high

Scope-risk: narrow

Directive: Keep bitmap length in bits; byte length alone cannot determine whether the final shifted source byte is valid.

Tested: ASan/UBSan standalone control and trigger; full arrow-feather-test suite (84 passed, 5 expected skips).

Not-tested: Full Arrow repository test suite.
@tcmartin
tcmartin requested a review from pitrou as a code owner July 31, 2026 18:53
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #50762 has no components, please add labels for components.

Comment on lines +95 to 111
for (int64_t i = 0; i < length; ++i) {
uint8_t r = static_cast<uint8_t>(*data++ >> bit_shift);
uint8_t l = static_cast<uint8_t>(*data << lshift);
uint8_t l =
data == data_end ? 0 : static_cast<uint8_t>(*data << lshift);
uint8_t value = l | r;
if (i == length - 1 && bit_length % 8 != 0) {
value &= bit_util::LeastSignificantBitMask<uint8_t>(bit_length % 8);
}
*buffer_it++ = value;
if (buffer_it == buffer_end) {
RETURN_NOT_OK(stream->Write(buffer, buffersize));
buffer_it = buffer;
}
}
if (buffer_it != buffer) {
RETURN_NOT_OK(stream->Write(buffer, buffer_it - buffer));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be easier to let the loop run only until i + 1 < length.
And then change L109-111:

uint8_t r = static_cast<uint8_t>(*data++ >> bit_shift);
*buffer_it++ = r;
RETURN_NOT_OK(stream->Write(buffer, buffer_it - buffer));

I.e. always handle the last byte after the loop.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, it might still need masking of the upper bits of r.

@pitrou pitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just seeing this PR, sorry @tcmartin

return WriteBuffer(buffer,
bit_util::BytesForBits(values.length() * fw_type.bit_width()),
bit_offset);
return WriteBuffer(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a DCHECK_EQ(fw_type.bit_width() % 8, 0) to make sure we're not handling any non-byte-aligned type here?

return WriteBitmap(prim_values.values()->data(), values.length(),
prim_values.offset());
}
const uint8_t* buffer =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the above is if constexpr, we might want to use an else branch here (making it constexpr too).

Comment on lines +100 to +102
if (i == length - 1 && bit_length % 8 != 0) {
value &= bit_util::LeastSignificantBitMask<uint8_t>(bit_length % 8);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually required? I don't think it matters that we are writing potentially non-zero bits past the logical end of the bitmap.

Comment on lines +343 to +344
ASSERT_GT(output_->size(), 8);
ASSERT_EQ(output_->data()[8], 0x01);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this testing the raw Feather bytes? Can you add a comment explaining these checks?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And by the way, if output_'s size is 8, then you cannot read beyond output_->data()[7].

Comment on lines +336 to +337
auto data =
ArrayData::Make(boolean(), 1, {nullptr, values}, /*null_count=*/0, /*offset=*/1);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have a loop running offset from 0 to 7?

}

TEST_P(TestFeather, ExactSizeSlicedBooleanBuffer) {
if (GetParam().version != kFeatherV1Version) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment at top pointing to the GH issue?

@github-actions github-actions Bot added awaiting review Awaiting review awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting committer review Awaiting committer review awaiting review Awaiting review Component: C++

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[C++][Feather] Avoid reading outside sliced bitmap buffers in V1 writer

3 participants