• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

realm / realm-core / james.stone_543

10 May 2024 08:59PM UTC coverage: 90.808% (-0.03%) from 90.837%
james.stone_543

Pull #7689

Evergreen

ironage
fix a test on windows
Pull Request #7689: RNET-1141 multiprocess encryption for writers with different page sizes

102068 of 181122 branches covered (56.35%)

202 of 223 new or added lines in 3 files covered. (90.58%)

119 existing lines in 13 files now uncovered.

214742 of 236479 relevant lines covered (90.81%)

5817458.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.08
/test/test_encrypted_file_mapping.cpp
1
/*************************************************************************
2
 *
3
 * Copyright 2016 Realm Inc.
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 * http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 *
17
 **************************************************************************/
18

19
#include "testsettings.hpp"
20

21
#if defined(TEST_ENCRYPTED_FILE_MAPPING)
22

23
#include <realm.hpp>
24
#include <realm/util/aes_cryptor.hpp>
25
#include <realm/util/encrypted_file_mapping.hpp>
26
#include <realm/util/file.hpp>
27

28
#include "test.hpp"
29
#include "util/spawned_process.hpp"
30

31
// Test independence and thread-safety
32
// -----------------------------------
33
//
34
// All tests must be thread safe and independent of each other. This
35
// is required because it allows for both shuffling of the execution
36
// order and for parallelized testing.
37
//
38
// In particular, avoid using std::rand() since it is not guaranteed
39
// to be thread safe. Instead use the API offered in
40
// `test/util/random.hpp`.
41
//
42
// All files created in tests must use the TEST_PATH macro (or one of
43
// its friends) to obtain a suitable file system path. See
44
// `test/util/test_path.hpp`.
45
//
46
//
47
// Debugging and the ONLY() macro
48
// ------------------------------
49
//
50
// A simple way of disabling all tests except one called `Foo`, is to
51
// replace TEST(Foo) with ONLY(Foo) and then recompile and rerun the
52
// test suite. Note that you can also use filtering by setting the
53
// environment varible `UNITTEST_FILTER`. See `README.md` for more on
54
// this.
55
//
56
// Another way to debug a particular test, is to copy that test into
57
// `experiments/testcase.cpp` and then run `sh build.sh
58
// check-testcase` (or one of its friends) from the command line.
59

60
#if REALM_ENABLE_ENCRYPTION
61

62
using namespace realm;
63
using namespace realm::util;
64
using realm::FileDesc;
65

66
namespace {
67
const uint8_t test_key[] = "1234567890123456789012345678901123456789012345678901234567890123";
68
}
69

70
TEST(EncryptedFile_CryptorBasic)
71
{
2✔
72
    TEST_PATH(path);
2✔
73

74
    AESCryptor cryptor(test_key);
2✔
75
    cryptor.set_file_size(16);
2✔
76
    const char data[4096] = "test data";
2✔
77
    char buffer[4096];
2✔
78

79
    File file(path, realm::util::File::mode_Write);
2✔
80
    cryptor.write(file.get_descriptor(), 0, data, sizeof(data));
2✔
81
    cryptor.read(file.get_descriptor(), 0, buffer, sizeof(buffer));
2✔
82
    CHECK(memcmp(buffer, data, strlen(data)) == 0);
2✔
83
}
2✔
84

85
TEST(EncryptedFile_CryptorRepeatedWrites)
86
{
2✔
87
    TEST_PATH(path);
2✔
88
    AESCryptor cryptor(test_key);
2✔
89
    cryptor.set_file_size(16);
2✔
90

91
    const char data[4096] = "test data";
2✔
92
    char raw_buffer_1[8192] = {0}, raw_buffer_2[8192] = {0};
2✔
93
    File file(path, realm::util::File::mode_Write);
2✔
94

95
    cryptor.write(file.get_descriptor(), 0, data, sizeof(data));
2✔
96
    file.seek(0);
2✔
97
    ssize_t actual_read_1 = file.read(raw_buffer_1, sizeof(raw_buffer_1));
2✔
98
    CHECK_EQUAL(actual_read_1, sizeof(raw_buffer_1));
2✔
99

100
    cryptor.write(file.get_descriptor(), 0, data, sizeof(data));
2✔
101
    file.seek(0);
2✔
102
    ssize_t actual_read_2 = file.read(raw_buffer_2, sizeof(raw_buffer_2));
2✔
103
    CHECK_EQUAL(actual_read_2, sizeof(raw_buffer_2));
2✔
104

105
    CHECK(memcmp(raw_buffer_1, raw_buffer_2, sizeof(raw_buffer_1)) != 0);
2✔
106
}
2✔
107

108
TEST(EncryptedFile_SeparateCryptors)
109
{
2✔
110
    TEST_PATH(path);
2✔
111

112
    const char data[4096] = "test data";
2✔
113
    char buffer[4096];
2✔
114

115
    File file(path, realm::util::File::mode_Write);
2✔
116
    {
2✔
117
        AESCryptor cryptor(test_key);
2✔
118
        cryptor.set_file_size(16);
2✔
119
        cryptor.write(file.get_descriptor(), 0, data, sizeof(data));
2✔
120
    }
2✔
121
    {
2✔
122
        AESCryptor cryptor(test_key);
2✔
123
        cryptor.set_file_size(16);
2✔
124
        cryptor.read(file.get_descriptor(), 0, buffer, sizeof(buffer));
2✔
125
    }
2✔
126

127
    CHECK(memcmp(buffer, data, strlen(data)) == 0);
2✔
128
}
2✔
129

130
TEST(EncryptedFile_InterruptedWrite)
131
{
2✔
132
    TEST_PATH(path);
2✔
133

134
    const char data[4096] = "test data";
2✔
135

136
    File file(path, realm::util::File::mode_Write);
2✔
137
    {
2✔
138
        AESCryptor cryptor(test_key);
2✔
139
        cryptor.set_file_size(16);
2✔
140
        cryptor.write(file.get_descriptor(), 0, data, sizeof(data));
2✔
141
    }
2✔
142

143
    // Fake an interrupted write which updates the IV table but not the data
144
    char buffer[4096];
2✔
145
    file.seek(0);
2✔
146
    size_t actual_pread = file.read(buffer, 64);
2✔
147
    CHECK_EQUAL(actual_pread, 64);
2✔
148

149
    memcpy(buffer + 32, buffer, 32);
2✔
150
    buffer[5]++; // first byte of "hmac1" field in iv table
2✔
151
    file.seek(0);
2✔
152
    file.write(buffer, 64);
2✔
153

154
    {
2✔
155
        AESCryptor cryptor(test_key);
2✔
156
        cryptor.set_file_size(16);
2✔
157
        cryptor.read(file.get_descriptor(), 0, buffer, sizeof(buffer));
2✔
158
        CHECK(memcmp(buffer, data, strlen(data)) == 0);
2✔
159
    }
2✔
160
}
2✔
161

162
TEST(EncryptedFile_LargePages)
163
{
2✔
164
    TEST_PATH(path);
2✔
165

166
    char data[4096 * 4];
2✔
167
    for (size_t i = 0; i < sizeof(data); ++i)
32,770✔
168
        data[i] = static_cast<char>(i);
32,768✔
169

170
    AESCryptor cryptor(test_key);
2✔
171
    cryptor.set_file_size(sizeof(data));
2✔
172
    char buffer[sizeof(data)];
2✔
173

174
    File file(path, realm::util::File::mode_Write);
2✔
175
    cryptor.write(file.get_descriptor(), 0, data, sizeof(data));
2✔
176
    cryptor.read(file.get_descriptor(), 0, buffer, sizeof(buffer));
2✔
177
    CHECK(memcmp(buffer, data, sizeof(data)) == 0);
2✔
178
}
2✔
179

180
TEST(EncryptedFile_IVRefreshing)
181
{
2✔
182
    using IVPageStates = realm::util::FlatMap<size_t, IVRefreshState>;
2✔
183
    constexpr size_t block_size = 4096;
2✔
184
    constexpr size_t blocks_per_metadata_block = 64;
2✔
185
    const size_t pages_per_metadata_block = block_size * blocks_per_metadata_block / page_size();
2✔
186

187
    auto verify_page_states = [&](const IVPageStates& states, off_t data_pos,
2✔
188
                                  std::vector<size_t> expected_pages_refreshed) {
30✔
189
        size_t start_page_ndx = ((data_pos / block_size) / blocks_per_metadata_block) * blocks_per_metadata_block *
30✔
190
                                block_size / page_size();
30✔
191
        size_t end_page_ndx = (((data_pos / block_size) + blocks_per_metadata_block) / blocks_per_metadata_block) *
30✔
192
                              blocks_per_metadata_block * block_size / page_size();
30✔
193

194
        CHECK_EQUAL(states.size(), end_page_ndx - start_page_ndx);
30✔
195
        for (size_t ndx = start_page_ndx; ndx < end_page_ndx; ++ndx) {
1,230✔
196
            CHECK_EQUAL(states.count(ndx), 1);
1,200✔
197
            bool expected_refresh = std::find(expected_pages_refreshed.begin(), expected_pages_refreshed.end(),
1,200✔
198
                                              ndx) != expected_pages_refreshed.end();
1,200✔
199
            CHECK(states.at(ndx) == (expected_refresh ? IVRefreshState::RequiresRefresh : IVRefreshState::UpToDate));
1,200✔
200
        }
1,200✔
201
    };
30✔
202

203
    TEST_PATH(path);
2✔
204
    // enough data to span two metadata blocks
205
    constexpr size_t data_size = block_size * blocks_per_metadata_block * 2;
2✔
206
    const size_t num_pages = data_size / page_size();
2✔
207
    char data[block_size];
2✔
208
    for (size_t i = 0; i < sizeof(data); ++i)
8,194✔
209
        data[i] = static_cast<char>(i);
8,192✔
210

211
    AESCryptor cryptor(test_key);
2✔
212
    cryptor.set_file_size(off_t(data_size));
2✔
213
    File file(path, realm::util::File::mode_Write);
2✔
214
    const FileDesc fd = file.get_descriptor();
2✔
215

216
    auto make_external_write_at_pos = [&](off_t data_pos) -> size_t {
16✔
217
        const size_t begin_write_block = data_pos / block_size * block_size;
16✔
218
        const size_t ndx_in_block = data_pos % block_size;
16✔
219
        AESCryptor cryptor2(test_key);
16✔
220
        cryptor2.set_file_size(off_t(data_size));
16✔
221
        cryptor2.read(fd, off_t(begin_write_block), data, block_size);
16✔
222
        ++data[ndx_in_block];
16✔
223
        cryptor2.write(fd, off_t(begin_write_block), data, block_size);
16✔
224
        return data_pos / page_size();
16✔
225
    };
16✔
226

227
    for (size_t i = 0; i < data_size; i += block_size) {
258✔
228
        cryptor.write(fd, off_t(i), data, block_size);
256✔
229
    }
256✔
230

231
    IVPageStates states = cryptor.refresh_ivs(fd, 0, 0, num_pages);
2✔
232
    std::vector<size_t> pages_needing_refresh = {};
2✔
233
    for (size_t i = 0; i < pages_per_metadata_block; ++i) {
82✔
234
        pages_needing_refresh.push_back(i);
80✔
235
    }
80✔
236
    // initial call requires refreshing all pages in range
237
    verify_page_states(states, 0, pages_needing_refresh);
2✔
238
    states = cryptor.refresh_ivs(fd, 0, 0, num_pages);
2✔
239
    // subsequent call does not require refreshing anything
240
    verify_page_states(states, 0, {});
2✔
241

242
    pages_needing_refresh = {};
2✔
243
    for (size_t i = 0; i < pages_per_metadata_block; ++i) {
82✔
244
        pages_needing_refresh.push_back(i + pages_per_metadata_block);
80✔
245
    }
80✔
246
    off_t read_data_pos = off_t(pages_per_metadata_block * page_size());
2✔
247
    states = cryptor.refresh_ivs(fd, read_data_pos, pages_per_metadata_block, num_pages);
2✔
248
    verify_page_states(states, read_data_pos, pages_needing_refresh);
2✔
249
    states = cryptor.refresh_ivs(fd, read_data_pos, pages_per_metadata_block, num_pages);
2✔
250
    verify_page_states(states, read_data_pos, {});
2✔
251

252
    read_data_pos = off_t(data_size / 2);
2✔
253
    size_t read_page_ndx = read_data_pos / page_size();
2✔
254
    states = cryptor.refresh_ivs(fd, read_data_pos, read_page_ndx, num_pages);
2✔
255
    verify_page_states(states, read_data_pos, {});
2✔
256

257
    read_data_pos = off_t(data_size - 1);
2✔
258
    read_page_ndx = read_data_pos / page_size();
2✔
259
    states = cryptor.refresh_ivs(fd, read_data_pos, read_page_ndx, num_pages);
2✔
260
    verify_page_states(states, read_data_pos, {});
2✔
261

262
    // write at pos 0, read half way through the first page
263
    make_external_write_at_pos(0);
2✔
264
    read_data_pos = off_t(page_size() / 2);
2✔
265
    states = cryptor.refresh_ivs(fd, read_data_pos, 0, num_pages);
2✔
266
    verify_page_states(states, read_data_pos, {0});
2✔
267

268
    // write at end of first page, read half way through first page
269
    make_external_write_at_pos(off_t(page_size() - 1));
2✔
270
    read_data_pos = off_t(page_size() / 2);
2✔
271
    states = cryptor.refresh_ivs(fd, read_data_pos, 0, num_pages);
2✔
272
    verify_page_states(states, read_data_pos, {0});
2✔
273

274
    // write at beginning of second page, read in first page
275
    make_external_write_at_pos(off_t(page_size()));
2✔
276
    read_data_pos = off_t(page_size() / 2);
2✔
277
    states = cryptor.refresh_ivs(fd, read_data_pos, 0, num_pages);
2✔
278
    verify_page_states(states, read_data_pos, {1});
2✔
279

280
    // write at last page of first metadata block, read in first page
281
    size_t page_needing_refresh = make_external_write_at_pos(blocks_per_metadata_block * block_size - 1);
2✔
282
    read_data_pos = off_t(page_size() / 2);
2✔
283
    states = cryptor.refresh_ivs(fd, read_data_pos, 0, num_pages);
2✔
284
    verify_page_states(states, read_data_pos, {page_needing_refresh});
2✔
285

286
    // test truncation of end_page: write to first page, and last page of first metadata block, read in first page,
287
    // but set the end page index lower than the last write
288
    make_external_write_at_pos(0);
2✔
289
    page_needing_refresh = make_external_write_at_pos(blocks_per_metadata_block * block_size - 1);
2✔
290
    REALM_ASSERT(page_needing_refresh >= 1); // this test assumes page_size is < 64 * block_size
2✔
291
    read_data_pos = off_t(0);
2✔
292
    constexpr size_t end_page_index = 1;
2✔
293
    states = cryptor.refresh_ivs(fd, read_data_pos, 0, end_page_index);
2✔
294
    CHECK_EQUAL(states.size(), 1);
2✔
295
    CHECK_EQUAL(states.count(size_t(0)), 1);
2✔
296
    CHECK(states[0] == IVRefreshState::RequiresRefresh);
2✔
297
    states = cryptor.refresh_ivs(fd, read_data_pos, 0, num_pages);
2✔
298
    verify_page_states(states, 0, {page_needing_refresh});
2✔
299

300
    // write to a block indexed to the second metadata block
301
    page_needing_refresh = make_external_write_at_pos(blocks_per_metadata_block * block_size);
2✔
302
    // a read anywhere in the first metadata block domain does not require refresh
303
    read_data_pos = off_t(page_size() / 2);
2✔
304
    states = cryptor.refresh_ivs(fd, read_data_pos, 0, num_pages);
2✔
305
    verify_page_states(states, read_data_pos, {});
2✔
306
    // but a read in a page controlled by the second metadata block does require a refresh
307
    read_data_pos = off_t(blocks_per_metadata_block * block_size);
2✔
308
    states = cryptor.refresh_ivs(fd, read_data_pos, page_needing_refresh, num_pages);
2✔
309
    verify_page_states(states, read_data_pos, {page_needing_refresh});
2✔
310

311
    // write to the last byte of data
312
    page_needing_refresh = make_external_write_at_pos(data_size - 1);
2✔
313
    // a read anywhere in the first metadata block domain does not require refresh
314
    read_data_pos = 0;
2✔
315
    states = cryptor.refresh_ivs(fd, read_data_pos, 0, num_pages);
2✔
316
    verify_page_states(states, read_data_pos, {});
2✔
317
    // but a read in a page controlled by the second metadata block does require a refresh
318
    read_data_pos = off_t(data_size - 1);
2✔
319
    states = cryptor.refresh_ivs(fd, read_data_pos, page_needing_refresh, num_pages);
2✔
320
    verify_page_states(states, read_data_pos, {page_needing_refresh});
2✔
321
}
2✔
322

323
static void check_attach_and_read(const char* key, const std::string& path, size_t num_entries)
324
{
606✔
325
    try {
606✔
326
        auto hist = make_in_realm_history();
606✔
327
        DBOptions options(key);
606✔
328
        auto sg = DB::create(*hist, path, options);
606✔
329
        auto rt = sg->start_read();
606✔
330
        auto foo = rt->get_table("foo");
606✔
331
        auto pk_col = foo->get_primary_key_column();
606✔
332
        REALM_ASSERT_3(foo->size(), ==, num_entries);
606✔
333
        REALM_ASSERT_3(foo->where().equal(pk_col, util::format("name %1", num_entries - 1).c_str()).count(), ==, 1);
606✔
334
    }
606✔
335
    catch (const std::exception& e) {
606✔
336
        auto fs = File::get_size_static(path);
×
337
        util::format(std::cout, "Error for num_entries %1 with page_size of %2 on file of size %3\n%4", num_entries,
×
338
                     page_size(), fs, e.what());
×
339
        throw;
×
340
    }
×
341
}
606✔
342

343
// This test changes the global page_size() and should not run with other tests.
344
// It checks that an encrypted Realm is portable between systems with a different page size
345
NONCONCURRENT_TEST(EncryptedFile_Portablility)
346
{
2✔
347
    const char* key = test_util::crypt_key(true);
2✔
348
    // The idea here is to incrementally increase the allocations in the Realm
349
    // such that the top ref written eventually crosses over the block_size and
350
    // page_size() thresholds. This has caught faulty top_ref + size calculations.
351
    std::vector<size_t> test_sizes;
2✔
352
#if TEST_DURATION == 0
2✔
353
    test_sizes.resize(100);
2✔
354
    std::iota(test_sizes.begin(), test_sizes.end(), 500);
2✔
355
    // The allocations are not controlled, but at the time of writing this test
356
    // 539 objects produced a file of size 16384 while 540 objects produced a file of size 20480
357
    // so at least one threshold is crossed here, though this may change if the allocator changes
358
    // or if compression is implemented
359
#else
360
    test_sizes.resize(5000);
361
    std::iota(test_sizes.begin(), test_sizes.end(), 500);
362
#endif
363

364
    test_sizes.push_back(1); // check the lower limit
2✔
365
    for (auto num_entries : test_sizes) {
202✔
366
        TEST_PATH(path);
202✔
367
        {
202✔
368
            // create the Realm with the smallest supported page_size() of 4096
369
            OnlyForTestingPageSizeChange change_page_size(4096);
202✔
370
            Group g;
202✔
371
            TableRef foo = g.add_table_with_primary_key("foo", type_String, "name", false);
202✔
372
            for (size_t i = 0; i < num_entries; ++i) {
110,104✔
373
                foo->create_object_with_primary_key(util::format("name %1", i));
109,902✔
374
            }
109,902✔
375
            g.write(path, key);
202✔
376
            // size_t fs = File::get_size_static(path);
377
            // util::format(std::cout, "write of %1 objects produced a file of size %2\n", num_entries, fs);
378
        }
202✔
379
        {
202✔
380
            OnlyForTestingPageSizeChange change_page_size(8192);
202✔
381
            check_attach_and_read(key, path, num_entries);
202✔
382
        }
202✔
383
        {
202✔
384
            OnlyForTestingPageSizeChange change_page_size(16384);
202✔
385
            check_attach_and_read(key, path, num_entries);
202✔
386
        }
202✔
387

388
        // check with the native page_size (which is probably redundant with one of the above)
389
        // and check that a write works correctly
390
        auto history = make_in_realm_history();
202✔
391
        DBOptions options(key);
202✔
392
        DBRef db = DB::create(*history, path, options);
202✔
393
        auto wt = db->start_write();
202✔
394
        TableRef bar = wt->get_or_add_table_with_primary_key("bar", type_String, "pk");
202✔
395
        bar->create_object_with_primary_key("test");
202✔
396
        wt->commit();
202✔
397
        check_attach_and_read(key, path, num_entries);
202✔
398
    }
202✔
399
}
2✔
400

401
static void verify_data(const Group& g)
402
{
210✔
403
    g.verify();
210✔
404
    // not all types have implemented verify() and our goal
405
    // here is to access all data, so we use to_json() to make
406
    // sure that all pages can be decrypted
407
    std::stringstream ss;
210✔
408
    g.to_json(ss);
210✔
409
}
210✔
410

411
NONCONCURRENT_TEST(Encrypted_empty_blocks)
412
{
2✔
413
    const char* key = test_util::crypt_key(true);
2✔
414

415
    TEST_PATH(path);
2✔
416
    DBOptions options(key);
2✔
417
    std::string pk_longer_than_block_size = std::string(5, 'a');
2✔
418
    std::string more_than_1_block = std::string(5000, 'b');
2✔
419

420
    {
2✔
421
        // create an initial state, 1 4096 block file
422
        // (total file size is: 4096 metadata block + 4096 data block = 8192)
423
        OnlyForTestingPageSizeChange change_page_size(4096);
2✔
424
        auto db = DB::create(path, options);
2✔
425
        WriteTransaction wt(db);
2✔
426
        auto table = wt.get_group().add_table_with_primary_key("table_foo", type_String, "pk");
2✔
427
        table->create_object_with_primary_key(Mixed{pk_longer_than_block_size});
2✔
428
        wt.commit();
2✔
429
    }
2✔
430
    // Now extend the file, adding 3 empty blocks to a total of 16384
431
    // This happens without a write transaction during DB::open
432
    // during alloc.align_filesize_for_mmap()
433
    OnlyForTestingPageSizeChange change_page_size(16384);
2✔
434
    auto db_16 = DB::create(path, options);
2✔
435
    ReadTransaction rt(db_16);
2✔
436
    verify_data(rt.get_group());
2✔
437
    ReadTransaction pin(db_16);
2✔
438

439
    {
2✔
440
        // open with 4k page size again, the file cannot be truncated, because
441
        // the presence of the previous reader means we are not the session initiator
442
        // write at least one extra block so we can verify it is read later on
443
        OnlyForTestingPageSizeChange change_again_size(4096);
2✔
444
        auto db_4k = DB::create(path, options);
2✔
445
        WriteTransaction wt(db_4k);
2✔
446
        auto table = wt.get_group().get_table("table_foo");
2✔
447
        table->create_object_with_primary_key(Mixed{more_than_1_block});
2✔
448
        verify_data(wt.get_group());
2✔
449
        wt.commit();
2✔
450
    }
2✔
451

452
    // back to the db with 16k page size, advance the reader
453
    // this requires the new block to be decrypted
454
    REALM_ASSERT_3(page_size(), ==, 16384);
2✔
455
    pin.get_group().verify();
2✔
456
    // advance and make sure the new page is read correctly from disk
457
    pin = ReadTransaction(db_16);
2✔
458
    verify_data(pin.get_group());
2✔
459

460
    // open a third instance with 16k page size
461
    // to verify that an external reader can also see all the data
462
    auto another_db = DB::create(path, options);
2✔
463
    WriteTransaction wt16(another_db);
2✔
464
    auto table = wt16.get_group().get_table("table_foo");
2✔
465
    table->create_object_with_primary_key(Mixed{"baz"});
2✔
466
    verify_data(wt16.get_group());
2✔
467
    wt16.commit();
2✔
468
}
2✔
469

470
#if !REALM_ANDROID && !REALM_IOS
471
// spawn process test is not supported on ios/android
472
NONCONCURRENT_TEST_IF(Encrypted_multiprocess_different_page_sizes, testing_supports_spawn_process)
473
{
2✔
474
    const char* key = test_util::crypt_key(true);
2✔
475
    DBOptions options(key);
2✔
476
    SHARED_GROUP_TEST_PATH(path);
2✔
477
    constexpr size_t num_transactions = 100;
2✔
478
    constexpr size_t num_objs_per_transaction = 10;
2✔
479

480
    if (test_util::SpawnedProcess::is_parent()) {
2✔
481
        std::unique_ptr<Replication> hist = make_in_realm_history();
2✔
482
        DBRef sg = DB::create(*hist, path, options);
2✔
483
        if (test_util::SpawnedProcess::is_parent()) {
2✔
484
            WriteTransaction wt(sg);
2✔
485
            TableRef tr = wt.get_group().add_table_with_primary_key("Foo", type_String, "pk");
2✔
486
            for (size_t j = 0; j < num_objs_per_transaction; j++) {
22✔
487
                tr->create_object_with_primary_key(util::format("parent_init_%1", j));
20✔
488
            }
20✔
489
            wt.commit();
2✔
490
        }
2✔
491
        auto process = test_util::spawn_process(test_context.test_details.test_name, "external_writer");
2✔
492
        for (size_t i = 0; i < num_transactions; ++i) {
202✔
493
            WriteTransaction wt(sg);
200✔
494
            TableRef tr = wt.get_table("Foo");
200✔
495
            for (size_t j = 0; j < num_objs_per_transaction; j++) {
2,200✔
496
                tr->create_object_with_primary_key(util::format("parent_%1_%2", i, j));
2,000✔
497
            }
2,000✔
498
            verify_data(wt.get_group());
200✔
499
            wt.commit();
200✔
500
        }
200✔
501
        process->wait_for_child_to_finish();
2✔
502
        ReadTransaction rt(sg);
2✔
503
        verify_data(rt.get_group());
2✔
504
    }
2✔
NEW
505
    else {
×
NEW
506
        OnlyForTestingPageSizeChange change_page(4096);
×
507

NEW
508
        try {
×
NEW
509
            std::unique_ptr<Replication> hist = make_in_realm_history();
×
NEW
510
            DBRef sg = DB::create(*hist, path, options);
×
NEW
511
            for (size_t i = 0; i < num_transactions; ++i) {
×
NEW
512
                WriteTransaction wt(sg);
×
NEW
513
                TableRef tr = wt.get_table("Foo");
×
NEW
514
                for (size_t j = 0; j < num_objs_per_transaction; j++) {
×
NEW
515
                    tr->create_object_with_primary_key(util::format("child_%1_%2", i, j));
×
NEW
516
                }
×
NEW
517
                verify_data(wt.get_group());
×
NEW
518
                wt.commit();
×
NEW
519
            }
×
NEW
520
        }
×
NEW
521
        catch (const std::exception& e) {
×
NEW
522
            REALM_ASSERT_EX(false, e.what());
×
NEW
523
            static_cast<void>(e); // e is unused without assertions on
×
NEW
524
        }
×
525

NEW
526
        exit(0);
×
NEW
527
    }
×
528
}
2✔
529

530
#endif // !REALM_ANDROID && !REALM_IOS
531

532
TEST_TYPES(Encrypted_truncation_is_not_an_error, std::true_type, std::false_type)
533
{
4✔
534
    class TestWriteObserver : public WriteObserver {
4✔
535
    public:
4✔
536
        bool no_concurrent_writer_seen() override
4✔
537
        {
184,508✔
538
            if (m_callback) {
184,508✔
539
                m_callback();
20✔
540
            }
20✔
541
            // simulate that a concurrent writer is seen
542
            // this forces the retry to wait for the max period
543
            // before ultimately giving up
544
            return false;
184,508✔
545
        }
184,508✔
546
        util::UniqueFunction<void()> m_callback;
4✔
547
    };
4✔
548

549
    std::unique_ptr<TestWriteObserver> observer;
4✔
550
    if (TEST_TYPE::value) {
4✔
551
        observer = std::make_unique<TestWriteObserver>();
2✔
552
    }
2✔
553

554
    TEST_PATH(path);
4✔
555
    constexpr size_t block_size = 4096;
4✔
556

557
    char data[block_size * 3];
4✔
558
    for (size_t i = 0; i < sizeof(data); ++i)
49,156✔
559
        data[i] = static_cast<char>(i);
49,152✔
560

561
    AESCryptor cryptor(test_key);
4✔
562
    cryptor.set_file_size(sizeof(data));
4✔
563
    char buffer[sizeof(data)];
4✔
564
    File file(path, realm::util::File::mode_Write);
4✔
565

566
    auto verify_blocks_with_middle_zerod = [&]() {
8✔
567
        for (size_t i = 0; i < 3 * block_size; ++i) {
98,312✔
568
            // middle block is zero'd out
569
            if (i >= block_size && i < 2 * block_size) {
98,304✔
570
                CHECK_EQUAL(buffer[i], 0);
32,768✔
571
                continue;
32,768✔
572
            }
32,768✔
573
            // but the blocks on either side are valid
574
            CHECK_EQUAL(buffer[i], static_cast<char>(i));
65,536✔
575
        }
65,536✔
576
    };
8✔
577
    auto read_to_buffer = [&]() -> size_t {
36✔
578
        return cryptor.read(file.get_descriptor(), 0, buffer, sizeof(buffer), observer.get());
36✔
579
    };
36✔
580

581
    auto write_encrypted_data = [&]() {
18✔
582
        cryptor.write(file.get_descriptor(), 0, data, sizeof(data));
18✔
583
        size_t bytes_read = read_to_buffer();
18✔
584
        CHECK_EQUAL(bytes_read, 3 * block_size);
18✔
585
        CHECK(memcmp(buffer, data, sizeof(data)) == 0);
18✔
586
    };
18✔
587

588
    // first write: iv2 is still zero, only the first block can be read
589
    write_encrypted_data();
4✔
590
    // zero out the second block
591
    file.seek(2 * block_size);
4✔
592
    memset(buffer, '\0', block_size);
4✔
593
    file.write(buffer, block_size);
4✔
594
    // this hits the case of "the very first write was interrupted"
595
    // we know this because iv2 is still 0
596
    size_t bytes_read = read_to_buffer();
4✔
597
    // reading the second block failed, but it was not a decryption error
598
    // this treats the second block as uninitialized data
599
    CHECK_EQUAL(bytes_read, block_size);
4✔
600

601
    // second write: iv2 is set now, but still only the first block can be read
602
    write_encrypted_data();
4✔
603
    // zero out the second block
604
    file.seek(2 * block_size);
4✔
605
    memset(buffer, '\0', block_size);
4✔
606
    file.write(buffer, block_size);
4✔
607
    bytes_read = read_to_buffer();
4✔
608
    // reading the second block failed, but it was not a decryption error
609
    // this treats the second block as uninitialized data
610
    CHECK_EQUAL(bytes_read, 3 * block_size);
4✔
611
    verify_blocks_with_middle_zerod();
4✔
612

613
    // third write: on a hmac check failure and no iv2 fallback, throw an exception
614
    write_encrypted_data();
4✔
615
    // zero out the second block, except for one byte
616
    file.seek(2 * block_size);
4✔
617
    memset(buffer, '\0', block_size);
4✔
618
    buffer[block_size - 1] = '\1';
4✔
619
    file.write(buffer, block_size);
4✔
620
    // this is a decryption error because the hmac check fails and it is not an entirely zero'd block
621
    // with an observer present, this takes an entire 5 seconds
622
    CHECK_THROW(read_to_buffer(), DecryptionFailed);
4✔
623

624
    // fourth write: if an uninitialized block is in the middle of
625
    // two valid blocks but it also has an iv1=0 all 3 blocks can be read
626
    write_encrypted_data();
4✔
627
    // zero out the second block
628
    file.seek(2 * block_size);
4✔
629
    memset(buffer, '\0', block_size);
4✔
630
    file.write(buffer, block_size);
4✔
631
    // now set the ivs of block 2 to 0
632
    constexpr size_t metadata_size = 64; // sizeof(iv_table)
4✔
633
    file.seek(1 * metadata_size);
4✔
634
    file.write(buffer, metadata_size);
4✔
635
    // force refresh the iv cache
636
    auto refresh_states = cryptor.refresh_ivs(file.get_descriptor(), 0, 0, 1);
4✔
637
    CHECK_EQUAL(refresh_states.size(), 1);
4✔
638
    CHECK_EQUAL(refresh_states.count(size_t(0)), 1);
4✔
639
    CHECK(refresh_states[0] == IVRefreshState::RequiresRefresh);
4✔
640
    bytes_read = read_to_buffer();
4✔
641
    CHECK_EQUAL(bytes_read, 3 * block_size);
4✔
642
    verify_blocks_with_middle_zerod();
4✔
643

644
    if (observer) {
4✔
645
        // simulate a second writer finally finishing the page write that matches the written iv
646
        char encrypted_block_copy[block_size];
2✔
647
        write_encrypted_data();
2✔
648
        file.seek(2 * block_size);
2✔
649
        file.read(encrypted_block_copy, block_size);
2✔
650
        file.seek(2 * block_size);
2✔
651
        file.write("data corruption!", 16);
2✔
652

653
        size_t num_retries = 0;
2✔
654
        bool did_write = false;
2✔
655
        observer->m_callback = [&]() {
20✔
656
            if (++num_retries >= 10 && !did_write) {
20✔
657
                file.seek(2 * block_size);
2✔
658
                file.write(encrypted_block_copy, block_size);
2✔
659
                did_write = true;
2✔
660
            }
2✔
661
        };
20✔
662
        // the retry logic does eventually succeed once the page data is restored
663
        bytes_read = read_to_buffer();
2✔
664
        CHECK_EQUAL(bytes_read, 3 * block_size);
2✔
665
        CHECK(memcmp(buffer, data, sizeof(data)) == 0);
2✔
666
    }
2✔
667
}
4✔
668

669

670
// The following can be used to debug encrypted customer Realm files
671
/*
672
static unsigned int hex_char_to_bin(char c)
673
{
674
    if (c >= '0' && c <= '9')
675
        return c - '0';
676
    if (c >= 'a' && c <= 'f')
677
        return c - 'a' + 10;
678
    if (c >= 'A' && c <= 'F')
679
        return c - 'A' + 10;
680
    throw std::invalid_argument("Illegal key (not a hex digit)");
681
}
682

683
static unsigned int hex_to_bin(char first, char second)
684
{
685
    return (hex_char_to_bin(first) << 4) | hex_char_to_bin(second);
686
}
687

688
ONLY(OpenEncrypted)
689
{
690
    std::string path = "my_path.realm";
691
    const char* hex_key = "792cddc553a0452ee893e7583735d4e9f0942d6c8404398f612fd5e415e115e19443aa3be112e072f5fc22b7e2"
692
                          "471a11ad2924c19413d84d19c32179c1063bd2";
693
    char crypt_key[64];
694
    for (int idx = 0; idx < 64; ++idx) {
695
        crypt_key[idx] = hex_to_bin(hex_key[idx * 2], hex_key[idx * 2 + 1]);
696
    }
697
    std::unique_ptr<Replication> hist_w(make_in_realm_history());
698

699
    CHECK_OR_RETURN(File::exists(path));
700
    SHARED_GROUP_TEST_PATH(temp_copy);
701
    File::copy(path, temp_copy);
702
    DBOptions options(crypt_key);
703
    DBRef db = DB::create(*hist_w, temp_copy, options);
704

705
    TransactionRef rt = db->start_read();
706
    TableKeys table_keys = rt->get_table_keys();
707
    for (auto key : table_keys) {
708
        TableRef table = rt->get_table(key);
709
        util::format(std::cout, "table %1 has %2 rows\n", table->get_name(), table->size());
710
        table->to_json(std::cout);
711
        table->verify();
712
        std::cout << std::endl;
713
    }
714
}
715
*/
716

717
#endif // REALM_ENABLE_ENCRYPTION
718
#endif // TEST_ENCRYPTED_FILE_MAPPING
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc