Fix one-byte out-of-bounds read in BITPOS with explicit end offset#1971
Open
hexonal wants to merge 2 commits into
Open
Fix one-byte out-of-bounds read in BITPOS with explicit end offset#1971hexonal wants to merge 2 commits into
hexonal wants to merge 2 commits into
Conversation
BitPosDriver clamped an out-of-range explicit end offset to inputLen (BYTE mode) / bitLen (BIT mode) instead of inputLen-1 / bitLen-1, i.e. one index past the last valid position. BitPosByteSearch and BitPosBitSearch iterate while currentOffset <= endOffset over an unsafe byte*, so BITPOS key 0 0 <big-end-offset> against a value with no matching bit reads one byte past the end of the value's backing buffer. BitCountDriver's BYTE-mode path already clamps to valLen-1 (fixed in microsoft#1138), and real Redis clamps end to totlen-1 in bitposCommand for both BYTE and BIT indexing; this brings BitPosDriver in line with both. Both offsetType branches had the same off-by-one, so both are fixed here. BitPosByteSearch/BitPosBitSearch are private and only reachable through BitPosDriver, so the single clamp fix closes the read for every caller; no additional bounds check inside the search loops is needed. Adds two regression tests that call BitmapManager.BitPosDriver directly against an unmanaged buffer with a canary byte placed one past the declared value length, so the overread is deterministically observable instead of depending on incidental adjacent-heap content. Confirmed both tests fail against the pre-fix clamp (returning a position inside the canary byte) and pass with the fix.
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes an off-by-one clamp in BitmapManager.BitPosDriver that could allow BITPOS (with an explicit end offset beyond the value length) to read one byte past the end of the backing buffer in both BYTE and BIT offset modes.
Changes:
- Clamp
endOffsettoinputLen - 1(BYTE mode) andbitLen - 1(BIT mode) to keep the inclusive search bound within the valid buffer. - Add two regression tests that deterministically detect the prior one-byte overread using an unmanaged “canary” byte placed immediately past the declared value length.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| libs/server/Resp/Bitmap/BitmapManagerBitPos.cs | Fixes inclusive endOffset clamping to prevent one-byte out-of-bounds reads in BYTE and BIT search paths. |
| test/standalone/Garnet.test.complexstring/GarnetBitmapTests.cs | Adds two regression tests using unmanaged buffers + a canary byte to catch the prior off-by-one overread deterministically. |
Comments suppressed due to low confidence (1)
libs/server/Resp/Bitmap/BitmapManagerBitPos.cs:51
- Same issue as the BYTE-mode branch: the comment says "return 0" when the start offset is beyond the end offset, but the code returns -1. Please align the comment with the actual return value.
if (startByteIndex > endByteIndex) // If start offset beyond endOffset return 0
return -1;
Both the BYTE-mode and BIT-mode early-return branches return -1, not 0 - the comment was just wrong, no behavior change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
BitPosDriver(libs/server/Resp/Bitmap/BitmapManagerBitPos.cs) clamps an explicitendoffset toinputLen(BYTE mode) /bitLen(BIT mode) instead ofinputLen - 1/bitLen - 1— one index past the last valid position:BitPosByteSearch/BitPosBitSearchiteratewhile (currentOffset <= endOffset)over anunsafe byte*, treatingendOffsetas an inclusive, valid last index (see e.g.remainder = endOffset - currentStartOffset + 1in the BYTE-mode search). When no matching bit exists within the real data and the loop actually reaches the clampedendOffset, it dereferencesinput[inputLen]— one byte past the value's backing buffer. Reproducible with e.g.BITPOS key 0 0 1000000against a short value with no0-bit... (i.e. any explicit end offset >= the value's real length, on a value with no match).Fix
Clamp to
inputLen - 1/bitLen - 1in both branches, matching:BitCountDriver's existing (correct) clamp in the same file family, itself fixed for this exact bug class in BITCOUNT Upper bound calculation fix #1138 for BYTE mode but apparently never mirrored to BITPOS (or to BitCountDriver's own BIT-mode path, which was already correct).bitposCommandinsrc/bitops.c, which clampsend = totlen - 1(totlen already converted to bits in BIT mode) for both indexing modes.BitPosByteSearch/BitPosBitSearchare private with a single caller (BitPosDriver), itself called from exactly one production call site, so no additional in-loop bounds check is warranted — see full investigation notes for the alternatives considered and rejected.Testing
Added two regression tests that call
BitmapManager.BitPosDriverdirectly against an unmanaged buffer with a "canary" byte placed immediately past the declared value length (rather than relying on whatever happens to follow the value on the managed heap, which is not reliable enough to deterministically catch a one-byte overread). Confirmed both tests fail against the pre-fix clamp (reporting a position inside the canary byte instead of -1) and pass with the fix.Full BITPOS test category (14/14) and
dotnet format --verify-no-changespass clean.Note: while investigating, I found a separate, unrelated pre-existing bug —
BitCountDriver'sBitIndexCounthelper (BIT-mode partial-byte counting) calls the x86-onlyPopcnt.X64.PopCountintrinsic with noIsSupported/portable fallback, which crashes the server process on ARM64 hosts. Confirmed pre-existing on unmodifiedmainand unrelated to this change; not fixed here to keep this PR scoped to the BITPOS offset-clamp bug — happy to file that separately if useful.