Skip to content

Commit c3ed40b

Browse files
authored
Fix more encryption issues on exFAT filesystem (#7162)
* refactor file operations * fix an issue on exFAT systems where the file unique id was not correct for new Realms leading to corruption for encrypted Realms * fix the remaining callsites where a unique id could be reused * add changelog note * fix an error in the tests that would cause spawned processes to not have the same fallback tmp path which could cause issues if running on a filesystem that doesn't support mkfifo such as exFAT * return none in an interprocess race on creating a lock file and getting its unique id * Throw an exception on unique id failure instead of asserting in consideration of external processes. Use prealloc to size the file instead of a separate resize operation. Add the file path to help debug exceptions. * better error handling and cache a File unique id to catch more errors
1 parent fed5000 commit c3ed40b

8 files changed

+140
-92
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
### Fixed
99
* <How do the end-user experience this issue? what was the impact?> ([#????](https://github.com/realm/realm-core/issues/????), since v?.?.?)
10+
* Fixed several causes of "decryption failed" exceptions that could happen when opening multiple encrypted Realm files in the same process while using Apple/linux and storing the Realms on an exFAT file system. ([#7156](https://github.com/realm/realm-core/issues/7156), since the beginning)
1011
* Update existing std exceptions thrown by the Sync Client to use Realm exceptions. ([#6255](https://github.com/realm/realm-core/issues/6255), since v10.2.0)
1112
* Having a class name of length 57 would make client reset crash as a limit of 56 was wrongly enforced (57 is the correct limit) ([#7176](https://github.com/realm/realm-core/issues/7176), since v10.0.0)
1213
* Automatic client reset recovery on flexible sync Realms would apply recovered changes in multiple write transactions, releasing the write lock in between. This had several observable negative effects:

src/realm/alloc_slab.cpp

+11-4
Original file line numberDiff line numberDiff line change
@@ -827,12 +827,19 @@ ref_type SlabAlloc::attach_file(const std::string& path, Config& cfg, util::Writ
827827
if (REALM_UNLIKELY(cfg.read_only))
828828
throw InvalidDatabase("Read-only access to empty Realm file", path);
829829

830-
const char* data = reinterpret_cast<const char*>(&empty_file_header);
831-
m_file.write(data, sizeof empty_file_header); // Throws
832-
833-
// Pre-alloc initial space
834830
size_t initial_size = page_size(); // m_initial_section_size;
831+
// exFAT does not allocate a unique id for the file until it is non-empty. It must be
832+
// valid at this point because File::get_unique_id() is used to distinguish
833+
// mappings_for_file in the encryption layer. So the prealloc() is required before
834+
// interacting with the encryption layer in File::write().
835+
// Pre-alloc initial space
835836
m_file.prealloc(initial_size); // Throws
837+
// seek() back to the start of the file in preparation for writing the header
838+
// This sequence of File operations is protected from races by
839+
// DB::m_controlmutex, so we know we are the only ones operating on the file
840+
m_file.seek(0);
841+
const char* data = reinterpret_cast<const char*>(&empty_file_header);
842+
m_file.write(data, sizeof empty_file_header); // Throws
836843

837844
bool disable_sync = get_disable_sync_to_disk() || cfg.disable_sync;
838845
if (!disable_sync)

src/realm/group.cpp

+4
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,10 @@ void Group::write(File& file, const char* encryption_key, uint_fast64_t version_
966966

967967
file.set_encryption_key(encryption_key);
968968

969+
// Force the file system to allocate a node so we get a stable unique id.
970+
// See File::get_unique_id(). This is used to distinguish encrypted mappings.
971+
file.resize(1);
972+
969973
// The aim is that the buffer size should be at least 1/256 of needed size but less than 64 Mb
970974
constexpr size_t upper_bound = 64 * 1024 * 1024;
971975
size_t min_space = std::min(get_used_space() >> 8, upper_bound);

src/realm/util/file.cpp

+87-10
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,7 @@ void File::open_internal(const std::string& path, AccessMode a, CreateMode c, in
414414
{
415415
REALM_ASSERT_RELEASE(!is_attached());
416416
m_path = path; // for error reporting and debugging
417+
m_cached_unique_id = {};
417418

418419
#ifdef _WIN32 // Windows version
419420

@@ -572,6 +573,32 @@ void File::close() noexcept
572573
#endif
573574
}
574575

576+
void File::close_static(FileDesc fd)
577+
{
578+
#ifdef _WIN32
579+
if (!fd)
580+
return;
581+
582+
if (!CloseHandle(fd))
583+
throw std::system_error(GetLastError(), std::system_category(),
584+
"CloseHandle() failed from File::close_static()");
585+
#else
586+
if (fd < 0)
587+
return;
588+
589+
int ret = -1;
590+
do {
591+
ret = ::close(fd);
592+
} while (ret == -1 && errno == EINTR);
593+
594+
if (ret != 0) {
595+
int err = errno; // Eliminate any risk of clobbering
596+
if (err == EBADF || err == EIO)
597+
throw SystemError(err, "File::close_static() failed");
598+
}
599+
#endif
600+
}
601+
575602
size_t File::read_static(FileDesc fd, char* data, size_t size)
576603
{
577604
#ifdef _WIN32 // Windows version
@@ -1558,22 +1585,50 @@ bool File::compare(const std::string& path_1, const std::string& path_2)
15581585
return true;
15591586
}
15601587

1561-
bool File::is_same_file_static(FileDesc f1, FileDesc f2)
1588+
bool File::is_same_file_static(FileDesc f1, FileDesc f2, const std::string& path1, const std::string& path2)
15621589
{
1563-
return get_unique_id(f1) == get_unique_id(f2);
1590+
return get_unique_id(f1, path1) == get_unique_id(f2, path2);
15641591
}
15651592

15661593
bool File::is_same_file(const File& f) const
15671594
{
15681595
REALM_ASSERT_RELEASE(is_attached());
15691596
REALM_ASSERT_RELEASE(f.is_attached());
1570-
return is_same_file_static(m_fd, f.m_fd);
1597+
return is_same_file_static(m_fd, f.m_fd, m_path, f.m_path);
1598+
}
1599+
1600+
FileDesc File::dup_file_desc(FileDesc fd)
1601+
{
1602+
FileDesc fd_duped;
1603+
#ifdef _WIN32
1604+
if (!DuplicateHandle(GetCurrentProcess(), fd, GetCurrentProcess(), &fd_duped, 0, FALSE, DUPLICATE_SAME_ACCESS))
1605+
throw std::system_error(GetLastError(), std::system_category(), "DuplicateHandle() failed");
1606+
#else
1607+
fd_duped = dup(fd);
1608+
1609+
if (fd_duped == -1) {
1610+
int err = errno; // Eliminate any risk of clobbering
1611+
throw std::system_error(err, std::system_category(), "dup() failed");
1612+
}
1613+
#endif // conditonal on _WIN32
1614+
return fd_duped;
15711615
}
15721616

1573-
File::UniqueID File::get_unique_id() const
1617+
File::UniqueID File::get_unique_id()
15741618
{
15751619
REALM_ASSERT_RELEASE(is_attached());
1576-
return File::get_unique_id(m_fd);
1620+
File::UniqueID uid = File::get_unique_id(m_fd, m_path);
1621+
if (!m_cached_unique_id) {
1622+
m_cached_unique_id = std::make_optional(uid);
1623+
}
1624+
if (m_cached_unique_id != uid) {
1625+
throw FileAccessError(ErrorCodes::FileOperationFailed,
1626+
util::format("The unique id of this Realm file has changed unexpectedly, this could be "
1627+
"due to modifications by an external process '%1'",
1628+
m_path),
1629+
m_path);
1630+
}
1631+
return uid;
15771632
}
15781633

15791634
FileDesc File::get_descriptor() const
@@ -1597,37 +1652,59 @@ std::optional<File::UniqueID> File::get_unique_id(const std::string& path)
15971652
throw SystemError(GetLastError(), "CreateFileW failed");
15981653
}
15991654

1600-
return get_unique_id(fileHandle);
1655+
return get_unique_id(fileHandle, path);
16011656
#else // POSIX version
16021657
struct stat statbuf;
16031658
if (::stat(path.c_str(), &statbuf) == 0) {
1659+
if (statbuf.st_size == 0) {
1660+
// On exFAT systems the inode and device are not populated correctly until the file
1661+
// has been allocated some space. The uid can also be reassigned if the file is
1662+
// truncated to zero. This has led to bugs where a unique id returned here was
1663+
// reused by different files. The following check ensures that this is not
1664+
// happening anywhere else in future code.
1665+
return none;
1666+
}
16041667
return File::UniqueID(statbuf.st_dev, statbuf.st_ino);
16051668
}
16061669
int err = errno; // Eliminate any risk of clobbering
16071670
// File doesn't exist
16081671
if (err == ENOENT)
16091672
return none;
1610-
throw SystemError(err, format_errno("fstat() failed: %1", err));
1673+
throw SystemError(err, format_errno("fstat() failed: %1 for '%2'", err, path));
16111674
#endif
16121675
}
16131676

1614-
File::UniqueID File::get_unique_id(FileDesc file)
1677+
File::UniqueID File::get_unique_id(FileDesc file, const std::string& debug_path)
16151678
{
16161679
#ifdef _WIN32 // Windows version
16171680
REALM_ASSERT(file != nullptr);
16181681
File::UniqueID ret;
16191682
if (GetFileInformationByHandleEx(file, FileIdInfo, &ret.id_info, sizeof(ret.id_info)) == 0) {
1620-
throw std::system_error(GetLastError(), std::system_category(), "GetFileInformationByHandleEx() failed");
1683+
throw std::system_error(GetLastError(), std::system_category(),
1684+
util::format("GetFileInformationByHandleEx() failed for '%1'", debug_path));
16211685
}
16221686

16231687
return ret;
16241688
#else // POSIX version
16251689
REALM_ASSERT(file >= 0);
16261690
struct stat statbuf;
16271691
if (::fstat(file, &statbuf) == 0) {
1692+
// On exFAT systems the inode and device are not populated correctly until the file
1693+
// has been allocated some space. The uid can also be reassigned if the file is
1694+
// truncated to zero. This has led to bugs where a unique id returned here was
1695+
// reused by different files. The following check ensures that this is not
1696+
// happening anywhere else in future code.
1697+
if (statbuf.st_size == 0) {
1698+
throw FileAccessError(
1699+
ErrorCodes::FileOperationFailed,
1700+
util::format("Attempt to get unique id on an empty file. This could be due to an external "
1701+
"process modifying Realm files. '%1'",
1702+
debug_path),
1703+
debug_path);
1704+
}
16281705
return UniqueID(statbuf.st_dev, statbuf.st_ino);
16291706
}
1630-
throw std::system_error(errno, std::system_category(), "fstat() failed");
1707+
throw std::system_error(errno, std::system_category(), util::format("fstat() failed for '%1'", debug_path));
16311708
#endif
16321709
}
16331710

src/realm/util/file.hpp

+9-3
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ class File {
176176
/// regardless of whether this instance currently is attached to
177177
/// an open file.
178178
void close() noexcept;
179+
static void close_static(FileDesc fd); // throws
179180

180181
/// Check whether this File instance is currently attached to an
181182
/// open file.
@@ -529,7 +530,9 @@ class File {
529530
/// Both instances have to be attached to open files. If they are
530531
/// not, this function has undefined behavior.
531532
bool is_same_file(const File&) const;
532-
static bool is_same_file_static(FileDesc f1, FileDesc f2);
533+
static bool is_same_file_static(FileDesc f1, FileDesc f2, const std::string& path1, const std::string& path2);
534+
535+
static FileDesc dup_file_desc(FileDesc fd);
533536

534537
/// Resolve the specified path against the specified base directory.
535538
///
@@ -604,18 +607,20 @@ class File {
604607
};
605608
// Return the unique id for the current opened file descriptor.
606609
// Same UniqueID means they are the same file.
607-
UniqueID get_unique_id() const;
610+
UniqueID get_unique_id(); // Throws
608611
// Return the file descriptor for the file
609612
FileDesc get_descriptor() const;
610613
// Return the path of the open file, or an empty string if
611614
// this file has never been opened.
612615
std::string get_path() const;
613616

614617
// Return none if the file doesn't exist. Throws on other errors.
618+
// If the file does exist but has a size of zero, the file may be resized
619+
// to force the file system to allocate a unique id.
615620
static std::optional<UniqueID> get_unique_id(const std::string& path);
616621

617622
// Return the unique id for the file descriptor. Throws if the underlying stat operation fails.
618-
static UniqueID get_unique_id(FileDesc file);
623+
static UniqueID get_unique_id(FileDesc file, const std::string& debug_path);
619624

620625
template <class>
621626
class Map;
@@ -641,6 +646,7 @@ class File {
641646
#endif
642647
std::unique_ptr<const char[]> m_encryption_key = nullptr;
643648
std::string m_path;
649+
std::optional<UniqueID> m_cached_unique_id;
644650

645651
bool lock(bool exclusive, bool non_blocking);
646652
bool rw_lock(bool exclusive, bool non_blocking);

0 commit comments

Comments
 (0)