Skip to content

Commit 03f1e7f

Browse files
committed
Merge branch 'fix/SDK-5874_Crash-at-advanceMacComputation' into 'release/v10.4.0'
SDK-5874. MAC computation thread safety [release v10.4.0] See merge request sdk/sdk!7145
2 parents a9a4de8 + 5111b74 commit 03f1e7f

File tree

5 files changed

+176
-18
lines changed

5 files changed

+176
-18
lines changed

include/mega/localpath.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ class LocalPath
182182
LocalPath(LocalPath&&) noexcept;
183183
LocalPath& operator=(LocalPath&&) noexcept;
184184
LocalPath(const LocalPath& p);
185-
LocalPath operator=(const LocalPath& p);
185+
LocalPath& operator=(const LocalPath& p);
186186

187187
virtual ~LocalPath() {}
188188

include/mega/syncinternals/mac_computation_state.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ struct MacComputationState
9898
std::atomic<bool> completed{false}; // True when local MAC computed
9999
std::atomic<bool> failed{false}; // True if read/compute error
100100

101+
// True when initialization is complete (first advanceMacComputation has returned).
102+
// Used to prevent checkPendingCloneMac from racing with initCloneCandidateMacComputation.
103+
// The sync thread (checkPendingCloneMac) should not proceed until this is true.
104+
std::atomic<bool> initializationComplete{false};
105+
101106
// Throttle tracking - true if we've acquired a slot from MacComputationThrottle
102107
bool throttleSlotAcquired{false};
103108

@@ -175,6 +180,28 @@ struct MacComputationState
175180
return failed.load(std::memory_order_acquire);
176181
}
177182

183+
/**
184+
* @brief Thread-safe: check if initialization is complete.
185+
*
186+
* Returns true after the initializing thread has finished setting up the computation
187+
* and the first advanceMacComputation call has returned. This prevents race conditions
188+
* where checkPendingCloneMac runs before initCloneCandidateMacComputation completes.
189+
*/
190+
bool isInitializationComplete() const
191+
{
192+
return initializationComplete.load(std::memory_order_acquire);
193+
}
194+
195+
/**
196+
* @brief Thread-safe: mark initialization as complete.
197+
*
198+
* Called after advanceMacComputation returns in the initialization function.
199+
*/
200+
void setInitializationComplete()
201+
{
202+
initializationComplete.store(true, std::memory_order_release);
203+
}
204+
178205
/**
179206
* @brief Check if stored context matches current state (CSF case only).
180207
*/

src/localpath.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ LocalPath::LocalPath(const LocalPath& p)
353353
assert(invariant());
354354
}
355355

356-
LocalPath LocalPath::operator=(const LocalPath& p)
356+
LocalPath& LocalPath::operator=(const LocalPath& p)
357357
{
358358
if (this != &p)
359359
{

src/syncinternals/syncinternals.cpp

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -793,11 +793,13 @@ enum class MacAdvanceResult
793793
*
794794
* @param mc MegaClient for file access and async queue.
795795
* @param state The MAC computation state (must have cipher params initialized).
796+
* Passed by value to ensure the state stays alive during the function,
797+
* even if another thread resets the original shared_ptr.
796798
* @param logPrefix Prefix for log messages.
797799
* @return MacAdvanceResult indicating current state.
798800
*/
799801
MacAdvanceResult advanceMacComputation(MegaClient& mc,
800-
std::shared_ptr<MacComputationState>& state,
802+
std::shared_ptr<MacComputationState> state,
801803
const std::string& logPrefix)
802804
{
803805
if (!state)
@@ -987,7 +989,9 @@ FsCloudComparisonResult asyncMacComputation(MegaClient& mc,
987989
// Check for existing computation
988990
if (syncNode.hasRare() && syncNode.rareRO().macComputation)
989991
{
990-
auto& macComp = syncNode.rare().macComputation;
992+
// Take a copy for consistency with clone candidate pattern (not strictly required
993+
// since sync core is single-threaded, but defensive and documents intent)
994+
auto macComp = syncNode.rare().macComputation;
991995

992996
// Validate context is still current (handles moves, deletes, content changes)
993997
if (!macComp->contextMatches(fs.fsid, cn.handle, fs.fingerprint, cn.fingerprint))
@@ -1089,6 +1093,10 @@ FsCloudComparisonResult asyncMacComputation(MegaClient& mc,
10891093
return {NODE_COMP_EREAD, INVALID_META_MAC, INVALID_META_MAC, FingerprintMismatch::Other};
10901094
}
10911095

1096+
// Mark initialization as complete (for consistency with clone candidate case,
1097+
// even though the sync core case is single-threaded)
1098+
macComp->setInitializationComplete();
1099+
10921100
return {NODE_COMP_PENDING, INVALID_META_MAC, INVALID_META_MAC, FingerprintMismatch::Other};
10931101
}
10941102

@@ -1186,34 +1194,36 @@ CloneMacStatus initCloneCandidateMacComputation(MegaClient& mc, SyncUpload_inCli
11861194

11871195
// Create computation state (no context needed - upload lifetime handles validity)
11881196
const m_off_t fileSize = upload.size;
1189-
upload.macComputation = std::make_shared<MacComputationState>(fileSize,
1190-
upload.getLocalname(),
1191-
mc.syncs.mMacComputationThrottle);
1197+
auto macComp = std::make_shared<MacComputationState>(fileSize,
1198+
upload.getLocalname(),
1199+
mc.syncs.mMacComputationThrottle);
1200+
upload.macComputation = macComp; // Store in upload, but keep local copy for safety
11921201

11931202
// Initialize cipher params from candidate node
1203+
// Using local macComp pointer ensures the object stays alive even if another thread
1204+
// resets upload.macComputation during this function.
11941205
const auto& nodeKey = candidateNode->nodekey();
1195-
auto& macComp = *upload.macComputation;
11961206

1197-
macComp.cloneCandidateHandle = candidateNode->nodeHandle();
1198-
macComp.cloneCandidateNodeKey = nodeKey;
1207+
macComp->cloneCandidateHandle = candidateNode->nodeHandle();
1208+
macComp->cloneCandidateNodeKey = nodeKey;
11991209

1200-
memcpy(macComp.transferkey.data(), nodeKey.data(), SymmCipher::KEYLENGTH);
1210+
memcpy(macComp->transferkey.data(), nodeKey.data(), SymmCipher::KEYLENGTH);
12011211
SymmCipher::xorblock((const byte*)nodeKey.data() + SymmCipher::KEYLENGTH,
1202-
macComp.transferkey.data());
1212+
macComp->transferkey.data());
12031213

12041214
const char* iva = nodeKey.data() + SymmCipher::KEYLENGTH;
1205-
macComp.ctriv = MemAccess::get<int64_t>(iva);
1215+
macComp->ctriv = MemAccess::get<int64_t>(iva);
12061216

12071217
// Try to acquire throttle now; if not, leave computation set up and return Pending
12081218
if (mc.syncs.macComputationThrottle().tryAcquireFile())
12091219
{
1210-
macComp.throttleSlotAcquired = true;
1220+
macComp->throttleSlotAcquired = true;
12111221

12121222
LOG_debug << logPre << "Initiating: " << upload.getLocalname() << " [size=" << fileSize
12131223
<< ", files=" << mc.syncs.macComputationThrottle().currentFiles() << "]";
12141224

1215-
// Start first chunk
1216-
auto result = advanceMacComputation(mc, upload.macComputation, logPre);
1225+
// Start first chunk - pass local macComp to ensure object stays alive
1226+
auto result = advanceMacComputation(mc, macComp, logPre);
12171227
if (result == MacAdvanceResult::Failed)
12181228
{
12191229
LOG_debug << logPre << "Failed to start computation of MAC for "
@@ -1228,19 +1238,33 @@ CloneMacStatus initCloneCandidateMacComputation(MegaClient& mc, SyncUpload_inCli
12281238
<< " [files=" << mc.syncs.macComputationThrottle().currentFiles() << "]";
12291239
}
12301240

1241+
// Mark initialization as complete - checkPendingCloneMac can now safely proceed
1242+
macComp->setInitializationComplete();
1243+
12311244
return CloneMacStatus::Pending; // Computation created; may be waiting for throttle or running
12321245
}
12331246

12341247
CloneMacStatus checkPendingCloneMac(MegaClient& mc, SyncUpload_inClient& upload)
12351248
{
12361249
static const std::string logPre{"checkPendingCloneMac: "};
12371250

1238-
if (!upload.macComputation)
1251+
// Take a copy of the shared_ptr to ensure the MacComputationState stays alive
1252+
// during this function, even if another thread resets upload.macComputation.
1253+
// This also makes the null check and subsequent use atomic with respect to
1254+
// other threads that might reset the shared_ptr.
1255+
auto macComp = upload.macComputation;
1256+
if (!macComp)
12391257
{
12401258
return CloneMacStatus::NoCandidates;
12411259
}
12421260

1243-
auto& macComp = upload.macComputation;
1261+
// Wait until initCloneCandidateMacComputation has finished setting up the computation.
1262+
// This prevents races where we try to process/advance a computation that's still
1263+
// being initialized on the client thread.
1264+
if (!macComp->isInitializationComplete())
1265+
{
1266+
return CloneMacStatus::Pending;
1267+
}
12441268

12451269
if (upload.mMetaMac.has_value())
12461270
{

tests/unit/localpath_test.cpp

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,113 @@ TEST(LocalPathTest, LocalPathToLocalPathStr)
346346
rootDrive + pathSep + "folder1" + pathSep + "Jos\xC3\xA9.txt");
347347
}
348348

349+
/**
350+
* @brief Tests that the copy assignment operator works correctly.
351+
*
352+
* Regression test for a bug where operator= returned LocalPath (by value)
353+
* instead of LocalPath& (by reference), which could leave the assigned-to
354+
* object with a null mImplementation after the temporary return value was destroyed.
355+
*/
356+
TEST(LocalPathTest, CopyAssignment)
357+
{
358+
// Create source path
359+
LocalPath source =
360+
LocalPath::fromAbsolutePath(rootDrive + pathSep + "folder" + pathSep + "file.txt");
361+
ASSERT_FALSE(source.empty());
362+
363+
// Test simple copy assignment
364+
LocalPath dest;
365+
dest = source;
366+
367+
// Both source and dest should be valid after assignment
368+
EXPECT_FALSE(source.empty()) << "Source should remain valid after copy assignment";
369+
EXPECT_FALSE(dest.empty()) << "Destination should be valid after copy assignment";
370+
EXPECT_EQ(source, dest) << "Source and destination should be equal";
371+
372+
// Verify dest can still be used (this would crash with null mImplementation)
373+
EXPECT_EQ(dest.toPath(false), source.toPath(false));
374+
EXPECT_EQ(dest.leafName(), source.leafName());
375+
}
376+
377+
/**
378+
* @brief Tests that chained copy assignment works correctly.
379+
*
380+
* This specifically tests the return type of operator= - it must return
381+
* LocalPath& for chained assignments to work.
382+
*/
383+
TEST(LocalPathTest, ChainedCopyAssignment)
384+
{
385+
LocalPath a = LocalPath::fromAbsolutePath(rootDrive + pathSep + "original.txt");
386+
LocalPath b;
387+
LocalPath c;
388+
389+
// Chained assignment: c = b = a
390+
// This requires operator= to return LocalPath&
391+
c = b = a;
392+
393+
// All three should be valid and equal
394+
EXPECT_FALSE(a.empty());
395+
EXPECT_FALSE(b.empty());
396+
EXPECT_FALSE(c.empty());
397+
EXPECT_EQ(a, b);
398+
EXPECT_EQ(b, c);
399+
400+
// Verify all can still be used
401+
std::string expectedPath = a.toPath(false);
402+
EXPECT_EQ(b.toPath(false), expectedPath);
403+
EXPECT_EQ(c.toPath(false), expectedPath);
404+
}
405+
406+
/**
407+
* @brief Tests that move assignment works correctly.
408+
*/
409+
TEST(LocalPathTest, MoveAssignment)
410+
{
411+
std::string originalPath = rootDrive + pathSep + "folder" + pathSep + "file.txt";
412+
LocalPath source = LocalPath::fromAbsolutePath(originalPath);
413+
414+
LocalPath dest;
415+
dest = std::move(source);
416+
417+
// dest should have the original value
418+
EXPECT_FALSE(dest.empty());
419+
EXPECT_EQ(dest.toPath(false), originalPath);
420+
}
421+
422+
/**
423+
* @brief Tests that copy constructor works correctly.
424+
*/
425+
TEST(LocalPathTest, CopyConstructor)
426+
{
427+
LocalPath source =
428+
LocalPath::fromAbsolutePath(rootDrive + pathSep + "folder" + pathSep + "file.txt");
429+
430+
// Copy construct
431+
LocalPath dest(source);
432+
433+
// Both should be valid and equal
434+
EXPECT_FALSE(source.empty());
435+
EXPECT_FALSE(dest.empty());
436+
EXPECT_EQ(source, dest);
437+
EXPECT_EQ(source.toPath(false), dest.toPath(false));
438+
}
439+
440+
/**
441+
* @brief Tests that move constructor works correctly.
442+
*/
443+
TEST(LocalPathTest, MoveConstructor)
444+
{
445+
std::string originalPath = rootDrive + pathSep + "folder" + pathSep + "file.txt";
446+
LocalPath source = LocalPath::fromAbsolutePath(originalPath);
447+
448+
// Move construct
449+
LocalPath dest(std::move(source));
450+
451+
// dest should have the original value
452+
EXPECT_FALSE(dest.empty());
453+
EXPECT_EQ(dest.toPath(false), originalPath);
454+
}
455+
349456
TEST(LocalPathTest, leafOrParentName)
350457
{
351458
LOG_debug << "#### Test1: get leafOrParentName for different example LocalPaths ####";

0 commit comments

Comments
 (0)