This is an automated email from the ASF dual-hosted git repository.

liaoxin01 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 91e566eaa9d [fix](cloud) Resolve packed segments when checking tablet 
encryption (#68063)
91e566eaa9d is described below

commit 91e566eaa9d54c6c5e8ea7799192df4db4b1632e
Author: Xin Liao <[email protected]>
AuthorDate: Thu Sep 17 00:31:08 2026 +0800

    [fix](cloud) Resolve packed segments when checking tablet encryption 
(#68063)
    
    ### What problem does this PR solve?
    
    Problem Summary:
    
    When small files are packed (`enable_packed_file`, on by default in
    cloud mode
    on S3), a rowset's segment no longer exists as a standalone object: its
    bytes
    are a slice inside a shared packed object, and the mapping from the
    segment
    path to that slice lives in `RowsetMetaPB.packed_slice_locations`.
    
    `GET /api/check_tablet_encryption?tablet_id=<id>&get_footer=true` fails
    on such
    a tablet with HTTP 500 and
    
    NOT_FOUND: failed to head s3 file
    <endpoint>/<bucket>/data/<tablet_id>/<rowset_id>_0.dat
    
    Root cause: `CheckEncryptionAction` opens segments through
    `RowsetMeta::physical_fs()`, which is the bare remote file system. The
    `PackedFileSystem` wrapper that resolves a segment path to its slice is
    only
    applied in `RowsetMeta::fs()`, so the action ends up asking S3 for an
    object
    that was never written. It cannot simply switch to `fs()` either: `fs()`
    also
    layers encryption on top, which would decrypt the segment and hide the
    very
    magic code and `FileEncryptionInfoPB` footer this check inspects.
    
    Fix: extract the packed wrapping out of `RowsetMeta::fs()` into a helper
    and
    expose `RowsetMeta::packed_physical_fs()` - packed-aware, but without
    the
    encryption layer - then use it at both read sites in
    `CheckEncryptionAction`.
    `PackedFileReader` reports the slice size and takes slice-relative
    offsets, so
    the existing footer read logic keeps working unchanged. After the fix
    the
    action returns the footer for packed and non-packed tablets alike.
    
    Two defects found while reading the same code are fixed as well:
    
    - `get_last_encrypt_footer()` dereferenced the file system without the
    null
    check its sibling `is_tablet_encrypted()` has, so a rowset whose storage
    resource cannot be resolved crashed the BE instead of returning an
    error.
    - The footer length buffer used `reserve()` where `resize()` was meant,
    writing
      8 bytes past the end of an empty vector.
---
 .../http/action/check_encryption_action.cpp        | 78 ++++++++++++++---
 be/src/storage/rowset/rowset_meta.cpp              | 59 +++++++------
 be/src/storage/rowset/rowset_meta.h                | 15 ++++
 be/test/storage/rowset/rowset_meta_test.cpp        | 98 ++++++++++++++++++++++
 4 files changed, 215 insertions(+), 35 deletions(-)

diff --git a/be/src/service/http/action/check_encryption_action.cpp 
b/be/src/service/http/action/check_encryption_action.cpp
index df395622edc..82d77e04f49 100644
--- a/be/src/service/http/action/check_encryption_action.cpp
+++ b/be/src/service/http/action/check_encryption_action.cpp
@@ -47,6 +47,12 @@ namespace doris {
 
 const std::string GET_FOOTER = "get_footer";
 
+// An encrypted file ends with a fixed-size footer region laid out as
+// [1 byte][uint64 encryption info length][encryption info][padding][8 byte 
magic code].
+constexpr size_t ENCRYPTION_FOOTER_SIZE = 256;
+constexpr size_t MAX_ENCRYPTION_INFO_SIZE =
+        ENCRYPTION_FOOTER_SIZE - sizeof(uint8_t) - sizeof(uint64_t);
+
 CheckEncryptionAction::CheckEncryptionAction(ExecEnv* exec_env, 
TPrivilegeHier::type hier,
                                              TPrivilegeType::type type)
         : HttpHandlerWithAuth(exec_env, hier, type) {}
@@ -68,7 +74,11 @@ Result<bool> is_tablet_encrypted(const BaseTabletSPtr& 
tablet) {
             rs_meta->end_version() == 1) {
             return;
         }
-        auto fs = rs_meta->physical_fs();
+        // Must not be `physical_fs()`: packed rowsets keep their segments as 
slices inside a
+        // shared object, and only the `PackedFileSystem` wrapper can resolve 
a segment path to
+        // that slice. `fs()` is not an option either, since it decrypts and 
would hide the
+        // encryption footer this check is looking for.
+        auto fs = rs_meta->packed_physical_fs();
         if (fs == nullptr) {
             st = Status::InternalError("failed to get fs for rowset: 
tablet={}, rs={}",
                                        tablet->tablet_id(), 
rs->rowset_id().to_string());
@@ -84,22 +94,27 @@ Result<bool> is_tablet_encrypted(const BaseTabletSPtr& 
tablet) {
             return;
         }
 
-        std::vector<std::string_view> file_paths;
+        // Owning strings: the V2 index path is built here and must outlive 
the loop below.
+        std::vector<std::string> file_paths;
         const auto& first_seg_path = maybe_seg_path.value();
         file_paths.emplace_back(first_seg_path);
         if (tablet->tablet_schema()->has_inverted_index() &&
             tablet->tablet_schema()->get_inverted_index_storage_format() == 
V2) {
-            std::string inverted_index_file_path = 
InvertedIndexDescriptor::get_index_file_path_v2(
-                    
InvertedIndexDescriptor::get_index_file_path_prefix(first_seg_path));
-            file_paths.emplace_back(inverted_index_file_path);
+            
file_paths.emplace_back(InvertedIndexDescriptor::get_index_file_path_v2(
+                    
InvertedIndexDescriptor::get_index_file_path_prefix(first_seg_path)));
         }
 
-        for (const auto path : file_paths) {
+        for (const auto& path : file_paths) {
             io::FileReaderSPtr reader;
             st = fs->open_file(path, &reader);
             if (!st) {
                 return;
             }
+            if (reader->size() < sizeof(uint64_t)) {
+                st = Status::Corruption("file is too small to hold a magic 
code: path={}, size={}",
+                                        path, reader->size());
+                return;
+            }
             std::vector<uint8_t> magic_code_buf;
             magic_code_buf.resize(sizeof(uint64_t));
             Slice magic_code(magic_code_buf.data(), sizeof(uint64_t));
@@ -108,6 +123,12 @@ Result<bool> is_tablet_encrypted(const BaseTabletSPtr& 
tablet) {
             if (!st) {
                 return;
             }
+            if (bytes_read != magic_code.size) {
+                st = Status::Corruption(
+                        "short read of the magic code: path={}, expected={}, 
got={}", path,
+                        magic_code.size, bytes_read);
+                return;
+            }
 
             std::vector<uint8_t> answer = {'A', 'B', 'C', 'D', 'E', 'A', 'B', 
'C'};
             is_encrypted &= Slice::mem_equal(answer.data(), magic_code.data, 
magic_code.size);
@@ -137,23 +158,58 @@ Result<std::string> get_last_encrypt_footer(const 
BaseTabletSPtr& tablet) {
     if (config::is_cloud_mode() && rs_meta->start_version() == 0 && 
rs_meta->end_version() == 1) {
         return "{}";
     }
-    auto fs = rs_meta->physical_fs();
+    // See the comment in `is_tablet_encrypted()` for why this is neither 
`physical_fs()`
+    // nor `fs()`.
+    auto fs = rs_meta->packed_physical_fs();
+    if (fs == nullptr) {
+        return ResultError(Status::InternalError("failed to get fs for rowset: 
tablet={}, rs={}",
+                                                 tablet->tablet_id(), 
rs->rowset_id().to_string()));
+    }
     io::FileReaderSPtr reader;
     RETURN_IF_ERROR_RESULT(fs->open_file(maybe_seg_path.value(), &reader));
 
+    // Every offset below is relative to the end of the file, so a file 
shorter than the footer
+    // region makes them underflow. On a packed slice such an underflow does 
not fail the read:
+    // it wraps into a neighbouring slice, whose bytes would then be decoded 
as this segment's
+    // footer.
+    if (reader->size() < ENCRYPTION_FOOTER_SIZE) {
+        return ResultError(Status::Corruption(
+                "file is too small to hold an encryption footer: path={}, 
size={}",
+                maybe_seg_path.value(), reader->size()));
+    }
+    const size_t footer_offset = reader->size() - ENCRYPTION_FOOTER_SIZE;
+
     std::vector<uint8_t> pb_len_buf;
-    pb_len_buf.reserve(sizeof(uint64_t));
+    pb_len_buf.resize(sizeof(uint64_t));
     Slice pb_len_slice(pb_len_buf.data(), sizeof(uint64_t));
     size_t bytes_read;
     RETURN_IF_ERROR_RESULT(
-            reader->read_at(reader->size() - 256 + sizeof(uint8_t), 
pb_len_slice, &bytes_read));
+            reader->read_at(footer_offset + sizeof(uint8_t), pb_len_slice, 
&bytes_read));
+    if (bytes_read != pb_len_slice.size) {
+        return ResultError(Status::Corruption(
+                "short read of the encryption info length: path={}, 
expected={}, got={}",
+                maybe_seg_path.value(), pb_len_slice.size, bytes_read));
+    }
     auto info_pb_size = decode_fixed64_le(pb_len_buf.data());
 
+    // `get_footer=true` reaches this parser even when the scan above found 
unencrypted files, so
+    // the decoded length may well be arbitrary bytes. It can never exceed 
what the footer holds.
+    if (info_pb_size > MAX_ENCRYPTION_INFO_SIZE) {
+        return ResultError(Status::Corruption(
+                "encryption info length {} exceeds the {} bytes available in 
the footer: path={}",
+                info_pb_size, MAX_ENCRYPTION_INFO_SIZE, 
maybe_seg_path.value()));
+    }
+
     std::vector<uint8_t> info_pb_buf;
     info_pb_buf.resize(info_pb_size);
     Slice pb_slice(info_pb_buf.data(), info_pb_size);
-    RETURN_IF_ERROR_RESULT(reader->read_at(
-            reader->size() - 256 + sizeof(uint8_t) + sizeof(uint64_t), 
pb_slice, &bytes_read));
+    RETURN_IF_ERROR_RESULT(reader->read_at(footer_offset + sizeof(uint8_t) + 
sizeof(uint64_t),
+                                           pb_slice, &bytes_read));
+    if (bytes_read != pb_slice.size) {
+        return ResultError(Status::Corruption(
+                "short read of the encryption info: path={}, expected={}, 
got={}",
+                maybe_seg_path.value(), pb_slice.size, bytes_read));
+    }
 
     FileEncryptionInfoPB info_pb;
     if (!info_pb.ParseFromArray(info_pb_buf.data(), 
static_cast<int>(info_pb_buf.size()))) {
diff --git a/be/src/storage/rowset/rowset_meta.cpp 
b/be/src/storage/rowset/rowset_meta.cpp
index e1259bd4c0f..05ca4be46b7 100644
--- a/be/src/storage/rowset/rowset_meta.cpp
+++ b/be/src/storage/rowset/rowset_meta.cpp
@@ -127,6 +127,39 @@ io::FileSystemSPtr RowsetMeta::physical_fs() {
     }
 }
 
+io::FileSystemSPtr RowsetMeta::_wrap_packed_fs(io::FileSystemSPtr fs) {
+    if (fs == nullptr || _rowset_meta_pb.packed_slice_locations_size() == 0) {
+        return fs;
+    }
+
+    std::unordered_map<std::string, io::PackedSliceLocation> index_map;
+    for (const auto& [path, index_pb] : 
_rowset_meta_pb.packed_slice_locations()) {
+        io::PackedSliceLocation index;
+        index.packed_file_path = index_pb.packed_file_path();
+        index.offset = index_pb.offset();
+        index.size = index_pb.size();
+        index.packed_file_size = index_pb.has_packed_file_size() ? 
index_pb.packed_file_size() : -1;
+        index.tablet_id = tablet_id();
+        index.rowset_id = _rowset_id.to_string();
+        index.resource_id = fs->id();
+        index_map[path] = index;
+    }
+    if (index_map.empty()) {
+        return fs;
+    }
+
+    io::PackedAppendContext append_info;
+    append_info.tablet_id = tablet_id();
+    append_info.rowset_id = _rowset_id.to_string();
+    append_info.txn_id = txn_id();
+    return std::make_shared<io::PackedFileSystem>(std::move(fs), 
std::move(index_map),
+                                                  std::move(append_info));
+}
+
+io::FileSystemSPtr RowsetMeta::packed_physical_fs() {
+    return _wrap_packed_fs(physical_fs());
+}
+
 io::FileSystemSPtr RowsetMeta::fs() {
     auto fs = physical_fs();
 
@@ -145,30 +178,8 @@ io::FileSystemSPtr RowsetMeta::fs() {
         return nullptr;
     }
 
-    // Apply packed file system first if enabled and index_map is not empty
-    io::FileSystemSPtr wrapped = fs;
-    if (_rowset_meta_pb.packed_slice_locations_size() > 0) {
-        std::unordered_map<std::string, io::PackedSliceLocation> index_map;
-        for (const auto& [path, index_pb] : 
_rowset_meta_pb.packed_slice_locations()) {
-            io::PackedSliceLocation index;
-            index.packed_file_path = index_pb.packed_file_path();
-            index.offset = index_pb.offset();
-            index.size = index_pb.size();
-            index.packed_file_size =
-                    index_pb.has_packed_file_size() ? 
index_pb.packed_file_size() : -1;
-            index.tablet_id = tablet_id();
-            index.rowset_id = _rowset_id.to_string();
-            index.resource_id = wrapped->id();
-            index_map[path] = index;
-        }
-        if (!index_map.empty()) {
-            io::PackedAppendContext append_info;
-            append_info.tablet_id = tablet_id();
-            append_info.rowset_id = _rowset_id.to_string();
-            append_info.txn_id = txn_id();
-            wrapped = std::make_shared<io::PackedFileSystem>(wrapped, 
index_map, append_info);
-        }
-    }
+    // Apply packed file system first
+    io::FileSystemSPtr wrapped = _wrap_packed_fs(std::move(fs));
 
     // Then apply encryption on top
     wrapped = io::make_file_system(wrapped, algorithm.value());
diff --git a/be/src/storage/rowset/rowset_meta.h 
b/be/src/storage/rowset/rowset_meta.h
index 9aa4b471f0c..9255cdb9a09 100644
--- a/be/src/storage/rowset/rowset_meta.h
+++ b/be/src/storage/rowset/rowset_meta.h
@@ -72,8 +72,19 @@ public:
     // Note that if the resource id cannot be found for the corresponding 
remote file system, nullptr will be returned.
     MOCK_FUNCTION io::FileSystemSPtr fs();
 
+    // The bare file system holding this rowset's files. It resolves neither 
packed files nor
+    // encryption, so it cannot open a segment whose bytes live inside a 
packed object. Prefer
+    // `fs()`, or `packed_physical_fs()` when the raw bytes are what you are 
after.
     io::FileSystemSPtr physical_fs();
 
+    // Same as `physical_fs()`, but additionally wrapped with 
`PackedFileSystem` when this
+    // rowset's files are packed into shared objects, so that segment/index 
paths still
+    // resolve. Unlike `fs()`, no encryption layer is applied, i.e. reads 
return the raw
+    // on-disk bytes. Callers that inspect the physical layout of a file 
(encryption footer,
+    // magic code, ...) must use this instead of `physical_fs()`, otherwise 
packed files
+    // cannot be opened at all.
+    io::FileSystemSPtr packed_physical_fs();
+
     Result<const StorageResource*> remote_storage_resource();
 
     void set_remote_storage_resource(StorageResource resource);
@@ -572,6 +583,10 @@ public:
     }
 
 private:
+    // Wraps `fs` with `PackedFileSystem` if this rowset has packed slice 
locations,
+    // otherwise returns `fs` unchanged.
+    io::FileSystemSPtr _wrap_packed_fs(io::FileSystemSPtr fs);
+
     bool _deserialize_from_pb(std::string_view value);
 
     bool _serialize_to_pb(std::string* value);
diff --git a/be/test/storage/rowset/rowset_meta_test.cpp 
b/be/test/storage/rowset/rowset_meta_test.cpp
index 45ca3a5f251..633c4f4999a 100644
--- a/be/test/storage/rowset/rowset_meta_test.cpp
+++ b/be/test/storage/rowset/rowset_meta_test.cpp
@@ -31,9 +31,14 @@
 #include "common/status.h"
 #include "cpp/sync_point.h"
 #include "gtest/gtest_pred_impl.h"
+#include "io/fs/file_reader.h"
+#include "io/fs/file_system.h"
+#include "io/fs/local_file_system.h"
 #include "storage/olap_common.h"
 #include "storage/olap_meta.h"
 #include "storage/tablet/tablet_schema.h"
+#include "util/defer_op.h"
+#include "util/slice.h"
 
 using ::testing::_;
 using ::testing::Return;
@@ -683,4 +688,97 @@ TEST_F(RowsetMetaTest, TestSegmentMetaView) {
     EXPECT_EQ(segment_ids, std::vector<int64_t>({0, 2, 5}));
 }
 
+TEST_F(RowsetMetaTest, TestPackedPhysicalFs) {
+    RowsetMeta rowset_meta;
+    EXPECT_TRUE(rowset_meta.init_from_json(_json_rowset_meta));
+
+    // Without packed slice locations there is nothing to map, so the physical 
fs is returned
+    // unchanged.
+    EXPECT_EQ(rowset_meta.packed_physical_fs(), rowset_meta.physical_fs());
+
+    // Lay out a packed file the way PackedFileWriter does: the segment bytes 
live at a
+    // non-zero offset inside a shared object, and no object exists at the 
segment path.
+    const std::string packed_file_path = "./packed_file.dat";
+    const std::string padding(7, 'x');
+    const std::string segment_content = "segment-payload";
+    {
+        std::ofstream out(packed_file_path, std::ios::binary);
+        out << padding << segment_content;
+    }
+    Defer defer {[&]() { 
static_cast<void>(std::filesystem::remove(packed_file_path)); }};
+
+    const std::string segment_path = "./data/15673/540081_0.dat";
+    io::FileReaderSPtr reader;
+    EXPECT_FALSE(rowset_meta.physical_fs()->open_file(segment_path, 
&reader).ok());
+
+    rowset_meta.add_packed_slice_location(
+            segment_path, packed_file_path, cast_set<int64_t>(padding.size()),
+            cast_set<int64_t>(segment_content.size()),
+            cast_set<int64_t>(padding.size() + segment_content.size()));
+
+    // The packed-aware fs resolves the segment path to its slice, while still 
handing out the
+    // raw bytes (no decryption layer on top).
+    auto fs = rowset_meta.packed_physical_fs();
+    ASSERT_NE(fs, nullptr);
+    ASSERT_TRUE(fs->open_file(segment_path, &reader).ok());
+
+    // Size and offsets stay slice-relative, so callers can address the 
segment as if it were a
+    // standalone file.
+    EXPECT_EQ(reader->size(), segment_content.size());
+    std::string buf(segment_content.size(), '\0');
+    size_t bytes_read = 0;
+    ASSERT_TRUE(reader->read_at(0, Slice(buf.data(), buf.size()), 
&bytes_read).ok());
+    EXPECT_EQ(bytes_read, segment_content.size());
+    EXPECT_EQ(buf, segment_content);
+
+    // A path with no packed slice location falls through to the physical fs, 
so non-packed
+    // rowsets and files keep their previous behaviour.
+    io::FileReaderSPtr missing;
+    EXPECT_FALSE(fs->open_file("./data/15673/540081_1.dat", &missing).ok());
+}
+
+TEST_F(RowsetMetaTest, TestPackedPhysicalFsResolvesV2IndexFile) {
+    RowsetMeta rowset_meta;
+    EXPECT_TRUE(rowset_meta.init_from_json(_json_rowset_meta));
+
+    // A V2 inverted index file is packed alongside its segment, as its own 
slice of the same
+    // packed object. `is_tablet_encrypted()` reads both, so both must resolve.
+    const std::string packed_file_path = "./packed_file_with_index.dat";
+    const std::string segment_content = "segment-payload";
+    const std::string index_content = "index-payload";
+    {
+        std::ofstream out(packed_file_path, std::ios::binary);
+        out << segment_content << index_content;
+    }
+    Defer defer {[&]() { 
static_cast<void>(std::filesystem::remove(packed_file_path)); }};
+
+    const std::string segment_path = "./data/15673/540081_0.dat";
+    const std::string index_path = "./data/15673/540081_0.idx";
+    const int64_t packed_file_size =
+            cast_set<int64_t>(segment_content.size() + index_content.size());
+    rowset_meta.add_packed_slice_location(segment_path, packed_file_path, 0,
+                                          
cast_set<int64_t>(segment_content.size()),
+                                          packed_file_size);
+    rowset_meta.add_packed_slice_location(
+            index_path, packed_file_path, 
cast_set<int64_t>(segment_content.size()),
+            cast_set<int64_t>(index_content.size()), packed_file_size);
+
+    auto fs = rowset_meta.packed_physical_fs();
+    ASSERT_NE(fs, nullptr);
+
+    auto read_all = [&](const std::string& path, size_t expected_size) {
+        io::FileReaderSPtr reader;
+        EXPECT_TRUE(fs->open_file(path, &reader).ok());
+        EXPECT_EQ(reader->size(), expected_size);
+        std::string buf(expected_size, '\0');
+        size_t bytes_read = 0;
+        EXPECT_TRUE(reader->read_at(0, Slice(buf.data(), buf.size()), 
&bytes_read).ok());
+        EXPECT_EQ(bytes_read, expected_size);
+        return buf;
+    };
+
+    EXPECT_EQ(read_all(segment_path, segment_content.size()), segment_content);
+    EXPECT_EQ(read_all(index_path, index_content.size()), index_content);
+}
+
 } // namespace doris


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to