Pitfall
SPDZ Multi-Threaded MAC Check
What can go wrong. SPDZ (Damgård–Pastro–Smart–Zakarias, 2012) is a maliciously-secure MPC protocol with a dishonest majority, where up to $n-1$ out of $n$ parties can be actively corrupted by an adversary. Shared values are authenticated by an information-theoretic MAC under a global key $\alpha$ that no party knows individually, and openings are verified by a MAC check that aborts if the opened value was tampered with. SPDZ is proven secure in the UC framework, which guarantees security under “concurrent execution” with arbitrary independent protocols. However, this guarantee does not extend to a multithreaded SPDZ implementation, where all threads share the same $\alpha$. In particular, when an implementation runs two MAC check instances concurrently in different threads, a malicious party can cheat in one of them to leak the entire MAC key $\alpha$ and use it in the other to forge MACs on arbitrary values.
Security implication. The paper Rushing at SPDZ: On the Practical Security of Malicious MPC Implementations (IEEE S&P 2025) shows that a malicious party can exploit the multi-thread interleaving to cause one MAC-check thread to abort, leaking the global SPDZ MAC key $\alpha$. The adversary then uses the leaked key to manipulate a concurrent thread of the honest parties, e.g. forging MACs on tampered values at will. The paper analyzed three SPDZ implementations and found two, MP-SPDZ and SCALE-MAMBA, vulnerable to this multi-thread MAC interleaving attack. The example below walks through the patches in MP-SPDZ, one of the two.
How to avoid. Treat the MAC check sub-protocol as an atomic critical section across all threads. Three concrete rules:
- Mutual exclusion on the MAC check. A mutex or semaphore prevents two threads from executing overlapping MAC-check instances, including the possible abort path.
- Unconditional verification on every open. The MAC
check()call must fire whenever secret values are opened, regardless of whether the opened values reach an output gate. - Design-level isolation. Where possible, avoid sharing secret state across threads entirely. Fresco’s design-by-construction single-thread-per-session model is a useful reference point.
Example
MP-SPDZ POpen and Commit_And_Open_ race conditions
Two bugs were found and patched in MP-SPDZ.
Bug 1 — Missing MAC check in multi-threaded POpen
(commit 5e714b2). The
SubProcessor<T>::POpen() function opens secret values. The MAC verification call
check() was only triggered by an explicit output-gate condition (inst.get_n()), so
in multi-threaded programs, some opened values could be used without the MAC checks
needed around the open:
1// FILE: Processor/Processor.hpp — MP-SPDZ (vulnerable, prior to fix)
2template <class T>
3void SubProcessor<T>::POpen(const Instruction& inst)
4{
5 if (inst.get_n())
6 check(); // ← MAC check only before the loop, only if inst.get_n() is truthy
7 // ... batched open setup ...
8 for (auto it = reg.begin(); it < reg.end(); it += 2)
9 for (int i = 0; i < size; i++)
10 C[*it + i] = MC.finalize_open();
11 // ← no MAC check after the loop, even when nthreads > 0
12}
The fix widens the pre-loop gate and adds a new post-loop MAC check with the same gate, so multi-threaded opens trigger both checks:
1// FILE: Processor/Processor.hpp — MP-SPDZ (fixed, commit 5e714b2)
2template <class T>
3void SubProcessor<T>::POpen(const Instruction& inst)
4{
5 if (inst.get_n() or BaseMachine::s().nthreads > 0)
6 check(); // ← gate widened to also fire under multi-threading
7 // ... batched open setup ...
8 for (auto it = reg.begin(); it < reg.end(); it += 2)
9 for (int i = 0; i < size; i++)
10 C[*it + i] = MC.finalize_open();
11 if (inst.get_n() or BaseMachine::s().nthreads > 0)
12 check(); // ← NEW: post-loop MAC check, same gate
13}
Bug 2 — Race condition in Commit_And_Open_
(commit b86f29b). Inside
Tools/Subroutines.cpp, a shared coordinator object lets one thread signal to the
others that its commitment phase is complete. That signal was raised before the
commitment-opening validation loop ran, so a second thread waiting on the coordinator
could observe the “finished” state and proceed with values that had not yet been
verified:
1// FILE: Tools/Subroutines.cpp — MP-SPDZ (vulnerable)
2
3P.Broadcast_Receive(Open_data);
4coordinator.finished(); // ← signals completion before verifying
5
6for (int i = 0; i < P.num_players(); i++)
7 if (!Open(datas[i], Comm_data[i], Open_data[i], i))
8 throw invalid_commitment();
The fix moves the signal to after the validation loop:
1// FILE: Tools/Subroutines.cpp — MP-SPDZ (fixed)
2
3P.Broadcast_Receive(Open_data);
4for (int i = 0; i < P.num_players(); i++)
5 if (!Open(datas[i], Comm_data[i], Open_data[i], i))
6 throw invalid_commitment();
7
8coordinator.finished(); // ← now after verifying
The attack exploits the race by having a malicious party controlling Thread B observe that Thread A’s coordinator has finished and immediately proceed to use the opened values in its own MAC check instance, before A has confirmed those values are authenticated. By carefully timing two concurrent MAC check instances the adversary extracts information about $\alpha$ through the unauthenticated intermediate state, then uses this to forge MACs on arbitrary output values.