
Security research firm V12 has disclosed two vulnerabilities in Signal's Contact Discovery Service that allowed the untrusted server host to break out of the Intel SGX enclave that protects users' address books. Researcher Nihal demonstrated both flaws on the same class of hardware Signal runs in production, extracting the enclave's private key and defeating the confidentiality guarantee that private contact discovery is built on.
Neither issue has been assigned a CVE ID or a CVSS score at the time of writing. Both are object-lifetime bugs in the enclave's C code: the first is a use-after-free that yields an arbitrary read of enclave memory, and the second is a time-of-check-to-time-of-use (TOCTOU) race that hands the host full control of the enclave's register context, allowing code execution inside the trusted boundary. V12 reported both privately, and Signal has fixed them.
What Is Signal's Contact Discovery Service?
Contact Discovery Service Icelake (CDSI) is how Signal tells you which people in your phone's address book already use Signal. The computation is trivial — it is the intersection of your contacts with the set of registered users — but doing it on an ordinary server means the server learns your entire social graph. TLS protects the address book in transit, but the data has to be decrypted to be processed.
Signal's answer is to run the matching inside an Intel SGX enclave. The enclave's contents are measured and signed so clients can verify exactly what code is running before sending anything, and its memory is encrypted in hardware so the machine's operator cannot read it. Signal layers Oblivious RAM (ORAM) on top, derived from the Oblix and Snoopy designs, so that even the pattern of memory accesses does not leak which numbers were queried.
This matters more than it might sound. Per V12, the Signal apps on Android and iOS submit the user's address book to CDSI every 48 hours, and the feature is enabled by default.
How the Duplicate Shard Worker Bug Leaked Enclave Memory
Because ORAM is computationally expensive, CDSI splits queries across shards, each with a queue serviced by a long-running worker thread. A query enqueues a lookup, then a wait request. With exactly one worker per shard processing in order, the wait to complete is proof that the lookup finished, which is what makes it safe to free the result buffer.
The enclave let the host spawn a worker with the enclave_run_shard(shard_id) call without checking whether that shard already had one. A malicious host could start a second worker, at which point the ordering guarantee collapses: worker B consumes the wait while worker A is still running the lookup, the buffer is freed, and worker A then writes its result into freed memory.
V12 chained that stale write into a read primitive. Because Signal's infrastructure handles registration, the host is responsible for loading directory records into the enclave via enclave_load_pb(), so an attacker can plant crafted records and query them with their own client to control what gets written. After heap grooming, that write lands on another client's response structure — specifically on a protobuf bytes field, which is just a pointer and a length:
struct pbtools_bytes_t {
uint8_t *buf_p;
size_t size;
};
Overwrite buf_p and size, and when the enclave encodes that client's response, the protobuf encoder obligingly copies out whatever memory range the host chose. V12's proof of concept used it to lift the 32-byte Noise responder private key, which is enough for the host to impersonate the enclave and decrypt client queries outright.
How the Client-Handle TOCTOU Bug Led to Code Execution
The second bug is the more severe of the two. Each connection is represented inside the enclave by a client_t object, and the enclave hands its address back to the host as an opaque handle. To validate a handle, the enclave checked a secret canary value, then separately marked the object in use with a compare-and-swap:
check(c->canary == g_client_secret);
CAS(c->state, CLIENT_UNUSED, CLIENT_INUSE);
Two operations, and a window between them. The host widens that window deliberately by marking the object's page read-only, forcing the CAS to page-fault, then suspending the thread. While it is stopped, the host frees the client with enclave_close_client(), grooms the heap, and uses enclave_rate_limit() to allocate a new 56-byte object at the same address with contents of its choosing.
The replacement is treated as an already-authenticated live client. Since client_t it holds the Noise cipher states, which carry encrypt and decrypt function pointers — the host controls what the enclave calls next. V12 pointed it at Open Enclave's register-restoration routine oe_continue_execution with an attacker-supplied register context, giving what the firm describes as "full control over the enclave's register context". Their PoC sets RIP to the enclave's own memcpy and copies the private key straight out to host memory.
Both exploits were validated on real SGX hardware on the same Azure machine type V12 says Signal uses in production, with the enclave built from unmodified official sources, debugging disabled, and the expected MRENCLAVE measurement. For each one, they extracted the private key, derived its X25519 public key, and matched it against the attested value — confirming compromise of a live, correctly attested enclave rather than a lab mock-up.
What an Attacker Would Actually Need
This is the part worth reading carefully, because it determines whether the disclosure affects you.
Neither vulnerability is remotely exploitable by an ordinary attacker on the internet. Every primitive in both chains — spawning a duplicate shard worker, flipping page permissions to stall a thread, loading crafted directory records, grooming the enclave heap — requires control of the host machine running the enclave. That is the entire point of SGX, and the entire point of the research: the threat model these enclaves exist to defend against is a malicious or compromised host.
So the practical set of parties who could have exploited this is narrow: Signal itself, anyone who compromised Signal's production infrastructure, the cloud provider operating the underlying hardware, or anyone able to legally compel one of those. What broke was not the secrecy of your messages but the cryptographic assurance that those parties could not see your contact queries even if they wanted to. Signal's architecture is designed so that assurance does not rest on trusting Signal. For the window, these bugs were live, it did.
There is no indication in V12's write-up that either flaw was exploited in the wild, and Cyber Kendra has seen no evidence of exploitation.
Are Signal Messages Affected?
No. Both vulnerabilities are confined to the server-side contact discovery enclave. The Signal Protocol's end-to-end encryption for one-to-one messages, group messages, voice and video calls is unaffected, and nothing here lets an attacker read message content, past or present. The exposure is limited to contact discovery queries — the phone numbers your device submits to find out which contacts are on Signal — and to secrets held inside the CDSI enclave itself.
Has Signal Patched It?
Yes. Both fixes landed in the public signalapp/ContactDiscoveryService-Icelake repository, authored by Signal engineer Rolfe Schmidt. Cyber Kendra independently reviewed both commits to confirm the changes match the disclosed root causes.
| Vulnerability | Root cause | Commit | Fix applied |
|---|---|---|---|
| Duplicate shard worker UAF → arbitrary read | enclave_run_shard() did not check whether a shard already had a worker |
df22988 | One worker per shard enforced inside the trusted boundary with an atomic state machine; duplicate starts return err_SHARD__ALREADY_RUNNING |
| Client-handle TOCTOU → code execution | Canary check and state compare-and-swap were separate operations | b1c5ac4 | Canary and state fused into a single atomic acq word, so validating and acquiring a client are one indivisible operation |
Both commits also add regression tests — one asserting a second worker is refused, another asserting a closed client handle cannot be reused. The disclosure and patch dates were not published in V12's write-up.
What Signal Users Should Do
Nothing, in practical terms. These were server-side flaws in infrastructure Signal operates, fixed by Signal; there is no client update to install and no setting to change. Keeping the app current remains sensible general hygiene, but no user action mitigated or mitigates these specific issues.
Users who want to minimise what reaches contact discovery at all can decline Signal's contacts permission, which limits discovery to numbers entered manually, at the cost of convenience.
Why This Research Matters
Signal's use of SGX has drawn academic scrutiny for years, most of it focused on side channels — attacks that infer which numbers were queried from observable behaviour, which is precisely what the ORAM layer was built to stop. V12's work is a different category: not inference from the outside, but memory-safety bugs in the enclave's own C code, exploited from the position SGX explicitly assumes is hostile.
It is also a reminder of a structural tension in enclave design. Keeping the trusted computing base small is good security practice, but it means handing lifetime, scheduling and page management to the untrusted host — and every one of those levers became an exploitation primitive here. Enclave code has to remain correct while an adversary decides when its threads run and when its pages fault. That is a much harder bar than ordinary server code has to clear, and two subtle ordering assumptions were enough to miss it.
Signal publishing its enclave source is what made this research possible, and what lets anyone verify the fixes. Cyber Kendra has contacted Signal for comment and will update this article with any response.
Updates
- 2026-08-27: Initial report published. Both fix commits independently verified in Signal's public repository. No CVE assigned at time of publication.