Skip to content

Commit a19489c

Browse files
authored
RCORE-1969 Fix bundled encrypted Realm regression (#7535)
* non-functional changes * fix a regression opening a bundled encrypted Realm on a device with a different page size * only request a page of memory if encryption is used * fix several other issues in encrypted file portability due to a miscalculation * add a stack trace to DecryptionFailed for debugging * mmap produces page-aligned addresses. In order to run tests which simulate a different page_size we need to remove the assumption that mmap addresses are page-aligned. * touchups * cleanup
1 parent f522692 commit a19489c

File tree

12 files changed

+216
-30
lines changed

12 files changed

+216
-30
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
* SyncUser::all_sessions() included sessions in every state *except* for waiting for access token, which was weirdly inconsistent. It now includes all sessions. ([PR #7300](https://github.com/realm/realm-core/pull/7300)).
1212
* App::all_users() included logged out users only if they were logged out while the App instance existed. It now always includes all logged out users. ([PR #7300](https://github.com/realm/realm-core/pull/7300)).
1313
* Deleting the active user left the active user unset rather than selecting another logged-in user as the active user like logging out and removing users did. ([PR #7300](https://github.com/realm/realm-core/pull/7300)).
14+
* Fixed several issues around encrypted file portability (copying a "bundled" encrypted Realm from one device to another):
15+
* Fixed `Assertion failed: new_size % (1ULL << m_page_shift) == 0` when opening an encrypted Realm less than 64Mb that was generated on a platform with a different page size than the current platform. ([#7322](https://github.com/realm/realm-core/issues/7322), since v13.17.1)
16+
* Fixed a `DecryptionFailed` exception thrown when opening a small (<4k of data) Realm generated on a device with a page size of 4k if it was bundled and opened on a device with a larger page size (since the beginning).
17+
* Fixed an issue during a subsequent open of an encrypted Realm for some rare allocation patterns when the top ref was within ~50 bytes of the end of a page. This could manifest as a DecryptionFailed exception or as an assertion: `encrypted_file_mapping.hpp:183: Assertion failed: local_ndx < m_page_state.size()`. ([#7319](https://github.com/realm/realm-core/issues/7319))
1418
* Fix compilation errors when using command-line `swift build` ([#7587](https://github.com/realm/realm-core/pull/7587), since v14.5.1).
1519
* Fixed crash when integrating removal of already removed dictionary key ([#7488](https://github.com/realm/realm-core/issues/7488), since v10.0.0).
1620
* Non-streaming download sync progress notification is fixed for flexible sync Realms where before it was sometimes stopping to emit values right after the registration of the callback (PR [#7561](https://github.com/realm/realm-core/issues/7561)).

src/realm/alloc.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ class Allocator {
154154

155155
static constexpr size_t section_size() noexcept
156156
{
157-
return 1 << section_shift;
157+
return 1UL << section_shift;
158158
}
159159

160160
protected:

src/realm/alloc_slab.cpp

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ size_t SlabAlloc::get_total_slab_size() noexcept
7474

7575
SlabAlloc::SlabAlloc()
7676
{
77-
m_initial_section_size = 1UL << section_shift; // page_size();
77+
m_initial_section_size = section_size();
7878
m_free_space_state = free_space_Clean;
7979
m_baseline = 0;
8080
}
@@ -718,16 +718,9 @@ bool SlabAlloc::align_filesize_for_mmap(ref_type top_ref, Config& cfg)
718718
// check if online compaction allows us to shrink the file:
719719
if (top_ref) {
720720
// Get the expected file size by looking up logical file size stored in top array
721-
constexpr size_t max_top_size = (Group::s_file_size_ndx + 1) * 8 + sizeof(Header);
722-
size_t top_page_base = top_ref & ~(page_size() - 1);
723-
size_t top_offset = top_ref - top_page_base;
724-
size_t map_size = std::min(max_top_size + top_offset, size - top_page_base);
725-
File::Map<char> map_top(m_file, top_page_base, File::access_ReadOnly, map_size, 0, m_write_observer);
726-
realm::util::encryption_read_barrier(map_top, top_offset, max_top_size);
727-
auto top_header = map_top.get_addr() + top_offset;
728-
auto top_data = NodeHeader::get_data_from_header(top_header);
729-
auto w = NodeHeader::get_width_from_header(top_header);
730-
auto logical_size = size_t(get_direct(top_data, w, Group::s_file_size_ndx)) >> 1;
721+
Array top(*this);
722+
top.init_from_ref(top_ref);
723+
size_t logical_size = Group::get_logical_file_size(top);
731724
// make sure we're page aligned, so the code below doesn't first
732725
// truncate the file, then expand it again
733726
expected_size = round_up_to_page_size(logical_size);
@@ -738,7 +731,6 @@ bool SlabAlloc::align_filesize_for_mmap(ref_type top_ref, Config& cfg)
738731
detach(true); // keep m_file open
739732
m_file.resize(expected_size);
740733
m_file.close();
741-
size = expected_size;
742734
return true;
743735
}
744736

@@ -849,7 +841,7 @@ ref_type SlabAlloc::attach_file(const std::string& path, Config& cfg, util::Writ
849841
if (REALM_UNLIKELY(cfg.read_only))
850842
throw InvalidDatabase("Read-only access to empty Realm file", path);
851843

852-
size_t initial_size = page_size(); // m_initial_section_size;
844+
size_t initial_size = page_size();
853845
// exFAT does not allocate a unique id for the file until it is non-empty. It must be
854846
// valid at this point because File::get_unique_id() is used to distinguish
855847
// mappings_for_file in the encryption layer. So the prealloc() is required before
@@ -882,6 +874,12 @@ ref_type SlabAlloc::attach_file(const std::string& path, Config& cfg, util::Writ
882874
DetachGuard dg(*this);
883875

884876
reset_free_space_tracking();
877+
878+
// the file could have been produced on a device with a different
879+
// page size than our own so don't expect the size to be aligned
880+
if (cfg.encryption_key && size != 0 && size != round_up_to_page_size(size)) {
881+
size = round_up_to_page_size(size);
882+
}
885883
update_reader_view(size);
886884
REALM_ASSERT(m_mappings.size());
887885
m_data = m_mappings[0].primary_mapping.get_addr();

src/realm/util/encrypted_file_mapping.cpp

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include <cstdlib>
3131
#include <algorithm>
3232
#include <chrono>
33+
#include <sstream>
3334
#include <stdexcept>
3435
#include <string_view>
3536
#include <system_error>
@@ -81,6 +82,13 @@ SharedFileInfo::SharedFileInfo(const uint8_t* key)
8182
// the ciphertext, we can still determine that we should use the old IV, since
8283
// the ciphertext's hash will match the old ciphertext.
8384

85+
// This produces a file on disk with the following layout:
86+
// 4k block of metadata (up to 64 iv_table instances stored here)
87+
// 64 * 4k blocks of data (up to 262144 bytes of data are stored here)
88+
// 4k block of metadata
89+
// 64 * 4k blocks of data
90+
// ...
91+
8492
struct iv_table {
8593
uint32_t iv1 = 0;
8694
std::array<uint8_t, 28> hmac1 = {};
@@ -102,6 +110,8 @@ const size_t block_size = 4096;
102110

103111
const size_t metadata_size = sizeof(iv_table);
104112
const size_t blocks_per_metadata_block = block_size / metadata_size;
113+
static_assert(metadata_size == 64,
114+
"changing the size of the metadata breaks compatibility with existing Realm files");
105115

106116
// map an offset in the data to the actual location in the file
107117
template <typename Int>
@@ -152,6 +162,10 @@ size_t check_read(FileDesc fd, off_t pos, void* dst, size_t len)
152162

153163
} // anonymous namespace
154164

165+
// first block is iv data, second page is data
166+
static_assert(c_min_encrypted_file_size == 2 * block_size,
167+
"chaging the block size breaks encrypted file portability");
168+
155169
AESCryptor::AESCryptor(const uint8_t* key)
156170
: m_rw_buffer(new char[block_size])
157171
, m_dst_buffer(new char[block_size])
@@ -683,7 +697,9 @@ void EncryptedFileMapping::refresh_page(size_t local_page_ndx, size_t required)
683697
memset(addr + actual, 0x55, size - actual);
684698
}
685699
else {
686-
throw DecryptionFailed();
700+
size_t fs = to_size_t(File::get_size_static(m_file.fd));
701+
throw DecryptionFailed(
702+
util::format("failed to decrypt block %1 in file of size %2", local_page_ndx + m_first_page, fs));
687703
}
688704
}
689705
}
@@ -1025,7 +1041,7 @@ void EncryptedFileMapping::read_barrier(const void* addr, size_t size, Header_to
10251041

10261042
void EncryptedFileMapping::extend_to(size_t offset, size_t new_size)
10271043
{
1028-
REALM_ASSERT(new_size % (1ULL << m_page_shift) == 0);
1044+
REALM_ASSERT_EX(new_size % page_size() == 0, new_size, page_size());
10291045
size_t num_pages = new_size >> m_page_shift;
10301046
m_page_state.resize(num_pages, PageState::Clean);
10311047
m_chunk_dont_scan.resize((num_pages + page_to_chunk_factor - 1) >> page_to_chunk_shift, false);
@@ -1084,3 +1100,13 @@ File::SizeType data_size_to_encrypted_size(File::SizeType size) noexcept
10841100
}
10851101
} // namespace realm::util
10861102
#endif // REALM_ENABLE_ENCRYPTION
1103+
1104+
namespace realm::util {
1105+
std::string DecryptionFailed::get_message_with_bt(std::string_view msg)
1106+
{
1107+
auto bt = Backtrace::capture();
1108+
std::stringstream ss;
1109+
bt.print(ss);
1110+
return util::format("Decryption failed: %1\n%2\n", msg, ss.str());
1111+
}
1112+
} // namespace realm::util

src/realm/util/encrypted_file_mapping.hpp

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ class EncryptedFileMapping {
104104
{
105105
m_observer = observer;
106106
}
107+
#if REALM_DEBUG
108+
std::string print_debug();
109+
#endif // REALM_DEBUG
107110

108111
private:
109112
SharedFileInfo& m_file;
@@ -167,12 +170,13 @@ class EncryptedFileMapping {
167170

168171
inline size_t EncryptedFileMapping::get_offset_of_address(const void* addr) const
169172
{
170-
return reinterpret_cast<size_t>(addr) & ((1 << m_page_shift) - 1);
173+
REALM_ASSERT_3(reinterpret_cast<size_t>(addr), >=, reinterpret_cast<size_t>(m_addr));
174+
return (reinterpret_cast<size_t>(addr) - reinterpret_cast<size_t>(m_addr)) & ((1ULL << m_page_shift) - 1);
171175
}
172176

173177
inline size_t EncryptedFileMapping::get_local_index_of_address(const void* addr, size_t offset) const
174178
{
175-
REALM_ASSERT_EX(addr >= m_addr, addr, m_addr);
179+
REALM_ASSERT_EX(addr >= m_addr, size_t(addr), size_t(m_addr));
176180

177181
size_t local_ndx =
178182
((reinterpret_cast<uintptr_t>(addr) - reinterpret_cast<uintptr_t>(m_addr) + offset) >> m_page_shift);
@@ -188,6 +192,46 @@ inline bool EncryptedFileMapping::contains_page(size_t page_in_file) const
188192
return page_in_file >= m_first_page && page_in_file - m_first_page < m_page_state.size();
189193
}
190194

195+
#if REALM_DEBUG
196+
inline std::string EncryptedFileMapping::print_debug()
197+
{
198+
auto state_name = [](const PageState& s) -> std::string {
199+
if (s == PageState::Clean) {
200+
return "Clean";
201+
}
202+
std::string state = "{";
203+
if (s & PageState::Touched) {
204+
state += "Touched";
205+
}
206+
if (s & PageState::UpToDate) {
207+
state += "UpToDate";
208+
}
209+
if (s & PageState::StaleIV) {
210+
state += "StaleIV";
211+
}
212+
if (s & PageState::Writable) {
213+
state += "Writable";
214+
}
215+
if (s & PageState::Dirty) {
216+
state += "Dirty";
217+
}
218+
state += "}";
219+
return state;
220+
};
221+
std::string page_states;
222+
for (PageState& s : m_page_state) {
223+
if (!page_states.empty()) {
224+
page_states += ", ";
225+
}
226+
page_states += state_name(s);
227+
}
228+
return util::format("%1 pages from %2 to %3: %4", m_page_state.size(), m_first_page,
229+
m_page_state.size() + m_first_page, page_states);
230+
}
231+
#endif // REALM_DEBUG
232+
233+
constexpr inline size_t c_min_encrypted_file_size = 8192;
234+
191235
} // namespace realm::util
192236

193237
#endif // REALM_ENABLE_ENCRYPTION
@@ -197,13 +241,14 @@ namespace realm::util {
197241
/// contain valid encrypted data
198242
struct DecryptionFailed : FileAccessError {
199243
DecryptionFailed()
200-
: FileAccessError(ErrorCodes::DecryptionFailed, "Decryption failed", std::string(), 0)
244+
: FileAccessError(ErrorCodes::DecryptionFailed, get_message_with_bt(""), std::string(), 0)
201245
{
202246
}
203247
DecryptionFailed(const std::string& msg)
204-
: FileAccessError(ErrorCodes::DecryptionFailed, util::format("Decryption failed: '%1'", msg), std::string())
248+
: FileAccessError(ErrorCodes::DecryptionFailed, get_message_with_bt(msg), std::string())
205249
{
206250
}
251+
static std::string get_message_with_bt(std::string_view msg);
207252
};
208253
} // namespace realm::util
209254

src/realm/util/file.cpp

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <realm/util/file_mapper.hpp>
2424

2525
#include <algorithm>
26+
#include <atomic>
2627
#include <cerrno>
2728
#include <climits>
2829
#include <cstddef>
@@ -49,6 +50,7 @@
4950
using namespace realm::util;
5051

5152
namespace {
53+
constexpr size_t c_min_supported_page_size = 4096;
5254
size_t get_page_size()
5355
{
5456
#ifdef _WIN32
@@ -60,13 +62,13 @@ size_t get_page_size()
6062
#else
6163
long size = sysconf(_SC_PAGESIZE);
6264
#endif
63-
REALM_ASSERT(size > 0 && size % 4096 == 0);
65+
REALM_ASSERT(size > 0 && size % c_min_supported_page_size == 0);
6466
return static_cast<size_t>(size);
6567
}
6668

6769
// This variable exists such that page_size() can return the page size without having to make any system calls.
6870
// It could also have been a static local variable, but Valgrind/Helgrind gives a false error on that.
69-
size_t cached_page_size = get_page_size();
71+
std::atomic<size_t> cached_page_size = get_page_size();
7072

7173
bool for_each_helper(const std::string& path, const std::string& dir, realm::util::File::ForEachHandler& handler)
7274
{
@@ -410,7 +412,18 @@ std::string make_temp_file(const char* prefix)
410412

411413
size_t page_size()
412414
{
413-
return cached_page_size;
415+
return cached_page_size.load(std::memory_order::memory_order_relaxed);
416+
}
417+
418+
OnlyForTestingPageSizeChange::OnlyForTestingPageSizeChange(size_t new_page_size)
419+
{
420+
REALM_ASSERT(new_page_size % c_min_supported_page_size == 0);
421+
cached_page_size = new_page_size;
422+
}
423+
424+
OnlyForTestingPageSizeChange::~OnlyForTestingPageSizeChange()
425+
{
426+
cached_page_size = get_page_size();
414427
}
415428

416429
void File::open_internal(const std::string& path, AccessMode a, CreateMode c, int flags, bool* success)

src/realm/util/file.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ std::string make_temp_file(const char* prefix);
114114

115115
size_t page_size();
116116

117+
struct OnlyForTestingPageSizeChange {
118+
OnlyForTestingPageSizeChange(size_t new_page_size);
119+
~OnlyForTestingPageSizeChange();
120+
};
117121

118122
/// This class provides a RAII abstraction over the concept of a file
119123
/// descriptor (or file handle).

src/realm/util/file_mapper.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@ int64_t fetch_value_in_file(const std::string& fname, const char* scan_pattern)
128128
return PageReclaimGovernor::no_match;
129129
}
130130

131-
132131
/* Default reclaim governor
133132
*
134133
*/
@@ -485,13 +484,14 @@ SharedFileInfo* get_file_info_for_file(File& file)
485484
return it->info.get();
486485
}
487486

488-
489487
namespace {
490488
EncryptedFileMapping* add_mapping(void* addr, size_t size, const FileAttributes& file, size_t file_offset)
491489
{
492490
size_t fs = to_size_t(File::get_size_static(file.fd));
493-
if (fs > 0 && fs < page_size())
494-
throw DecryptionFailed();
491+
if (fs > 0 && fs < c_min_encrypted_file_size)
492+
throw DecryptionFailed(
493+
util::format("file size %1 is less than the minimum encrypted file size of %2 for '%3'", fs,
494+
c_min_encrypted_file_size, file.path));
495495

496496
LockGuard lock(mapping_mutex);
497497

src/realm/util/file_mapper.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ File::SizeType encrypted_size_to_data_size(File::SizeType size) noexcept;
200200
File::SizeType data_size_to_encrypted_size(File::SizeType size) noexcept;
201201

202202
size_t round_up_to_page_size(size_t size) noexcept;
203+
203204
} // namespace util
204205
} // namespace realm
205206
#endif

test/test_alloc.cpp

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,10 @@ NONCONCURRENT_TEST_IF(Alloc_MapFailureRecovery, _impl::SimulatedFailure::is_enab
325325
Group().write(path);
326326

327327
SlabAlloc::Config cfg;
328+
// mimic a DB initiating a session
329+
cfg.is_shared = true;
330+
cfg.session_initiator = true;
331+
328332
SlabAlloc alloc;
329333

330334
{ // Initial Header mapping fails
@@ -350,19 +354,31 @@ NONCONCURRENT_TEST_IF(Alloc_MapFailureRecovery, _impl::SimulatedFailure::is_enab
350354
_impl::SimulatedFailure::prime_mmap(nullptr);
351355
auto top_ref = alloc.attach_file(path, cfg);
352356
CHECK(alloc.is_attached());
357+
if (cfg.encryption_key) {
358+
// the internal baseline has capacity for a page but it is not entirely backed by a file (yet)
359+
CHECK_EQUAL(alloc.get_baseline(), page_size);
360+
}
361+
else {
362+
CHECK_NOT_EQUAL(alloc.get_baseline(), page_size);
363+
}
353364
// file has not (yet) been expanded to match:
354-
CHECK(alloc.get_baseline() != page_size);
365+
CHECK_NOT_EQUAL(alloc.get_file_size(), page_size);
355366
// convert before aligning file size
356367
alloc.convert_from_streaming_form(top_ref);
357368
// file is expanded:
358369
CHECK(alloc.align_filesize_for_mmap(top_ref, cfg));
370+
CHECK(!alloc.is_attached());
359371
// and consequently needs to be reattached:
360372
alloc.detach();
361373
alloc.attach_file(path, cfg);
362374
alloc.init_mapping_management(1);
375+
CHECK(alloc.is_attached());
376+
// now both the allocator and the backing file agree on the capacity
377+
CHECK_EQUAL(alloc.get_baseline(), page_size);
378+
CHECK_EQUAL(alloc.get_file_size(), page_size);
363379
}
364380

365-
{ // Extendind the first mapping
381+
{ // Extending the first mapping
366382
const auto initial_baseline = alloc.get_baseline();
367383
const auto initial_version = alloc.get_mapping_version();
368384
const char* initial_translated = alloc.translate(1000);

0 commit comments

Comments
 (0)