Files
doris/cloud/test/doris_txn_test.cpp
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

277 lines
11 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-service/doris_txn.h"
#include <brpc/closure_guard.h>
#include <brpc/controller.h>
#include <gtest/gtest.h>
#include <array>
#include <bit>
#include <cstring>
#include <utility>
#include "common/config.h"
#include "common/util.h"
#include "meta-service/meta_service.h"
#include "meta-store/txn_kv.h"
#include "meta-store/versionstamp.h"
namespace {
#if defined(__x86_64__) || defined(__aarch64__)
// Preserve the original, unsafe load solely as a historical output oracle on
// hosts that support unaligned integer loads. Suppress only its known alignment
// violation, not instrumentation of the memcpy reference or production decoder.
// Matching its results does not establish that the old code was well-defined.
__attribute__((noinline, no_sanitize("alignment"))) int64_t legacy_aliased_load(const char* data) {
return *reinterpret_cast<const int64_t*>(data);
}
#endif
int64_t legacy_memcpy_load(const char* data) {
int64_t value;
std::memcpy(&value, data, sizeof(value));
return value;
}
// Freeze the pre-Versionstamp algorithm, including the overlapping eight-byte
// sequence read and its signed intermediate values. Do not use Versionstamp or
// its byte-swap helpers here: this must remain an independent regression oracle.
template <int64_t (*load)(const char*)>
int legacy_get_txn_id_from_fdb_ts(std::string_view fdb_vts, int64_t* txn_id) {
if (fdb_vts.size() != 10) {
return 1;
}
static_assert(std::endian::native == std::endian::little);
auto to_little = [](int64_t v) {
v = ((v & 0xffffffff00000000) >> 32) | ((v & 0x00000000ffffffff) << 32);
v = ((v & 0xffff0000ffff0000) >> 16) | ((v & 0x0000ffff0000ffff) << 16);
v = ((v & 0xff00ff00ff00ff00) >> 8) | ((v & 0x00ff00ff00ff00ff) << 8);
return v;
};
int64_t ver = to_little(load(fdb_vts.data()));
int64_t seq = to_little(load(fdb_vts.data() + 2));
seq &= 0xffff;
static constexpr int SEQ_RETAIN_BITS = 10;
if (seq >= (1L << SEQ_RETAIN_BITS)) {
return 2;
}
seq &= ((1L << SEQ_RETAIN_BITS) - 1L);
ver <<= SEQ_RETAIN_BITS;
ver |= seq;
*txn_id = ver;
return 0;
}
using TxnIdDecoder = int (*)(std::string_view, int64_t*);
void check_decoded_txn_id(TxnIdDecoder legacy_decode, std::string_view input, int expected_ret) {
int64_t expected_txn_id = -1;
int64_t actual_txn_id = -1;
ASSERT_EQ(legacy_decode(input, &expected_txn_id), expected_ret);
ASSERT_EQ(doris::cloud::get_txn_id_from_fdb_ts(input, &actual_txn_id), expected_ret);
ASSERT_EQ(actual_txn_id, expected_txn_id);
if (expected_ret != 0) {
ASSERT_EQ(actual_txn_id, -1);
}
}
void check_legacy_txn_id_compatibility(TxnIdDecoder legacy_decode) {
// Include the positive txn_id limit, the sign transition, and discarded high
// version bits. These latter cases preserve old behavior, not uniqueness.
constexpr std::array<uint64_t, 8> versions = {0,
1,
0x00000182a5ed173f,
0x001f82a5ed173f80,
0x001fffffffffffff,
0x0020000000000000,
0x0040000000000000,
0x7fffffffffffffff};
alignas(int64_t) std::array<char, 10 + alignof(int64_t) - 1> buffer {};
for (uint64_t ver : versions) {
SCOPED_TRACE(ver);
std::array<uint8_t, 10> bytes {};
for (size_t i = 0; i < 8; ++i) {
bytes[i] = static_cast<uint8_t>(ver >> (8 * (7 - i)));
}
for (uint32_t seq = 0; seq <= 0xffff; ++seq) {
bytes[8] = static_cast<uint8_t>(seq >> 8);
bytes[9] = static_cast<uint8_t>(seq);
// Also check the complete fields: txn_id packing discards version
// bits and rejects most orders, so output equality alone misses them.
const doris::cloud::Versionstamp versionstamp(bytes);
ASSERT_EQ(std::make_pair(versionstamp.version(), versionstamp.order()),
std::make_pair(ver, static_cast<uint16_t>(seq)));
for (size_t offset = 0; offset < alignof(int64_t); ++offset) {
std::memcpy(buffer.data() + offset, bytes.data(), bytes.size());
const std::string_view input(buffer.data() + offset, bytes.size());
ASSERT_NO_FATAL_FAILURE(
check_decoded_txn_id(legacy_decode, input, seq < 1024 ? 0 : 2))
<< "seq=" << seq << " offset=" << offset;
}
}
}
}
void check_invalid_txn_id_inputs(TxnIdDecoder legacy_decode) {
// Check short/long inputs and that failures leave the output untouched.
std::array<char, 17> buffer {};
for (size_t size = 0; size <= buffer.size(); ++size) {
if (size == 10) {
continue;
}
SCOPED_TRACE(size);
const std::string_view input(buffer.data(), size);
ASSERT_NO_FATAL_FAILURE(check_decoded_txn_id(legacy_decode, input, 1));
}
check_decoded_txn_id(legacy_decode, {}, 1);
}
} // namespace
int main(int argc, char** argv) {
doris::cloud::config::init(nullptr, true);
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
TEST(TxnIdConvert, TxnIdTest) {
using namespace doris::cloud;
// Correctness test
{
// 00000182a5ed173f0012
std::string ts("\x00\x00\x01\x82\xa5\xed\x17\x3f\x00\x12", 10);
ASSERT_EQ(ts.size(), 10);
int64_t txn_id0;
int ret = get_txn_id_from_fdb_ts(ts, &txn_id0);
ASSERT_EQ(ret, 0);
std::string str((char*)&txn_id0, sizeof(txn_id0));
std::cout << "fdb_ts0: " << hex(ts) << " "
<< "txn_id0: " << txn_id0 << " hex: " << hex(str) << std::endl;
// 00000182a5ed173f0013
ts = std::string("\x00\x00\x01\x82\xa5\xed\x17\x3f\x00\x13", 10);
ASSERT_EQ(ts.size(), 10);
int64_t txn_id1;
ret = get_txn_id_from_fdb_ts(ts, &txn_id1);
ASSERT_EQ(ret, 0);
ASSERT_GT(txn_id1, txn_id0);
str = std::string((char*)&txn_id1, sizeof(txn_id1));
std::cout << "fdb_ts1: " << hex(ts) << " "
<< "txn_id1: " << txn_id1 << " hex: " << hex(str) << std::endl;
// 00000182a5ed174f0013
ts = std::string("\x00\x00\x01\x82\xa5\xed\x17\x4f\x00\x13", 10);
ASSERT_EQ(ts.size(), 10);
int64_t txn_id2;
ret = get_txn_id_from_fdb_ts(ts, &txn_id2);
ASSERT_EQ(ret, 0);
ASSERT_GT(txn_id2, txn_id1);
str = std::string((char*)&txn_id2, sizeof(txn_id2));
std::cout << "fdb_ts2: " << hex(ts) << " "
<< "txn_id2: " << txn_id2 << " hex: " << hex(str) << std::endl;
}
// Boundary test
{
// 1024
// 00000182a5ed174f0400
std::string ts("\x00\x00\x01\x82\xa5\xed\x17\x4f\x04\x00", 10);
ASSERT_EQ(ts.size(), 10);
int64_t txn_id;
int ret = get_txn_id_from_fdb_ts(ts, &txn_id);
ASSERT_EQ(ret, 2); // Exceed max seq
// 1023
// 00000182a5ed174f03ff
ts = std::string("\x00\x00\x01\x82\xa5\xed\x17\x4f\x03\xff", 10);
ret = get_txn_id_from_fdb_ts(ts, &txn_id);
ASSERT_EQ(ret, 0);
// 0000
// 00000182a5ed174f0000
ts = std::string("\x00\x00\x01\x82\xa5\xed\x17\x4f\x03\x00", 10);
ret = get_txn_id_from_fdb_ts(ts, &txn_id);
ASSERT_EQ(ret, 0);
// Insufficient length
ts = std::string("\x00\x00\x01\x82\xa5\xed\x17\x4f\x03\x00", 9);
ret = get_txn_id_from_fdb_ts(ts, &txn_id);
ASSERT_EQ(ret, 1);
}
}
TEST(TxnIdConvert, UnalignedVersionstamp) {
// Cover every input alignment, including odd addresses.
// The payload has high bits set in both version bytes and sequence bytes.
constexpr std::array<unsigned char, 10> versionstamp = {0x00, 0x1f, 0x82, 0xa5, 0xed,
0x17, 0x3f, 0x80, 0x03, 0xff};
constexpr int64_t expected_txn_id = 0x7e0a97b45cfe03ff;
alignas(int64_t) std::array<char, 10 + alignof(int64_t) - 1> buffer {};
for (size_t offset = 0; offset < alignof(int64_t); ++offset) {
SCOPED_TRACE(offset);
std::memcpy(buffer.data() + offset, versionstamp.data(), versionstamp.size());
int64_t txn_id = -1;
ASSERT_EQ(doris::cloud::get_txn_id_from_fdb_ts(
std::string_view(buffer.data() + offset, versionstamp.size()), &txn_id),
0);
EXPECT_EQ(txn_id, expected_txn_id);
}
}
TEST(TxnIdConvert, SequenceValues) {
// Exercise all two-byte sequences at an odd address. In particular, 0x00ff
// must remain positive after decoding, and 0x0400 and above must fail.
alignas(int64_t) std::array<unsigned char, 11> buffer = {0, 0x00, 0x00, 0x01, 0x82, 0xa5,
0xed, 0x17, 0x3f, 0, 0};
const std::string_view versionstamp(reinterpret_cast<const char*>(buffer.data() + 1), 10);
constexpr int64_t base_txn_id = 0x00060a97b45cfc00;
for (uint32_t seq = 0; seq <= 0xffff; ++seq) {
SCOPED_TRACE(seq);
buffer[9] = static_cast<unsigned char>(seq >> 8);
buffer[10] = static_cast<unsigned char>(seq);
int64_t txn_id = -1;
const int ret = doris::cloud::get_txn_id_from_fdb_ts(versionstamp, &txn_id);
if (seq < 1024) {
ASSERT_EQ(ret, 0);
EXPECT_EQ(txn_id, base_txn_id + seq);
} else {
ASSERT_EQ(ret, 2);
EXPECT_EQ(txn_id, -1);
}
}
}
TEST(TxnIdConvert, LegacyMemcpyCompatibility) {
ASSERT_NO_FATAL_FAILURE(
check_legacy_txn_id_compatibility(legacy_get_txn_id_from_fdb_ts<legacy_memcpy_load>));
check_invalid_txn_id_inputs(legacy_get_txn_id_from_fdb_ts<legacy_memcpy_load>);
}
TEST(TxnIdConvert, LegacyReinterpretCastCompatibility) {
#if defined(__x86_64__) || defined(__aarch64__)
ASSERT_NO_FATAL_FAILURE(
check_legacy_txn_id_compatibility(legacy_get_txn_id_from_fdb_ts<legacy_aliased_load>));
check_invalid_txn_id_inputs(legacy_get_txn_id_from_fdb_ts<legacy_aliased_load>);
#else
GTEST_SKIP() << "The original decoder requires a host that supports unaligned integer loads";
#endif
}