Files
doris/cloud/test/versionstamp_test.cpp
T
Gavin Chou d11c5e7e79 [fix](cloud) Decode transaction IDs safely through Versionstamp (#67641)
### What problem does this PR solve?

Problem Summary:

`get_txn_id_from_fdb_ts` decodes a character buffer through `int64_t*`
dereferences, including an overlapping read at offset 2. These reads
violate alignment and aliasing requirements.

- Copy the 10 input bytes into a byte array and use
`Versionstamp::version()` / `order()` to decode the big-endian fields in
one place.
- Preserve the transaction ID bit layout, sequence limit, error codes,
unchanged output on rejected input, and existing little-endian
restriction. This does not change the version range or overflow policy.
- Remove the `fmt` dependency from `versionstamp.h`, preserving its
exact 20-character lowercase hexadecimal output. Add an explicit
`byteswap.h` include to the consumer that previously relied on the
transitive include.
- Add differential tests against both the pre-Versionstamp memcpy
implementation and the original reinterpret-cast implementation. Each
reference covers 8 version boundaries, all 65,536 sequence values, and 8
input alignments (4,194,304 combinations), plus malformed lengths and
empty views. Also check the complete decoded Versionstamp fields and
compare hexadecimal formatting against the previous fmt rule.

The unsafe historical load is retained only as a test oracle on
x86_64/AArch64, with its known alignment sanitizer violation locally
suppressed. Production code and the memcpy reference retain normal
instrumentation; matching the historical output does not make the old
implementation well-defined.

### Release note

None

### Check List (For Author)

- Test
    - [ ] Regression test
    - [x] Unit Test
    - [ ] Manual test (add detailed scripts or steps below)
    - [ ] No need to test or manual test. Explain why:

  Validation:
- Cloud ASAN build and compilation/linking of all Cloud unit-test
targets pass on Linux x86_64 with Clang 20.1.8.
- `doris_txn_test:TxnIdConvert.*`: 5 tests pass;
`versionstamp_test:VersionstampTest.*`: 5 tests pass. GTest XML reports
zero failures, errors, or skips.
- Nine isolated related test bodies pass under ASAN+UBSAN on local
ARM64.
- A standalone program including only `versionstamp.h` compiles, links,
and runs without third-party include paths or fmt linkage.
- Changed-line clang-tidy, clang-format 16.0.6, and `git diff --check`
pass.
  - External FDB/S3 integration tests were not run.

- Behavior changed:
    - [x] No.
    - [ ] Yes.

- Does this need documentation?
    - [x] No.
    - [ ] Yes.

### Check List (For Reviewer who merge this PR)

- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
2026-09-14 20:26:28 +08:00

179 lines
5.8 KiB
C++

// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#include "meta-store/versionstamp.h"
#include <bthread/bthread.h>
#include <bthread/countdown_event.h>
#include <fmt/format.h>
#include <gtest/gtest.h>
#include <array>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <random>
#include <variant>
#include <vector>
#include "common/util.h"
#include "meta-store/codec.h"
#include "meta-store/keys.h"
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
TEST(VersionstampTest, ByteSwap) {
using namespace doris::cloud;
// Test byte swapping functions
constexpr uint64_t original64 = 0x0102030405060708;
constexpr uint64_t swapped64 = Versionstamp::byteswap64(original64);
EXPECT_EQ(swapped64, 0x0807060504030201);
constexpr uint16_t original16 = 0x0102;
constexpr uint16_t swapped16 = Versionstamp::byteswap16(original16);
EXPECT_EQ(swapped16, 0x0201);
}
TEST(VersionstampTest, ToString) {
using doris::cloud::Versionstamp;
EXPECT_EQ(Versionstamp::min().to_string(), "00000000000000000000");
EXPECT_EQ(Versionstamp::max().to_string(), "ffffffffffffffffffff");
const Versionstamp versionstamp(0x0001020304050607, 0x08ff);
EXPECT_EQ(versionstamp.to_string(), "000102030405060708ff");
// Verify every byte at every position against an independent formatter.
for (size_t position = 0; position < 10; ++position) {
for (unsigned int byte = 0; byte <= 0xff; ++byte) {
std::array<uint8_t, 10> bytes {};
bytes[position] = static_cast<uint8_t>(byte);
const auto text = Versionstamp(bytes).to_string();
std::string expected(20, '0');
expected.replace(position * 2, 2, fmt::format("{:02x}", byte));
EXPECT_EQ(text, expected) << "position=" << position << " byte=" << byte;
}
}
}
TEST(VersionstampTest, Usage) {
using namespace doris::cloud;
// Create a versionstamp from a byte array
constexpr uint8_t data[10] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A};
constexpr Versionstamp vs(data);
// Check the minimum and maximum versionstamps
EXPECT_EQ(Versionstamp::min() < vs, true);
EXPECT_EQ(Versionstamp::max() > vs, true);
// Check equality and ordering
constexpr Versionstamp vs2(data);
EXPECT_EQ(vs == vs2, true);
EXPECT_EQ(vs < Versionstamp::max(), true);
// Create a versionstamp from a version and order
constexpr uint64_t version = 0x0102030405060708;
constexpr uint16_t order = 0x090A;
constexpr Versionstamp vs3(version, order);
EXPECT_EQ(vs3.version(), version);
EXPECT_EQ(vs3.order(), order);
constexpr Versionstamp vs4(vs.version(), vs.order());
EXPECT_EQ(vs4.version(), vs.version());
EXPECT_EQ(vs4.order(), vs.order());
EXPECT_EQ(vs4.data(), vs.data());
// The default constructor creates a zeroed versionstamp
constexpr Versionstamp vs_default;
EXPECT_EQ(vs_default.version(), 0);
EXPECT_EQ(vs_default.order(), 0);
EXPECT_EQ(vs_default == Versionstamp::min(), true);
}
TEST(VersionstampTest, EncodeDecode) {
using namespace doris::cloud;
// Create a versionstamp
uint8_t data[10] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A};
Versionstamp vs(data);
// Encode the versionstamp
std::string encoded;
uint32_t index = encode_versionstamp(vs, &encoded);
EXPECT_EQ(encoded.size(), 11); // 1 byte for tag + 10 bytes for versionstamp
EXPECT_EQ(index, 1); // The index of the versionstamp in the buffer
// Decode the versionstamp
std::string_view encoded_view(encoded);
Versionstamp decoded_vs;
int result = decode_versionstamp(&encoded_view, &decoded_vs);
EXPECT_EQ(result, 0);
EXPECT_EQ(decoded_vs == vs, true); // Check if data matches
}
TEST(VersionstampTest, Compare) {
using namespace doris::cloud;
Versionstamp v1(0x0001, 1);
Versionstamp v2(0x0001, 2);
Versionstamp v3(0x0100, 1);
{
EXPECT_EQ(v1.version(), 0x0001);
EXPECT_EQ(v1.order(), 1);
EXPECT_EQ(v2.version(), 0x0001);
EXPECT_EQ(v2.order(), 2);
EXPECT_EQ(v3.version(), 0x0100);
EXPECT_EQ(v3.order(), 1);
}
{
// Test comparison operators
EXPECT_EQ(v1 < v2, true);
EXPECT_EQ(v1 > v2, false);
EXPECT_EQ(v1 <= v2, true);
EXPECT_EQ(v1 >= v2, false);
EXPECT_EQ(v1 == v2, false);
EXPECT_EQ(v2 < v3, true);
EXPECT_EQ(v2 > v3, false);
EXPECT_EQ(v2 <= v3, true);
EXPECT_EQ(v2 >= v3, false);
EXPECT_EQ(v2 == v3, false);
EXPECT_EQ(v1 < v3, true);
EXPECT_EQ(v1 > v3, false);
EXPECT_EQ(v1 <= v3, true);
EXPECT_EQ(v1 >= v3, false);
EXPECT_EQ(v1 == v3, false);
}
{
EXPECT_EQ(v1.order() < v2.order(), true);
EXPECT_EQ(v1.version() < v3.version(), true);
}
std::string encoded("\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01", 10);
for (size_t i = 0; i < 10; ++i) {
EXPECT_EQ(encoded.data()[i], v1.data().data()[i]) << "i: " << i;
}
}